经典伴读系列文章,想写的不是读书笔记,也不是读后感,自己的理解加上实际项目中运用,让大家3-4天读懂这本书
一、为什么要关心java8
当看到这篇文章或者想要看《java8实战》这本书的时候,说明你可能正在为工作中出现的各种各样“奇怪”的语法结构而感到困惑。不知什么时候起java已经让你有些不认识,或是总听到函数式编程,想弄清楚那到底是什么,没关系,让我们花点时间彻底解决它们。
1、像大多数技术书籍一样,第1章从总体概述开始,介绍java8新特性
- 方法引用
- Lambda表达式
- Stream流
- default默认方法
- Optional
这些名字工作中或多或少都听到过,这里先把它们当作新学期的课表,知道我们要学习什么就可以了。
2、对函数的概念要有清晰的认识
编程语言中的函数一词通常是指方法,尤其是静态方法;这是在数学函数,也就是没有副 作用的函数之外的新含义。幸运的是,你将会看到,在Java 8谈到函数时,这两种用法几乎是一 致的。
这里把编程语言中的方法和函数做了明确区分,这里的函数指的是单纯的数学函数,只用于计算或逻辑,没干别的,如:在函数中修改成员变量的值,访问数据库,访问网络等。这些都是所谓的“副作用”。java8希望大家在使用上区分开函数和方法。从面向对象的思维中,开辟出一条新路面向函数。
编程语言的整个目的就在于操作值,要是按照历史 上编程语言的传统,这些值因此被称为一等值
这里定义了可以操作或传递的值就是一等公民,java8和以往最大的不同,就是将方法引用或函数本身作为值,变成一等公民。当然类还是二等。函数式编程的种子从这里开始埋入,像不像小说开头留下的最大伏笔?
二、通过行为参数化传递代码
java8新特性中Lambda表达式最出名,而使用Lambda表达式最大的好处之一,当然就是简化代码。本章旨在激发你的兴趣,直观感受简化代码的过程,因此,不要在意Lambda语法细节,这并不是本章重点。
1、本章标题有些拗口,第一次看我也不明白,其实“行为”就是算法。换句话说就是将算法当做参数传递。那么封装算法成了必要条件,自然联想到设计模式中的策略模式。现在假设我们要选苹果,有时选大的,有时选红的,有时要选又大又红的。每一种选择都是一个算法。
/*
* 一个返回boolean值的函数称为谓词
*/
interface Predicate<T> {
boolean test(T t);
}
/*
* 选大苹果
*/
static class AppleWeightPredicate implements Predicate<Apple> {
@Override
public boolean test(Apple t) {
return t.getWeight() > 100;
}
}
/*
* 选红苹果
*/
static class AppleColorPredicate implements Predicate<Apple> {
@Override
public boolean test(Apple t) {
return t.getColor().equals("red");
}
}
/*
* 按照要求过滤苹果
*/
static List<Apple> filterApples(List<Apple> apples, Predicate<Apple> p) {
List<Apple> fas = new ArrayList<>();
for (Apple apple : apples) {
if (p.test(apple)) {
fas.add(apple);
}
}
return fas;
}
public static void main(String[] args) {
List<Apple> apples = new ArrayList<>();
apples.add(new Apple(80, "red"));
apples.add(new Apple(100, "red"));
apples.add(new Apple(120, "green"));
apples.add(new Apple(140, "red"));
//我要大苹果
List<Apple> fas = filterApples(apples, new AppleWeightPredicate());
System.out.println("大苹果:" + fas);
//我要红苹果
fas = filterApples(apples, new AppleWeightPredicate());
System.out.println("红苹果:" + fas);
}
大苹果:[ [weight=120, color=green], Apple [weight=140, color=red]]
红苹果:[ [weight=80, color=red], Apple [weight=100, color=red], Apple [weight=140, color=red]]
如果这时要又大又红的苹果,是不是得再写一个算法类AppleWeightAndColorPredicate。
2、上面的策略模式其实已省略上下文类Context,现改用Lambda表达式重构,感受下极致的简化(Lambda语法细节先不考虑),这次连算法类AppleWeightPredicate,AppleColorPredicate都直接省略掉。
public static void main(String[] args) {
List<Apple> apples = new ArrayList<>();
apples.add(new Apple(80, "red"));
apples.add(new Apple(100, "red"));
apples.add(new Apple(120, "green"));
apples.add(new Apple(140, "red"));
//要大苹果
List<Apple> fas = filterApples(apples, a -> a.getWeight() > 100);
System.out.println("大苹果:" + fas);
//要红苹果
fas = filterApples(apples, a -> a.getColor().equals("red"));
System.out.println("红苹果:" + fas);
//还要又大又红的苹果
fas = filterApples(apples, a -> a.getWeight() > 100 && a.getColor().equals("red"));
System.out.println("又大又红:" + fas);
}
大苹果:[[weight=120, color=green], [weight=140, color=red]]
红苹果:[[weight=80, color=red], [weight=100, color=red], [weight=140, color=red]]
又大又红:[[weight=140, color=red]]
3、上面的例子体现了算法不仅可以封装,还可以当做方法参数传递,Lambda表达式在平常项目开发时,已经有很多“真实例子”,如:Runnable 和 Comparator接口 分别对应"开线程 "和"比较"两种行为。
new Thread(() -> {
apples.sort((a1, a2) -> a2.getWeight().compareTo(a1.getWeight()));//按照苹果重量倒序
System.out.println("倒序:" + apples);
}).run();
三、Lambda表达式
第一部分的重点,从语法到应用具体分析Lambda表达式。请沉下心理解,上主食。
1、初识
用个大家熟知的概念类比,先可以把Lambda表达式看成是java中匿名内部类的简化。
//使用匿名内部类
List<Apple> fas = filterApples(apples, new Predicate<Apple>() {
@Override
public boolean test(Apple a) {
return a.getWeight() > 100; //选大苹果
}
});
//使用对等的Lambda表达式
fas = filterApples(apples, a -> a.getWeight() > 100);
System.out.println("大苹果:" + fas);
2、Lambda表达式格式
(1)(参数列表) -> 表达式,这里默认返回表达式的值,如:
(Apple a) -> a.getWeight() > 100
(2)(参数) -> {语句},如:
(Apple a) -> {
boolean b = a.getWeight() > 100;
return b;
}
2、函数式接口
java8想要引入的函数式编程,把函数当作值一样传递,也就是所谓的一等公民,那么整形的类型是int,字符串的类型是String,函数的类型呢?如何申明函数变量,函数自己被当做参数调用另一个方法时,形参类型又是什么?
如上例中的Predicate,这是一种特殊的接口,只有一个抽象方法,称为函数式接口。可以添加@FunctionalInterface注解,它不仅是一种标志,还能在编译时限制函数式接口不能出现多个抽象方法。(可以有多个default方法,后面再说)
/*
* 一个返回boolean值的函数称为谓词
*/
@FunctionalInterface
interface Predicate<T> {
boolean test(T t);
}
有了函数类型,函数就能和普通变量一样,声明定义,
Predicate<Apple> p1 = a -> a.getWeight() > 100;
Predicate<Apple> p2 = (Apple a) -> {
boolean b = a.getWeight() > 100;
return b;
};
好了,现在回头思考下Lambda是什么,Lambda并不能完全认为是匿名内部类的语法糖,除了明显简化了代码之外,他们的产生的初衷不同。在一切皆是对象的美好愿望下,匿名内部类,当只需要创建一个对象时使用,本质传递的是对象(有多个方法),是面向对象编程的产物。而Lambda表达式,却是java想要引入函数式编程的基石,虽然表面上也是一个接口类,本质却是函数,Java8的设计师们想让我们把它只当做函数,请忽略它的接口类名吧,奈何受限于JDK的兼容性,不允许函数脱离类单独存在。因此我更想称它为匿名函数,java的闭包。
主角的背景终于被揭开,像不像皇帝流落民间的小儿子。
3、函数描述符
每一个方法都有方法签名,让编译器检查传入的参数类型和返回值类型是否正确。那么对于Lambda表达式或者说匿名函数,编译器如何做类型检查。答案是函数描述符,即函数式接口的抽象方法签名就是Lambda表达式的签名。如:
public boolean test(Apple a) 函数描述符是 (Apple a) -> boolean
由此有了一个想法,以前的java方法主要通过方法名,方法参数区分,但匿名函数的函数描述符中只有参数类型和返回值类型,没有方法的名字。那么是否可以创建一套公用的函数式接口,如:
/*
* 无参数
*/
@FunctionalInterface
interface Function0 {
void apply();
}
/*
* 1个参数
*/
@FunctionalInterface
interface Function1<T, R> {
R apply(T t);
}
/*
* 2个参数
*/
@FunctionalInterface
interface Function2<T, U, R> {
R apply(T t, U u);
}
这样一来不必每次使用Lambda之前还得写个函数式接口,像Predicate这一类的,都用同一套公用接口就好。现在让我们写一个测试方法耗时的工具,如:
/*
* 检测方法耗时(无参数的函数式接口)
*/
public static void testMethodTime(Function0 func) {
System.out.println("开始执行...");
long begin = System.currentTimeMillis();
func.apply();
long end = System.currentTimeMillis();
System.out.println("执行结束,耗时:" + (end - begin) + "ms");
}
/*
* 按照要求过滤苹果(1个参数的函数式接口)
*/
static List<Apple> filterApples(List<Apple> apples, Function1<Apple, Boolean> f) {
List<Apple> fas = new ArrayList<>();
for (Apple apple : apples) {
if (f.apply(apple)) {
fas.add(apple);
}
}
return fas;
}
public static void main(String[] args) {
testMethodTime(() -> {
List<Apple> apples = new ArrayList<>();
apples.add(new Apple(80, "red"));
apples.add(new Apple(100, "red"));
apples.add(new Apple(120, "green"));
apples.add(new Apple(140, "red"));
// 我要大苹果
List<Apple> fas = filterApples(apples, a -> a.getWeight() > 100);
System.out.println("大苹果:" + fas);
});
}
开始执行…
大苹果:[[weight=120, color=green], [weight=140, color=red]]
执行结束,耗时:1ms
4、使用通用的函数式接口
正当我们高兴之余,发现JDK中有同样一个Function类,深入一看,发现我们重复造了轮子。Java8的设计师们早已内置了通用的函数式接口,全部都在java.util.function包中。可以按照参数个数分类。
(1)单参或无参的函数式接口
- 用于判断
@FunctionalInterface
public interface Predicate<T> {
boolean test(T t); //函数描述符:(T t) -> boolean,
- 用于消费
@FunctionalInterface
public interface Consumer<T> {
void accept(T t); //函数描述符: (T t) -> void,
- 用于加工
@FunctionalInterface
public interface Function<T, R> {
R apply(T t);
- 用于获取
@FunctionalInterface
public interface Supplier<T> {
T get(); //函数描述符: () -> T
(2)两个参数的函数式接口(单参接口名加Bi前缀,不用记)
@FunctionalInterface
public interface BiPredicate<T, U> {
boolean test(T t, U u); //函数描述符: (T t, U u) -> boolean
@FunctionalInterface
public interface BiConsumer<T, U> {
void accept(T t, U u); //函数描述符:(T t, U u) -> void
@FunctionalInterface
public interface BiFunction<T, U, R> {
R apply(T t, U u); //函数描述符:(T t, U u) -> R
那么有三个参数的函数式接口,是否要加Tri前缀?,这个JDK8到没有,我们可以自己扩展。
(3)比较器接口
@FunctionalInterface
public interface Comparator<T> {
int compare(T o1, T o2); //函数描述符:(T o1, T o2) -> int
另外,UnaryOperator 和 BinaryOperator 用于一元 和 二元的计算,完全可以被Function替代。因此只用记住常用的5个,基本不用再写函数式接口。
5、Lambda表达式tips
(1)Lambda和泛型一样都具有类型推断的特点,如:
(Apple a1, Apple a2) -> a2.getWeight().compareTo(a1.getWeight())
省略参数类型后变成:
(a1, a2) -> a2.getWeight().compareTo(a1.getWeight())
这里的参数类型,从函数式接口的抽象方法推断而来。
(2)Lambda表达式中无法修改局部变量
这就和匿名内部类中只能使用final类型局部变量一样,Lambda表达式实际访问的是局部变量的副本。
private int x = 0;
public void add() {
int y = 0;
new Thread(() -> {
x++; //实例变量可以修改+1
y++; //报错,局部变量默认final
}).run();
}
报错原因,书中有一段表达的非常清楚,不再追叙。
你可能会问自己,为什么局部变量有这些限制。第一,实例变量和局部变量背后的实现有一 个关键不同。实例变量都存储在堆中,而局部变量则保存在栈上。如果Lambda可以直接访问局 部变量,而且Lambda是在一个线程中使用的,则使用Lambda的线程,可能会在分配该变量的线 程将这个变量收回之后,去访问该变量。因此,Java在访问自由局部变量时,实际上是在访问它 的副本,而不是访问原始变量。如果局部变量仅仅赋值一次那就没有什么区别了——因此就有了 这个限制。
第二,这一限制不鼓励你使用改变外部变量的典型命令式编程模式(我们会在以后的各章中 解释,这种模式会阻碍很容易做到的并行处理)。
方法引用
既然匿名函数都可以传递,那么有名字的方法能够传递么?当然,只要满足函数描述符。
1、静态方法引用
格式:类名::静态方法名,下面将引用LambdaTest类的testWeight静态方法,过滤出大苹果。
/*
* java.util.function.Predicate<Apple>的函数描述符是(Apple a) -> boolean
*/
public static List<Apple> filterApples(List<Apple> apples, Predicate<Apple> p) {
List<Apple> result = new ArrayList<Apple>();
for(Apple apple : apples) {
if (p.test(apple)) {
result.add(apple);
}
}
return result;
}
/*
* 函数描述符:(Apple a) -> boolean
*/
public static boolean testWeight(Apple a) {
return a.getWeight() > 100;
}
public static void main(String[] args) {
List<Apple> apples = new ArrayList<>();
apples.add(new Apple(80, "red"));
apples.add(new Apple(100, "red"));
apples.add(new Apple(120, "green"));
apples.add(new Apple(140, "red"));
//满足函数描述符,可以引用的静态方法testWeight
List<Apple> fas = filterApples(apples, LambdaTest::testWeight);
System.out.println("大苹果:" + fas);
}
2、实例方法引用
格式:实例对象::实例方法名,下面将先创建出LambdaTest类的实例,再引用实例方法,过滤出大苹果。
/*
* 函数描述符:(Apple a) -> boolean
*/
public boolean testWeightNonStatic(Apple a) {
return a.getWeight() > 100;
}
public static void main(String[] args) {
List<Apple> apples = new ArrayList<>();
apples.add(new Apple(80, "red"));
apples.add(new Apple(100, "red"));
apples.add(new Apple(120, "green"));
apples.add(new Apple(140, "red"));
LambdaTest lt = new LambdaTest();
//满足函数描述符,可以引用实例方法testWeightNonStatic
List<Apple> fas = filterApples(apples, lt::testWeightNonStatic);
System.out.println("大苹果:" + fas);
}
3、内部实例方法引用
格式:类名::实例方法名,需要引用的实例方法,是调用对象内部的方法。这种方式并不容易理解,
我们之前一直通过处理Apple列表,过滤出想要的苹果,而判断苹果的方法都写在LambdaTest类中,无论是静态方法还是实例方法,现在我们不再依赖LambdaTest类,直接将判断的方法写在要处理对象,也就是Apple内部。如:
public static class Apple {
private Integer weight;
private String color;
public Apple(Integer weight, String color) {
this.weight = weight;
this.color = color;
}
/*
* 函数描述符:(Apple a) -> boolean
*/
public boolean testWeightSelf() {
return weight > 100; //this.weight
}
...
现在我们要引用Apple类的实例方法testWeightSelf,就得先知道函数描述符,testWeightSelf没有参数,为什么函数描述符是(Apple a) -> boolean?这是因为java方法默认带有一个this指针,这里可以理解为Apple的方法默认都带有Apple a的参数。
public static void main(String[] args) {
List<Apple> apples = new ArrayList<>();
apples.add(new Apple(80, "red"));
apples.add(new Apple(100, "red"));
apples.add(new Apple(120, "green"));
apples.add(new Apple(140, "red"));
//满足
List<Apple> fas = filterApples(apples, Apple::testWeightSelf);
System.out.println("大苹果:" + fas);
}
再看个例子,按照苹果重量排序
apples.sort(Comparator.comparing(Apple::getWeight)); //按重量排序
Comparator.comparing的方法签名:
public static <T, U extends Comparable<? super U>> Comparator<T> comparing(
Function<? super T, ? extends U> keyExtractor)
看起来好复杂,没错,泛型多了有时会有些晕,参数Function<? super T, ? extends U> keyExtractor,让我们去掉super 和 extends,发现剩下的就是一个参数一个返回值的函数式接口。
@FunctionalInterface
public interface Consumer<T> {
void accept(T t); //函数描述符: (T t) -> R
再看Apple::getWeight的函数描述符是(Apple a)->int ,函数描述符相同,可以传递方法引用。
注意,如果需要引用的内部方法,带有参数(除了默认的this外),如Apple的setWeight方法。
public void setWeight(Integer weight)
这时就需要用多参数的函数式接口类传递方法,如:
BiConsumer<Apple, Integer> bc = Apple::setWeight;
带有参数的实力方法,不建议再使用这种方式传递,会让代码难以理解,更难以维护。
4、构造方法引用
格式:类名::new,之前不知在哪本书中读到构造函数是一种静态方法,也有方法签名,那么自然也能够被引用。给Apple类添加两个子类GoodApple,BadApple,重写toString方法,下面代码不知还算不算是工厂?
public static class AppleFactory {
public static Apple create(Supplier<Apple> s) {
Apple apple = s.get();
apple.setWeight(100);
apple.setColor("green");
return apple;
}
}
public static void main(String[] args) {
Apple apple = AppleFactory.create(GoodApple::new);
System.out.println(apple);
apple = AppleFactory.create(BadApple::new);
System.out.println(apple);
}
GoodApple[weight=100, color=green]
BadApple[weight=100, color=green]
上面的例子调用的是无参构造函数,接着我们看下多参构造函数如何引用?
//无参构造函数,函数描述符:() -> Apple
Supplier<Apple> s = Apple::new;
Apple apple = s.get();
//1参构造函数,函数描述符:(Integer) -> Apple
Function<Integer, Apple> f = Apple::new;
apple = f.apply(100);
//2参构造函数,函数描述符:(Integer, String) -> Apple
BiFunction<Integer, String, Apple> bf = Apple::new;
apple = bf.apply(100, "green");
Lambda表达式复合
1、函数复合
函数既然已经可以作为值一样传递,那么能否将多个函数动态编排成一个新的函数。就像数学中的复合函数一样,如在数学中:
g(x) = x²
f(x) = x+1
复合函数f(g(x)) = x² + 1,当x=2,函数值多少?口答是5,让我们看下对应代码:
Function<Integer, Integer> g = (x) -> x * x; //g(x)
Function<Integer, Integer> f = (x) -> x + 1; //f(x)
Function<Integer, Integer> f_g = g.andThen(f); //f(g(x))
Integer y = f_g.apply(2);
System.out.println(y);
这里g.andThen(f)还可以写成f.compose(g),只是谁复合了谁的问题,用一个就可以了。
另外,还可以看出函数式接口想要复合,主要依靠的是接口内的default方法,如andThen,compose,它们都是Function中的默认方法。(后面再说)
2、谓词复合
函数都能复合,那能否把谓词(返回boolean值的函数)也复合下,又大又红的苹果它又来了。
List<Apple> apples = new ArrayList<>();
apples.add(new Apple(80, "red"));
apples.add(new Apple(100, "red"));
apples.add(new Apple(120, "green"));
apples.add(new Apple(140, "red"));
Predicate<Apple> weightPre = (a) -> a.getWeight() > 100; //大苹果
Predicate<Apple> colorPre = (a) -> a.getColor().equals("red"); //红苹果
Predicate<Apple> weightAndColorPre = weightPre.and(colorPre); //又大又红的苹果
List<Apple> fas = filterApples(apples, weightAndColorPre);
System.out.println("又大又红的苹果:" + fas);
weightPre.and(colorPre),既然有and,当然还有or或,negate非。
3、比较器复合
上文已经提到比较器Comparator也是一种函数式接口,同样也能够复合。我们给Apple类增加甜度sweetness属性。
public Apple(Integer weight, String color, Integer sweetness) {
this.weight = weight;
this.color = color;
this.sweetness = sweetness;
}
按照重量倒序排列,如果同样重,甜度优先。
List<Apple> apples = new ArrayList<>();
apples.add(new Apple(80, "red", 90));
apples.add(new Apple(100, "green", 50));
apples.add(new Apple(100, "red", 80));
apples.add(new Apple(120, "green", 40));
apples.add(new Apple(100, "red", 70));
Comparator<Apple> c1 = (o1, o2) -> o2.getWeight().compareTo(o1.getWeight()); //按照重量逆序
Comparator<Apple> c2 = (o1, o2) -> o2.getSweetness().compareTo(o1.getSweetness()); // 按照甜度逆序
Comparator<Apple> c3 = c1.thenComparing(c2); //先按照重量逆序,再按照甜度逆序
apples.sort(c3);
System.out.println(apples);
[[weight=120, sweetness=40, color=green], [weight=100, sweetness=80, color=red], [weight=100, sweetness=70, color=red], [weight=100, sweetness=50, color=green], [weight=80, sweetness=90, color=red]]
让我们换一种更贴合自然语言的写法
Comparator<Apple> c1 = Comparator.comparing(Apple::getWeight).reversed(); //按照重量逆序
Comparator<Apple> c2 = Comparator.comparing(Apple::getSweetness).reversed(); // 按照甜度逆序
Comparator<Apple> c3 = c1.thenComparing(c2); //先按照重量逆序,再按照甜度逆序
apples.sort(c3);
至此,Lambda表达式的内容全部学完。包含了《java8实战》这本书主要内容的四分之一,你花了多长时间呢?下一篇,将会看到Lambda真正的用武之地。《经典伴读_java8实战_Stream基础》
未完待续