java迭代的趋势
更好的并发
流的引入,lambda,一切都是在为了适应现在的硬件架构: 多核,分布式网络架构。即期望给用户提供“轻松”,“安全”的并发编程接口。 流处理,几乎免费的并行,用户在高层次写逻辑代码,具体的执行由底层的lib来选择最合适的执行方式,比如把计算分布到不同的cpu核上。吸取函数式语言里高阶函数,增加了lambda表达式和函数Function,增强了java语言的抽象能力,表达能力,“把行为作为参数传递给函数”,即高阶函数的能力,又称为“行为参数化“(与此对应的还有”类型参数化“,即泛型),自此函数也成为了java语言的一等公民,但是还有方法和类仍然是二等公民,不过提供了一些设施,将二等公民转化为一等公民。
基于Stream的并发,很少使用synchronized关键字,因为是不同的指导思想,stream的并发关注数据分块 而不是 协调访问 , 这样就像函数式编程在靠齐,“无共享可变数据” + “高阶函数” ,不使用synchronized,来协调共享数据的访问(互斥与并发),而是将数据拆分,不共享。
流
流的与集合的一个差异是: 流用于表达计算,集合的元素是计算完之后添加进来或者删除的,但是流是在固定的数据结构上,不能直接删除和增加,按需计算,定义流的时候计算并不发生,且只能遍历一次生成新的流(Java里是这样,scala里面的流可以多次使用)。
使用流的好处,是能写出具有如下特点的代码:
- 声明式:
- 可复合:
- 抽象度高
- 免费并行:
流的定义是: 从支持数据处理的源生成特定的元素序列。定义决定设计
如果自己设计一条流,也应该按照这样的思路来。
所以,流的使用一般就是三件事:
- 定义数据源
- 定义中间操作链 形成一条流水线
- 终端操作 执行流水线(按需计算)生成结果
无状态流和有状态流的区别
有:流内部的算子有用户提供的lambda, 或者 方法引用(方法不是纯函数)
无:没有内部状态,没有用户提供的lambda或者方法引用,没有内部可变状态。
无状态流对并行友好,无缝切换到parallel,而有状态的流不行,比如求和,如果用外部变量进行累加,则parallel很容易出错,但是如果是利用reduce的分开累加,最终将每个累加结果再累加,就不会有并发问题。
数值流存在的原因不是流的复杂性,而是 基本类型和对应的对象类型 之间的装箱和拆箱性能。
流的生成: 万物皆可流
万物都可以作为流的元素,也可以作为流的源头生成流元素。
- 数值
- 集合
- 文件
- 空对象
- 函数 (比如无限流,
Stream.iterate(0, n -> n + 2)
偶数的无限流,Stream.iterate(new int[]{0,1}, t-> new int[]{t[1], t[0] + t[1]})
斐波那契流,Stream.generate(Math::random)
随机流
流收集:最终计算: 归约
流水线是lazy的数据集的计算迭代器,最终的计算由 terminal action出发,通用的操作即collect,collect接受一个参数Collector来表示最终的流元素去往何处。
Collectors工具类提供了许多直接的预定义的归约器,也提供了一些高阶方法生成归约器,而这一切都离不开背后的基本归约方法:java.util.stream.Collectors#reducing(U, java.util.function.Function<? super T,? extends U>, java.util.function.BinaryOperator<U>)
U 归约的初始元素
Function是将流内元素转化为待归约的元素
BinaryOperator是待归约元素的计算
归约计算的一个目的,收集,也可由归约完成。这就涉及到范畴论的理论来,以数组的收集举例:
reducing(new List<>(),
(l, e) -> { l.add(e); return l;},
(l1, l2) -> { List l = new List();
l.addAll(l1);
l.addAll(l2);
return l;} }
并且上面的归约属于“无状态”,可以轻松的用来做 并行。
reducing这个方法之所以能够作为基本方法是它提供了两个基本能力:
- 元素到范畴的映射
- 范畴到范畴的映射
代码实际实现是Collector类,另外Stream提供了一个collect方法,接受三个参数- supplier, accumulator 和 combiner,来自定义收集,其语义和Collector接口相应方法返回的函数完全相同。
分组: Collector的连接 : groupingBy( , [ toList toSet]), collectAndThen,
复杂的归约,可以通过groupingBy以及partitioningBy完成,并且他们之间可以通过多个Collector的连接完成
如:
Map<Type, List<String>> =
collect(groupingBy(Dish::getType,
mapping(Dish:getName, toList())));
或者多级分组
Map<Type, Map<CaloricLevel, List<Dish>>> =
.collect( groupingBy(Dish::getType,
groupingBy( dish -> {
if (dish.getCateGory <= 400 ) return CaloricLevel.DIET;
......
})
));
或者分组统计
Map<Dish.Type, Long> = .collect( groupintBy(Dish::getType, counting() ));
或者分组统计后计算
Map<Dish.Type, Optional<Dish>> = .collect(groupingBy(Dish::getType, maxBy(comparingInt(Dish::getCalories))));
groupingBy(Dish::getType, collectAndThen( maxBy(comparingInt(Dish::getCalories)), Optional::get));
分区 partitioningBy
将流的元素,利用一个谓词做分类。分区可以理解为产生了两个流,并且由 partitioningBy实现的Map实现(特殊map)更高效,紧凑,因为只包含两个键: true 和 false
分区和分组一样,可以多级分区,只要 连接 partitioningBy 就可以
收集器性能对比
实现自定义的Collector的目的是为了获得 比 ’使用内置工厂方法创建的收集器‘ 更好的性能,但是也可能让性能更拉垮,只能说:提供了设置,让有能力的同学可以自由发挥。
JMH框架来测试。
并行
在Java8之前的并行流,需要主动的拆分数据,分配给不同的线程,然后还要进行协调和同步以避免可能发生的竞争条件,等到每个线程都完成后,再合并结果。并且java7引入了一个 “分支/合并”的框架,可以更稳定,更不容易出错的完成这一件事。
不过,相对于java8的并行流还是落后了些,Java8的Stream接口让用户免费使用并行,一个指令就可以将顺序流转化为并行流(当然,前提是你的数据能够接受并行处理),控制数据切分的过程主要是: Spliterator (splitable , iterator)
someStream.parallel() 方法并不会改变实际的流,而是设置了一个flag,表示 parallel之后的所有操作都是可以并行执行的,指需要再调用 sequential 就可以再变回顺序流。
并行流内部使用的还是:ForJoinPool,默认的个数等于cpu的个数。
java.util.concurrent.ForkJoinPool. common.parallelism 来修改线程池大小, 缺点是jvm级别的,会修改所有并行流的线程池大小,所以一般不修改,如下所示:
System.setProperty("java.util.concurrent.ForkJoinPool.common.parallelism","12");
并行流的思想是:数据分块,任务分块,然后利用Fork/Joinpool, 实现任务的计算。
任务的分块由Spliterator实现。
Fork/Join Pool框架
思想分治,将任务拆分,最后合并。
- 可拆分的任务抽象
ForkJoinTask<T> <= RecursiveTask<T>, 或者 ForkJoinTask<Void> <= RecursiveAction
- 任务执行: compute, 任务在compute内部再次拆分,不能拆的计算然后返回结果,将之前的拆分都合并。
- 执行任务的框架: work stealing。由于任务拆分的子任务的计算负载不均衡,导致虽然每个线程的队列都只有两个任务,但是其中一个队列的任务特别简单,瞬间完成,而另一个队列的任务可能特别耗时,第二个任务就在等待第一个完成,不能并发。steal能够让空闲/或者负载低的线程偷取忙碌线程的双端队列的task来帮助执行,这也是为什么 任务 要分细的原因。
流的拆分: Spliterator
参考:
- https://zhuanlan.zhihu.com/p/504958543
- https://cloud.tencent.com/developer/article/1333605?areaSource=106001.15
interface Spliterator<T> {
boolean tryAdvance(COnsumer< ? super T> action);
Spliterator<T> trySplit();
long estimateSize(); // 不是很准确
int characteristics();
}
trySplit会递归调用,调用的时候,它会划分一些元素给它返回的第二个Spliterator,让他们两个并行处理。
递归的拆分,拆分的过程会影响整个的执行效率,在自定义Spliterator的时候,注意效率,采用物理分割,比如将数据复制一份,显然没有逻辑分割保存原数据引用,修改数据范围来的效率高。另外,拆分的过程收到了 “特性”影响(characteristics方法声明)
如果当前的Spliterator实例X是可分割的,trySplit()方法会分割X产生一个全新的Spliterator实例Y,原来的X所包含的元素(范围)也会收缩,类似于X = [a,b,c,d] => X = [a,b], Y = [c,d];如果当前的Spliterator实例X是不可分割的,此方法会返回NULL),具体的分割算法由实现类决定
todo: 对于无限流的并发,是怎么split的?
集合与lambda带来的高效编程和改变
集合工厂的增强,对于List,Set,map等,SomeCollection.of() 方法会生成 小规模的高性能的不可变集合类,即集合常量 ,虽然生成的数据结构不可变,但是效率更高,就类似于: Arrays.asList() 方法一样,生成的是视图。
另外,对于大家常用集合类时候,用到的一些常用操作,都增加了相应语义的方法:
-
removeIf: list 和set提供,可以删除满足条件的元素,而不会触发ConcurrentModificationException,不然就要显式的使用迭代器和迭代器的删除方法;
正确的代码如下
replaceAll: 替换集合的元素,而不用新生成集合。
remove: 删除map里面指定的 key和value 对
merge: 把两个map对元素进行合并的逻辑
computeIfXXXX,compute:高效的计算和填充
ConcurrentHashMap
- set视图:一个可以时刻同步map的set视图,xxx.keySet()方法,随时在set里看到map的变化。
- 基础类型的归约:使用对应的基础类型方法会更快,少去了装箱拆箱的步骤 :
reduceValuesToInt、reduce-KeysToLong
lambda重构设计模式
新的语言特性常常让现存的编程模式或设计黯然失色,对设计经验的总结陈称为“设计模式”。
lambda之所以可以重构设计模式,是因为:将行为参数化,传递给高阶函数。而策略模式,模板模式的核心就是封装了不同的行为; 观察者模式,是在发生一些事件之后,触发一些行为的执行;责任链模式本质是对数据多次的操作,完全可以用函数式编程的组合模式完成;工厂模式,也只是某种特定的函数罢了:Function<someParameter, SomeClassInstance>;
基于lambda的DSL todo
精读此章节内容后,最好搭配:
英文版本的: Domain-Specific Languages Martin的书,中文翻译贼拉垮
DSLs in Action,有引进的话,先看中文看看
还有 antlr4的两本,因为 martin的书是设计思想,指导原则,用的工具还是 antlr4 做解析。
Optional,时间 : 非常简单,略
默认方法和模块系统
默认方法
模块系统
模块系统比较复杂: 搭配:Nicolai Parlog《 The Java Module System 》
设计的高层次(软件架构层次)设计模式:
关注点分离(separation of concern,SoC)和信息隐藏(information hiding)
- 关注点分离 推崇的是将:单体的计算机程序分解为一个个相互独立的特性
采用关注点分离,可以将软件的功能,作用等划分到名为“模块“的独立组成部分中去,所以,模块是具有“内聚”特质的一组代码,它与其他模块的代码很少耦合;通过模块组织类,可以清晰地描绘出应用程序类与类之间的可见性关系。
Java的包机制并为从本质上支持模块化,它的粒度太粗。而模块化的粒度更细,且控制检查是编译期的。带来的好处就是:可以使得各项工作独立开展,减少组件之间的依赖,便于团队合作,有利于推动组建重用,系统整体的维护性更好。
- 信息隐藏: 隐藏信息能够减少局部变更对其他部分程序的影响,从而避免“变更传递”。
在低层次(代码层次) 的表现就是封装。虽然我们具有private,protected,public 关键字,但是就语言层面而言,Java 9 出现之前, 编译器无法依据语言结构判断某个类或者包仅供某个特定目标访问。
Java9之前的Java内置模块化的问题:
- 有限的可见性控制:三个描述符只能控制包级别的类访问,无法描述 包之间的访问。比如:“希望一个包中的某个类或接口可以被另外一个包中的类或接口访问,那么只能将它声明为 public。这样一来,任何人都可以访问这些类和接口了”。这样就可能让代码被随意使用,给开发者演进自己的代码带来困难。
- 类路径的问题:Java编译器把所有的类都打入一个扁平的jar包中,并且把jar包放到class path上,jvm可以动态一句类的路径从中定位并且加载相关的类。然后,这样存在了几个严重的问题:
- 无法通过路径指定版本。比如如果类路径上存在同一个库的两个版本,会发生什么。而大型项目中很常见,它的不同组件使用同一个库的不同版本:解决方法有
- 使用自定义ClassLoader来隔离: http://www.blogjava.net/landon/category/54860.html(值得注意的两点是:公有接口可以由系统类加载器加载,旧的类的实例和class很难被卸载)。比如: elasticsearch中的插件加载机制,实现了自定义的classLoader 。 甚至可以用ClassLoader来实现热部署:https://cloud.tencent.com/developer/article/1915650
- 或者是OSGI
- sofa-ark是动态热部署和类隔离框架,支付宝开源
-
类路径不支持显式的依赖:
Java9提供了一个新的单位: 模块。通过 module声明,紧接着的是模块的名字和主体的内容,定义在特殊的文件:module-info.class
, 下图可知,module的层级是高于package的。
使用命令可以指导哪些目录和类文件会被打包进入生成的JAR文件中:
javac module-info.java xxx/yyyy/zzz.java -d target
jar cvfe xxxxx.jar xxx.yyy.zzz -C target
然后运行执行命令:
java --module-path xxxxx.jar --module moduleName.xxx.yyy.zzz
并发性
无论是基于 Future 的异步 API 还是反应式异步 API,被调方法的概念体(conceptual body) 都在另一个线程中执行,调用方很可能已经退出了执行,不在调用异常处理器的作用域内。很明显,这种非常规行为触发的异常需要通过其他的动作来处理。
CompletableFuture
和Scala的Future很类似
反应式编程
直接看《反应式设计模式》就好了
函数式
个人了解较多,忽略。