Generics in Java are hard (sad, but true), however, Kotlin makes them way more accessible 🤩.
Suppose you are doing property-based testing. Just like I suggest, I was creating a jqwik test, and I ran into an issue with generic signatures.
Here's a minimal example, and it already has two issues.
Take a second and find both of them 😉
import java.util.function.Predicate;
interface Arbitrary<T> {
/** Generate a sample object */
T sample();
/**
* Create a new arbitrary of the same type but only allows
* values that are accepted by the {@code filterPredicate}
*/
Arbitrary<T> filter(Predicate<T> filterPredicate);
For instance, if you have Arbitrary<String> names
, you can't filter it with Predicate<Object> hashCodeIsZero
filter.
Even though hashCodeIsZero
is applicable to any object, the signature of filter(Predicate<T>
just does not allow to pass Predicate<Object>
there.
The Java solution is to add ? super T
wildcard (Producer Extends Consumer Super):
Arbitrary<T> filter(Predicate<? super T> filterPredicate);
Now it works.
However, there's a more severe issue there 🤔.
Java compiler does not know the intention of the code. It does not know Arbitrary<T>
is supposed to produce T
, and it does not know Predicate<T>
is supposed to consume T
.
Here's a case when Optional<T>
was replaced with Optional<? extends T>
for better API: SpongePowered/SpongeAPI#369
Is it that obvious that you need to declare returning values as Optional<? extends T>
? I doubt so.
The worse is that in Java you have to duplicate ? extends T
again and again within the same class.
For instance, if you want to generate a list out of an arbitrary generator, the proper signature is Arbitrary<? extends T>
.
You never know that unless someone comes to you and suggests that something does not compile with subclasses.
interface Arbitrary<T> {
// Note how "? extends T" is duplicated, and it is easy to forget :-/
<T> ListArbitrary<T> list(Arbitrary<? extends T> elementArbitrary)
<T> SetArbitrary<T> set(Arbitrary<? extends T> elementArbitrary)
In Kotlin, the variance is specified at declaration site. That is a game-changer.
interface Arbitrary<out T> {
sample(): T;
filter(filterPredicate: Predicate<T>): Arbitrary<T>
-
The code is easier to read. The interface declaration means "this interface produces T".
-
The code fails to compile 🎉
Type parameter T is declared as 'out' but occurs in 'invariant' position in type Predicate<T>
What it says is that "ok, you said
T
was supposed for producing values, however, we don't know ifPredicate<T>
produces or consumes"With Java you have to remember that you need specify wildcards in cases like
filter(Predicate<? super T> predicate)
, and Kotlin either infers the variance from the type variable declaration or it stops with a compilation error.
It is sad there's no way to teach Kotlin compiler that java.util.function.Predicate<T>
means java.util.function.Predicate<in T>
,
however, it is really helpful that Kotlin detects issues before you even start using the API.
The fix would be:
interface Arbitrary<out T> {
sample(): T;
// Explicit `Predicate<in T>` is needed since Java's Predicate does not declare variance
filter(filterPredicate: Predicate<in T>): Arbitrary<T>
The case with list
is easier with Kotlin:
interface Arbitrary<out T> {
// It just works, and it generates `Arbitrary<? extends T> elementArbitrary` in the bytecode
<T> list(elementArbitrary: Arbitrary<T>): ListArbitrary<T>
<T> set(elementArbitrary: Arbitrary<T>): SetArbitrary<T>
Unfortunately, Kotlin does not capture all the issues when using Java classes.
For instance, if you use java.util.function.Function
, most likeky you would need to specify out
variance.
interface Arbitrary<out T> {
// Fails to compile since T has out variance in Arbitrary and invariant in Function
fun <U> map(mapper: Function<T, U>): Arbitrary<U>
// Compiles, however it does not allow passing functions that return a subtype
fun <U> map(mapper: Function<in T, U>): Arbitrary<U>
// Perfect declaration, `out` needs to be added manually :-/
fun <U> map(mapper: Function<in T, out U>): Arbitrary<U>
If you use Kotlin types (e.g. (T) -> R
), then you get the variance for free since Kotlin functions declare it: interface Function1<in P1, out R>
.
Unfortunately, Java does not allow to specify generic variance at the declaration site.
There's an open Kotlin compiler issue: KT-41062 Allow Java classes to specify declaration-site variance, and the takeaway there is
- You could migrate the interfaces to Kotlin
- You could attach the metadata to your Java classes. kotlinx.metadata project might be helpful for generating the metadata if you absolutely want to avoid Kotlin in dependencies.
- You could vote for
@In
and@Out
annotations in JSpecify
All-in-all, I would definitely recommend using Kotlin generics to a friend:
- Code with generics in Kotlin is easier to read and write
- Kotlin compiler captures more bugs, and it captures programmer's intention better than Java
If you want to see how it fits in a bigger picture, here's a draft PR for jqwik that migrates core API to Kotlin: jqwik-team/jqwik#302
PS If you like testing, consider proposing a talk via https://heisenbug.ru/en/callforpapers/ PS If you have a cool JVM story, consider proposing a talk via https://jpoint.ru/en/callforpapers/