Lambda 表达式是 Java 8 最重要的新特性之一,它让我们可以用很简洁的代码完成一个功能。
01
—
举个栗子
我们知道,Java 中编写多线程主要有两种方式 :继承 Thread 类或实现 Runnable 接口。通常情况下如果逻辑比较简单,我们会写出如下代码:
Thread t1 = new Thread(new Runnable() { @Override public void run() { for (int i = 0; i < 10; i++) System.out.println("Without Lambda Expression"); }});
上述代码创建一个匿名类传入 Thread 构造函数里。下面我们看 Java 8 中我们怎么使用 Lambda 表达式简化上面的代码。
Thread t2 = new Thread(() -> { for (int i = 0; i < 10; i++) System.out.println("With Lambda Expression");});
02
—
函数式接口
在介绍 Lambda 之前,我们需要了解 Java 8 中的一个新的概念:函数式接口。
函数式接口的定义:只包含一个方法的抽象接口。可以使用 @FunctionalInterface 注解,将一个接口标注为函数式接口,该接口只能包含一个抽象方法,如果添加第二个抽象方法的话,会编译失败。
像比较常用的 java.lang.Runnable、java.util.concurrent.Callable、java.util.Comparator 等都是只包含一个抽象方法的接口,默认都是函数式接口。
函数式接口有如下要求:
-
只包含一个抽象方法,如果有继承父接口,则要计算父接口的抽象方法数量。
-
可以声明从 java.lang.Object 继承来的方法,如果除去继承自 Object 的抽象方法,如果只有一个抽象方法,则该接口也是函数式接口。
-
可以有任意数量的默认方法或静态方法。
-
@FunctionalInterface 注解可有可无,只要该接口只有一个抽象方法,则该接口默认为函数式接口,这个注解只是检查它是否是一个函数式接口。
Java 8 内置的四大核心函数式接口:
函数式接口 | 方法 | 用途 |
---|---|---|
Consumer | void accept(T t) | 消费性接口,对类型 T 的对象应用操作 |
Supplier | T get() | 供给型接口,返回类型为 T 的对象 |
Function | R apply(T t) | 应用型接口,对类型为 T 的对象应用操作,并返回类型为 R 的对象 |
Predicate | boolean test(T t) | 断定型接口,判断类型为 T 的对象是否满足某约束 |
03
—
语法
Lambda 表达式的基本语法如下:
(parameters) -> { statements; }
parameters: 参数,可以是一个或多个参数,不必指定参数类型,类型会自动推断,当只有一个参数是,小括号可省略。
statements:语句,可以是一行或多行代码,当只有一行代码,大括号可省略,return 也可省略。
下面是我们加法和平方运算的接口:
public interface MathAdd { int add(int x, int y);}public interface MathSquare { int square(int x);}
我们可以使用下面的方法来使用这两个方法:
MathAdd sum = (int x, int y) -> { return x + y;}; // 语句1int z = sum.add(1, 2);// 一个参数可省略参数列表的小括号,只有一行语句的话可以省略大括号// 和 return 关键字 MathSquare mul = x -> x*x; // 语句2int y = mul.square(5);
我们再看上面的 语句1 和 语句2 ,为什么能够等号右边的语句能够赋值给左边的变量呢?这个问题的本质是:一个 Lambda 表达式的类型是什么?
答案是:它的类型是由其上下文推导而来。
看下面的例子,有两个接口,方法签名完全一样:
public interface MathAdd { int add(int x, int y);}public interface MathSub { int sub(int x, int y);}// 测试方法@Testpublic void test2 () { MathAdd sum = (int x, int y) -> { return x + y;}; MathSub sub = (int x, int y) -> { return x + y;}; System.out.println("MathAdd.add(2, 4): "+sum.add(2, 4)); System.out.println("MathSub.sub(2, 4): "+sub.sub(2, 4));}
上面的代码能顺利编译并执行通过,结果如下:
那么同样的语句 (int x, int y) -> {return x + y;} ,为什么既能是 MathAdd 类型又能是 MathSub 类型呢?奥妙就在于 MathAdd、 MathSub 这两个接口的方法签名一样。
下面是 Lambda 表达式的目标类型 T
的条件:
-
T
是一个函数式接口,只有函数式接口才可以使用 Lambda 表达式。 -
Lambda 表达式的参数和
T
的方法参数在数量和类型上一一对应,注意:类型必须完全一致,如 Integer 和 int 并不能对应上。 -
Lambda 表达式的返回值和
T
的方法返回值兼容,这里并不要求返回值类型完全一致,只要类型兼容即可,如 Lambda 表达式返回 Integer 和T
的方法返回 int 也是满足的。 -
Lambda 表达式内所抛出的异常和
T
的方法throws
类型相兼容,这和条件3的类似的。
04
—
方法引用
方法引用是lambda表达式的一种简写形式。 如果 lambda 表达式只是调用一个特定的已经存在的方法,则可以使用方法引用,实现抽象方法的参数列表,必须与方法引用方法的参数列表保持一致。
语法:T::method
使用“::”操作符将方法名和对象或类的名字分隔开来。以下是四种使用情况:
-
实例对象的成员方法,对象::实例方法
-
静态方法引用,类::静态方法
-
实例方法引用,类::实例方法
-
构造器引用,类::new
下面的代码是打印字符串,其中 语句1 可改为 语句2 的形式。
Consumercon = (x) -> System.out.println(x);// 语句1con.accept("test"); //方法引用,对象::实例方法名Consumerconsumer = System.out::println;// 语句2consumer.accept("test");
方法引用相比 Lambda 更加简洁,能够用更少的代码量实现相同的功能。
下面通过一个我们开发中最常见的排序例子说明:
// 原始集合Listlist = Arrays.asList("Lakers", "Celtics", "Bulls", "Nets", "Magic", "Warriors", "Spurs", "Rockets", "Thunder");// Before java 8Collections.sort(list, new Comparator () { public int compare(String x, String y) { return x.compareTo(y); }});System.out.println("Before java 8 : "+list);// Lambda expressionsCollections.sort(list, (x, y) -> y.compareTo(x));System.out.println("Lambda 表达式 : "+list);// method referenceCollections.sort(list, String::compareTo);System.out.println("方法引用 : "+list);
输出结果如下:
使用 Lambda 能极大的减少代码量,不仅仅是不需要使用匿名类了,而且由于它的类型推断,也能减少很多 import 语句,但是可读性比较差。
总结一下,Lambda 的优缺点:
优点:
-
简洁,这可能是最大的优点了,Lambda 能减少非常多的代码量;
-
非常方便写并行计算,这一点会在下一篇 Stream API 中讲到;
-
外部变量不再必须声明为 final 的了;
-
Lambda 不会引入新的作用域,比如在 Lambda 中使用 this 和在外部使用 this 是同一个对象。
缺点:
-
代码可读性变差,这也是优点1所带来的副作用,更少的代码对机器友好但对开发者不友好;特别是类型推断,如果对 API 不熟悉的话读别人的代码会很艰难;
-
调试不友好;
-
用途有限,只能用于函数式接口。
识别二维码关注我