Java guide¶
The library is written in Kotlin and designed to be used from Java. Most pages on this site carry a Java tab. This page explains the handful of places where Java and Kotlin genuinely diverge, so those tabs make sense.
Pick the Java tab once
The language tabs are linked across the whole site. Choose "Java" on any page and every other page follows.
Extensions become static facades¶
Most of the etcd surface is Kotlin extension functions, which have no Java equivalent.
Each extension file is therefore annotated @file:JvmName(...), which compiles it to a
class of static methods. A fun Client.putValue(...) becomes
KVUtils.putValue(client, ...) — the receiver is just the first parameter.
// A Kotlin extension `fun Client.putValue(...)` in a file annotated
// @file:JvmName("KVUtils") becomes a static KVUtils.putValue(client, ...).
// The receiver simply becomes the first parameter.
try (Client client = connectToEtcd(List.of("http://localhost:2379"))) {
putValue(client, "/config/mode", "live");
String mode = getValue(client, "/config/mode", "default");
boolean present = isKeyPresent(client, "/config/mode");
long children = getChildCount(client, "/config");
System.out.printf("%s present=%s children=%d%n", mode, present, children);
}
The full map:
| Source file | Java class |
|---|---|
ClientExtensions.kt |
ClientUtils |
KVExtensions.kt |
KVUtils |
TypedKVExtensions.kt |
TypedKVUtils |
ChildrenExtensions.kt |
ChildrenUtils |
WatchExtensions.kt |
WatchUtils |
TxnExtensions.kt |
TxnUtils |
LeaseExtensions.kt |
LeaseUtils |
KeepAliveExtensions.kt |
KeepAliveUtils |
LockExtensions.kt |
LockUtils |
BuilderExtensions.kt |
BuilderUtils |
ByteSequenceExtensions.kt |
ByteSequenceUtils |
KeyValueExtensions.kt |
KeyValueUtils |
PairExtensions.kt |
PairUtils |
PathExtensions.kt |
PathUtils |
TypedServiceInstance.kt |
TypedServiceInstances |
Static-import them and the code reads well:
import static io.etcd.recipes.common.ClientUtils.connectToEtcd;
import static io.etcd.recipes.common.KVUtils.putValue;
Conversion helpers work the same way, with one wrinkle:
ByteSequence bytes = ByteSequence.from("hello".getBytes());
// Kotlin's `val ByteSequence.asString` is an extension *property*, so Java
// sees the getter name: getAsString(...), not asString(...).
String text = getAsString(bytes);
System.out.println(text);
Extension properties get a get prefix
asString, asInt, asLong and asByteSequence are extension properties, not
functions. Java sees their getter names:
| Kotlin | Java |
|---|---|
bytes.asString |
ByteSequenceUtils.getAsString(bytes) |
bytes.asInt |
ByteSequenceUtils.getAsInt(bytes) |
bytes.asLong |
ByteSequenceUtils.getAsLong(bytes) |
"x".asByteSequence |
ByteSequenceUtils.getAsByteSequence("x") |
Extension functions like putValue keep their name. If a static import will not
resolve, this is the first thing to check.
The recipe classes are not extensions
DistributedMutex, LeaderLatch, PathChildrenCache and friends are ordinary
classes. Construct them with new. Only the common extension layer goes through a
facade.
Default arguments become an overload ladder¶
Kotlin default arguments do not exist in Java, so every public constructor and method with
defaults is annotated @JvmOverloads. That generates one overload per trailing default:
// @JvmOverloads generates a ladder of constructors, so Java can stop at any
// trailing default. But Java has no named arguments: to reach a later
// parameter you must supply every earlier one positionally.
DistributedMutex a = new DistributedMutex(client, "/locks/orders");
DistributedMutex b = new DistributedMutex(client, "/locks/orders", 10L);
No named arguments in Java
Kotlin can skip to a late parameter by name:
Java cannot. To reach interruptOnLockLoss you must supply every preceding
parameter positionally, defaults included:
When a Java example here looks more verbose than its Kotlin twin, this is usually why. Comment the positional arguments — future readers will not remember the order.
Duration: use the TimeUnit overloads¶
Every timeout takes both a kotlin.time.Duration and a (long, TimeUnit) overload. Use
the latter:
mutex.tryLock(5, TimeUnit.SECONDS);
semaphore.tryAcquire(2, TimeUnit.SECONDS);
latch.await(30, TimeUnit.SECONDS);
Except EtcdConnectionConfig
That one class uses java.time.Duration — it is what the underlying jetcd builder
wants. So Duration.ofSeconds(5) there, (5, TimeUnit.SECONDS) everywhere else. See
Connecting.
Properties become getters¶
A Kotlin val lockPath: String is getLockPath(); a val isLeader: Boolean is
isLeader(). The readLock/writeLock views of a read/write lock are
getReadLock()/getWriteLock().
Listeners are SAM interfaces¶
Every callback type is a fun interface, so Java lambdas work directly:
mutex.addLockLostListener(cause -> log.warn("lost the lock: {}", cause));
cache.addListener(event -> {
System.out.println(event.getType() + " " + event.getChildName());
});
LeaderSelectorListener has two methods, so it needs an anonymous class — or extend
LeaderSelectorListenerAdapter and override only what you care about.
What is Kotlin-only¶
| Feature | Java story |
|---|---|
Coroutines and Flow |
No equivalent. Use the blocking API and listeners — every suspending function is a twin of one. |
withXxx { } scoped functions |
Use try (…). Same lifecycle, no magic. |
withLock { } / withPermit { } |
Callable as EtcdLockKt.withLock(lock, Function0), but a Java lambda must return Unit.INSTANCE. Not worth it — use try/finally. |
Reified helpers (jsonCodec<T>()) |
Use JacksonCodec — that module exists for exactly this. |
try (DistributedMutex mutex = new DistributedMutex(client, "/locks/orders")) {
// Possible, but not worth it: the Kotlin `withLock { }` becomes a static
// taking a Function0, so a void body has to return Unit.INSTANCE.
EtcdLockKt.withLock((EtcdLock) mutex, () -> {
System.out.println("Critical section");
return Unit.INSTANCE;
});
// Prefer plain try/finally in Java. It reads better and does the same thing.
mutex.lock();
try {
System.out.println("Critical section");
} finally {
mutex.unlock();
}
}
Typed values from Java¶
Core's jsonCodec<T>() is a reified Kotlin helper, and KotlinxJsonCodec needs a
kotlinx-serialization KSerializer — neither is pleasant from Java. The
Jackson module is the Java answer: EtcdCodec<T> backed by an
ObjectMapper.
StringCodec.INSTANCE and ByteSequenceCodec.INSTANCE are Kotlin objects, reached
through INSTANCE.
The EtcdRecipes facade¶
If you would rather not construct each recipe by hand, EtcdRecipes is an ordinary class
and reads naturally from Java:
// EtcdRecipes is a plain class, so it reads naturally from Java.
EtcdRecipes recipes = new EtcdRecipes(client);
try (DistributedMutex mutex = recipes.mutex("/locks/orders")) {
System.out.println("Got a mutex from the facade: " + mutex.getLockPath());
}
See Integrations for what it covers — and what it does not.
Spring Boot¶
The Spring Boot starter is the most Java-friendly entry
point: put it on the classpath and inject Client or EtcdRecipes.
Runnable Java examples¶
The repo ships Java programs for most recipes under
etcd-recipes-examples/src/main/java.
Unlike the snippets on this site, those have main() methods and expect an etcd at
localhost:2379.