Skip to main content

Kotlin Collections Interoperability

Introduction

When working with Kotlin in a project that also uses Java, or when integrating with Java libraries, understanding how Kotlin collections interoperate with Java collections becomes essential. Kotlin provides its own collection framework that is designed to work seamlessly with Java's collections, making the interoperability between these two languages smooth and efficient.

In this tutorial, you'll learn:

  • How Kotlin collections map to Java collections
  • Converting between Kotlin and Java collection types
  • Mutability differences and how to handle them
  • Common interoperability pitfalls and how to avoid them
  • Best practices for working with collections across both languages

Kotlin Collections Overview

Before diving into interoperability, let's quickly review Kotlin's collection types:

  1. Read-only collections (List, Set, Map): These interfaces don't have methods for modifying the collection
  2. Mutable collections (MutableList, MutableSet, MutableMap): These extend the read-only interfaces and add methods for modification
kotlin
// Read-only collections
val readOnlyList: List<String> = listOf("Kotlin", "Java", "Swift")
val readOnlySet: Set<String> = setOf("Kotlin", "Java", "Swift")
val readOnlyMap: Map<String, Int> = mapOf("Kotlin" to 2011, "Java" to 1995)

// Mutable collections
val mutableList: MutableList<String> = mutableListOf("Kotlin", "Java", "Swift")
val mutableSet: MutableSet<String> = mutableSetOf("Kotlin", "Java", "Swift")
val mutableMap: MutableMap<String, Int> = mutableMapOf("Kotlin" to 2011, "Java" to 1995)

How Kotlin Collections Map to Java

When you use a Kotlin collection in Java code, it's automatically mapped to the corresponding Java collection type:

Kotlin CollectionJava Collection
List<T>java.util.List<T>
MutableList<T>java.util.List<T>
Set<T>java.util.Set<T>
MutableSet<T>java.util.Set<T>
Map<K,V>java.util.Map<K,V>
MutableMap<K,V>java.util.Map<K,V>

This automatic mapping happens because Kotlin collections are actually implemented using Java collections under the hood.

The Mutability Challenge

The main interoperability challenge comes from handling mutability. In Java, all collections are mutable by default, while Kotlin distinguishes between read-only and mutable interfaces.

When a Kotlin read-only collection is passed to Java code, Java sees it as a mutable collection, even though you intended it to be immutable!

kotlin
// In Kotlin
fun printLanguages(languages: List<String>) {
// languages is expected to be read-only
println(languages.joinToString())
}
java
// In Java
public void addCSharp(List<String> languages) {
// From Java, we can modify the list!
languages.add("C#"); // This will work if the list is actually mutable
}

If the list passed to addCSharp() was created as a mutable list, the modification will succeed. If it was created as a read-only list (like from listOf()), it will throw an UnsupportedOperationException.

Handling Kotlin Collections in Java

When working with Kotlin collections in Java, keep these points in mind:

  1. Collections created with listOf(), setOf(), or mapOf() are immutable in Java too (they'll throw exceptions on modification attempts)
  2. Collections created with mutableListOf(), mutableSetOf(), or mutableMapOf() can be modified in Java
  3. Arrays in Kotlin are represented as Java arrays, so there's no conversion needed

Handling Java Collections in Kotlin

When you receive a Java collection in Kotlin code:

  1. It's seen as a mutable collection by default
  2. You can cast it to a read-only interface if needed
  3. Even if you cast it to a read-only interface, remember that it might still be modified by other Java code
kotlin
// Receiving a Java collection in Kotlin
fun processJavaList(javaList: java.util.List<String>) {
// In Kotlin, this is seen as a MutableList<String>

// If you want to ensure it's not modified by your Kotlin code:
val readOnlyList: List<String> = javaList

// But be aware that the underlying list could still be modified
// by other Java code that has a reference to it
}

Common Collection Conversion Examples

Converting Java Collections to Kotlin Collections

kotlin
import java.util.ArrayList
import java.util.HashMap

fun convertJavaCollectionsToKotlin() {
// Java ArrayList to Kotlin List
val javaList = ArrayList<String>()
javaList.add("Java")
javaList.add("Kotlin")

// Convert to read-only Kotlin List
val kotlinReadOnlyList: List<String> = javaList

// Convert to mutable Kotlin List
val kotlinMutableList: MutableList<String> = javaList

// Java HashMap to Kotlin Map
val javaMap = HashMap<String, Int>()
javaMap.put("Java", 1995)
javaMap.put("Kotlin", 2011)

// Convert to read-only Kotlin Map
val kotlinReadOnlyMap: Map<String, Int> = javaMap

// Convert to mutable Kotlin Map
val kotlinMutableMap: MutableMap<String, Int> = javaMap

println("Kotlin read-only list: $kotlinReadOnlyList")
println("Kotlin mutable list: $kotlinMutableList")
println("Kotlin read-only map: $kotlinReadOnlyMap")
println("Kotlin mutable map: $kotlinMutableMap")
}

Output:

Kotlin read-only list: [Java, Kotlin]
Kotlin mutable list: [Java, Kotlin]
Kotlin read-only map: {Java=1995, Kotlin=2011}
Kotlin mutable map: {Java=1995, Kotlin=2011}

Converting Kotlin Collections to Java Collections

kotlin
fun convertKotlinCollectionsToJava() {
// Kotlin collections
val kotlinList = listOf("Kotlin", "Java")
val kotlinMutableList = mutableListOf("Kotlin", "Java")
val kotlinMap = mapOf("Kotlin" to 2011, "Java" to 1995)

// These are already compatible with Java methods
addToJavaList(kotlinMutableList) // Works fine
// addToJavaList(kotlinList) // Would throw UnsupportedOperationException

readFromJavaMap(kotlinMap) // Works fine

println("After Java interaction: $kotlinMutableList")
}

// Simulating Java methods
fun addToJavaList(list: MutableList<String>) {
list.add("C#") // This modifies the list
}

fun readFromJavaMap(map: Map<String, Int>) {
println("Java was created in: ${map["Java"]}")
}

Output:

Java was created in: 1995
After Java interaction: [Kotlin, Java, C#]

Kotlin Specialized Collection Functions in Java

Kotlin adds many useful extension functions to collections. When calling from Java, they are accessible as static methods in special *Kt classes:

java
// In Java
import kotlin.collections.CollectionsKt;

public class JavaClass {
public void useKotlinCollectionFunctions() {
List<String> list = new ArrayList<>();
list.add("Java");
list.add("Kotlin");

// Using Kotlin's filter function from Java
List<String> filtered = CollectionsKt.filter(list, s -> s.contains("a"));
System.out.println(filtered); // [Java, Kotlin]

// Using Kotlin's map function
List<Integer> lengths = CollectionsKt.map(list, String::length);
System.out.println(lengths); // [4, 6]
}
}

Real-World Example: Building a Multi-Platform Library

Let's create a simple library that could be used from both Kotlin and Java, handling collections appropriately:

kotlin
/**
* A language repository that works with both Kotlin and Java clients.
*/
class LanguageRepository {
private val languages = mutableListOf("Kotlin", "Java", "Swift", "Python")

// Safe for Kotlin and Java - returns a read-only list
fun getAllLanguages(): List<String> = languages.toList()

// For Java clients that need to modify the list
fun getMutableLanguages(): MutableList<String> = languages

// Safe mutation method for both Kotlin and Java
fun addLanguage(language: String) {
if (!languages.contains(language)) {
languages.add(language)
}
}

// Method showing filtering with Kotlin features
fun getLanguagesStartingWith(prefix: String): List<String> {
return languages.filter { it.startsWith(prefix, ignoreCase = true) }
}
}

Using from Kotlin:

kotlin
fun demoFromKotlin() {
val repo = LanguageRepository()

// Get read-only list
val allLanguages = repo.getAllLanguages()
println("All languages: $allLanguages")

// Add a language
repo.addLanguage("JavaScript")
println("After adding JavaScript: ${repo.getAllLanguages()}")

// Filter languages
val jLanguages = repo.getLanguagesStartingWith("J")
println("Languages starting with J: $jLanguages")
}

Output:

All languages: [Kotlin, Java, Swift, Python]
After adding JavaScript: [Kotlin, Java, Swift, Python, JavaScript]
Languages starting with J: [Java, JavaScript]

Using from Java:

java
public void demoFromJava() {
LanguageRepository repo = new LanguageRepository();

// Get all languages
List<String> allLanguages = repo.getAllLanguages();
System.out.println("All languages: " + allLanguages);

// Try to modify the list returned by getAllLanguages
try {
allLanguages.add("Ruby");
System.out.println("Modified list: " + allLanguages);
} catch (UnsupportedOperationException e) {
System.out.println("Cannot modify the read-only list!");
}

// Use the safe mutation method
repo.addLanguage("Ruby");
System.out.println("After properly adding Ruby: " + repo.getAllLanguages());

// Get mutable list if you really need it
List<String> mutableLanguages = repo.getMutableLanguages();
mutableLanguages.add("C++");
System.out.println("After modifying mutable list: " + repo.getAllLanguages());
}

Output:

All languages: [Kotlin, Java, Swift, Python, JavaScript]
Cannot modify the read-only list!
After properly adding Ruby: [Kotlin, Java, Swift, Python, JavaScript, Ruby]
After modifying mutable list: [Kotlin, Java, Swift, Python, JavaScript, Ruby, C++]

Best Practices for Collection Interoperability

  1. Return defensive copies: When returning collections that shouldn't be modified, use .toList(), .toSet(), or .toMap() to create a snapshot

  2. Document mutability expectations: Clearly indicate in your API documentation whether collections can be modified

  3. Be careful with read-only collections in Java: Remember that Java doesn't understand Kotlin's read-only concept

  4. Provide safe mutation methods: Instead of giving direct access to collections, create methods to safely modify them

  5. Consider specialized collection libraries: For complex requirements, consider using immutable collection libraries like Guava or Vavr in Java

Summary

Kotlin collections interoperate smoothly with Java collections through automatic mapping of types. However, managing mutability expectations requires careful consideration due to Java's lack of read-only collection interfaces.

Key points to remember:

  • Kotlin distinguishes between read-only and mutable collections; Java doesn't
  • Kotlin collections are implemented using Java collections under the hood
  • When passing Kotlin collections to Java, be aware that Java can attempt to modify even read-only collections
  • Use defensive copying when returning collections that shouldn't be modified
  • Provide clear APIs that handle mutations safely for both Kotlin and Java consumers

By following these guidelines, you can create libraries and applications that work seamlessly across both Kotlin and Java codebases.

Further Exercises

  1. Create a function that safely converts a Java collection to an immutable Kotlin collection
  2. Build a wrapper class for a Java collection that enforces immutability
  3. Create a custom collection implementation that works well with both Kotlin and Java
  4. Experiment with collection performance differences between Kotlin and Java implementations
  5. Explore third-party immutable collection libraries and how they interoperate with Kotlin

Additional Resources



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