1、函数式编程思想
java8的核心改变是提出函数式编程的思想,书中把这称作jdk变革最大的一次,函数式编程的应用场景十分普遍,如过去我们筛选苹果,筛选绿的会写一个方法,筛选大的又要写一个方法,在函数式编程里我们只需要定义一个叫做筛选苹果的方法,把筛选条件作为参数传入;又比如我们更常见的getUserById和getUserByName这些,函数式编程只要getUser(Function<T,R> f)。这里我们说可以把方法看作一个参数传给另一个方法,这些函数参数本质是interface,jdk给出了一些常用的函数式接口,上一篇里有,我们自己定义函数式接口的话,只需要接口里有且只有一个非默认方法即可(java8新引入的接口方法默认实现,为了兼容之前的类库)。当然函数式编程的思想不仅仅如此,引用透明性,科里化,策略模式,模板方法,观察者模式,责任链模式,工厂模式中函数式思想的优势,这些需要未来有机会实践体会了。
2、直观的代码风格改变
java8对开发者带来最直观的改变就是lambda和stream,这两种东西改变了编程风格,这里先提前说一句lambda并不是匿名内部类的另一种表达方式,二者编译后的字节码都不一样,lambda创建额外的类被invokedynamic代替了(jdk7引入的)。stream更不是迭代器的另一种表达方式,stream可以表示无限流,延迟计算,有出色的并发能力。
2.1、Lambda
lambda是函数式接口的具体实现,形如(类型A 参数a,类型B 参数b) -> 表达式 或 (类型A 参数a,类型B 参数b) -> {表达式;return 结果},前边的小括号里要是只有一个参数可以省略类型声明和小括号,多个参数但类型一致的话也能省略类型声明,没参数的话只写小括号。“->”前边的代表函数传入的参数,后边的代表表达式返回的结果代表函数返回的结果,如Function<T,R>的Lambda签名就是就是 T -> R,Cosumer<T> 的就是T -> void,Supplier<T>就是 ()->T。
由于对并发性的支持,lambda内的变量要是final或近似final的,外部的引用变量可以出现再lambda内,只要引用不变就行,内容可以变,基础类型的话出现再lambda内不能修改,比如外面有个int i=0;lambda内部不能i++。
2.2、方法引用
方法引用也是一种函数实现,形如xxx::f,比如一个对象a中的一个方法是 public T f(R r);a::f的Lambda签名就是T -> R,这就可以让你自己写出方法来,然后通过方法引用当作函数形式的参数传入其他方法。
lambda表达式中若只有一个参数,表达式里只调用了该参数的方法,方法引用可直接协作参数类型的引用,如(Apple a) -> a.getWeight() 等效于 Apple::getWeight;
若只有一个参数(或没有),表达式里的方法也以这个参数为参数,方法引用可直接用表达式里的方法,如 (String s) -> System.out.println(s) 等效于System.out::println
其他方法引用的方式不一一列举了。
2.3、新的排序表达和比较器复合
过去的排序我们常用Collections.sort传入list和匿名内部类,java8在List类里加了sort方法,直接list.sort(Comparator<T> c),Comparator加了很多如comparing,thenComparating,reversed,还有一些避免拆装箱的comparingInt之类,这些比较器可以复合后传给sort,比如comparing(Apple:getWeight).reversed().thenComparating(Apple::getType)
2.4、函数复合
函数之间可以复合成一个新的函数,用andThen或compose连接,如f = x -> x + 1; g = x -> x * 2; f.andThen(g)相当于g(f(x)),x=1时的值时4;f.compose(g)相当于f(g(x)),x=1时的值是3。
2.5、流的概念和特性
Collection新增了stream方法,直接list.stream()获取流,还可以Stream.of(a,b,c,d)创建简单的流,Stream.empty()创建空流,Arrays.stream(T[] t)由数组创建流。
对流的操作是流的内部迭代,其具有延迟性,被使用时才真正执行分配内存,所以能表示无限的东西,如Stream.iterate(0, n -> n+2)和Stream.generate(Math::random),iterate是给个起点和规则,generate接受一个Supplier<T>为参数。
对流的操作分中间操作和终端操作,顾名思义终端操作是在最后的,而且只有一次,中间操作可以有多个。流只能消费一次,如一个流s,执行s.findFirst();再执行s.forEach()会报IllegalStateException因为流已经关闭。
常用中间操作:filter筛选,sorted排序,limit截取,map映射,skip跳过,distinct去重,flatMap扁平化映射(对于一个Stream<String>如果进行map(Function<String,Stream<String>>)则之后的流就成了Stream<Stream<String>>,如果用flatMap则会扁平化成Stream<String>)。
常见终端操作:count总数,forEach遍历,collect收集,allMatch、anyMatch、noneMatch、findFirst、findAny这些查找匹配,sum求和,reduce归约(reduce接受一个初始值和一个运算规则做参数,初始值和流的第一个做运算,得出的结果和流的下一个做运算,得出最终结果,也可以没有初始值,代表直接第一个和第二个做运算的结果和下一个运算,返回值是一个Optional)。
不考虑并行流的情况下(其实并行流也是拆分成几个同步流),流在有多个中间操作时,执行顺序也是流水线一样,一个元素执行完第一个中间操作就会执行第二个中间操作,等第一个元素执行完终端操作后下一个元素才开始执行,并不是所有元素都执行完第一个中间操作然后所有元素再执行第二个中间操作。
2.5.1、 filter 谓词复合
filter接受一个Predicate<T>(签名为 T -> boolean)作为参数,可以用and与,or或,negate非连接多个谓词,但运算顺序是从左到右的,如a.or(b).and(c)的意思是(a || b) && c。
2.5.2、 收集器
终端操作collect接受一个收集器Collector做参数,Collectors提供了一些方法返回一个Collector:
toList
toMap(Funtion<A,B>,Function<A,C>),collect之后返回一个以B为key,C为value的map,但B不能重复,否则会报重复插入的异常。
maxBy(Comparator<T>),
summingInt,sumingLong,summingDouble,
averagingInt,averagingLong,averagingDouble
summarizingInt(返回一个IntSummaryStatistics类型的结果,里面由count,sum,min,average,max属性,相应的还有long的和double的)
joining连接字符串,可接受一个String参数,用这个连接如joining(",").
reducing归约,不同于终端操作reduce,它接受三或一个参数:初始值,转换操作,归约操作或者不用转换直接传归约操作。collect(reducing)返回的和reduce一样也是一个Optional.
groupingBy(Function<T,R>),collect后返回一个以R为key,List<T>为value的map。也可以接收两个参数groupingBy(Funtion<T,R>,收集器),以R为key,收集器结果为value的map,比如作为参数的收集器也是一个groupingBy这样可以实现多级分组。collectingAndThen<收集器,Function<T,R>>收集器用来把参数里收集器的结果再做一次操作,mapping<Function<T,R>,收集器>用来把流里的对象做一下映射再收集。collectingAndThen和mapping常作为groupingBy的第二个参数进行分组后的加工。
partitioningBy分区,传个谓词,返回一个map,key只有true和false
收集器实现的是Collector接口,包含以下五种方法:
1、Supplier<A> supplier();
用来创建一个空的累加器实例,不需要任何参数传入,返回一个Supplier<A>,A就是收集后返回的结果类型,比如toList()的supplier就是:
- public static Supplier<List<T>> supplier(){return () -> new ArrayList();}
- public static BiConsumer<List<T>,T> accumulator{return (list,item) -> list.add(item);}
3、Function<A,R> finisher(); 最后对累加器进行转换,给出最终结果比如toList(),supplier提供的就是需要的结果类型,所以只需要传进来什么传回设呢,所以:
- public static <span style="color:rgb(52,52,52);font-family:'Source Code Pro', monospace;font-size:12px;white-space:pre-wrap;">Function<A,R> finisher()</span>{return Function.identity();}
4、BinaryOperator<A> combiner(); 提供并行时各个子部分如何合并,比如toList
- public static <span style="color:rgb(52,52,52);font-family:'Source Code Pro', monospace;font-size:12px;white-space:pre-wrap;">BinaryOperator<A> combiner()</span>{return (list1,list2) -> {list1.addAll(list2);return list1;}}
5、Set<Characteristics> characteristics();返回一个不可变的Charateristics集合,定义了收集器的行为。Characteristics包含三个项目的枚举:UNORDERED——归约结果不收留中项目的遍历和积累顺序的影响;CONCURRENT——accumulator函数可以从多个线程同时调用,且该收集器可以并行归约流,如果没被标位UNORDERED,流只有在无序数据源才可以并行归约;IDENTITY_FINISH——表明返回的是一个恒等函数,累加器不加检查的转化为结果是安全的,toList就是。
通过以上可以自定义收集器了。
2.5.3、peek
peek是一个中间操作,接收一个Consumer函数为参数,多用来debug时展示流在中间过程后的结果。
3、并发和其他改动
3.1 并行流原理
可以在stream中间操作时调用.parallel()把流转化为并行流,也可以直接用parallStream创建并行流。
并行流内部使用了ForkJoinPoll(分支合并框架,jdk7引入),默认的线程数量是处理器的数量,可以通过ForkJoinPool.common.parallelism改变线程池大小。
过去的分支合并框架操作的对象是RecursiveTask任务,内容是将一个任务按照一定规则(规则在RecursiveTask内的方法自定义实现)分成多个任务,每个任务继续分,分到不能再分为止,这些任务差不多被平均分配到ForkJoinPoll的所有线程上,每个线程都为分配给他的任务保存一个双向链式队列,每完成一个任务,就用队列头上取出下一个任务开始执行,若某一线程空了,会通过工作窃取(work stealing)“偷”走任务去执行,提高效率。java8里有新的自动机制去拆分流,就是下面要说的 Spliterator。
3.1.1 Spliterator
Spliterator接口包含以下四种方法:
1、boolean tryAdvance(Consumer<T> action),行为类似Iterator,按顺序操作Spliterator的元素,如果还有其他元素要遍历就返回true。
2、Spliterator<T> trySplit(),决定流的划分逻辑,划出去一部分元素形成新的Spliterator返回
3、long estimateSize(),估算剩下多少元素要遍历
4、int characteristics(),返回int对应相应的Spliterator特性码
stream在创建时是需要spliterator为参数的,可以看构造方法的源码,有特殊需要时,可以自定义spliterator进行分割并行。
3.2 默认方法
默认方法是java8在interface里新加的机制,方法声明前带着default关键字,可以实现方法的具体逻辑,实现带有默认方法接口的类不必须重写默认方法。这些抽象类和接口更相似了,java8之所以提出这么个东西是因为改的太多了,改了接口之后之前的类库要全部一起改,为了兼容老类库,就弄出了默认方法这东西。
3.3 Optional
Optional<T> 把值再封装一层,为了防止流里出现空指针异常,取值直接get就行
3.4 异步CompletableFuture
对Future,Callable的升级,工厂方法创建CompletableFuture对象:CompletableFuture.supplyAsync(Supplier<T>)返回一个CompletableFuture<T>,对结果进行get(抛异常)或join(不抛)可以拿到结果(一直等待异步执行,知道有结果),get还可以设定等待的时间,超过这个时间就抛异常。supplyAsync还可以多传入一个Executor参数,根据具体情况设定线程池大小,达到最优并行效率。对于多个异步操作CompletableFuture.allOf(CompletableFuture[]).join可以保证所有的都执行完,同样anyOf可以任意一个执行完就可以拿它的返回值。
3.5 时间
java8的时间api提高线程安全,想解决Date和Calendar各自缺陷,新佳乐LocalDate,LocalTime,Instant,Duration,Period
3.6 重复注解
java8之前不允许重复注解,例如
- @Author(name="roc") @Author(name="qiang")
- class Book{}
java8里只要在注解里加@Repeatable注解,在提供一个数组作为多注解容器:
- @Repeatable(Authors.class)
- @interface Author {String name();}
- @interface Authors{
- Author[] value();
- }
那么上例中的多注解就可以支持,编译时会被认定为
- @Authors({@Author(name="roc"),@Author(name="qiang")})