【总结】集合类特性剖析【不可变、懒计算、并行、视图、协变等特点与应用】
本文禁止转载!
集合类API设计及其底层实现涉及诸多特性,比如函数式编程中常见的不可变实现、Stream 流式处理的懒计算和并行执行实现,本文将详细分析这些API或者实现的优缺点。
1. 可变与不可变
Java 集合库基于可变实现,提供的接口都支持增删改查,可变实现指的是对集合对象进行增删改。
不可变实现指的是集合类存储的对象引用的不可变,Java 官方对于其命名进行了详细的区分,UnmodifiableCollection 仅仅指的是集合类的元素引用不变,ImmutableCollection 指的是既满足元素引用不变,同时所有的元素自己也是不可变的。
不过一般来说,我们说不可变时指的是 Java 官方说的 UnmodifiableCollection。
Guava 提供了不可变实现,不可变实现为可变实现的子类;Kotlin 中可变实现是不可变实现的子类/子接口。这两种实现方式都会带来一定的问题,不符合替换原则。
最好的实现形式是各自独立,就像 Scala 提供的实现一样。Guava graph 实现就采取了这种形式。
工厂方法
List#of,List#copyOf,Map#ofEnties 等工厂方法提供了创建不可变对象的实现。
Java 并不提供可变类的工厂方法,可以通过其默认提供的拷贝构造器实现构造,如:new ArrayList<>(Arrays.asList(1, 2, 3))
不建议自己封装可变类的工厂方法。
2. 懒计算与积极计算(lazy vs eager)
对于集合的遍历操作通常是立即计算的,Stream 流式操作提供了懒计算实现。
懒计算和流畅API很多时候是相辅相成的。实际上,很多流畅API设计都是可以实现懒计算的,比如 Flogger 提出的流畅日志API。
懒计算的原则是按需计算,当调用最终操作后,仅仅会进行需要的计算。其优点是可以对操作进行拆分,比如过滤 filter -> 映射 map -> 收集计算结果 collect / reduce。
懒计算的性能不一定差,这么说可能显得没有底气,更自信的说法是懒计算的性能和常规遍历操作相比不相上下,有时性能更好。其内部会进行很多优化处理(挖坑,以后详细说)。举例来说,比如 filter(p).map(mapper) 只相当于一次遍历,其计算的顺序是可以变化的,只要能返回正确的结果;map(mapper).count() 大概率不会执行 mapper。很多时候,我们不能去意想内部迭代的执行顺序,而且正因为此,传入无副作用的函数才理所当然。
懒计算的缺点是难以进行监控,所以我们每次使用 Stream 通常只解决一个问题。简单的任务可以,复杂的任务有时会很难用。那么如何确定问题是简单还是困难呢?方法很简单,Stream 提供的接口能解决问题就是简单的,不提供的接口想要自己实现就是复杂的和难以理解的。
总之,选择那种实现最好取决于具体的问题。豆浆机也许可以用来做果汁,但不一定好喝。
3. 并行执行和串行执行
Stream 提供了 parallel 方法支持并行处理,其可以轻松地支持大量数据的并行计算。底层使用的是 Fork/Join 框架,应用了分治思想,但是使用了公共池,在大型项目中容易出现单点故障。
若 stream 底层依赖的 Spliterator 支持对半拆分(比如ArrayList),并行执行通常可以获得良好的性能,但是如果底层是 HashSet, HashMap,底层实现每个桶内的链表(或红黑树)大小是不固定的,其性能是不稳定的。此时使用不可变实现ImmutableMap 的性能好于可变实现,因为 Guava 和 Java 中的不可变类实现为了保证迭代顺序,底层依赖为数组。
并行执行常常涉及的性能问题包括:上下文切换的开销、数据需要同步锁、计算并行度问题等。性能调优时,一般需要进行基准测试和生产环境测试。
Eclipse-Collection 提供了并行执行支持,可以执行线程池,可以参考。
4. 视图
视图的使用常常被忽略,其实使用得当的话,其可以极大地减少内存的使用。其还具有一些有意思的方法,可以和被委托的底层实现类实现一定程度的联动。以下是一些有趣的例子:
Maps.filterValues(map, Objects::nonNull)
可以过滤掉值为空的键值对。
subList 是可变的:
List<String> originalList = new ArrayList<>(Arrays.asList("A", "B", "C", "D", "E"));
List<String> subList = originalList.subList(1, 4);
subList.add("X");
System.out.println(originalList); // Output: [A, B, C, D, X, E]
Guava 不可变类提供了 asList 方法,其底层使用的对象和被委托对象一致。
Arrays#asList 返回List,其是数组的视图,所以其不支持增删等功能。
Collections#unmodifiableCollection 提供了不可修改视图,但是如果可以获得其底层对象引用,依然可以修改。
5. 可变性
这里的可变性指的是集合类之间的父类子类关系,考虑到类型安全,Java 中的集合可变性为不变
,其实不可变类可以支持协变,但是在实际使用过程中我们需要手动强转类型,如:ListeningExecutorService
拓展了ExecutorService,使其返回的Future均支持回调功能,类型为ListenableFuture。但是由于 Java 语言的限制只能invokeAll 只能声明返回List<Future>
<T extends @Nullable Object> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks)
throws InterruptedException;
解决方法是强转 + SuppressWarning
@SuppressWarning("unchecked")
List<ListenableFuture<T>> futures = (List) executor. invokeAll(tasks);