看源码的时候因为不少地方用了新特性,如果不知道会看得很难受,我实在没法忍受这种感觉…跟着上次λ学习的脚步,这次来学学核心的Stream编程以及其他常用java8新特性
0x00.发展
认真说来,我对java最深的应该是在jdk1.6的时期,经过了1.5的大变.Java达到了一个成熟期.
然后1.8时期,可以说是1.5后最大的改变. 通过上次学习λ就知道,函数式编程的确优雅许多,对任何追求优雅的人来说是必不可少的.但是单纯使用λ还是不够灵活,所以这里要配合其他特性来说.
先看看1.5~1.7版本的常用高级特性:
- 枚举(enum)
- 泛型(
<T>) [单独抽文章来说明] - 并发库(Concurrent)
- for-each遍历
- try-with-resource (
try(){}) - 可变参数(
T... t)
那么1.8我们需要着重了解的特性就是: (注: 8借鉴了优秀开源包比如Guava,Apache Common)
- Stream流式编程和λ表达式
- 函数式接口
- 接口默认方法和静态方法
- Optional判空类 (读
Optional源码) - 新的时间日期API
- 并行数组 && 方法引用
0x01.函数式接口与λ
函数式接口就是指只有一个函数的接口,并且为了保证只有一个普通方法,还加了个新的注解@FunctionalInterface 来检查. 初听可能觉得这有什么卵用… 但是它是Java实现函数式编程的一种巧妙途径… (悬而未决)
还是通过源码中的实际使用来学习吧, 因为东西其实挺多的..
n.Map中新增的compute()
这是Java8新增的, 底层是
concurrentHashMap实现,其中有个粒度锁是个比较大的亮点. (what?)首先来一句话总结:
compute()及其子方法, 其实就是把Map的put(),remove(),replace()整合了一下.方便λ调用..
首先,compute()从字面上看是计算, 是一个抽象的概念, 好在我们经过了图的磨练….已经比较习惯计算这个说法了, 然后注意关键词 : Present(存在) ,Absent(不存在) ,它们会在后续出现,并帮助理解
首先给个初始化的Map数据:
1 | Map<Integer,String> kvs = new HashMap<>(){{ //注意<>写法可能需要Java9优化 |
0x02.Stream处理数据
顺便说一句,除了流式处理,它还有个小弟,并行数组.这个之后看看放哪说.
0x03.Optional用法
虽然个人觉得引入Optional 的处理方法目前并不够优雅, Optional 的参考可能是来自Scala ,但是很多时候并非单纯引入语法糖就能有对应的好处…
0x04.日期API
- Instant 取代 Date : 用于直接输出时间戳
- Duration:持续时间,时间差
- LocalDateTime 取代 Calendar :包含日期和时间
- DateTimeFormatter 取代 SimpleDateFormat : 格式化日期输出
1 | //直接获取当前的日期+时间 e.g: 2018-05-30T14:22:33.322 |
可以发现熟悉之后会方便很多,整个设计思想也是借鉴了好的第三方时间库。
整个专门的time包里面包含了大量时间操作的类和方法,好好研究一下就不用重复造不安全的轮子&冗余的xxxUtil了,后续继续补充,重点包括如何改造老旧的Date和SDF函数,快速的升级并记录文档
关键是要搞清楚,为什么要格式化,格式化之后怎么存,跟谁对接,Date也是一个Object而已,但是千万注意转化老代码后的测试. 以及兼容问题…
以下是java8之前的一些特性的补充,因为网上很多人说的不清不楚,我有了自己的理解。之后多了单独拆出来说.
0x05.可变参数
语法type ... name ,只能放在最后,本质是语法糖等价于数组[]。可是它的意义到底在哪呢,很多人没说清楚,没有举关键例子,光说缺点去了。。这个语法糖在某些场合是很有用的,先说结论:
- 如果某几个参数是可选的,且是相同类型,那么推荐使用可变参数,而不是写多个方法重载(太鱼)
- 可变参数有时候不需要使用
new去实例,所以调用更简单. - 可变参数即使不传任何值,结果也不是null,因为被提前初始化了. 应该用
params.length==0去判断.
再上例子:
1 | void f1 (String[] strs){...} //里面遍历输出strs,下面f2也是 |
看了这个例子,相信你会清晰很多,可变参数是语法糖,但是在对用户提供接口的时候会友好许多,但是切忌不要滥用.
还记得C Primer Plus里的经典话“自由的代价是永远的警惕”。也不要上层方法用String... ,下面的方法入参又改为String[] ,虽然本质一样,但是这样会有很大隐患.
0x06.方法引用
方法引用的语法比较特别 ,
::(双冒号) , 语法格式常见是类名::方法名或对象::方法名或类名::new, 一般用来简写λ表达式 .底层本质是Function <T,R>(T代表传入的类型(type) , R表示返回类型return)
其实之前说λ时已经用过它, 但是那时候不清不楚. 所以后续基本除了输出函数的简化, 其他基本没用..
1 | //想要遍历输出list的元素,但是obj这里其实有点多余.只是一个临时变量.括号多了看起来也繁杂 |
看了这个你可能会觉得很奇怪, 虽然这是很大程度简写了代码, 但是看起来这种推断远比 var i = 1 去推断整形要难的多, 这样写感觉比较晦涩, 不会容易出错么? 下面看看原理分析:
常见我们说的方法引用有4种具体情况:
- 指向某个对象(
student)的方法 - 指向某个类型(
String)的方法 - 指向某个静态方法(Arrays.
sort) - 指向构造方法(
new HashMap())
首先, lambda在java中其实就是一种类似的推断语法糖, 帮我们分析了类型然后简化了以前的繁琐语法. 在此基础上, 我们进而再想简化 ,甚至不想看到那个临时的变量 x, y 或是() 的传入, 就衍生出了方法引用. 让编译器从简化的代码中推断我们想要做的事情 .好比:
例子: 我想要买一只狗 --方法引用--> 买狗 (我们输入买狗, 编译器自动帮我们还原缺失的部分)
但是那你肯定会说 , 那如果情景稍微复杂一点, 这个推断是不是就\可能会不支持或者推断失败呢? 当然! 比如你System.out::println 就只能单纯输出一组元素, 而不能做其它修饰或者组合.
比如可以试着读读以下方法引用, 你觉得它还原为λ表达式是什么样, 还原为最初是什么样?
1 | /7 //通用的变量,想想处理完后是什么,功能是什么. |
所以需要你对所使用的方法引用类型, 返回值, 是否static足够清晰. 不过推断不了IDE一般也会自动提示. 所以这个问题目前还好.但是需要注意 , JVM本身并不支持方法引用, 也就代表着VM的执行引擎并不会知道方法引用这个概念, 它本身还是去读取编译器推断后的原本接口对象/函数 .其实也还是语法糖.
从表面看,大家可能觉得方法引用就是用来简化类/对象调用各种方法的. 其实不然, 它本质注意只能用于函数式接口的方法. (也就是你从IDEA点::跳转的类一定是函数式的接口) ,你并不能随便用方法引用调用你的普通接口, 或者随便的省略简化. 并不是你想的那么简单~
几个建议:
- 没了解
map , reduce ,filter前, 不要使用方法引用. - 不熟悉
super,this以及元素生命周期 的基本使用之前, 不要使用方法引用 - 没搞清楚
lambda和函数式接口/stream前,不要使用方法引用 - 不清楚
Comparator ,Predicate, Arrays, Function<T,R> ,Consumer的关系前, 不要使用方法引用 - 不确定方法引用的推断效果或还需要反复思考时 ,不要使用方法引用
- 使用方法引用的方法/对象名一定要足够清晰, 不然会对其他阅读者产生很大理解障碍
- 使用方法引用后 , 用IDEA的自动转换
lambda功能(甚至for-loop)确认一下是否是匹配.
个人觉得, 方法引用已经是lambda编程中比较高阶的用法, 真正用好用到融会贯通, 肯定是基于已经有了扎实的函数式编程基础和对整个集合/流/Optional的使用.
最后, 方法引用肯定是λ可替代的. 新手建议仔细阅读, 小心使用.