
Java8
1. 引言
写这个文档的主要原因是自己使用java8时间并不长,对java的认知还停留在java6的基础上,很多新功能是:然也,知之不详;所以然也,不知。 所有平时就抽一些时间出来,做些记录和总结,就形成了这个文档,权当学习笔记之用。
2. Lambda
(1)什么是Lambda?
1 | Lambda是从数学中的lambda运算引申出来的,在计算机中主要指匿名函数,也是函数式编程的核心。你可能会问什么是函数式编程,它与命令式编程不同在哪里? 笼统的来说,函数式编程关心的数据映射的问题,命令式编程更关心的是解决问题的步骤。 |
(2)Lambda基本语法
lambda表达式主要包括三部分:Argument List, Arrow, Body
对应的比如:
1 | (int x, int y) -> x+y |
(a) 简单示例
1 | public class RunnableTest { |
上面代码反应了两个情况,一是lambda确实简洁,之前要6行代码做的事情,lambda一行就搞定了;二是其实lambda并没有引入新东西,lambda做的事情,用普通的java也能做,只是效率没那么高而已。
当然还有别的原因,在集合的处理上,lambda表达式能简化多线程或者多核的处理,这也是java8打动人的原因。
(b) Comparator
1 | public class Car { |
上面例子中也明显可以看出,使用Lambda做一些自定义的排序,更为灵活简便。升序和降序只用改变一下位置就可以。注意降序排列跟升序排的写法并不一致,Collections.sort(carList, (car1, car2) -> (car2.getPrice() - car1.getPrice()));
lambda会自动做参数类型推导,所以写的时候省略也是参数类型可以的!
遍历list用了forEachcarList.forEach(c -> System.out.print(c.getPrice() + " "));
(c) ::的使用
使用::有三种方式
- object::instanceMethod
- Class::staticMethod
- Class::instanceMethod
- 第一种方式
1 | public class Greeter { |
上面代码运行后,会调用父类Greeter的greet方法
- 第二种方式
1 | list.forEach(System.out::println); |
上述代码也等价与list.forEach(l -> System.out.println(l));
- 第三种方式
1 | List<String> labels = ...; |
stream也是java8中的核心内容之一,第二章仔细介绍类stream的用法。
Lambda最佳实践
待补充
2. Stream(流)
(a)简介
stream也叫流,大概是java8最受追捧和欢迎的特性,stream最大的特点有两个:
- 代码简介,函数式编程的的写法让代码更易读的同时,长度也缩短了很多
- 多核友好, 程序员几乎不用关心串并行的事儿,吃着火锅,唱着歌就能写出高效率的代码。
(b) stream是如何工作的
1 | List <String> myList = |
流函数有两种类型,一种是中间类型,一种是终止类型,中间类型返回一个stream对象方便大家做优雅的链式调用,终止类型或者说终端类型是返回一个对象或者是吗都不返回,比如List或者map等等。总而言之,stream到这个函数就终止了,所以叫终止类型。
上述例子中, filter, map, sorted就是中间类型, forEach就是终止类型。
大部分stream函数是接受lambda作为参数的, 其实也容易理解,要用lambda定义这个流函数具体要做什么,filter是做过滤,但具体怎么做,过滤哪些,留下哪些?都是要lambda来定义,当然可以用java函数代替,但明显没有lambda优雅,可读性高,这也是为什么要用labmda的原因之一。
注意,不要在流函数中,改变集合,比如增加或者删除元素。
(c) stream类型介绍
流可以从串并行的缴费可以分为stream和parallelStream,即串行流和并行流,但其实一种stream.从类型上有可以分几类,创建方式如下:
- 第一种也是最常见的一种是通过List,Set等对象的stream()方法,获取stream
1 | Arrays.asList("a1", "a2", "a3") |
- 通过Stream.of()函数创建流
1 | Stream.of("a1", "a2", "a3") |
- java8也定义了几种基础类型的流, 比如IntStream, LongStream, DoubleStream
1 | IntStream.range(1, 4) |
基础类型的流跟对象流的用法相同又有所不同,相同之处是他们都继承自BaseStream。 不同之处是部分函数参数有所不同, 比如IntStream使用IntFunction而不是Function, 用IntPredicate而不是Predicate; 基础类型的流支持一些聚合流函数,比如sum, average等。
1 | Arrays.stream(new int[] {1, 2, 3}) |
不同的流类型之间也可以做转换, 比如IntStream可以转为DoubleStream
1 | IntStream.range(1,10).mapToDouble(Double::new).forEach(System.out::println); |
对象流可以转换为基础类型流
1 | Stream.of("a12", "a23", "a34") |
基础类型流可以转换为对象流
1 | IntStream.range(1, 4) |
360度前空翻转体两周半流类型转换
1 | Stream.of(1.0, 2.0, 3.0) |
(d) 流函数执行顺序
流的本质跟sql有些类似,即使是为了获取相同的数据,不同的写法效率也大大不同。
先看一段代码
1 | Stream.of("d2", "a2", "b1", "b3", "c") |
这个的输出结果是
1 | map: d2 |
如果改变下顺序呢?
1 | Stream.of("d2", "a2", "b1", "b3", "c") |
运行结果:
1 | filter: d2 |
map跟forEach仅执行了一次
(e) stream复用
正常情况下stream是不能被复用的,运行下面代码会报错
1 | Stream<String> stream = |
复用的办法是使用supplier每次都构建一个新的流
1 | Supplier<Stream<String>> streamSupplier = |
(f) stream进阶
除了上面例子中用到的filer,map, forEach等还有一些也是会用到的流函数,先构造一个对象List
1 | class Person { |
collect
collect是比较常用的流终止函数,与Collectors类搭配使用,奇妙无穷。
获取Person中name以”P”开头的人:
1 | List<Person> filtered = |
当然也可以获取Set,使用Collectors.toSet()即可。
1 | Map<Integer, List<Person>> personsByAge = persons |
collect好像还好,但groupingBy一开始看有点晕,看下函数源码知道传入的lambda,是作为classfier把item映射成key, collect是会返回Person.age为key, 如果Person年纪相同,会放入同一个list当中。
最后得到结果
1 | age 18: [Paomo] |
Collectors非常灵活,如果你想计算这些Person的平均年龄:
1 | Double averageAge = persons |
获取这些人的一些统计数据:
1 | IntSummaryStatistics ageSummary = |
所有人拼接成一个字符串输出
1 | String phrase = persons |
joining的三个参数,第一个为分隔符,第二个为前缀,第三个为后缀。
Person转换为map
1 | Map<Integer, String> map = persons |
toMap传入的第三个参数是mergeFunction, 即当key相同时,把value合并的函数。可以自定Collector,完成一些定制化的转换
1 | Collector<Person, StringJoiner, String> personNameCollector = |
collector里面要传4个参数,分别的supplier, accumulator, combiner和finisher, 关于这几个函数具体含义后面会有更详细的介绍。但是可以看出collect使用起来非常非常灵活。
reduce
reduce会将stream中的所有元素combine到一块儿,形成一个最终结果。reduce函数有三种重载形式:
1 | Optional<T> reduce(BinaryOperator<T> accumulator); |
BinaryOperator,BinaryOperator其实是继承自BiFunction,主要是两种相同类型的参数,经过运算产生一个相同类型的结果的运算符;BiFunction定义类似,是接收两个参数,产生一个结果的函数。
第一种方式,获取年纪最大的人,代码如下:
1 | persons |
第二种调用方式:
1 | Person result = |
上面方法返回的是一个年纪为list中所有person的age之和,name为所有人名字的拼接结果
第三种调用方式:
1 | Integer ageSum = persons |
啊? reduce只调用了accumulator没调用combiner? 其实这是因为combiner是在串行流中才会用到。
1 | Integer ageSum = persons |
accumulator是并行被调用所以需要combiner把accumalotr中的值归并累加。
(g) Parallel Streams(并行流)
面对数据量比较大的情况,可以使用parallelStream,以充分利用系统的多核和性能,可以使用如下代码获取到当前的并行线程:
1 | ForkJoinPool commonPool = ForkJoinPool.commonPool(); |
针对上面那个例子,我们可以看下,到底每个操作是在哪个线程中执行的。
1 | persons |
可以看出accumulator和combiner都是并行执行的。
3. 新增API
(1) 时间处理
原来时间处理函数存在的问题:
- 线程安全: Date和Calendar不是线程安全的,你需要编写额外的代码处理线程安全问题
- API设计和易用性: 由于Date和Calendar的设计不当你无法完成日常的日期操作
- ZonedDate和Time: 你必须编写额外的逻辑处理时区和那些旧的逻辑
(a) 获取当前日期
1 | System.out.println("localDate: " + LocalDate.now()); |
(b) 判断是否为闰年
1 | //leap year |
(c) 日期比较
1 | // time comparison |
1 | // first day or last day of the month |
(d) 日期格式化
string format容易被忽视,但是也很重要yyyy是指当天所在的年份, YYYY是指当前周所在的年份。
如果不注意跨年可能出现bug(https://www.atatech.org/articles/97733)MM是月份, mm是分钟
1 | // format date |
(e) 计算时间间隔
1 | LocalDateTime finalDate = LocalDateTime.now().plus(Period.ofDays(10)); |
(2)interface的default函数
java8还是支持的接口可以设置default方法
1 | interface MathTool { |
5. 奇技淫巧
(1)捕获多个Exception
1 | try { |
这种每个都捕获的写法并不优雅,其实jdk7就提供了一种相对优雅的方式:
1 | try { |
(2) 字符串拼接
1 | String str = String.join(",", "a", "b", "c"); |