[kotlin-java interop] I don't see a suitable chann...
# getting-started
d
[kotlin-java interop] I don't see a suitable channel for my topic, so I am posting the question here. Is there any way how to achieve covariance in Java when implementing a Kotlin interface? Example: Following Kotlin interfaces compile fine:
Copy code
interface Dog : Animal

interface AnimalHouse {
    val animals: List<Animal>
}

interface DogHouse : AnimalHouse {
    override val animals: List<Dog>
}
But trying to implement the
DogHouse
interface in Java
Copy code
class DogHouseImpl implements DogHouse {
    @Override
    public List<Dog> getAnimals() {
        return animals;
    }
}
leads to an error
DogHouseImpl is not abstract and does not override abstract method getAnimals() in AnimalHouse
. That's because contrary to Kotlin
List
is invariant in Java. How does actually Kotlin achieve that on the bytecode level? Isn't there a way to use a similar technique also in Java? In my case, the Java code is generated so I can use any dirty tricks there 🙂
r
If you can change
AnimalHouse
you can fix it:
Copy code
interface AnimalHouse<A : Animal> {
    val animals: List<A>
}
d
Yes, that seems to work (although I was not yet able to make the example fully work since
AnimalHouse
is used at lot of places and those are used recursively and so on... which requires quite a lot of refactoring). Anyway, why it actually works? 🙂
r
I don't really know - I can only make informed guesses.
d
That sounds like some kind of a catchphrase 🤣 I meant whether you details of the general mechanism, not of my particular setup of course.
r
Both
java.util.List<T>
and
kotlin.collections.List<T>
get compiled to the erased type
java.util.List
. So at runtime the JVM is happy that
DogHouse.animals
has the same return type as
AnimalHouse.animals
- they both just return a type erased
List
.
kotlinc
is its own compiler - it knows as you say that the type parameter of
kotlin.collections.List
is covariant, so it's fine for
DogHouse.animals
to have a covariant return type when it overrides
AnimalHouse.animals
. But it emits the generic type in the class's metadata, so when you use
javac
to try and compile a class that implements both
DogHouse
and
AnimalHouse
it sees the generic type as invariant on
java.util.List
. So you effectively get the same compile error you would have got if you'd tried to write
AnimalHouse
and
DogHouse
in Java. That's my guess, at any rate.
d
But what changes in your proposal with the generic parameter? There's still a generic parameter
A
in the class metadata, isn't it?
r
Using the generic type
A : Animal
on
AnimalHouse
allows you to declare a Java instance with the covariant type
? extends Animal
as so:
AnimalHouse<? extends Animal> animalHouse = new DogHouseImpl()
- at that point
javac
will correctly prevent you calling
animalHouse.animals.add(new CatImpl())
. It's just the same in Java, see the snippet.
There's a mismatch between what
kotlinc
knows about
kotlin.collections.List
- that it's covariant because it's unmodifiable without downcasting - and what
kotlinc
emits - a modifiable, and hence invariant,
java.util.List
.
d
Well, the problem is not in the
DogHouseImpl
usage but in the class itself. With your setup above
DogHouseImpl
compiles but I'm more interested in why it doesn't compile with my original code. Oh.. and thanks for your answers! They are very useful so far 🙂
r
I thought I'd offered an explanation... basically your version of
DogHouse
would not compile using
javac
, so
DogHouseImpl
can't compile in
javac
. It's seeing
Animals.getAnimals()
as having return type
java.util.List<Animal>
and
javac
will not allow you to override a method that returns
java.util.List<Animal>
and narrows the type to
java.util.List<Dog>
because then you could upcast it and add a
Cat
.
d
Ah, now it's clear. Thanks again!
i
You can declare the AnimalHouse interface in Kotlin as
Copy code
interface AnimalHouse {
    val animals: List<@JvmWildcard Animal>
}
This way,
getAnimals
method will get the signature
Copy code
java.util.List<? extends Animal> getAnimals()
instead of just
Copy code
java.util.List<Animal> getAnimals()
and it will be possible to override its return type covariantly in Java with
List<Dog>
👌 2
👍 1
You can read more about this aspect of Kotlin-Java interop here https://kotlinlang.org/docs/java-to-kotlin-interop.html#variant-generics
🙏 1
r
When it's a return value, wildcards are not generated, because otherwise Java clients will have to deal with them (and it's against the common Java coding style)
Hmm... not immediately convinced by that argument
i
AFAIK, the motivation was that it would otherwise be inconvenient for Java users to consume return types if they are littered with wildcard types, e.g. declare their variables as
List<? extends Animal>
when calling `getAnimals`: https://youtrack.jetbrains.com/issue/KT-10254/Inconsistent-translation-of-covariant-types-to-Java#focus=Comments-27-1240427.0-0 Probably, in Java >= 10 where it became possible to use
var
to avoid spelling types, it is much less of concern, but the train has gone now.
d
Btw I'm trying to see this "with my own eyes" so I'm decompiling the Kotlin interfaces to Java and the result is always a raw type
Copy code
@Nullable
   List getApplicants();
Is this just a limitation of the IntelliJ decompiler?
r
I think I'd prefer to declare my variables as
List<? extends Animal>
and have the type system tell me I couldn't add to the List, particularly if it avoided inheritance pain like this. Too late now though as you say.