Fixing ugly Java APIs: read-only generic varargs
and how Kotlin generic types are superior to Java’s
There is an Android API I hate using because of a specific confusing method call. I’m certain there are many other examples of similar methods in the Java world. What’s interesting about it is that it illustrates how the limitations of a programming language (Java in this case) can lead to forcing developers to write unnecessarily cryptic code just to call a single method.
It’s also a good opportunity to explore the obscure world of generic types and variance that even experienced developers often struggle to fully understand.
While I’ll be using the Android platform as an example, the problems described hereafter apply to any Java code.
The problematic method
The Java method I want to discuss is part of the Android API designed to launch Activity transitions with shared elements.
ActivityOptions makeSceneTransitionAnimation (Activity activity,
Pair...<View, String> sharedElements)
A similar method is also available in the Android support library. A typical call forces me to write code looking like this:
@SuppressWarnings("unchecked")
Bundle options = ActivityOptions.makeSceneTransitionAnimation(this,
Pair.<View, String>create(textView, "elem1"),
Pair.<View, String>create(imageView, "elem2"))
.toBundle();
Not so pretty. There are 2 reasons why this method call is made difficult.
1. Arrays of generic types are unsafe
The first problem you’ll notice is that, no matter which arguments you pass to this method, the compiler will always show a warning message.
// Compiles with warning:
// "unchecked generics array creation for varargs parameter"
ActivityOptions.makeSceneTransitionAnimation(this, elem1, elem2);
The reason behind that warning is that Java does not allow creating arrays of parameterized types.
// Does NOT compile
Pair<View, String>[] array = new Pair<View, String>[2];// Compiles with warning: "Unchecked assignment"
Pair<View, String>[] array = new Pair[2];
This is forbidden because Java arrays are covariant and they check the type of values written to them at runtime. But at runtime, generic types have been erased and the array can not differentiate a Pair<View, String>
from a Pair<Integer, Integer>
, leading to potential heap pollution and breaking the array contract.
When you call a method with a variable number of arguments, an array is created to pass all the arguments and since the type of that array can not be parameterized, an unchecked assignment has to be made.
To sum it up, as soon as you want to put generic types in a Java array, you’re on your own to guarantee the safety of your code and to prevent a ClassCastException
from occurring at runtime. This is why collections should be preferred to arrays when dealing with generic types in Java. Changing the method signature to use a List<T>
instead of T...
would avoid compiler warnings and guarantee type safety at compile time:
List<Pair<View, String>> args = new ArrayList<>(2);
args.add(elem1);
args.add(elem2);
ActivityOptions.makeSceneTransitionAnimation(this, args);
However, it would also make the method call even more verbose and we don’t want that either.
The fix: @SafeVarargs
The recommended way to fix the variable arguments declaration is to add the @SafeVarargs
annotation (available since Java 7) to the method signature.
@SafeVarargs
public static ActivityOptions makeSceneTransitionAnimation(
Activity activity,
Pair<View, String>... sharedElements) {
...
}
This prevents the compiler from issuing a warning when calling the method with the multiple arguments syntax (not passing a single array reference). Adding this annotation tells the compiler that we guarantee that the body of this method only expects each argument in the varargs array to be of the exact declared type and will not perform potentially unsafe operations on it.
On the caller side, when the method is called with this syntax, we rely on the compiler to generate an array populated with objects of the exact same type as well.
Overall this solution feels like a hack, but that’s the best we can get with varargs using the Java language. It’s preferable to add this annotation once on your public API methods, rather than forcing developers to add @SuppressWarnings("unchecked")
annotations above each method call.
2. Read-only arguments of generic types should not be invariant
The second problem with this method is the overly restrictive type of the Pair
arguments. The Pair
class is actually read-only by nature because its two fields are final
, so it would be perfectly reasonable to provide for example a Pair<TextView, String>
as argument and not only strict instances of Pair<View, String>
(TextView
inherits from View
).
Suppose I previously declared a variable of type TextView
and I want to pass it in the Pair arguments. In order to write shorter code, I tend to always use the Pair.create()
generic factory method which allows creating a parameterized Pair<A, B>
instance without having to specify the types:
// Creates a Pair<TextView, String>
Pair.create(textView, "elem1");// Creates a Pair<View, String>
Pair.<View, String>create(textView, "elem1");// Creates a Pair<View, String>
new Pair<View, String>(textView, "elem1");
The first syntax is the shortest but can’t be used here because unlike arrays, Java generic types are always invariant by default.
// Does NOT compile: incompatible types
Pair<View, String> elem1 = Pair.create(textView, "elem1");
The fix: wildcards declaration
Fortunately, there is a way to make a generic type covariant in Java: using wildcards in the type declaration. This is called use-site variance because it has to be specified in the type of each variable where you want to use covariance (read-only generic parameters) or contravariance (write-only generic parameters).
// Compiles (View type can only be consumed)
Pair<? extends View, String> elem1 = Pair.create(textView, "elem1");
Declaring wildcards for read-only or write-only generic arguments in methods of public APIs should be systematic. But they are easy to forget, especially for those who are not too familiar with variance and generic types.
Here is the final fixed signature of our Java method:
@SafeVarargs
public static ActivityOptions makeSceneTransitionAnimation(
Activity activity,
Pair<? extends View, ? extends String>... sharedElements) {
...
}
And now our caller code gets simpler with less room for errors:
Bundle options = ActivityOptions.makeSceneTransitionAnimation(this,
Pair.create(textView, "elem1"),
Pair.create(imageView, "elem2"))
.toBundle();
Cleaner APIs using Kotlin
If that method had been written in Kotlin, those 2 limitations would have been avoided entirely.
Kotlin arrays are safer
Kotlin arrays are different from Java arrays: they behave just like standard classes and are invariant by default. This means that:
- The compiler guarantees compile-time type safety for arrays as well;
- Arrays of parameterized types are allowed;
- No compiler warning will be shown when calling a method with a variable number of parameterized arguments.
Furthermore, variable arguments arrays are declared as covariant automatically, which makes them read-only inside the varargs function.
fun someFunction(vararg words: String) {
if (words.isNotEmpty()) {
// Does NOT compile: read-only
words[0] = "Test"
}
}
Use-site variance is supported in Kotlin as well, but instead of complicated wildcards, type projections are used. This is done using the out
(read-only types) and in
(write-only types) keywords.
// Type-safe universal array copy function
fun <T> copy (from: Array<out T?>, to: Array<in T?>) {
assert(from.size == to.size)
for (i in from.indices)
to[i] = from[i]
}
Declaration-site variance is easier
But the real killer feature of Kotlin regarding generic types is the added ability to declare variance of generic types directly in a parameterized class or interface. This is called declaration-site variance.
Kotlin provides its own Pair
class which uses this feature.
data class Pair<out A, out B>(
val first: A,
val second: B
) : Serializable
Because both values passed in the constructor are read-only, their type can be declared as covariant using the out
keyword. Then the parameters of the Pair<A, B>
class will always be covariant, no matter where it’s used. Variance only has to be declared once inside the class, eliminating the risk of forgetting to declare it in every public method signature using that class.
// Creates a Pair<TextView, String>
val pair = Pair(textView, "elem1")
// OK: it's covariant by default
val elem1: Pair<View, String> = pair
Our method in a Kotlin world
Due to the language features described above, our API method could have been written like the following extension function:
fun Activity.makeSceneTransitionAnimation(
vararg sharedElements: Pair<View, String>): ActivityOptions {
...
}
There is no need to add annotations or specify variance for the Pair
parameters because the language does the right thing by default. We also get null safety for free.
Finally, calling this function from Kotlin code would be even more readable than the fixed Java version, partly thanks to the sweeter syntax to create Pair
objects:
val options = makeSceneTransitionAnimation(
textView to "elem1", imageView to "elem2").toBundle()
This concludes this small demonstration of the benefits of the Kotlin approach to generic types and how the language helps you write cleaner and safer code by default. I hope you learned a few things and as always, please share if you like.