loading...

Pains of Java generics solved in Kotlin

rapasoft profile image Pavol Rajzak ・2 min read

The problem

Why does this not compile in Java?

Optional<Integer> optionalInteger = Optional.of(1);
Optional<Number> optionalNumber = optionalInteger;

int i = optionalNumber.get().intValue();

I am usually very conservative when using generics in Java. When I try to "be smart" I usually over-complicate things and end up with @Suppress("Unchecked") annotations all over the code. Programming language should always be both easy to write and read. So why is it not possible to do the cast in the example above?

I would expect the above to work because:

  • Number is parent to Integer class in Java
  • optionalNumber is a producing Number

The Java "solution"

It can be solved by using the extends/super keywords to specify the upper bound.

Optional<? super Integer> optionalInteger = Optional.of(1);
Optional<Number> optionalNumber = (Optional<Number>) optionalInteger;

int i = optionalNumber.get().intValue();

This will work, but we will get Unchecked cast warning on the second line, which does make sense in the world of Java generics, but it's still ugly. Also, what does it say to reader when <? super Integer> is used?

The Kotlin solution

Kotlin has solved this with declaration-site variance using keywords in/out. We will simulate the Optional class (since there isn't one in Kotlin) with get() function for this example:

class Optional<out T>(val t: T) {
    fun get(): T = t
}

val optionalInteger = Optional(1)
val optionalNumber: Optional<Number> = optionalInteger

val i = optionalNumber.get().toInt()

By annotating the T parameter with out keyword we will specify that T will always be used as output value (e.g. produced), thus it is safe to perform above cast (since the produced value is safe to cast). Also, if you try adding member function fun set(t: T) you will get a compiler warning, that T is out parameter but occurs in in position. It is easy to write, read and understand.

As I mentioned, Kotlin does not have separate Optional class. Instead it has some cool language features for null checking that you can find in any other modern language:

haveReadArticle?.giveItA❤️()

😎

Posted on by:

rapasoft profile

Pavol Rajzak

@rapasoft

Software developer. Mostly Java and JavaScript. Kotlin enthusiast.

Discussion

markdown guide
 

I think you're missing a point here. Uncle Bob described this problem, but he used List<Shape> and List<Circle> I believe. Where Shape is a parent class for a Circle, obviously. I don't remember where it was exactly, maybe in one of his Clean Coder videos. The point is, even though Shape and Circle are related, List<Shape> and List<Circle> are not. And one is not a substitute for another. It is even dangerous, since that way you could add some other shape to what you think is a List<Shape> but really is List<Circle>, which could break your program. Reduced to absurdity: you and your spouse are related, but your lawyers (in case of divorce, for example) are not. :)

With that in mind, I believe the correct Java solution would be:

Optional<Integer> optionalInteger = Optional.of(1);
Optional<Number> optionalNumber = Optional.of(optionalInteger.get());

Or even

Optional<Integer> optionalInteger = Optional.of(1);
Optional<Number> optionalNumber = Optional.ofNullable(optionalInteger.orElse(null));

And about Kotlin... I wonder how would Kotlin behave in the case with Lists above? :)

 

With all respect I think you've missed the point of the article. Of course it does not apply to Lists, since they both produce and consume objects. It could apply to immutable lists, where you cannot add/set items, only get.

My point was to show that classes with generic types that are used in producer role are not detected by Java compiler as safe to cast, when covariant types are used.

 

I see. Thanks for the clarification. I'm still not sure that this technique is really safe and won't be abused in some very unobvious ways, but that's completely different story. :)