Edit Page

Java 和 Kotlin 中的集合(Collection)

最终更新: 2024/03/21

集合 是一组可变数量(可以为 0)的元素, 解决问题时起到重要作用, 而且经常被用到. 本文解释并比较 Java 和 Kotlin 中集合的概念以及操作方式. 本文将帮助你从 Java 迁移到 Kotlin, 并以真正 Kotlin 的方式编写你的代码.

本文第 1 部分包括在 Java 和 Kotlin 中对同一个集合进行操作的快速介绍. 分为 共同的操作只存在于 Kotlin 中的操作. 本文第 2 部分, 从 可变性(Mutability) 开始, 通过例子来解释一些区别.

关于集合的介绍, 请参见 [集合概述(collections-overview.html), 或观看 Sebastian Aigner 讲解的这个 视频, 他是 Kotlin 开发者 Advocate.

下文中的所有示例都只使用 Java 和 Kotlin 标准库 API.

在 Java 和 Kotlin 中相同的操作

在 Kotlin 中, 有很多集合操作与在 Java 中的对应操作完全相同.

对 List, Set, Queue, 和 Deque 的操作

描述 共通操作 Kotlin 中的更多选择
添加一个或多个元素 add(), addAll() 使用 加然后赋值(plusAssign)(+=) 操作符: collection += element, collection += anotherCollection.
检查集合是否包含一个或多个元素 contains(), containsAll() 使用 in 关键字 以操作符的形式调用 contains() 函数: element in collection.
检查集合是否为空 isEmpty() 使用 isNotEmpty() 检查集合是否为非空.
指定条件删除 removeIf()  
只保留指定的元素 retainAll()  
从集合删除所有元素 clear()  
从集合得到一个 Stream stream() Kotlin 有自己的方式来处理 Stream: 序列(Sequence), 以及方法, 比如 map()filter().
从集合得到一个 Iterator iterator()  

对 Map 的操作

描述 共通操作 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].
检查 Map 是否包含一个或多个元素 containsKey(), containsValue() 使用 in 关键字 以操作符形式调用 contains() 函数: element in map.
检查 Map 是否为空 isEmpty() 使用 isNotEmpty() 检查 Map 是否为非空.
删除元素 remove(key), remove(key, value) 使用 减然后赋值(minusAssign)(-=) 操作符: map -= key.
从 Map 删除所有元素 clear()  
从 Map 得到一个 Stream entries, keys, 或 values 的 stream() 函数  

只对 List 有效的操作

描述 共通操作 Kotlin 中的更多选择
得到元素下标 indexOf()  
得到元素的最后下标 lastIndexOf()  
得到元素 get() 使用下标访问操作符得到元素: list[index].
获取一个子 List subList()  
替换一个或多个元素 set(), replaceAll() 使用下标访问操作符, 而不是 set(): list[index] = value.

略有不同的操作

对任何集合类型都有效的操作

描述 Java Kotlin
得到集合的大小 size() count(), size
平展访问(Flat Access) 嵌套的集合元素 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()
构建从集合元素到关联值的 Map stream().collect(toMap(keyMapper, valueMapper)) associate()

要对 Map 执行上述所有操作, 你首先需要得到 Map 的 entrySet .

对 List 的操作

描述 Java Kotlin
按照自然顺序排序 List sort(null) sort()
按照逆序排序 List sort(comparator) sortDescending()
从 List 删除元素 remove(index), remove(element) removeAt(index), remove(element)collection -= element
将 List 的所有元素填充为指定的值 Collections.fill() fill()
从 List 得到不重复的元素 stream().distinct().toList() distinct()

在 Java 标准库中不存在的操作

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

可变性

在 Java 中, 有可变的集合:

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

也有部分可变的集合:

// Java
List<String> numbers = Arrays.asList("one", "two", "three", "four");
numbers.add("five"); // 在运行时刻会发生 `UnsupportedOperationException` 错误

还有不可变的集合:

// Java
List<String> numbers = new LinkedList<>();
// 这个 List 是 不可变的!
List<String> immutableCollection = Collections.unmodifiableList(numbers);
immutableCollection.add("five"); // 在运行时刻会发生 `UnsupportedOperationException` 错误

如果你在 IntelliJ IDEA 中编写后面两段代码, IDE 会提出警告, 告诉你正在修改不可变的对象. 这段代码能够编译, 并在运行时刻发生 UnsupportedOperationException 错误. 你不能通过集合的类型判断它是否可变.

与 Java 不同, 在 Kotlin 中, 你会根据需要明确声明可变的或只读的集合. 如果你试图修改只读集合, 代码将会无法编译:

// Kotlin
val numbers = mutableListOf("one", "two", "three", "four")
numbers.add("five")            // 这是正确的
val immutableNumbers = listOf("one", "two")
//immutableNumbers.add("five") // 编译错误 - 无法解析的引用: add

关于可变性, 详情请参见 Kotlin 编码规约.

协变(Covariance)

在 Java 中, 如果函数的参数是祖先类型元素的集合, 那么你不能传递一个后代类型元素的集合. 比如, 如果 Rectangle 继承 Shape, 对于参数是 Shape 元素集合的函数, 你不能传递 Rectangle 元素类型的集合. 要让代码能够编译, 需要使用 ? extends Shape 类型, 才能让函数接受从 Shape 继承的后代类型元素的集合:

// Java
class Shape {}

class Rectangle extends Shape {}

public void doSthWithShapes(List<? extends Shape> shapes) {
/* 如果只使用 List<Shape>, 那么如下面的例子那样, 使用 List<Rectangle> 作为参数调用
   这个函数时, 代码将无法编译 */
}

public void main() {
    var rectangles = List.of(new Rectangle(), new Rectangle());
    doSthWithShapes(rectangles);
}

在 Kotlin 中, 只读集合类型是 协变的(Covariant). 因此, 如果 Rectangle 类继承自 Shape 类, 那么在要求 List<Shape> 类型的地方, 你可以使用 List<Rectangle> 类型. 也就是说, 集合类型之间的子类型关系与元素类型之间相同. Map 根据 value 类型协变, 而不是根据 key 类型. 可变的集合不是协变的 – 否则会导致运行时错误.

// 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)
}

详情请参见 集合类型.

值范围(Range)与数列(Progression)

在 Kotlin 中, 你可以使用 值范围(Range) 创建数值范围. 比如, Version(1, 11)..Version(1, 30) 包括从 1.111.30 的所有版本. 你可以使用 in 操作符检查你的版本是否在范围中: Version(0, 9) in versionRange.

在 Java 中, 你需要手动检查一个 Version 是否在边界条件之内:

// 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 进行比较:

// 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() 函数 . 比如, 要按照姓名和年龄比较人:

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 中, 你只需要列举你希望比较的属性:

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)))
}

序列(Sequence)

在 Java 中, 你可以这样生成一个数值序列(Sequence):

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

在 Kotlin 中, 请使用 序列(Sequence). 对序列的多个步骤处理会尽可能延迟执行 – 只有在需要整个处理串的结果时, 实际的计算才会发生.

fun main() {
//sampleStart
    // Kotlin
    val sum = generateSequence(1) {
        it + 3
    }.take(10).sum()
    println(sum) // 输出结果为 145
//sampleEnd
}

对于一些过滤操作, 序列可能会减少需要执行的步骤. 详情请参见 序列的处理示例, 这篇文档会演示 IterableSequence 的区别.

从 List 删除元素

在 Java 中, remove() 函数接受需要删除的元素下标.

要删除整数元素时, 使用 Integer.valueOf() 函数作为 remove() 函数的参数:

// 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 中, 有 2 种类型的元素删除函数: 根据下标删除 removeAt(), 以及根据值删除 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
}

遍历 Map

在 Java 中, 你可以通过 forEach 来遍历 Map:

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

在 Kotlin 中, 请使用 forforEach 循环, 类似于 Java 的 forEach, 来遍历 Map:

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

从可能为空的集合得到第 1 个和最后 1 个元素

在 Java 中, 你可以检查集合大小, 并使用下标安全的得到第 1 个和最后 1 个 元素:

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

Deque 和它的后代类, 你还可以使用 getFirst()getLast() 函数:

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

在 Kotlin 中, 有专门的函数 firstOrNull()lastOrNull(). 使用 Elvis 操作符, 你可以根据函数结果执行更多操作. 比如, firstOrNull():

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

从 List 创建 Set

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

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

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

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

对元素分组

在 Java 中, 你可以使用 Collectors 函数 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);
}

在 Kotlin 中, 使用函数 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
}

过滤元素

在 Java 中, 要过滤集合的元素, 你需要使用 Stream API. Stream API 包括 中间(Intermediate)终止(Terminal) 操作. filter() 是一个中间操作, 返回一个 Stream. 要得到输出的集合, 你需要使用终止操作, 比如 collect(). 比如, 要只保留 key 以 1 结尾并且 value 大于 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);
}

在 Kotlin 中, 过滤是集合内建的操作, filter() 返回与过滤之前相同的集合类型. 因此, 你需要编写的代码只是 filter() 以及它的过滤条件:

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
}

详情请参见 过滤 Map.

根据类型过滤元素

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

// 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>(), 类型转换会由 智能类型转换 完成:

// 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
}

验证判定条件

一些任务要求你检查是否所有元素, 不存在元素, 或存在某些元素符合某个条件. 在 Java 中, 你可以通过 Stream API 函数 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
}

在 Kotlin 中, 对所有的 Iterable 对象, 可以使用 扩展函数 none(), any(), and all():

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
}

详情请参见 验证判定条件.

集合变换操作

合并(Zip)元素

在 Java 中, 你可以同时遍历两个集合, 将同一位置的两个元素变换为 pair :

// 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));
   }
}

如果你希望做某些更加复杂的操作, 而不仅仅是将元素 pair 打印输出, 你可以使用 Record. 在上面的示例中, Record 是 record AnimalDescription(String animal, String color) {}.

在 Kotlin 中, 使用 zip() 函数可以完成相同的功能:

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() 返回 Pair 对象组成的 List.

如果集合大小不同, zip() 的结果将是较小的那个集合大小. 结果中不包括较大那个集合的后面部分元素.

关联(Associate)元素

在 Java 中, 你可以使用 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);
}

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

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

下一步做什么?

如果你有喜欢的惯用法, 欢迎你发送一个 pull request, 分享给我们.