java 8学习随笔

最近学习了下函数式编程,归纳记录下学习笔记,方便以后复习回顾,也share给可能感兴趣的同学,节约一些学习时间。

Java 为啥要引入Lambda?

  • 让并行(parallel)程序的编写更加容易
  • OO聚焦于对数据的封装和抽象,函数式聚焦于对行为的抽象(注:从而行为也有了类型)
  • 让写码更容易,更易读,Bug更少 (注:主要是从how到what;附带地能让代码写的不啰嗦比较简洁)
  • 把行为做为数据传递,可以比较容易地写出只在必要时才执行的lazy code,对性能比较好 (注:e.g. logger.debug(expensiveOperation()) --> logger.debug(pointerToExpensiveOperation))

函数式编程的实质
问题域 = 若干个不可变的值 + 一堆翻译/转移函数 (注:=一个状态机)

类型问题
Java8强化了类型推断,在编译器可以推断出类型的时候可以在代码中不显式地声明类型

Lambda的参数类型推断原则

  • 如果target函数类型只有一个可能性,那么从Target的functional interface声明中推断参数类型
  • 如果target函数类型有多个可能性,更具体的函数类型优先级更高,比如FunctionA extends FunctionB, 那么FunctionA的优先级更高
  • 如果target函数类型有多个可能性,且没有哪个函数的类型更具体,那么必须手动指定类型
    虽然Lambda可以写更简洁的代码,同时Java8强化了类型推断(注:在可以推断出类型的地方可以不写类型声明),但是Lambda代码依然是静态类型的,有时为了代码的可读性,加上类型声明更好
    虽然Lambda close over的变量在Java 8中不必显示地声明为final,但它必须实质上是final的,也就是不可二次赋值(注:一个collection的reference虽然不可2次赋值,但是这个collection自己是可以进行写操作的),换句话说这里是使用value而不是variable
  • 注1:从编译器的角度,也只有final,才可以实现close over,这样在栈里保持一个变量Reference就好了
  • 注2:状态是Bug之源,通过对值而不是变量的引用,能减少一些tricky的Bug,在分布式系统中更是如此

行为/函数的类型由“Functional Interface”定义(注:感觉是为了兼容函数式和Java的静态类型引入的,语言层面不是很简洁),一个Functional Interface里面只有一个函数(不算default 和 static 函数),也正因为只有一个函数,所以编译器就可以比较容易地做类型推断了。Java 8中关键的Functional Interface如下:
通过给一个接口加上@FunctionalInterface这个annotation可以让编译器来检查该接口的定义是否符合Functional Interface的要求
Method Reference: ClassName::methodName

Steam
Java 8 中函数式体现最明显的地方就是集合操作(注:多数函数式语言的唯一数据类型就是集合),主要集中体现在java.util.stream这个package.
Stream是对集合的一个包装,然后抽象为一个数据流

  • 注1:因为是一个流,所以隐含着一个流只能被操作一次,再次操作就得再次把底层的collection包装为一个流
  • 注2:也因为是一个流,所以一些collection上比较方便的操作都变得不方便了(比如size,split,duplicate,last,index),需要反复包装底层collection为流,这一点不如clojure等函数式语言方便,感觉是Java8的一大缺陷

关键概念:外循环 → 内循环,命令式的写法,需要告诉代码如何遍历一个数据集;函数式写法,只需声明遍历时要干什么,怎么遍历则交给系统类库解决

  • 注1:这使得更容易地编写并行代码成为可能
  • 注2:这也是IOC (Inverse of Control)思想的一个直接体现
  • 注3:本来没觉得这有多大不了,但想想一个程序的基本控制结构也就是 if, for等有限的几个,所以这个内循环的变化对代码风格的影响是非常深远的

Terminal Operation 和 lazy Operation:一般Mapper是lazy的,因为这里只是声明要干什么,而Reduce一般是Terminal operation,也就是这种操作需要真的遍历流,做函数/表达式的evaluation
Lazy Operations:map, flatmap, filter, predicate,partitioningBy, groupingBy…
Terminal Operations: reduce, collect, max, min, maxBy, minBy, size,forEach…
当对基本数据类型操作时,使用基本类型的Stream比通用的Stream性能要好一些 (e.g.IntStream),这在数据量比较大的时候尤为明显
Stream中元素的遍历顺序依赖于包装的底层的collection的顺序
如果一个输入的stream是无序的,那么对该stream操作后输出的stream也是无需的
即使一个stream是有序的,如果进行并行操作那么也不能保证元素的操作顺序

并行(Parallel)

  • 通过ParallelStream来把操作并行化:Collection.parallelStream()
  • 并行操作潜在意味着各个操作是1) 彼此独立的 2) 不依赖顺序的 3) 最终的操作结果是可合并的,简单说就是可以map-reduce的
  • 避免在并行的操作中使用锁,应该尽量让操作无状态(注:比如只从输入参数接受状态数据)
    影响并行操作性能的一些因素:
  • 操作数据集的大小,如果数据集很小,窜行操作有时比并行操作更高效
  • 如果map-reduce本身比要并行的操作消耗的性能更多,窜行操作有时比并行操作更高效
  • CPU个数
  • 是否是基本类型,基本类型比wrapper类型要高效很多
  • 底层数据类型是否容易做split:ArrayList, Array, IntStream.range性能比较好,因为支持随机访问;LinkedList性能很差,因为只能顺序访问;HashSet和TreeSet介于两者之间,性能一般;

高阶函数
消费或者返回一个函数的函数

高阶函数使得我们可以在更高的层面进行抽象,而通过参数函数化的办法把不相干的细节交给外部(注:再次体现IOC),从而写出bug更少的代码

高阶函数使得我们的编程思考方式从How 到 What成为可能

Default Method
为什么要引入Default Method, 主要目的是向后兼容。比如Java 8 在collection之上增加了很多新的特性,为了保证Java 8 之前的代码在Java 8 上也可以运行,就需要修改以前的代码使其也具有这些新引入的特性,对于官方API还好办,把以前的类库重写一下就OK了,但是由于以前的类库是开放的,如果由第三方的开发者实现了这些开放的接口,没有办法保证这些第三方也修改他们的代码增加这些新引入的特性,从而也就无法100%确保他们的代码在Java 8 上也可以运行。基于此,Java语言不得不引入接口的Default Method, 相当于帮助第三方对新特性做一个默认的实现,从而保证向后兼容性 (注:比较丑陋的搞法,但为了兼容历史,只能如此不得已而为之了)

对继承和多态的影响:最核心的记住Default Method都是虚方法就OK了

具体的继承规则(注:简单说就是具体比抽象的优先级更高)

  • class比接口的优先级高:如果一个接口里有一个default method,其子类又自己实现了这个method,那么子类中的实现优先级更高
  • 子类比父类优先级高:如果一个接口派生自另外一个接口,两个接口都有同样一个default method,当一个类同时实现这两个接口时,将继承子接口中的default method
  • 如果上述两条规则不成立,那么子类要么自己实现该方法,要么将该方法声明为abstract
  • 虽然接口的Default Method带来了多继承的问题,但多继承问题最核心的还是多个数据(状态)的继承问题,而Default Method只是行为层面的多继承,问题并不是很大
  • 接口中的静态方法,类似Default Method,也是Java 8 引入的一个新的语言特性,不过没啥特别,因为是静态方法,放哪其实都OK,但是有时候有些共性的方法放在一个接口的具体的哪个实现都比较怪,所以放在接口中也比较自然

Optional
Java 8中一个新的语言特性,NPE(Null Pointer Exception)可以说是无数RD心中永远的痛,通过引入Optional,让我们在语言层面强制Optional变量的使用者检查值是否为null从而避免一些Bug。明确指定实际期望的值(通过orElse方法,如果计算orElse的值比较消耗性能,可以调用orElseGet()传入一个函数/Supplier来延迟计算)
Lambda的一些应用场景
反复地查询/操作一个对象,然后做些计算再把一个值传回给这个对象,这时可以用IOC的思路把计算值的操作当做参数传给这个对象,这也是外循环 -> 内循环思路的一个体现
比如类似下面这样的写法
if (logger.isDebugEnabled()) {
logger.debug("Look at this " + expensiveOperation());

}
可以写成
logger.debug(() -> “Look at this” + expensiveOperation());

一些需要通过lazy来优化性能的场景

为了进行某个特定的计算,在该计算之前和之后不断重复代码的场景。比如下面列举的常见的数据库访问场景,执行一句SQL需要执行下列重复操作,此时就可以用IOC注入函数来简化代码
在执行前获取数据库连接,创建PreparedStatement 和ResultSet对象
在执行代码前后加上Try,Catch
在执行后加上Finally
各种回调的使用场景
匿名内部类的使用场景 (注:提高代码可读性)
策略模式的使用场景(注:简化类的继承体系)
命令模式的使用场景 (注:简化类的继承体系)
观察者模式的使用场景(注:简化类的继承体系)
模板模式 (注:用IOC的方式来简化类的继承体系)

Lambda给UT带来的挑战
由于很多Lambda函数是没有名字的,所以导致UT无法直接调用这些函数而进行测试,解决这个问题有两个办法:
我们要UT的public的代码行为,所以不需要测试这些Lambda函数,只要测试调用这些函数的有名字的函数就好了
先把相关的逻辑写成一个具体的函数,然后在需要使用Lambda的地方使用Method Reference

Lambda给Debug带来的挑战
当代码逻辑比较复杂时,我们有时希望能做Debug或者在代码执行的过程中打印一些log出来,但是函数式编程中很多代码是Lazy执行的,这导致Debug和Logging变得比较麻烦。如果用一些Terminal 操作(如forEach)真的对Stream进行遍历,又导致该Stream先被使用而影响正常的业务逻辑。为了应对这个两难的局面,Java 8 在Stream接口中提供了一个peek()函数,使得我们在执行正常业务逻辑的同时,还可以检查具体操作的元素的值,同时还能把这两个操作解耦开来

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

拥春飞翔

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值