Kotlin Help

Migrating from Java to Kotlin: Collections and iterating through them

Collections are groups of a variable number of items (possibly zero) that share significance to the problem being solved and are commonly operated upon. This guide explains and compares collection concepts and operations in Java and Kotlin. It will help you migrate from Java to Kotlin and write your code in the authentic Kotlin way.

The first part contains a quick reference to operations on the same collections in Java and Kotlin. On the whole, there are operations that are the same and operations that exist only in Kotlin. The second part starting from Mutability covers several cases that explain the difference more deeply.

For the introduction to collections, see the Collections overview or watch a video by Sebastian Aigner, Kotlin Developer Advocate.

Operations that are the same in Java and Kotlin

In Kotlin, there are many operations on collections that look absolutely the same as in Java.

Operations on lists, sets, queues, and deques

Description

Common operations

More Kotlin alternatives

Add an element or elements

add(), addAll()

Use the plusAssign (+=) operator: collection += element, collection += anotherCollection.

Check if a collection contains an element or elements

contains(), containsAll()

Use the in keyword to call contains() in the operator form: element in collection.

Check if a collection is empty

isEmpty()

Use isNotEmpty() to check if a collection is not empty.

Remove by a condition

removeIf()

Leave only selected elements

retainAll()

Remove all elements from a collection

clear()

Get a stream from a collection

stream()

Kotlin has its own way for stream processing – sequences, and methods like map(), filter().

Get an iterator from a collection

iterator()

Operations on maps

Description

Common operations

More Kotlin alternatives

Add an element or elements

put(), putAll(), putIfAbsent()

In Kotlin, the assignment map[key] = value behaves the same as put(key, value). Also, you may use the plusAssign (+=) operator: map += Pair(key, value) or map += anotherMap.

Replace an element or elements

put(), replace(), replaceAll()

Use the indexing operator map[key] = value instead of put() and replace().

Get an element

get()

Use the indexing operator to get an element: map[index].

Check if a map contains an element or elements

containsKey(), containsValue()

Use the in keyword to call contains() in the operator form: element in map.

Check if a map is empty

isEmpty()

Use isNotEmpty() to check if a map is not empty.

Remove an element

remove(key), remove(key, value)

Use the minusAssign (-=) operator: map -= key.

Remove all elements from a map

clear()

Get a stream from a map

stream() on entries, keys or values

Operations that exist only for lists

Description

Common operations

More Kotlin alternatives

Get an index of some element

indexOf()

Get the last index of some element

lastIndexOf()

Get an element

get()

Use the indexing operator to get an element: list[index].

Take a sublist

subList()

Replace an element or elements

set(), replaceAll()

Use the indexing operator instead of set(): list[index] = value.

Operations differ a bit

Operations on any collection type

Description

Java

Kotlin

Get a collection's size

size()

count(), size

Get flat access to nested collection elements

collectionOfCollections.forEach(flatCollection::addAll) or collectionOfCollections.stream().flatMap().collect()

flatten() or flatMap()

Apply the given function to every element

stream().map().collect()

map()

Apply the provided operation to the collection elements sequentially and return the accumulated result

stream().reduce()

reduce(), fold()

Group elements by a classifier and count them

stream().collect(Collectors.groupingBy(classifier, counting()))

eachCount()

Filter by a condition

stream().filter().collect()

filter()

Check whether collection elements satisfy a condition

stream().noneMatch(), stream().anyMatch(), stream().allMatch()

none(), any(), all()

Sort elements

stream().sorted().collect()

sorted()

Take first N elements

stream().limit(N).collect()

take(N)

Take elements with a predicate

stream().takeWhile().collect()

takeWhile()

Skip first N elements

stream().skip(N).collect()

drop(N)

Skip elements with a predicate

stream().dropWhile().collect()

dropWhile()

Build maps from the collection elements and certain values associated with them

stream().collect(toMap(keyMapper, valueMapper))

associate()

To perform all the operations listed above on maps, get an entrySet of a map firstly.

Operations on lists

Description

Java

Kotlin

Sort a list with a natural order

sort(null)

sort()

Sort a list with a descending order

sort(comparator)

sortDescending()

Remove an element from a list

remove(index), remove(element)

removeAt(index), remove(element) or collection -= element

Fill all elements of a list with some value

Collections.fill()

fill()

Get unique elements of the list

stream().distinct().toList()

distinct()

Operations that don't exist in Java's standard library

If you want to dive deep into zip(), chunked(), windowed(), and some other operations, watch a video by Sebastian Aigner about advanced collection operations in Kotlin:

Mutability

In Java, there are mutable collections:

// Java // This list is mutable! public List<Customer> getCustomers() { … }

Partially mutable:

// Java List<String> numbers = Arrays.asList("one", "two", "three", "four"); numbers.add("five"); // Fails in runtime with `UnsupportedOperationException`

And immutable:

// Java List<String> numbers = new LinkedList<>(); // This list is immutable! List<String> immutableCollection = Collections.unmodifiableList(numbers); immutableCollection.add("five"); // Fails in runtime with `UnsupportedOperationException`

If you write the last two pieces of code in IntelliJ IDEA, the IDE will warn you that you're trying to modify an immutable object. This code will compile and fail in runtime with UnsupportedOperationException. You can't say if a collection is mutable looking at its type.

Apart from Java, in Kotlin, you explicitly declare mutable or read-only collections depending on your needs. If you try to modify a read-only collection, the code won't compile:

// Kotlin val numbers = mutableListOf("one", "two", "three", "four") numbers.add("five") // This is OK val immutableNumbers = listOf("one", "two") //immutableNumbers.add("five") // Compilation error - Unresolved reference: add

Read more about immutability in the Kotlin coding conventions.

Covariance

In Java, you can't pass a collection with a descendant type to a function that takes a collection of the ancestor type. For example, if Rectangle extends Shape, you can't pass a collection of Rectangle elements to a function that takes a collection of Shape elements. To make the code compilable, use the type ? extends Shape, so the function can take collections with any inheritors of Shape:

// Java class Shape {} class Rectangle extends Shape {} public void doSthWithShapes(List<? extends Shape> shapes) { /* If using just List<Shape>, the code won't compile when calling this function with the List<Rectangle> as the argument as below */ } public void main() { var rectangles = List.of(new Rectangle(), new Rectangle()); doSthWithShapes(rectangles); }

In Kotlin, the read-only collection types are covariant. This means that, if a Rectangle class inherits from the Shape class, you can use the type List<Rectangle> anywhere the List<Shape> type is required. In other words, the collection types have the same subtyping relationship as the element types. Maps are covariant on the value type, but not on the key type. In turn, mutable collections aren't covariant; otherwise, this would lead to runtime failures.

// Kotlin open class Shape(val name: String) class Rectangle(private val rectangleName: String) : Shape(rectangleName) fun doSthWithShapes(shapes: List<Shape>) { println("The shapes are: ${shapes.joinToString { it.name }}") } fun main() { val rectangles = listOf(Rectangle("rhombus"), Rectangle("parallelepiped")) doSthWithShapes(rectangles) }

Read more details about collection types.

Ranges and progressions

In Kotlin, you can create intervals using ranges. For example, Version(1, 11)..Version(1, 30) stays for all the versions from 1.11 to 1.30. You can check that your version is in the range using the in operator: Version(0, 9) in versionRange.

In Java, you need to manually check if a Version fits both bounds:

// Java class Version implements Comparable<Version> { int major; int minor; Version(int major, int minor) { this.major = major; this.minor = minor; } @Override public int compareTo(Version o) { if (this.major != o.major) { return this.major - o.major; } return this.minor - o.minor; } } public void compareVersions() { var minVersion = new Version(1, 11); var maxVersion = new Version(1, 31); System.out.println( versionIsInRange(new Version(0, 9), minVersion, maxVersion)); System.out.println( versionIsInRange(new Version(1, 20), minVersion, maxVersion)); } public Boolean versionIsInRange(Version versionToCheck, Version minVersion, Version maxVersion) { return versionToCheck.compareTo(minVersion) >= 0 && versionToCheck.compareTo(maxVersion) <= 0; }

In Kotlin, you operate with a range as a whole object. You don't need to create two variables and compare a Version with them:

// Kotlin class Version(val major: Int, val minor: Int): Comparable<Version> { override fun compareTo(other: Version): Int { if (this.major != other.major) { return this.major - other.major } return this.minor - other.minor } } fun main() { val versionRange = Version(1, 11)..Version(1, 30) println(Version(0, 9) in versionRange) println(Version(1, 20) in versionRange) }

As soon as you need to exclude one of the bounds, for example, to check if a version is greater or equal (>=) than the minimal version and less (<) than the maximal version, such inclusive ranges won't help.

Comparison by several criteria

In Java, to compare objects by several criteria, you may use the comparing() and thenComparingX() functions from the Comparator interface. For example, to compare people by their name and age:

class Person implements Comparable<Person> { String name; int age; public String getName() { return name; } public int getAge() { return age; } Person(String name, int age) { this.name = name; this.age = age; } @Override public String toString() { return this.name + " " + age; } } public void comparePersons() { var persons = List.of(new Person("Jack", 35), new Person("David", 30), new Person("Jack", 25)); System.out.println(persons.stream().sorted(Comparator .comparing(Person::getName) .thenComparingInt(Person::getAge)).collect(toList())); }

In Kotlin, you just enumerate by which fields you want to compare:

data class Person( val name: String, val age: Int ) fun main() { val persons = listOf(Person("Jack", 35), Person("David", 30), Person("Jack", 25)) println(persons.sortedWith(compareBy(Person::name, Person::age))) }

Sequences

In Java, you can generate a sequence of numbers this way:

// Java int sum = IntStream.iterate(1, e -> e + 3) .limit(10).sum(); System.out.println(sum); // Prints 145

In Kotlin, use sequences. Multi-step processing of sequences is executed lazily when possible: actual computing happens only when the result of the whole processing chain is requested.

fun main() { //sampleStart // Kotlin val sum = generateSequence(1) { it + 3 }.take(10).sum() println(sum) // Prints 145 //sampleEnd }

Sequences may reduce the number of steps that are needed to perform some filtering operation. See the sequence processing example that shows the difference between Iterable and Sequence.

Removal of elements from a list

In Java, the remove() function accepts an index of an element to remove.

In case of removal an integer element, use the Integer.valueOf() function as the argument for the remove() function:

// Java public void remove() { var numbers = new ArrayList<>(); numbers.add(1); numbers.add(2); numbers.add(3); numbers.add(1); numbers.remove(1); // This removes by index System.out.println(numbers); // [1, 3, 1] numbers.remove(Integer.valueOf(1)); System.out.println(numbers); // [3, 1] }

In Kotlin, there are two types of element removal: by index with removeAt() and by value with remove().

fun main() { //sampleStart // Kotlin val numbers = mutableListOf(1, 2, 3, 1) numbers.removeAt(0) println(numbers) // [2, 3, 1] numbers.remove(1) println(numbers) // [2, 3] //sampleEnd }

Traverse a map

In Java, you can traverse a map via forEach:

// Java numbers.forEach((k,v) -> System.out.println("Key = " + k + ", Value = " + v));

In Kotlin, use a for loop or similar to Java forEach to traverse a map:

// Kotlin for ((k, v) in numbers) { println("Key = $k, Value = $v") } // Or numbers.forEach { (k, v) -> println("Key = $k, Value = $v") }

Get the first and the last items of a possibly empty collection

In Java, you can safely get the first and the last items by checking the size of the collection and using indices:

// Java var list = new ArrayList<>(); //... if (list.size() > 0) { System.out.println(list.get(0)); System.out.println(list.get(list.size() - 1)); }

And with the getFirst() and getLast() functions for Deque and its inheritors:

// Java var deque = new ArrayDeque<>(); //... if (deque.size() > 0) { System.out.println(deque.getFirst()); System.out.println(deque.getLast()); }

In Kotlin, there are special functions firstOrNull() and lastOrNull(). Using the Elvis operator, you can perform further actions right away depending on the result of a function. For example, firstOrNull():

// Kotlin val emails = listOf<String>() // Might be empty val theOldestEmail = emails.firstOrNull() ?: "" val theFreshestEmail = emails.lastOrNull() ?: ""

Create a set from a list

In Java, to create a Set from a List, you can use the Set.copyOf function:

// Java public void listToSet() { var sourceList = List.of(1, 2, 3, 1); var copySet = Set.copyOf(sourceList); System.out.println(copySet); }

In Kotlin, use the function toSet():

fun main() { //sampleStart // Kotlin val sourceList = listOf(1, 2, 3, 1) val copySet = sourceList.toSet() println(copySet) //sampleEnd }

Group elements

In Java, you may group elements with the Collectors function groupingBy():

// Java public void analyzeLogs() { var requests = List.of( new Request("https://kotlinlang.org/docs/home.html", 200), new Request("https://kotlinlang.org/docs/home.html", 400), new Request("https://kotlinlang.org/docs/comparison-to-java.html", 200) ); var urlsAndRequests = requests.stream().collect( Collectors.groupingBy(Request::getUrl)); System.out.println(urlsAndRequests); }

In Kotlin, use the function groupBy():

class Request( val url: String, val responseCode: Int ) fun main() { //sampleStart // Kotlin val requests = listOf( Request("https://kotlinlang.org/docs/home.html", 200), Request("https://kotlinlang.org/docs/home.html", 400), Request("https://kotlinlang.org/docs/comparison-to-java.html", 200) ) println(requests.groupBy(Request::url)) //sampleEnd }

Filter elements

In Java, to filter elements from a collection, you need to use the Stream API. The Stream API has intermediate and terminal operations. filter() is an intermediate operation, which returns a stream. To receive a collection as the output, you need to use a terminal operation, for example, collect(). For example, to leave only those pairs which keys end with 1 and values are greater than 10:

// Java public void filterEndsWith() { var numbers = Map.of("key1", 1, "key2", 2, "key3", 3, "key11", 11); var filteredNumbers = numbers.entrySet().stream() .filter(entry -> entry.getKey().endsWith("1") && entry.getValue() > 10) .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); System.out.println(filteredNumbers); }

In Kotlin, filtering is built-in into collections, and filter() returns the same collection type that was filtered. So, all you need to write is the filter() and its predicate:

fun main() { //sampleStart // Kotlin val numbers = mapOf("key1" to 1, "key2" to 2, "key3" to 3, "key11" to 11) val filteredNumbers = numbers.filter { (key, value) -> key.endsWith("1") && value > 10 } println(filteredNumbers) //sampleEnd }

Learn more about filtering maps.

Filter elements by type

In Java, to filter elements by their type and do some actions on them, you need to check its type with the instanceof operator and then do the type cast:

// Java public void objectIsInstance() { var numbers = new ArrayList<>(); numbers.add(null); numbers.add(1); numbers.add("two"); numbers.add(3.0); numbers.add("four"); System.out.println("All String elements in upper case:"); numbers.stream().filter(it -> it instanceof String) .forEach( it -> System.out.println(((String) it).toUpperCase())); }

In Kotlin, you just call filterIsInstance<NEEDED_TYPE>() on your collection, and type cast is done by Smart casts:

// Kotlin fun main() { //sampleStart // Kotlin val numbers = listOf(null, 1, "two", 3.0, "four") println("All String elements in upper case:") numbers.filterIsInstance<String>().forEach { println(it.uppercase()) } //sampleEnd }

Test predicates

There are some tasks like checking if all, none, or any elements satisfy a condition. In Java, you can make all these checks via the Stream API functions allMatch(), noneMatch(), anyMatch():

// Java public void testPredicates() { var numbers = List.of("one", "two", "three", "four"); System.out.println(numbers.stream().noneMatch(it -> it.endsWith("e"))); // false System.out.println(numbers.stream().anyMatch(it -> it.endsWith("e"))); // true System.out.println(numbers.stream().allMatch(it -> it.endsWith("e"))); // false }

In Kotlin, there are extension functions none(), any(), and all() for every Iterable object:

fun main() { //sampleStart // Kotlin val numbers = listOf("one", "two", "three", "four") println(numbers.none { it.endsWith("e") }) println(numbers.any { it.endsWith("e") }) println(numbers.all { it.endsWith("e") }) //sampleEnd }

Learn more about test predicates.

Collection transformation operations

Zip elements

In Java, you can make pairs from elements with the same positions in two collections iterating simultaneously over them:

// Java public void zip() { var colors = List.of("red", "brown"); var animals = List.of("fox", "bear", "wolf"); for (int i = 0; i < Math.min(colors.size(), animals.size()); i++) { String animal = animals.get(i); System.out.println("The " + animal.substring(0, 1).toUpperCase() + animal.substring(1) + " is " + colors.get(i)); } }

If you want to do something more complex than just printing couples of elements into the output, you can use Records. In the example above, the record would be record AnimalDescription(String animal, String color) {}.

In Kotlin, use the zip() function to do the same:

fun main() { //sampleStart // Kotlin val colors = listOf("red", "brown") val animals = listOf("fox", "bear", "wolf") println(colors.zip(animals) { color, animal -> "The ${animal.replaceFirstChar { it.uppercase() }} is $color" }) //sampleEnd }

zip() returns the List of Pair objects.

Associate elements

In Java, to associate elements with some characteristics, you may use the Stream API:

// Java public void associate() { var numbers = List.of("one", "two", "three", "four"); var wordAndLength = numbers.stream() .collect(toMap(number -> number, String::length)); System.out.println(wordAndLength); }

In Kotlin, use the associate() function:

fun main() { //sampleStart // Kotlin val numbers = listOf("one", "two", "three", "four") println(numbers.associateWith { it.length }) //sampleEnd }

What's next?

If you have a favorite idiom, contribute it by sending a pull request.

Last modified: 12 November 2021