7.1.5.2 Java 与 Kotlin 中的集合

原文链接: https://kotlinlang.org/docs/java-to-kotlin-collections-guide.html

7.1.5.2 Java 与 Kotlin 中的集合

了解如何从 Java 集合迁移到 Kotlin 集合。本指南涵盖 Kotlin 与 Java 的 List、ArrayList、Map、Set 等数据结构。

_集合_是数量可变(可能为零)的一组条目,它们对所解决的问题有意义,并且通常会被一起操作。本指南解释并比较 Java 与 Kotlin 中的集合概念和操作。它将帮助你从 Java 迁移到 Kotlin,并以地道的 Kotlin 风格编写代码。

本指南的第一部分包含 Java 和 Kotlin 中相同集合操作的快速对照表,分为相同的操作和仅 Kotlin 才有的操作。指南的第二部分从可变性开始,通过具体案例解释一些差异。

关于集合的入门介绍,请参阅集合概览,或观看 Kotlin 开发者布道师 Sebastian Aigner 的这个视频。

注意: 下面所有示例都只使用 Java 和 Kotlin 标准库 API。

Java 与 Kotlin 中相同的操作

在 Kotlin 中,许多集合操作看起来与 Java 中的对应操作完全相同。

对列表、集合、队列和双端队列的操作

| 说明 | 常见操作 | 更多 Kotlin 替代方案 |

| 添加一个或多个元素 | add()、addAll() | 使用 plusAssign(+=)运算符:collection += element、collection += anotherCollection。 | | 检查集合是否包含一个或多个元素 | contains()、containsAll() | 使用 in 关键字以运算符形式调用 contains():element in collection。 | | 检查集合是否为空 | isEmpty() | 使用 isNotEmpty() 检查集合是否不为空。 | | 按某个条件移除 | removeIf() | | | 只保留选中的元素 | retainAll() | | | 移除集合中的所有元素 | clear() | | | 从集合获取流 | stream() | Kotlin 有自己的流处理方式:序列以及 map() 和 filter() 等方法。 | | 从集合获取迭代器 | iterator() | |

对映射的操作

| 说明 | 常见操作 | 更多 Kotlin 替代方案 |

| 添加一个或多个元素 | put()、putAll()、putIfAbsent() | 在 Kotlin 中,赋值 map[key] = value 与 put(key, value) 行为相同。你也可以使用 plusAssign(+=)运算符:map += Pair(key, value) 或 map += anotherMap。 | | 替换一个或多个元素 | put()、replace()、replaceAll() | 使用索引运算符 map[key] = value 代替 put() 和 replace()。 | | 获取元素 | get() | 使用索引运算符获取元素:map[index]。 | | 检查映射是否包含一个或多个元素 | containsKey()、containsValue() | 使用 in 关键字以运算符形式调用 contains():element in map。 | | 检查映射是否为空 | isEmpty() | 使用 isNotEmpty() 检查映射是否不为空。 | | 移除元素 | remove(key)、remove(key, value) | 使用 minusAssign(-=)运算符:map -= key。 | | 移除映射中的所有元素 | clear() | | | 从映射获取流 | 对 entries、keys 或 values 调用 stream() | |

仅列表才有的操作

| 说明 | 常见操作 | 更多 Kotlin 替代方案 |

| 获取元素的索引 | indexOf() | | | 获取元素的最后一个索引 | lastIndexOf() | | | 获取元素 | get() | 使用索引运算符获取元素:list[index]。 | | 获取子列表 | subList() | | | 替换一个或多个元素 | set()、replaceAll() | 使用索引运算符代替 set():list[index] = value。 |

略有差异的操作

对任何集合类型的操作

| 说明 | Java | Kotlin |

| 获取集合大小 | size() | count()、size | | 获得对嵌套集合元素的扁平访问 | collectionOfCollections.forEach(flatCollection::addAll) 或 collectionOfCollections.stream().flatMap().collect() | flatten() 或 flatMap() | | 对每个元素应用给定函数 | stream().map().collect() | map() | | 按顺序对集合元素应用给定操作并返回累积结果 | stream().reduce() | reduce()、fold() | | 按分类器对元素分组并计数 | stream().collect(Collectors.groupingBy(classifier, counting())) | eachCount() | | 按条件过滤 | stream().filter().collect() | filter() | | 检查集合元素是否满足条件 | stream().noneMatch()、stream().anyMatch()、stream().allMatch() | none()、any()、all() | | 对元素排序 | stream().sorted().collect() | sorted() | | 取前 N 个元素 | stream().limit(N).collect() | take(N) | | 取满足谓词的元素 | stream().takeWhile().collect() | takeWhile() | | 跳过前 N 个元素 | stream().skip(N).collect() | drop(N) | | 跳过满足谓词的元素 | stream().dropWhile().collect() | dropWhile() | | 根据集合元素与和它们关联的某些值构建映射 | stream().collect(toMap(keyMapper, valueMapper)) | associate() |

要在映射上执行上面列出的所有操作,你首先需要获取映射的 entrySet。

对列表的操作

| 说明 | Java | Kotlin |

| 把列表按自然顺序排序 | sort(null) | sort() | | 把列表按降序排序 | sort(comparator) | sortDescending() | | 从列表中移除元素 | remove(index)、remove(element) | removeAt(index)、remove(element) 或 collection -= element | | 用某个值填充列表的所有元素 | Collections.fill() | fill() | | 从列表中获取不重复的元素 | stream().distinct().toList() | distinct() |

Java 标准库中不存在的操作

如果你想深入了解 zip()、chunked()、windowed() 及其他一些操作,请观看 Sebastian Aigner 关于 Kotlin 高级集合操作的视频:

视频:Advanced Collection Operations

可变性

在 Java 中,有可变集合:

1
2
3
// Java
// 这个列表是可变的!
public List<Customer> getCustomers() { ... }

有部分可变的:

1
2
3
// Java
List<String> numbers = Arrays.asList("one", "two", "three", "four");
numbers.add("five"); // 运行时会失败,抛出 UnsupportedOperationException

还有不可变的:

1
2
3
4
5
// Java
List<String> numbers = new LinkedList<>();
// 这个列表是不可变的!
List<String> immutableCollection = Collections.unmodifiableList(numbers);
immutableCollection.add("five"); // 运行时会失败,抛出 UnsupportedOperationException

如果你在 IntelliJ IDEA 中编写后两段代码,IDE 会警告你正在尝试修改不可变对象。这段代码能编译通过,但会在运行时以 UnsupportedOperationException 失败。你无法仅通过类型判断一个集合是否可变。

与 Java 不同,在 Kotlin 中你要根据需求显式声明可变集合或只读集合。如果你尝试修改只读集合,代码将无法编译:

1
2
3
4
5
// Kotlin
val numbers = mutableListOf("one", "two", "three", "four")
numbers.add("five") // 这样是可以的
val immutableNumbers = listOf("one", "two")
//immutableNumbers.add("five") // 编译错误 - Unresolved reference: add

在 Kotlin 编码约定页面中进一步了解不可变性。

协变

在 Java 中,你不能把元素为子类型的集合传给接受父类型集合的函数。例如,如果 Rectangle 继承自 Shape,你就不能把由 Rectangle 元素组成的集合传给接受 Shape 元素集合的函数。要让代码可编译,请使用 ? extends Shape 类型,这样该函数就可以接受元素为 Shape 任意继承者的集合:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// 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);
}

在 Kotlin 中,只读集合类型是协变的。这意味着如果 Rectangle 类继承自 Shape 类,那么在任何需要 List<Shape> 类型的地方都可以使用 List<Rectangle> 类型。换句话说,集合类型具有与元素类型相同的子类型关系。映射在值类型上是协变的,但在键类型上不是。可变集合不是协变的 —— 那会导致运行时失败。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// 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)
}

在这里进一步了解集合类型。

区间与递进

在 Kotlin 中,你可以使用区间创建间隔。例如 Version(1, 11)..Version(1, 30) 包含从 1.11 到 1.30 的所有版本。你可以使用 in 运算符检查你的版本是否落在该区间内:Version(0, 9) in versionRange。

在 Java 中,你需要手动检查 Version 是否同时落在两个边界内:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// 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;
}

在 Kotlin 中,你把区间当作一个整体对象来操作。你无需创建两个变量再拿 Version 与它们比较:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// 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)
}

但一旦你需要排除其中某个边界,例如检查版本大于等于(>=)最小版本且小于(<)最大版本,这些闭区间就帮不上忙了。

按多个条件比较

在 Java 中,要按多个条件比较对象,你可以使用 Comparator 接口中的 comparing() 和 thenComparingX() 函数。例如按姓名和年龄比较人员:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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()));
}

在 Kotlin 中,你只需列出想比较的字段:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
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)))
}

序列

在 Java 中,你可以这样生成数字序列:

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

在 Kotlin 中,请使用_序列_。序列的多步处理在可能的情况下是惰性执行的 —— 只有当请求整条处理链的结果时才真正进行计算。

1
2
3
4
5
6
7
fun main() {
    // Kotlin
    val sum = generateSequence(1) {
        it + 3
    }.take(10).sum()
    println(sum) // 输出 145
}

序列可以减少执行某些过滤操作所需的步骤数。请参阅序列处理示例,它展示了 Iterable 与 Sequence 之间的差异。

从列表中移除元素

在 Java 中,remove() 函数接受要移除元素的索引。

移除整数元素时,请把 Integer.valueOf() 函数的结果作为 remove() 函数的实参:

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

在 Kotlin 中,移除元素有两种方式:用 removeAt() 按索引移除,用 remove() 按值移除。

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

遍历映射

在 Java 中,你可以通过 forEach 遍历映射:

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

在 Kotlin 中,使用 for 循环或与 Java 的 forEach 类似的 forEach 来遍历映射:

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

获取可能为空的集合的第一个和最后一个元素

在 Java 中,你可以通过检查集合大小并使用索引来安全地获取第一个和最后一个元素:

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

在 JDK 21 及更高版本中,你还可以使用所有 SequencedCollection 实现上都可用的 getFirst() 和 getLast() 方法。这包括所有 List 实现以及 LinkedHashSet 等其他集合。例如,使用 ArrayList 时:

1
2
3
4
5
6
7
// Java
var list = new ArrayList<>();
//...
if (!list.isEmpty()) {
    System.out.println(list.getFirst());
    System.out.println(list.getLast());
}

在 Kotlin 中,有专门的 firstOrNull() 和 lastOrNull() 函数。使用 Elvis 运算符,你可以根据函数的结果立即执行进一步操作。例如 firstOrNull():

1
2
3
4
// Kotlin
val emails = listOf<String>() // 可能为空
val theOldestEmail = emails.firstOrNull() ?: ""
val theFreshestEmail = emails.lastOrNull() ?: ""

从列表创建集合

在 Java 中,要从 List 创建 Set,你可以使用 Set.copyOf 函数:

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

在 Kotlin 中,请使用 toSet() 函数:

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

对元素分组

在 Java 中,你可以用 Collectors 的 groupingBy() 函数对元素分组:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// 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);
}

在 Kotlin 中,请使用 groupBy() 函数:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
data class Request(
    val url: String,
    val responseCode: Int
)

fun main() {
    // 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))
}

过滤元素

在 Java 中,要从集合中过滤元素,你需要使用 Stream API。Stream API 有 intermediate 和 terminal 两类操作。filter() 是中间操作,它返回一个流。要获得集合作为输出,你需要使用终端操作,例如 collect()。例如,只保留键以 1 结尾且值大于 10 的键值对:

1
2
3
4
5
6
7
8
// 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);
}

在 Kotlin 中,过滤内建于集合之中,filter() 返回与被过滤集合相同的集合类型。因此你只需写出 filter() 及其谓词:

1
2
3
4
5
6
fun main() {
    // 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)
}

在这里进一步了解过滤映射。

按类型过滤元素

在 Java 中,要按类型过滤元素并对其执行操作,你需要用 instanceof 运算符检查类型,然后进行类型转换:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// 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()));
}

在 Kotlin 中,你只需在集合上调用 filterIsInstance<NEEDED_TYPE>(),类型转换由智能转换完成:

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

测试谓词

有些任务需要你检查所有元素、没有元素或有任何元素满足某个条件。在 Java 中,你可以通过 Stream API 的 allMatch()、noneMatch() 和 anyMatch() 函数完成所有这些检查:

1
2
3
4
5
6
7
// 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
}

在 Kotlin 中,每个 Iterable 对象都可以使用扩展函数none()、any() 和 all():

1
2
3
4
5
6
7
fun main() {
// 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") })
}

进一步了解测试谓词。

集合转换操作

压缩元素

在 Java 中,你可以通过同时遍历两个集合,把位置相同的元素组成键值对:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// 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));
   }
}

如果你想做比把元素对打印到输出更复杂的事情,可以使用 Record。在上面的例子中,record 会是 record AnimalDescription(String animal, String color) {}。

在 Kotlin 中,使用 zip() 函数做同样的事:

1
2
3
4
5
6
7
8
fun main() {
    // 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" })
}

zip() 返回由 Pair 对象组成的 List。

如果集合大小不同,zip() 的结果以较小的那个大小为准。较大集合末尾的元素 不会包含在结果中。

关联元素

在 Java 中,你可以使用 Stream API 把元素与特征关联起来:

1
2
3
4
5
6
7
// 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);
}

在 Kotlin 中,请使用 associate() 函数:

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

接下来做什么?

如果你有喜欢的惯用法,欢迎通过提交拉取请求来分享它。