Skip to main content

Kotlin & Java Generics

Generics are one of the most powerful features in modern programming languages, allowing for type-safe collections and functions. When interoperating between Kotlin and Java, understanding how each language handles generics is crucial for writing clean, safe, and effective code.

Introduction to Generics

Generics allow you to write code that works with different types while maintaining type safety. Both Kotlin and Java support generics, but they have some important differences in their implementations.

kotlin
// A generic function in Kotlin
fun <T> printItem(item: T) {
println(item)
}

// Usage
printItem("Hello") // Works with String
printItem(42) // Works with Int

When working across language boundaries, these differences can sometimes lead to challenges. Let's explore how Kotlin and Java generics interact.

Basic Generics Interoperability

Calling Java from Kotlin

Kotlin can seamlessly work with Java generics. When you use a Java class with generics in Kotlin, the syntax feels natural.

kotlin
// Using Java's ArrayList in Kotlin
val javaList: java.util.ArrayList<String> = java.util.ArrayList()
javaList.add("Kotlin")
javaList.add("calling")
javaList.add("Java")

println(javaList.joinToString(" ")) // Output: Kotlin calling Java

Calling Kotlin from Java

Similarly, Java can work with Kotlin's generic classes and functions:

kotlin
// Kotlin file
class Box<T>(var value: T)

fun <T> createBox(value: T): Box<T> = Box(value)
java
// Java file
public class JavaUsage {
public static void main(String[] args) {
// Using Kotlin's Box class in Java
Box<String> stringBox = KotlinFileKt.createBox("Hello from Java");
System.out.println(stringBox.getValue()); // Output: Hello from Java
}
}

Type Erasure and Reified Types

Both Java and Kotlin use type erasure for generics, which means generic type information is not available at runtime. However, Kotlin offers a way around this with reified type parameters in inline functions.

kotlin
// Java-like approach with type check
fun <T> printType(value: T) {
// Can't do: if (value is T) - T is not available at runtime
println(value?.javaClass?.name)
}

// Kotlin's reified approach
inline fun <reified T> isType(value: Any?): Boolean {
return value is T
}

// Usage
fun main() {
val number = 42
println(isType<Int>(number)) // Output: true
println(isType<String>(number)) // Output: false
}

When calling Java generic methods from Kotlin, you cannot use reified type parameters because Java doesn't support this feature.

Variance: The Key Difference

One of the most significant differences between Kotlin and Java generics is how they handle variance.

Java: Use-site Variance

Java uses wildcard types for variance:

java
// Java
// Producer - use extends (read-only)
List<? extends Number> numbers = new ArrayList<Integer>();
Number n = numbers.get(0); // OK
// numbers.add(2); // Error: can't add to List<? extends Number>

// Consumer - use super (write-only)
List<? super Integer> intList = new ArrayList<Number>();
intList.add(10); // OK
// Integer i = intList.get(0); // Error: can't guarantee type

Kotlin: Declaration-site Variance

Kotlin introduces the concepts of out (covariance) and in (contravariance) right in the type parameter declaration:

kotlin
// Kotlin
// Producer - use "out" (covariant)
class Producer<out T>(private val value: T) {
fun get(): T = value
// Cannot have functions that take T as parameter
}

// Consumer - use "in" (contravariant)
class Consumer<in T> {
fun process(value: T) {
// Do something with value
}
// Cannot have functions that return T
}

Interoperability with Variance

When using Java collections in Kotlin:

kotlin
// Java's List<? extends Number> becomes List<out Number> in Kotlin
fun takeNumbers(numbers: List<out Number>) {
val first: Number = numbers.first() // OK
// numbers.add(2) // Error: can't add to read-only List
}

// Java's List<? super Integer> becomes List<in Int> in Kotlin
fun addNumbers(numbers: MutableList<in Int>) {
numbers.add(10) // OK
// val x: Int = numbers.first() // Error: can't guarantee type
}

When using Kotlin collections in Java:

kotlin
// Kotlin
class Fruit
class Apple : Fruit()
class Orange : Fruit()

// ReadOnlyBox is covariant (produces T, doesn't consume it)
class ReadOnlyBox<out T>(val item: T)

// Exposing to Java
fun createAppleBox(): ReadOnlyBox<Apple> = ReadOnlyBox(Apple())
fun processFruitBox(box: ReadOnlyBox<Fruit>) {
println("Processing: ${box.item}")
}
java
// Java
public class JavaClient {
public static void main(String[] args) {
// This works because ReadOnlyBox<Apple> is a subtype of ReadOnlyBox<Fruit>
ReadOnlyBox<Apple> appleBox = KotlinFileKt.createAppleBox();
KotlinFileKt.processFruitBox(appleBox); // Output: Processing: Apple@...
}
}

Star Projections

Kotlin uses star projections (*) as a safer alternative to Java's raw types:

kotlin
// Java raw type in Kotlin becomes a star projection
val javaRawList: java.util.ArrayList<*> = java.util.ArrayList<String>()

// You can only read from this list with Any? type
val item: Any? = javaRawList[0]

// But you cannot write to it (except null)
// javaRawList.add("test") // Error
javaRawList.add(null) // OK

Practical Examples

Generic Repository Pattern

This example shows a repository pattern implementation that works across Kotlin and Java:

kotlin
// Kotlin file: Repository.kt
interface Repository<T> {
fun save(item: T)
fun findById(id: String): T?
fun findAll(): List<T>
}

class InMemoryRepository<T> : Repository<T> {
private val storage = mutableMapOf<String, T>()

override fun save(item: T) {
if (item is Identifiable) {
storage[item.id] = item
}
}

override fun findById(id: String): T? = storage[id]

override fun findAll(): List<T> = storage.values.toList()
}

interface Identifiable {
val id: String
}

data class User(override val id: String, val name: String) : Identifiable
java
// Java file: JavaUsage.java
public class JavaUsage {
public static void main(String[] args) {
// Using Kotlin's generic repository from Java
Repository<User> userRepo = new InMemoryRepository<>();

User user1 = new User("1", "Alice");
User user2 = new User("2", "Bob");

userRepo.save(user1);
userRepo.save(user2);

// Get all users
List<User> users = userRepo.findAll();
for (User user : users) {
System.out.println(user.getName());
}

// Find by id
User found = userRepo.findById("1");
if (found != null) {
System.out.println("Found user: " + found.getName());
}
}
}

Output:

Alice
Bob
Found user: Alice

Generic Event System

This example demonstrates a generic event system that can be used from both Kotlin and Java:

kotlin
// Kotlin file: EventSystem.kt
interface Event

class MessageEvent(val message: String) : Event
class ErrorEvent(val error: String, val code: Int) : Event

interface EventListener<in E : Event> {
fun onEvent(event: E)
}

class EventBus {
private val listeners = mutableMapOf<Class<*>, MutableList<EventListener<*>>>()

fun <T : Event> subscribe(eventType: Class<T>, listener: EventListener<T>) {
listeners.getOrPut(eventType) { mutableListOf() }.add(listener)
}

@Suppress("UNCHECKED_CAST")
fun publish(event: Event) {
val eventClass = event.javaClass
listeners[eventClass]?.forEach { listener ->
(listener as EventListener<Event>).onEvent(event)
}
}
}
java
// Java file: JavaEventUsage.java
public class JavaEventUsage {
public static void main(String[] args) {
EventBus eventBus = new EventBus();

// Java subscribing to Kotlin events
eventBus.subscribe(MessageEvent.class, event -> {
System.out.println("Message received: " + event.getMessage());
});

eventBus.subscribe(ErrorEvent.class, event -> {
System.out.println("Error received: " + event.getError() +
" (code: " + event.getCode() + ")");
});

// Publish events
eventBus.publish(new MessageEvent("Hello from Java!"));
eventBus.publish(new ErrorEvent("Something went wrong", 500));
}
}

Output:

Message received: Hello from Java!
Error received: Something went wrong (code: 500)

Gotchas and Tips

  1. Array Generics: Arrays in Java are invariant and reified, which can cause issues when working with generics:
kotlin
// This is valid in Kotlin
fun printArray(array: Array<Any>) {
array.forEach { println(it) }
}

// But this will fail
val stringArray: Array<String> = arrayOf("a", "b", "c")
printArray(stringArray) // Type mismatch error
  1. Non-null Type Parameters: Kotlin's generics integrate with its null safety system:
kotlin
// T is nullable by default
class Box<T>(var value: T)

// T is non-nullable
class NonNullBox<T : Any>(var value: T)

// Usage
val nullableBox = Box<String?>(null) // OK
val nonNullBox = NonNullBox<String>("value") // OK
// val illegalBox = NonNullBox<String?>(null) // Compile error
  1. Type Projections Across Languages:
kotlin
// Kotlin
fun takesCovariantList(list: List<out Number>) { /*...*/ }

// When called from Java
// In Java, you'd use:
// takesCovariantList(List<? extends Number> list)

Summary

Working with generics across Kotlin and Java requires understanding a few key differences:

  1. Variance: Java uses use-site variance with wildcards (? extends, ? super), while Kotlin uses declaration-site variance (out, in).

  2. Type Parameters: Kotlin integrates its null safety system with generics, allowing for non-null type parameters with T : Any.

  3. Reified Types: Kotlin's inline functions with reified type parameters provide a way to access type information at runtime, which Java doesn't support.

  4. Star Projections: Kotlin's * provides a safer alternative to Java's raw types.

By understanding these differences, you can write code that interoperates effectively between Kotlin and Java, leveraging the strengths of both languages.

Exercises

  1. Create a generic Pair<A, B> class in Kotlin and use it from Java code.

  2. Write a generic Converter<From, To> interface in Kotlin with an convert(from: From): To method, and implement it in both Kotlin and Java.

  3. Implement a generic Result<T> class that can represent either a success with a value of type T or a failure with an error message.

  4. Create a Java collection that uses wildcards, and then access and manipulate it from Kotlin code.

Additional Resources



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)