环境
- 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,只能:
- 先克隆成可变List
- 再反序
- 最后再克隆成不可变List(如果需要的话)
var list3 = new ArrayList<>(list1);
Collections.reverse(list3);
var list4 = List.of(list3);
对于Stream,更是没办法,只能先收集成可变List,再反序(或者在收集时,使用一些手段来人工处理,更麻烦)。
为什么Stream没有提供一个反序的方法呢?这可能也是因为函数式编程的理念吧:专注于对流的数据处理,而不是改变顺序(会误认为新数据是从对应位置的原始数据变化而来的)。
总结
collect(Collectors.toList()) | toList() | |
---|---|---|
返回值 | 一般是ArrayList | 不可变List |
JDK版本 | 8 | 16 |
适用场景 | 后续需要修改数据 | 典型的流式处理 |
是否推荐 | N | Y |