5

I need a Collection in Kotlin to contain only elements implementing a given interface.

For example: a Map containing Collection of Animals:

interface Animal { val name: String }
data class Monkey(override val name: String): Animal
data class Snake(override val name: String): Animal

From reading the documentation and blogs and SO questions, I wrote this code that uses the Generics in keyword:

class Test {
    private val data = HashMap<String, ArrayList<in Animal>>()        
    init {
        data.put("Monkeys", arrayListOf(Monkey("Kong"), Monkey("Cheetah")))
        data.put("Snakes", arrayListOf(Snake("Monthy"), Snake("Kaa")))
    }        
}

Now I want to add a method in the Test class that reads the content of 'data', for example to print it to the console:

fun printAll() {
   data.forEach { collectionName: String, animals: ArrayList<in Animal> -> 
       println(collectionName)
       animals.forEach { animal: Animal ->
           println("\t$animal")
       }
    }
}

If I do that, I have a compilation error:

Error:(27, 21) Kotlin: Type inference failed: Cannot infer type parameter T in inline fun <T> Iterable<T>.forEach(action: (T) -> Unit): Unit
None of the following substitutions
receiver: Iterable<Any?>  arguments: ((Any?) -> Unit)
receiver: Iterable<Animal>  arguments: ((Animal) -> Unit)
can be applied to
receiver: kotlin.collections.ArrayList<in Animal> /* = java.util.ArrayList<in Animal> */  arguments: ((Animal) -> Unit)

My solution is to force my animal to a an ArrayList<out Animal>:

...
(animals as ArrayList<out Animal>).forEach { animal: Animal ->
    println("\t$animal")
}
...

But I'm not sure that this is the best way to write this kind of code. Is there a better way to tell Kotlin that I want to use sub types in generics for both producers and consumers ?

1 Answer 1

4

I suppose you don't need the in keyword in the type of data.

Using in here means that you want the type arguments of those ArrayLists to be at least as general as Animal, meaning that an ArrayList<in Animal> may actually be parameterized with a supertype of Animal as well: you could even put an ArrayList<Any> into the map, which makes it clear that it's not type-safe to expect the lists to hold only Animals.

Consider removing the in keyword and leaving just ArrayList<Animal> (or even List<Animal>, which is the interface for a read-only list):

private val data = HashMap<String, List<Animal>>()

init {
    data.put("Monkeys", listOf(Monkey("Kong"), Monkey("Cheetah")))
    data.put("Snakes", listOf(Snake("Monthy"), Snake("Kaa")))
}

fun printAll() {
    data.forEach { collectionName: String, animals: List<Animal> ->
        println(collectionName)
        animals.forEach { animal: Animal ->
            println("\t$animal")
        }
    }
}
Sign up to request clarification or add additional context in comments.

3 Comments

Indeed, switching to immutable Lists has an added benefit: it means that one can now treat listOf(Monkey("Kong"), Monkey("Cheetah")) as a List<Monkey> rather than List<Animal>.
this solution works well with the MutableList interface too, wich is convenient if I need a function that retrieve a list in order to modify it.
It is more common to see the in/out generic keywords in class signatures to define whether the class "consumes" or "produces" that type. You can read about these keywords here: kotlinlang.org/docs/reference/…

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.