Java8学习笔记(1)

前言

Java8 于2014年发布,新项目基本都开始使用这个版本了。个人工作中经常使用它,有时也会和朋友讨论。讨论中略有感悟,但终归是“一时之快”。又有朋友建议我说,既然你自己理解了,又能通俗地讲出来,为什么不写下来呢?你觉得简单粗浅,但对别人来说就不一定了。

我想了想,觉得“写下来”最大的障碍是“坚持写”。自从离开校园就很少书写了,多年下来的后果就是词汇贫乏、语句零碎,偏向口语、网络语言。比如:一句万能的“卧槽”,通过不同语气音调表达不同含义,甚为好用;某些时候词不达意,找不到表达自己想法的词句;最后,就像这段话一样,啰里啰嗦废话多多。

于是,发挥特长多说废话,就开这个坑了,但愿能坚持下去。

“新”特性

所谓新特性也只是相对上个版本,5年过去也不算新了。如果面试时自称熟悉Java8,面试官大概会问都有些什么新特性,然后追问其中几条,诸如内存模型、GC以及并发、线程池、锁之类的。

口头表达(背诵)的是原理、概念,手上的才是学以致用。光说不练是假,光练不思更是蠢。这里还是按部就班,先讲个人觉得最有趣的“新”特性——函数式接口以及lambda表达式。

一. 函数式接口以及lambda表达式

1. 前情扩展

从此我假设大家都有足够的基础知识

众所周知,在Java中几乎可以在任何地方定义一个class,而在Java中几乎没有函数(function)这个概念,只能在class里面定义一个函数,而且它们一律被叫做方法(method)。这是为什么?

按C++的习惯,定义在class里面的是方法,不在class里面的叫做函数。而在Java这种“class至上”的语言中,所有东西都必定能找到它的类型,不可能存在一个定义在class外面的、被称为function的东西,更没有函数指针。一句话就是:“class是一等公民,function不是”。

但是实际运用中,我们又很需要有这种东西,能描述并自定义一类行为,并且当做参数传递给别人。

其实这种事情非常常见,例如你告诉同桌:“老师过来了就通知下我”,然后开始打瞌睡。即是说,你把 “send _通知_ to _我_” 这种具体的行动过程传递给了别人,这个行为也由别人来执行。当事件真的发生的时候,别人来进行判断、执行。

由于你睡得太死没能收到同桌的提醒,付出了一定的代价。第二天你再次告诉同桌:“老师过来了就弄醒我”,这回传递的是 “send _唤醒_ to _我_”。

那么,既然举了这个例子,难道Java8之前就无法实现吗?当然不是!

回忆(抄写)一下经典代码:

1
2
3
4
5
6
deskmate.addEventListener(new EventListener() {
@Override
public void onTeacherLaunchDetected(Event e) {
deskmate.whisper("老师来了!", me);
}
});

第二天则是:

1
2
3
4
5
6
7
8
deskmate.addEventListener(new EventListener() {
@Override
public void onTeacherLaunchDetected(Event e) {
while(me.isSleep()){
deskmate.awakeByShake(me);
}
}
});

没错,匿名内部类,为了传递一种行为,实例化出来一个描述该行为具体实现的对象,把这个对象传递给别人。

如果经常写事件驱动的程序,比如swing或者Android,应该经常能看到它。匿名内部类本身就是自定义(@Override)某类型的一个或多个方法并直接实例化出该类型的对象,而且还不给这个实现类取名字。

但匿名内部类最大的问题是使得代码逻辑分散,显得冗长,导致可读性差。而在Java8里,这些事情有所改变。

2. 函数式接口(Functional Interfaces)

在Java8 里接口里可以定义已经实现的方法。例如:

1
2
3
4
5
6
7
8
9
10
11
interface AAA {

void doSth1();

default void doSth2() {
System.out.println("invoke doSth2()");
}
static void doSth3() {
System.out.println("invoke doSth3()");
}
}

从这个角度看,接口和抽象类的界限变得更加模糊了。当然,别忘记还是有区别的。当接口里有且仅有一个抽象方法时,它可以被称作“函数式接口”。给它加上@FunctionalInterface 注解,可以让编译器检查它是否符合约定。例如经典的Runnable接口就是一个典型的函数式接口,在Java8里,它也被加上了这么一个注解:

1
2
3
4
@FunctionalInterface
public interface Runnable {
public abstract void run();
}

那么,新增这个约定没什么稀罕的,它的意义是什么?

它的意义是,给Java加上新功能的同时,不破坏原有的代码,尽力保持兼容性。说明如下:

  1. 新增了一包java.util.stream,给Java加上了Stream操作,定义了一些常用的对流进行操作的方法,而这些操作方法的入参又都是之前讲过的“某一类行为的具体操作”,使得并行操作大集合变得方便,充分发挥多核CPU特性,减少并发操作的开发难度

  2. 紧接着通过添加接口以及扩展原本就符合约定的接口,得到了一堆函数式接口用于描述行为,能与Stream互相配合。例如将某对象映射为另一对象的Function接口。如果用过guavaLists.transform(List<F> fromList, Function<? super F, ? extends T> function) 方法,应该印象深刻,当时全部都是匿名内部类来实现Function<? super F, ? extends T> function。在Java8中就新增了这个接口:

    1
    2
    3
    4
    @FunctionalInterface
    public interface Function<T, R> {
    R apply(T t);
    }
  3. 为了让原有的类支持Stream操作,需要扩展接口,并实现新增的方法。例如定义在Collection接口中的stream()方法,可以将集合转为一个stream

    1
    2
    3
    default Stream<E> stream() {
    return StreamSupport.stream(spliterator(), false);
    }

    如果按之前的规范,接口里不可有已实现的方法,那么要么在原来的基础上插入一层抽象类,在抽象类里补上实现;要么就把实现复制粘贴到所有实现类或者中间的抽象类里面。

    前者破坏力太强就不用想了,而后者是把这三行代码复制粘贴到所有Collection的实现类/抽象类里去——呃,相信我,在项目里一定不要这么干,除非你是“我挖坑别人填”的爱好者,天知道是不是哪天就成了“别人挖坑自己填”。

  4. 用lambda表达式实例化一个接口的充分必要条件就是“该接口是函数式接口”。否则将无法确定lambda表达式实现的是接口的哪一个抽象方法。同时lambda表达式也无法实例化抽象类,哪怕抽象类的确只有一个抽象方法。可以说,正是有了函数式接口,才能引入lambda表达式,否则必须对原有的Java世界进行天翻地覆的改造。也正因为有了lambda表达式,Stream 操作才好用,才有人用,不然就是“链式匿名内部类地狱”了。

可以说,Java8增加的lambda, Stream特性,就是建立在函数式接口基础上的。根本原因就是前面提到的“class是一等公民,function不是”,这符合“一切都是class”的思想,而function又恰好不是class

我相信这一定不是给Java增加lambda, Stream的最优解,依然充满了很多“为什么这也没有”、“为什么不能这么干”之类的,但我更相信它是目前不破坏原有的代码,尽力保持兼容性前提下的近似解。况且,Java也是依然出在发展中,在不断进步。

3. lambda表达式和方法引用

于是终于进入了这一段。在不少语言中,都有lambda表达式,这里也不掉书袋了。拿众所周知的Runnable接口举例:

1
2
3
4
5
6
7
8
9
10
11
// 以前的写法
threadPool.execute(new Runnable() {
@Override
public void run() {
// 随便干点啥
}
});
// lambda 的写法
threadPool.execute(() -> {
// 随便干点啥
});

看起来简洁不少,这里简述lambda语法和方法引用:

  1. 箭头->左边的()表示无需入参,因为lambda实例化接口时所需实现的方法是public abstract void run();

    1. 如果需要入参,参数用圆括号包裹,圆括号中的参数是形参,名称也不能和外部的变量重名
    2. 由于lambda形参有类型自动推断,所以一般无需显式书写类型
    3. 当只需要一个入参时,可以省略圆括号
    1
    2
    3
    4
    5
    6
    7
    8
    // 完整版
    BiFunction<Integer, Integer, Integer> sum = (Integer a, Integer b) -> {
    return a + b;
    };
    // 可简化为
    BiFunction<Integer, Integer, Integer> sum = (a, b) -> {
    return a + b;
    };
  2. 箭头->就是lambda表达式的符号,不是指针。如果你用IDE工具,点击这个符号应该能看到此处的lambda是实例化的哪一个接口, 上面的例子里,实例化的接口如下:

    1
    2
    3
    4
    @FunctionalInterface
    public interface BiFunction<T, U, R> {
    R apply(T t, U u);
    }
  1. 箭头->右边是就是方法体(body),由一对花括号包裹具体实现。当实现只有一句话时,花括号都可以省略,因此:

    1
    2
    3
    4
    // 前例可简化为
    BiFunction<Integer, Integer, Integer> sum = (a, b) -> a + b;
    // 又如打印一对值
    BiConsumer<String, Integer> bic = (name, value) -> System.out.println(name + ": " + value);
  2. 当lambda是方法体部分没有对形参做任何操作,可以进一步简化为方法引用。其符号是两个连续的英文冒号::。该符号左边表示方法所在,而右边表示所引用方法的名称,无需圆括号,自然也无需形参,这么做能使代码更加简洁,便于阅读。例如:

    1
    Consumer<Integer> printIntger = integer -> System.out.println(integer);

    直接拿到形参integer调用println()方法,即可简化为:

    1
    Consumer<Integer> printIntger = System.out::println;

    不光可以调用静态方法,构造方法、成员方法都可以。例如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    // 1. 构造方法
    Function<Integer, int[]> createIntArrayFunc = len -> new int[len];
    // 方法引用,简化后:
    Function<Integer, int[]> createIntArrayFunc = int[]::new;

    // 2. 形参自身类的成员方法
    Function<List<?>, Integer> getLenOfListBySelf = list -> list.size();
    // 方法引用,简化后:
    Function<List<?>, Integer> getLenOfListBySelf = List::size;

    // 3. 本类成员方法:
    Function<List<?>, Integer> getLenOfListByMe = list -> this.customGetSize(list);
    // 方法引用,简化后:
    Function<List<?>, Integer> getLenOfListByMe = this::customGetSize;

    // 4. 其他类成员方法:
    Function<List<?>, Integer> getLenOfListByMe = list -> service.customGetSize(list);
    // 方法引用,简化后:
    Function<List<?>, Integer> getLenOfListByMe = service::customGetSize;

写得更完整当然没问题,但在符合语言规范的前提下,适当简化代码提高可读性是更好的选择。

4. 小结

许多代码读起来都比较头疼,常常是刚读到“想干什么”之后,立刻就是具体的“怎么怎么干”,看完具体“怎么怎么干”之后,接着又回头去看接下来再干什么。

其实关键不在于怎么写,而在于怎么思考的。一个变量从被定义到被使用跨过一个屏幕的高度,对人脑的压力实在太大了,而在for循环里嵌套多层if语句块、for语句块,在里面修改一个或多个外部变量,而且还特别长。回想一下被面条式代码(Spaghetti code)支配的恐惧吧!

熟悉lambda这套之后,我最大的收获不是什么减少了for循环遍历,而是习惯把业务过程分解成一个个的“行为”,每个“行为”都尽量简单,只干一件简单明了的事。

是的,做任何事情都有一个度,你完全可以激进地重构面条式代码,但有可能得到了馄饨代码(Ravioli Code)

没有银弹,没有万能药。并不是掌握了某种语言、语法、技巧就能解决一切问题。编码的过程中多多思考,实现功能的同时,减少重复、提高可读性,才是最终目标。如果觉得困惑了,多去阅读、交流,并思考、总结。

最后举一个例子,已有一个全公司雇员信息的list,从中找到所有大于35岁的程序员,统计一下他们的工资水平。

传统的写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// List<Employee> employeeList = ...
double maxSalary = Double.NEGATIVE_INFINITY;
double minSalary = Double.POSITIVE_INFINITY;
double total = 0;
int count = 0;
for (Employee e : employeeList) {
if (e.getAge() >= 35 && e.isProgrammer()) {
double salary = e.getSalary();
maxSalary = Math.max(maxSalary, salary);
minSalary = Math.min(minSalary, salary);
total += salary;
count++;
}
}
double avgSalary = count == 0 ? 0 : total / count;

Java8 的写法:

1
2
3
4
5
6
7
8
9
// List<Employee> employeeList = ...
DoubleSummaryStatistics stats = employeeList.stream()
.filter(e -> e.getAge() >= 35)
.filter(Employee::isProgrammer)
.mapToDouble(Employee::getSalary)
.summaryStatistics();
double maxSalary = stats.getMax();
double avgSalary = stats.getAverage();
double minSalary = stats.getMin();

是不是觉得一下子省事儿了许多?下一篇再详细讨论这个吧