Bootstrap

从Stream的 toList() 和 collect(Collectors.toList()) 方法看Java的不可变流

环境

  • JDK 21
  • Windows 11 专业版
  • IntelliJ IDEA 2024.1.6

背景

在使用Java的Stream的时候,常常会把流收集为List。

假设有List list1 如下:

        var list1 = List.of("aaa", "bbbbbb", "cccc", "d", "eeeee");

要找到所有长度大于3的字符串。

Java 8的做法是:

        var list2 = list1.stream().filter(e -> e.length() > 3).collect(Collectors.toList());

但是IDEA会给出一个提示:

‘collect(toList())’ can be replaced with ‘toList()’

如下图所示:

在这里插入图片描述
可见,代码可以简化如下:

        var list3 = list1.stream().filter(e -> e.length() > 3).toList();

注意: toList() 方法是Java 16引入的。

区别

虽然 collect(Collectors.toList())toList() 方法都返回List,但是二者是有一些差异的。

前者返回的一般是一个ArrayList,是可以修改的,而后者返回的是一个不可修改的List。

如下图所示:

在这里插入图片描述

可见,如果尝试给 list3 添加元素,IDEA会提示:

Immutable object is modified

注意:编译并不会报错,因为 list3 是List,调用 add() 方法是OK的,但是在运行期,会抛出 UnsupportedOperationException 异常:

Exception in thread "main" java.lang.UnsupportedOperationException
	at java.base/java.util.ImmutableCollections.uoe(ImmutableCollections.java:142)
	at java.base/java.util.ImmutableCollections$AbstractImmutableCollection.add(ImmutableCollections.java:147)
	at org.example.Test1119_41.main(Test1119_41.java:17)

同理,使用 list3.set() 方法来改变元素的值,也会在运行期抛出异常。

原因

把Stream收集为List是一个非常常用的操作,最初,Java 8提供了 collect(Collectors.toList()) 方法,显然,因为这个操作太常用了,所以在Java 16里,将其简化为了 toList() 方法。那么问题来了:

为什么Java 8里返回的是ArrayList,而Java 16简化后,返回的是不可变List呢?

咨询了豆包,它的回答里提到好几点,比如防止意外修改,线程安全,可维护性等等,不过下面这一点我觉得最有意义:

Java 8 引入了函数式编程特性,如流(Stream)和 Lambda 表达式。在函数式编程范式中,数据的不可变性是一个重要原则。函数式编程强调无副作用的操作,不可变数据结构符合这一要求。例如,在使用流操作(如map、filter等)处理数据时,不可变列表可以保证在每一步操作中,数据的原始状态不会被改变,使得流操作的结果更加可预测和符合函数式编程的语义。

说的挺有道理的。那么问题又来了:

既然在函数式编程中,数据的不可变性很重要,很有意义,那为什么不在最初Java 8的时候, collect(Collectors.toList()) 就返回不可变List呢?

答案是:Java语言一直在演进。

在Java 8的时候,对于数据不可变性的强调还没有像 Java 9 及以后那样深入。随着对函数式编程理念的深入理解,以及在实际应用中对数据安全、代码质量等方面的更高要求,Java 设计团队逐渐认识到不可变数据结构的重要性,从而在 Java 9 及后续版本中开始大力推广和完善不可变列表等相关特性。

当然,考虑到兼容性,Java高版本不可能把 collect(Collectors.toList()) 方法的返回值修改为不可变List。

话说回来,可变List也是必要的需求。即使不可变List是主流,总会有需求要对List做修改的。

其它

Arrays.asList() 和 List.of()

本文开头有如下代码:

        var list1 = List.of("aaa", "bbbbbb", "cccc", "d", "eeeee");

这里, List.of() 方法是Java 9引入的,返回的是一个不可变List

相应的,从Java 1.2就引入的 Arrays.asList() 方法:

        var list2 = Arrays.asList("aaa", "bbb", "ggg", "ddd", "eee", "fff");

它返回的是一个受限的可变List:不能改变List的长度,只能改变元素的值:

        list2.set(3, "hhh"); // OK
        list2.add("iii"); // UnsupportedOperationException

反序(注意不是排序中的逆序)

给定一个List或Stream,如何获取反序的List或Stream(比如把 "a", "c", "b" 变成 "b", "c", "a" )?

好像没有什么特别简单的办法,一个办法是利用 Collections.reverse() 方法,比如:

        Collections.reverse(list2);

这时问题就来了,对于不可变List,没法直接reverse,只能:

  1. 先克隆成可变List
  2. 再反序
  3. 最后再克隆成不可变List(如果需要的话)
        var list3 = new ArrayList<>(list1);
        Collections.reverse(list3);
        var list4 = List.of(list3);

对于Stream,更是没办法,只能先收集成可变List,再反序(或者在收集时,使用一些手段来人工处理,更麻烦)。

为什么Stream没有提供一个反序的方法呢?这可能也是因为函数式编程的理念吧:专注于对流的数据处理,而不是改变顺序(会误认为新数据是从对应位置的原始数据变化而来的)。

总结

collect(Collectors.toList())toList()
返回值一般是ArrayList不可变List
JDK版本816
适用场景后续需要修改数据典型的流式处理
是否推荐NY
;