craigmiller160
craigmiller160

Reputation: 6263

Kotlin Any type to Java Object type

I have an odd situation I've never run into before. At my company we have a mix of projects with most in Java but a growing number in Kotlin. So we have this existing application written in Java, and we are adding a library written in Kotlin.

The Kotlin code has a function argument that takes in a lambda expression. That lambda has this type, with the "params" argument being a map with values of "Any":

(params: Map<String,Any>) -> List<Item>

In the Java code, we implement this lambda by taking "params" and passing them to a Java function:

params -> receiverFunction(params)

However, the Java function accepts a map argument with values of "Object":

public List<Item> receiverFunction(final Map<String,Object> params)

I now get a compile error, where "params" in the Java lambda is seen as having type Map<String,capture of ?>, which cannot be passed to Map<String,Object>.

What is the recommended way to achieve this level of Java/Kotlin interoperability?

Upvotes: 3

Views: 2782

Answers (3)

Joffrey
Joffrey

Reputation: 37700

Short answer: the correct Java equivalent of Kotlin's Map<String, Any> is Map<String, ?>. So on java side, you should make your function take a wildcard ? instead of Object for the map's values:

public List<Item> receiverFunction(final Map<String, ?> params) {
    
}

This prevents you from inserting things in this Map, but it is only fair because Kotlin doesn't provide a MutableMap anyway, so you shouldn't be allowed to put stuff in it.

More background

Kotlin has declaration-site variance. This means you can declare constraints on your generics at the declaration site of the class or interface, and it's as if the constraint was applied everywhere the type is used.

Kotlin's Map uses this in its declaration: interface Map<K, out V>. Therefore, when receiving a Map<String, Any> from Kotlin, the actual type you're getting is Map<String, out Any>, which in Java is written Map<String, ? extends Object>, or in short Map<String, ?>.

Java's Map<String, Object> represents a mutable map in which you can insert values of type Object (basically any value). A Map<String, ?> on the other hand represents a Map for which you don't know the exact type of the values. The Java compiler won't let you insert anything in it, because it cannot know if it's allowed. For instance it's illegal to do params.put("key", 42) because the map could very well be a Map<String, String>, and you would be polluting the map with an improper type (this is actually the problem with using raw types like just Map without <>).

However, Java lets you get values out of this map, because it's safe to assign any value to a variable of type Object (that's where the Kotlin terminology out comes from). This makes it convenient to express a read-only map with values of unconstrained type.

Upvotes: 3

Tenfour04
Tenfour04

Reputation: 93591

The Map you defined on the Kotlin side was a read-only Map, so the value types are marked out. Kotlin's out Any is roughly equivalent to Java's ? extends Object. But since your receiverFunction defines the value type as the more strict Object, they are not a match.

Solution 1: Change Java value type to ?, which is equivalent to ? extends object.

Solution 2: Change the Kotlin parameter from Map to MutableMap.

If receiverFunction doesn't need to mutate the map (it probably doesn't), Solution 1 is much preferred. And the Java method should probably have been written that way in the first place for better encapsulation, even without having to keep Kotlin in mind.

Upvotes: 5

TheKvist
TheKvist

Reputation: 743

As your application is the one written in Java and the library seems to be a third-party thing, you could solve this by making the params in the receiver function generic. Since it accepts a Map<String, Object>, you can always replace the concrete Object with a generic type, as every type in Java implicitly extends Object, so there is no difference between the method accepting a Map<String, Object> or a Map<String, T> except that the T passed in can be of literally any type will still exposing methods inherited from Object. These are the mocks I've written to reproduce your issue and demonstrate the solution:

Test.kt

class Item

fun theFunction(params: (Map<String,Any>) -> List<Item>) {}

Main.java

import java.util.List;
import java.util.Map;

public class Main {

  public static <T> List<Item> receiverFunction(Map<String, T> params) {
    return List.of();
  }

  public static void main(String[] args) {
    TestKt.theFunction(params -> receiverFunction(params));
  }
}

Upvotes: 0

Related Questions