1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 方式1
view.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
System.out.println("I am clicked");
}
});


// 方式2
view.setOnClickListener(v -> System.out.println("I am clicked"));

// 方式3
view.setOnClickListener(this::onViewClick);
private void onViewClick(View view) {
System.out.println("I am clicked");
}

上面三种方式最终都是为view 设置了点击的监听,最终效果都是一样的, 但是哪种方式更好呢?

几个基本的概念

  • 函数式接口(Functional Interface): 有且仅有一个抽象方法,但是可以有多个非抽象方法的接口。

  • 函数式编程(functional programming),也称函数程序设计,或者泛函编程: 是一种编程典范,它将电脑运算视为数学上的函数计算,并且避免使用程序状态以及易变对象。其实,函数式并非是 Java8 首创的概念,其本身是范畴论(Category Theory)的数学分支,用来解决数学问题的,后来有人将这种方法论运用在了编程上,所以才形成了函数式编程

  • 函数编程语言最重要的基础是λ演算(lambda calculus)。而且λ演算的函数可以接受函数当作输入(引数)和输出(传出值)。比起指令式编程,函数式编程更加强调程序执行的结果而非执行的过程,倡导利用若干简单的执行单元让计算结果不断渐进,逐层推导复杂的运算,而不是设计一个复杂的执行过程。

  • Lambda表达式: Lambda允许把函数作为一个方法的参数(函数作为参数传递进方法中)

  • 方法引用: lambda表达式的一种特殊形式,如果正好有某个方法满足一个lambda表达式的形式,那就可以将这个lambda表达式用方法引用的方式表示,但是如果这个lambda表达式的比较复杂就不能用方法引用进行替换。实际上方法引用是lambda表达式的一种语法糖。


上述几个概念似乎没有什么关联,那么我们再次回到文章开头给出的三种实现方式. 第一种方式是最常见的一种方式,view 的 setOnClickListener 方法要传入一个接口, 我们一般都是直接在参数传入的地方,传入一个匿名内部类,但是这样使得代码的整体结构变得略微有些复杂,并且不够直观,那么有没有一种更简洁的写法呢? 于是 Java8 便退出了更为简洁的lambda 表达式,一行代码就可以替代之前臃肿的结构.

但是,并非所有参数是接口的方法,都可以用 lambda 表达式来替代,这个接口有一个特殊的限定—有且仅包含一个抽象方法的接口(函数式接口). Java8 并没有在之前的基础上提供额外的语法来创建函数式接口,而是在原有接口的范畴内,进行限定.这样就可以在参数为函数式接口的方法中,使用 lambda 来表达.

透过开篇的示例代码,我们发现 setOnClickListener 方法要传入一个接口,本质上是在view 接受到点击事件后,要往外抛出一个事件,而接口正是 view 内部点击事件与外部处理点击事件的桥梁,实际上,就是需要一个特定的代码块来处理点击事件,所以lambda表达式就产生了,但是如果我们再进一步分析,似乎我们并没有必要特意去强调实现了该接口,才可以接受回调,只要满足其接口内的方法签名,任意一个方法都可以传入用来处理对应的回调,甚至都不需要满足函数式接口的约束,这就是方法引用产生的背景.

方法引用提供了非常有用的语法,可以直接引用已有Java类或对象(实例)的方法或构造器。与lambda联合使用,方法引用可以使语言的构造更紧凑简洁,减少冗余代码。

Lambda 简介

Lambda 表达式,也可称为闭包,其替代的是函数式接口被当做参数传入方法是的一种场景.

Lambda 表达式的语法格式如下(两种方式):

1
2
3
(parameters) -> expression

(parameters) ->{ statements; }
  • 可选类型声明:不需要声明参数类型,编译器可以统一识别参数值。
  • 可选的参数圆括号:一个参数无需定义圆括号,但多个参数需要定义圆括号。
  • 可选的大括号:如果主体包含了一个语句,就不需要使用大括号。
  • 可选的返回关键字:如果主体只有一个表达式返回值则编译器会自动返回值. 如果使用大括号,则需要指定明表达式返回了一个数值。

lambda 表达式实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 1. 不需要参数,返回值为 5  
() -> 1

// 2. 接收一个参数(数字类型),返回其2倍的值
x -> 2 * x

// 3. 接受2个参数(数字),并返回他们的和
(x, y) -> x + y

// 4. 接收2个int型整数,返回他们的和
(int x, int y) -> x + y

// 5. 接受一个 string 对象,并在控制台打印,不返回任何值
(String s) -> System.out.print(s)

再次重申:单纯的定义lambda表达式,没有任何意义, lambda 表达式只有在传入函数式接口为参数的方法被调用时,才可以替代为lambda表达式

函数式编程的核心思想

最主要的特征是,函数是第一等公民。强调将计算过程分解成可复用的函数只有引用透明的、没有副作用的函数,才是合格的函数.

这里有三个特性需要特别说明:

  • 函数是”第一等公民”: 所谓”第一等公民”(first class),指的是函数与其他数据类型一样,处于平等地位,它不仅拥有一切传统函数的使用方式(声明和调用),可以赋值给其他变量(赋值),也可以作为参数,传入另一个函数(传参),或者作为别的函数的返回值(返回)。函数可以作为参数进行传递,意味我们可以把行为”参数化”,处理逻辑可以从外部传入,这样程序就可以设计得更灵活。

  • 引用透明(Referential transparency),指的是函数的运行不依赖于外部变量或”状态”,只依赖于输入的参数,任何时候只要参数相同,引用函数所得到的返回值总是相同的。这里强调了一点”输入”不变则”输出”也不变,就像数学函数里面的f(x),只要输入的x一样那得到的结果也肯定定是一样的。

  • 所谓”副作用”(side effect),指的是函数内部与外部互动(最典型的情况,就是修改全局变量的值),产生运算以外的其他结果。函数式编程强调没有”副作用”,意味着函数要保持独立,所有功能就是返回一个新的值,没有其他行为,尤其是不得修改外部变量的值。

根据函数式编程的特性,我们发现函数式编程有以下天然的优势:

  1. 代码简洁,开发快速。

    函数式编程大量使用函数,减少了代码的重复,因此程序比较短,开发速度较快。Paul Graham在《黑客与画家》一书中写道:同样功能的程序,极端情况下,Lisp代码的长度可能是C代码的二十分之一。如果程序员每天所写的代码行数基本相同,这就意味着,”C语言需要一年时间完成开发某个功能,Lisp语言只需要不到三星期。反过来说,如果某个新功能,Lisp语言完成开发需要三个月,C语言需要写五年。”当然,这样的对比故意夸大了差异,但是”在一个高度竞争的市场中,即使开发速度只相差两三倍,也足以使得你永远处在落后的位置。”

  2. 接近自然语言,易于理解

    函数式编程的自由度很高,可以写出很接近自然语言的代码。这基本就是自然语言的表达了,大家应该一眼就能明白它的意思吧。

  3. 更方便的代码管理

    函数式编程不依赖、也不会改变外界的状态,只要给定输入参数,返回的结果必定相同。因此,每一个函数都可以被看做独立单元,很有利于进行单元测试(unit testing)和除错(debugging),以及模块化组合。

  4. 易于”并发编程”

    函数式编程不需要考虑”死锁”(deadlock),因为它不修改变量,所以根本不存在”锁”线程的问题。不必担心一个线程的数据,被另一个线程修改,所以可以很放心地把工作分摊到多个线程,部署”并发编程”(concurrency)。请看下面的代码:

    1
    2
    3
    var s1 = f1();
    var s2 = f2();
    var s3 = concat(f1, f2);

    由于s1和s2互不干扰,不会修改变量,谁先执行是无所谓的,所以可以放心地增加线程,把它们分配在两个线程上完成。其他类型的语言就做不到这一点,因为s1可能会修改系统状态,而s2可能会用到这些状态,所以必须保证s2在s1之后运行,自然也就不能部署到其他线程上了。多核CPU是将来的潮流,所以函数式编程的这个特性非常重要。

  5. 代码的热升级

    函数式编程没有副作用,只要保证接口不变,内部实现是外部无关的。所以,可以在运行状态下直接升级代码,不需要重启,也不需要停机。Erlang语言早就证明了这一点,它是瑞典爱立信公司为了管理电话系统而开发的,电话系统的升级当然是不能停机的。

best practice

  1. Lambda表达式的底层实现

    Java8 内部 Lambda 表达式的实现方式在本质是以匿名内部类的形式的实现的,但是注意, lambda 和 匿名内部类并不是等价的,这里面有个重要的上下文的概念是不同的,也就是this的指代,匿名内部类中的 this 就是其内部类对象,而 lambda 中的 this 指的是外部定义该 lambda 的对象,当然如果是定义在静态方法中的 lambda 是无法使用 this 的

  2. lambda 中使用变量的作用域

  • 和匿名内部类相似, 不能在 lambda 内部修改定义在域外的局部变量,否则会编译错误。
  • lambda 表达式的局部变量可以不用声明为 final,但是必须不可被后面的代码修改(即隐性的具有 final 的语义),但是依然强烈建议标记为 final,用来表达更明确的语义
  • 在 Lambda 表达式当中不允许声明一个与局部变量同名的参数或者局部变量。
  1. 使用 @FunctionalInterface 注解

    如果你确定了某个interface是用于Lambda表达式,请一定要加上@FunctionalInterface,表明你的意图。不然将来说不定某个不知情的同事,在这个interface上面加了另外一个抽像方法时,那么你之前的代码就无法正常运行. 其实 Java8 中已经提供了一些默认的函数式接口供外部使用; 优先使用java.util.function包下面的函数式接口 java.util.function 这个包下面提供了大量的功能性接口,可以满足大多数开发人员为 lambda 表达式和方法引用提供目标类型的需求。每个接口都是通用的和抽象的,使它们易于适应几乎任何lambda表达式。开发人员应该在创建新的功能接口之前研究这个包,避免重复定义接口。另外一点就是,里面的接口不会被别人修改;

函数式接口 参数类型 返回类型 用途
Consumer(消费型接口) T void 对类型为T的对象应用操作。void accept(T t)
Supplier(供给型接口) T 返回类型为T的对象。 T get();
Function<T, R>(函数型接口) T R 对类型为T的对象应用操作并返回R类型的对象。R apply(T t);
Predicate(断言型接口) T boolean 确定类型为T的对象是否满足约束。boolean test(T t);
  1. 不要在Lambda表达中执行有”副作用”的操作

    “副作用”是严重违背函数式编程的设计原则,在工作中我经常看到有人在forEach操作里面操作外面的某个List或者设置某个Map这其实是不对的

  2. 尽量避免在lambda中使用{},也就是说lambda的结构体应该尽量简单,如果内部语句复杂,最好定义一个方法,然后在 lambda 结构体中去引用.

  3. 相比于lambda,更推荐使用方法引用,虽然两者能起到相同的作用,但两者相比,方法引用通常可读性更高并且代码会简短

方法引用共分为四类:

  • 类名::静态方法名
  • 对象::实例方法名
  • 类名::实例方法名(相比于2,其表达含义更清楚,但是这种方式语义的理解有点绕)
  • 类名::new
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
public class Main {
public static void main(String[] args) {

Student student1 = new Student("aa", 60);
Student student2 = new Student("bb", 70);
Student student3 = new Student("cc", 80);
Student student4 = new Student("dd", 90);
List<Student> students = Arrays.asList(student1, student2, student3, student4);


students.sort(new Comparator<Student>() {
@Override
public int compare(Student o1, Student o2) {
return o1.age - o2.age;
}
});

students.sort((o1, o2) -> o1.age - o2.age);

students.sort(Main::compare1);

students.sort(Student::compare2);


}

private static int compare1(Student o1, Student o2) {
return o1.age - o2.age;
}

}


class Student {

String name;
int age;
public Student(String name, int age) {
this.name = name;
this.age = age;
}

public int compare2(Student other) {
return this.age - other.age;
}
}

不要盲目的开启并行流

Lambda的并行流虽好,但也要注意使用场景。如果平常的业务处理比如过滤,提取数据,没有涉及特别大的数据和耗时操作,则不需要开启并行流。如果一个只有几十个元素的列表的过滤操作也开启了并行流,其实这样做会更慢。因为多行线程的开启和同步这些花费的时间往往比你真实的处理时间要多很多。但一些耗时的操作比如I/O访问,DB查询,远程调用,这些如果可以并行的话,则开启并行流是可提升很大性能的。因为并行流的底层原理是fork/join,如果你的数据分块不是很好切分,也不建议开启并行流。举个例子ArrayList的Stream可以开启并行流,而LinkedList则不建议,因为LinkedList每次做数据切分要遍历整个链表,这本身就已经很浪费性能,而ArrayList则不会

参考