Java 与 Kotlin 中的集合

Collections are groups of a variable number of items (possibly zero) that are significant to the problem being solved and are commonly operated on. 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 authentically Kotlin way.

The first part of this guide contains a quick glossary of operations on the same collections in Java and Kotlin. It is divided into operations that are the same and operations that exist only in Kotlin. The second part of the guide, starting from Mutability, explains some of the differences by looking at specific cases.

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

All of the examples below use Java and Kotlin standard library APIs only.

集合 - 图1

在 Java 与 Kotlin 中相同的操作

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

对 list、 set、 queue 与 deque 的操作

描述共有操作更多 Kotlin 替代方式
Add an element or elementsadd(), addAll()Use the plusAssign(+=) operator: collection += element, collection += anotherCollection.
Check whether a collection contains an element or elementscontains(), containsAll()Use the in keyword to call contains() in the operator form: element in collection.
Check whether a collection is emptyisEmpty()Use isNotEmpty() to check whether a collection is not empty.
Remove under a certain conditionremoveIf()
Leave only selected elementsretainAll()
Remove all elements from a collectionclear()
Get a stream from a collectionstream()Kotlin has its own way to process streams: sequences and methods like map() and filter().
Get an iterator from a collectioniterator()

对 map 的操作

描述共有操作更多 Kotlin 替代方式
Add an element or elementsput(), 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 elementsput(), replace(), replaceAll()Use the indexing operator map[key] = value instead of put() and replace().
Get an elementget()Use the indexing operator to get an element: map[index].
Check whether a map contains an element or elementscontainsKey(), containsValue()Use the in keyword to call contains() in the operator form: element in map.
Check whether a map is emptyisEmpty()Use isNotEmpty() to check whether a map is not empty.
Remove an elementremove(key), remove(key, value)Use the minusAssign(-=) operator: map -= key.
Remove all elements from a mapclear()
Get a stream from a mapstream() on entries, keys, or values

仅针对 list 的操作

描述共有操作更多 Kotlin 替代方式
Get an index of an elementindexOf()
Get the last index of an elementlastIndexOf()
Get an elementget()Use the indexing operator to get an element: list[index].
Take a sublistsubList()
Replace an element or elementsset(), replaceAll()Use the indexing operator instead of set(): list[index] = value.

略有不同的操作

对任意集合类型的操作

描述JavaKotlin
Get a collection’s sizesize()count(), size
Get flat access to nested collection elementscollectionOfCollections.forEach(flatCollection::addAll) or collectionOfCollections.stream().flatMap().collect()flatten() or flatMap()
Apply the given function to every elementstream().map().collect()map()
Apply the provided operation to collection elements sequentially and return the accumulated resultstream().reduce()reduce(), fold()
Group elements by a classifier and count themstream().collect(Collectors.groupingBy(classifier, counting()))eachCount()
Filter by a conditionstream().filter().collect()filter()
Check whether collection elements satisfy a conditionstream().noneMatch(), stream().anyMatch(), stream().allMatch()none(), any(), all()
Sort elementsstream().sorted().collect()sorted()
Take the first N elementsstream().limit(N).collect()take(N)
Take elements with a predicatestream().takeWhile().collect()takeWhile()
Skip the first N elementsstream().skip(N).collect()drop(N)
Skip elements with a predicatestream().dropWhile().collect()dropWhile()
Build maps from collection elements and certain values associated with themstream().collect(toMap(keyMapper, valueMapper))associate()

To perform all of the operations listed above on maps, you first need to get an entrySet of a map.

对 list 的操作

描述JavaKotlin
Sort a list into natural ordersort(null)sort()
Sort a list into descending ordersort(comparator)sortDescending()
Remove an element from a listremove(index), remove(element)removeAt(index), remove(element) or collection -= element
Fill all elements of a list with a certain valueCollections.fill()fill()
Get unique elements from a liststream().distinct().toList()distinct()

Java 标准库中不存在的操作

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

YouTube 视频:Advanced Collection Operations

可变性

In Java, there are mutable collections:

  1. // Java
  2. // This list is mutable!
  3. public List<Customer> getCustomers() { ... }

Partially mutable ones:

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

And immutable ones:

  1. // Java
  2. List<String> numbers = new LinkedList<>();
  3. // This list is immutable!
  4. List<String> immutableCollection = Collections.unmodifiableList(numbers);
  5. 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 tell whether a collection is mutable by looking at its type.

Unlike in 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:

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

Read more about immutability on the Kotlin coding conventions page.

协变性

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 ? extends Shape type so the function can take collections with any inheritors of Shape:

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

In Kotlin, 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. Mutable collections aren’t covariant – this would lead to runtime failures.

  1. // Kotlin
  2. open class Shape(val name: String)
  3. class Rectangle(private val rectangleName: String) : Shape(rectangleName)
  4. fun doSthWithShapes(shapes: List<Shape>) {
  5. println("The shapes are: ${shapes.joinToString { it.name }}")
  6. }
  7. fun main() {
  8. val rectangles = listOf(Rectangle("rhombus"), Rectangle("parallelepiped"))
  9. doSthWithShapes(rectangles)
  10. }

Read more about collection types here.

区间与数列

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

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

  1. // Java
  2. class Version implements Comparable<Version> {
  3. int major;
  4. int minor;
  5. Version(int major, int minor) {
  6. this.major = major;
  7. this.minor = minor;
  8. }
  9. @Override
  10. public int compareTo(Version o) {
  11. if (this.major != o.major) {
  12. return this.major - o.major;
  13. }
  14. return this.minor - o.minor;
  15. }
  16. }
  17. public void compareVersions() {
  18. var minVersion = new Version(1, 11);
  19. var maxVersion = new Version(1, 31);
  20. System.out.println(
  21. versionIsInRange(new Version(0, 9), minVersion, maxVersion));
  22. System.out.println(
  23. versionIsInRange(new Version(1, 20), minVersion, maxVersion));
  24. }
  25. public Boolean versionIsInRange(Version versionToCheck, Version minVersion,
  26. Version maxVersion) {
  27. return versionToCheck.compareTo(minVersion) >= 0
  28. && versionToCheck.compareTo(maxVersion) <= 0;
  29. }

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:

  1. // Kotlin
  2. class Version(val major: Int, val minor: Int): Comparable<Version> {
  3. override fun compareTo(other: Version): Int {
  4. if (this.major != other.major) {
  5. return this.major - other.major
  6. }
  7. return this.minor - other.minor
  8. }
  9. }
  10. fun main() {
  11. val versionRange = Version(1, 11)..Version(1, 30)
  12. println(Version(0, 9) in versionRange)
  13. println(Version(1, 20) in versionRange)
  14. }

As soon as you need to exclude one of the bounds, like to check whether a version is greater than or equal to (>=) the minimum version and less than (<) the maximum version, these inclusive ranges won’t help.

按照多个维度比较

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:

  1. class Person implements Comparable<Person> {
  2. String name;
  3. int age;
  4. public String getName() {
  5. return name;
  6. }
  7. public int getAge() {
  8. return age;
  9. }
  10. Person(String name, int age) {
  11. this.name = name;
  12. this.age = age;
  13. }
  14. @Override
  15. public String toString() {
  16. return this.name + " " + age;
  17. }
  18. }
  19. public void comparePersons() {
  20. var persons = List.of(new Person("Jack", 35), new Person("David", 30),
  21. new Person("Jack", 25));
  22. System.out.println(persons.stream().sorted(Comparator
  23. .comparing(Person::getName)
  24. .thenComparingInt(Person::getAge)).collect(toList()));
  25. }

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

  1. data class Person(
  2. val name: String,
  3. val age: Int
  4. )
  5. fun main() {
  6. val persons = listOf(Person("Jack", 35), Person("David", 30),
  7. Person("Jack", 25))
  8. println(persons.sortedWith(compareBy(Person::name, Person::age)))
  9. }

序列

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

  1. // Java
  2. int sum = IntStream.iterate(1, e -> e + 3)
  3. .limit(10).sum();
  4. 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.

  1. fun main() {
  2. //sampleStart
  3. // Kotlin
  4. val sum = generateSequence(1) {
  5. it + 3
  6. }.take(10).sum()
  7. println(sum) // Prints 145
  8. //sampleEnd
  9. }

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

从列表中删除元素

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

When removing an integer element, use the Integer.valueOf() function as the argument for the remove() function:

  1. // Java
  2. public void remove() {
  3. var numbers = new ArrayList<>();
  4. numbers.add(1);
  5. numbers.add(2);
  6. numbers.add(3);
  7. numbers.add(1);
  8. numbers.remove(1); // This removes by index
  9. System.out.println(numbers); // [1, 3, 1]
  10. numbers.remove(Integer.valueOf(1));
  11. System.out.println(numbers); // [3, 1]
  12. }

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

  1. fun main() {
  2. //sampleStart
  3. // Kotlin
  4. val numbers = mutableListOf(1, 2, 3, 1)
  5. numbers.removeAt(0)
  6. println(numbers) // [2, 3, 1]
  7. numbers.remove(1)
  8. println(numbers) // [2, 3]
  9. //sampleEnd
  10. }

遍历 map

In Java, you can traverse a map via forEach):

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

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

  1. // Kotlin
  2. for ((k, v) in numbers) {
  3. println("Key = $k, Value = $v")
  4. }
  5. // Or
  6. numbers.forEach { (k, v) -> println("Key = $k, Value = $v") }

获取可能会空的集合的首末元素

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

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

You can also use the getFirst()) and getLast()) functions for Deque and its inheritors:

  1. // Java
  2. var deque = new ArrayDeque<>();
  3. //...
  4. if (deque.size() > 0) {
  5. System.out.println(deque.getFirst());
  6. System.out.println(deque.getLast());
  7. }

In Kotlin, there are the 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():

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

由 list 创建 set

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

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

In Kotlin, use the function toSet():

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

元素分组

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

  1. // Java
  2. public void analyzeLogs() {
  3. var requests = List.of(
  4. new Request("https://kotlinlang.org/docs/home.html", 200),
  5. new Request("https://kotlinlang.org/docs/home.html", 400),
  6. new Request("https://kotlinlang.org/docs/comparison-to-java.html", 200)
  7. );
  8. var urlsAndRequests = requests.stream().collect(
  9. Collectors.groupingBy(Request::getUrl));
  10. System.out.println(urlsAndRequests);
  11. }

In Kotlin, use the function groupBy():

  1. class Request(
  2. val url: String,
  3. val responseCode: Int
  4. )
  5. fun main() {
  6. //sampleStart
  7. // Kotlin
  8. val requests = listOf(
  9. Request("https://kotlinlang.org/docs/home.html", 200),
  10. Request("https://kotlinlang.org/docs/home.html", 400),
  11. Request("https://kotlinlang.org/docs/comparison-to-java.html", 200)
  12. )
  13. println(requests.groupBy(Request::url))
  14. //sampleEnd
  15. }

过滤元素

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, like collect(). For example, to leave only those pairs whose keys end with 1 and whose values are greater than 10:

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

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

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

Learn more about filtering maps here.

按类型过滤元素

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

  1. // Java
  2. public void objectIsInstance() {
  3. var numbers = new ArrayList<>();
  4. numbers.add(null);
  5. numbers.add(1);
  6. numbers.add("two");
  7. numbers.add(3.0);
  8. numbers.add("four");
  9. System.out.println("All String elements in upper case:");
  10. numbers.stream().filter(it -> it instanceof String)
  11. .forEach( it -> System.out.println(((String) it).toUpperCase()));
  12. }

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

  1. // Kotlin
  2. fun main() {
  3. //sampleStart
  4. // Kotlin
  5. val numbers = listOf(null, 1, "two", 3.0, "four")
  6. println("All String elements in upper case:")
  7. numbers.filterIsInstance<String>().forEach {
  8. println(it.uppercase())
  9. }
  10. //sampleEnd
  11. }

检验谓词

Some tasks require you to check whether all, none, or any elements satisfy a condition. In Java, you can do all of these checks via the Stream API functions allMatch()), noneMatch()), and anyMatch()):

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

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

  1. fun main() {
  2. //sampleStart
  3. // Kotlin
  4. val numbers = listOf("one", "two", "three", "four")
  5. println(numbers.none { it.endsWith("e") })
  6. println(numbers.any { it.endsWith("e") })
  7. println(numbers.all { it.endsWith("e") })
  8. //sampleEnd
  9. }

Learn more about test predicates.

集合转换操作

元素合拢

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

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

If you want to do something more complex than just printing pairs 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 thing:

  1. fun main() {
  2. //sampleStart
  3. // Kotlin
  4. val colors = listOf("red", "brown")
  5. val animals = listOf("fox", "bear", "wolf")
  6. println(colors.zip(animals) { color, animal ->
  7. "The ${animal.replaceFirstChar { it.uppercase() }} is $color" })
  8. //sampleEnd
  9. }

zip() returns the List of Pair objects.

If collections have different sizes, the result of zip() is the smaller size. The last elements of the larger collection are not included in the result.

集合 - 图2

元素关联

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

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

In Kotlin, use the associate() function:

  1. fun main() {
  2. //sampleStart
  3. // Kotlin
  4. val numbers = listOf("one", "two", "three", "four")
  5. println(numbers.associateWith { it.length })
  6. //sampleEnd
  7. }

下一步做什么?

If you have a favorite idiom, we invite you to share it by sending a pull request.