在上一篇文章中,我们介绍了行为参数化(Java JDK1.8 核心特性详解------行为参数化),并且简单展示了Lambda表达式给我们带来的好处。今天这篇文章将会介绍如何构建Lambda,它的使用场合,以及如何利用它使代码更加简洁。
Lambda表达式与方法引用
Lambda表达式介绍
Lambda表达式可以理解为简洁地表示可传递匿名函数的一种简单方式:没有名称,但是有参数列表,函数主体,返回类型,可能还有可抛出的异常。
- 匿名:它不像普通的方法一样有一个具体的名称,比较像匿名类
- 函数:Lambda不像方法一样属于某个特定的类,但和方法一样,有参数列表、函数主体、返回类型以及异常
- 传递:Lambda表达式可以作为参数传递给方法或者储存在变量中
- 简洁:不用像匿名类那样写很多模版代码
在上一篇的最后一部分代码中,你可以发现匿名函数被下面代码取代,而这个就是Lambda表达式。
(People people)-> "男".equals(people.getName())
Lambda表达式由参数列表(People people),箭头 -> 、和Lambda主体 "男".equals(people.getName()) 三部分组成。
参数列表就是方法需要传入的参数,其写法有以下不同几种
//完整的参数列表
(People people)-> "男".equals(people.getName())
//参数类型可以省略
(people) -> "男".equals(people.getName())
//当传入参数只有一个时,括号可以省略
people -> "男".equals(people.getName())
//当传入多个参数,或者不传参数时,括号不能省略
(People people,People people1) -> people.getName.equals(people1.getName())
//当传入多个参数,或者不传参数时,括号不能省略,多个参数的参数类型也可以省略
(people, people1) -> people.getName.equals(people1.getName())
箭头(->)用来分隔参数列表和方法主体。
主体中的表达式是方法的主要功能。其写法有以下两种
//当没有花括号时,表达式结果就是返回值,隐含了return,
(people) -> "男".equals(people.getName())
//当有花括号时,要显性返回方法要求的返回信息
(people) -> {return "男".equals(people.getName())}
在哪里使用Lambda表达式
Lambda表达式可以用来替换函数式接口的实现类。例如上面用Lambda表示式,避免了定义新的类或者使用匿名函数。那什么是函数式接口呢?
函数式接口是指接口里面只定义了一个抽象方法,换句话说,函数式接口就是只定义了一个抽象方法的接口。(上篇文章中的FilterPeople 接口就是一个函数式接口)
//这个是函数式接口,因为只定义了一个抽象方法
public interfaceFunctionalInterface1 {
int functional();
}
//这个不是函数式接口,因为里面定义了两个抽象方法
public interfaceNotFunctionalInterface2 {
int functional();
int functional2();
}
//这个也是函数式接口,因为里面只定义了一个抽象方法
public interface FunctionalInterface3 {
int functional();
//functional2是默认方法,后面的文章会提到,这里你就把他当作普通的方法。
default int functional2() {
return 1;
}
}
在Java8中,新增了一个注解@FunctionalInterface,这个注解使用来接口上,用来声明这是一个函数式接口,如果有这个注解的接口内,存在一个以上的抽象方法,在编译的时候就会报错。 类似@Override,标注方法被重写了,如果没有上级没有这个方法,就会报错。
函数式接口的抽象方法的签名基本上就是Lambda表达式的签名,我们经常使用函数描述符来描述Lambda表达式和函数式接口的签名。例如interfaceFunctionalInterface1 接口中的functional方法,它可以用()->int来表示。()->int表示传入参数为空,返回值为int。又如前一章FilterPeople接口,也是一个函数式接口,我们用(People,People)->boolean,来表示boolean test(People people)这个方法,这表明传入两个People类型的参数,然后方法返回一个boolean值。又例如Runnable的函数描述符是()->void,这表示run这个方法的传入值为空,返回值为void;
如何使用Lambda表达式
Lambda表达式不是随时随地都可以使用的,一般来说,我们是用在某些传入参数是某些函数式接口(或者函数式接口子类,但是调用了函数式接口内的方法)的地方,它可以用来代替一些匿名类。或者是赋给一个变量。最常见地方就是策略设计模式,模版方法,观察者模式,责任链模式以及工厂模式。我们会演示策略设计模式时如何使用Lambda:
例如我们需要完成一个将int类型的值按照一定的业务转成String类型的值,方法接口设计如下:
public static String castIntToString(int i, Function a) {
return a.cast(i);
}
通过传入不同的Function策略来对i进行转化。这里的Function是一个函数式接口。具体代码如下:
@FunctionalInterface
public interface CastFunction {
String castClass(int r);
}
当我们要求将传入的int按不同业务逻辑转化为String的时候,我们以前可能会这么写:
//直接转为String
String string = castIntToString(1, new CastFunction () {
@Override
public String cast(int r) {
return r + "";
}
});
//将int值乘2以后返回
String string1 = castIntToString(1, new CastFunction () {
@Override
public String cast(int r) {
return r*2 + "";
}
});
但是,如果我们通过Lambda表达式,方法就会简单很多。首先我们先转化它的抽象方法,(int)->String。结合上面的讲的Lambda的组成,那么我们可以这样写 :
//直接转为String
String string = castIntToString(1, r -> r + "");
//将int值乘2以后返回
String string1 = castIntToString(1, r -> r*2 + "");
你会发现代码一下子简单了很多。在这里,你可能会觉得这个代码比较难理解,因为这里隐藏了实现的接口名,以及实现的抽象方法。但是当你熟练使用Lambda以后,你会觉得这样很酷。
Java8给我们提供了部分常用的函数式接口,让我们可以直接使用
使用案例 | 函数描述符 | 对应的函数式接口 | 功能要求 | Lambda 的例子 |
布尔表达式 | T ->boolean | Predicate<T> | 判断List<String>数量是否为空 | List<String> list -> list.isEmpty() |
创建对象 | ()->T | Supplier<T> | 创建一个People | () -> new People |
消费一个对象 | T->void | Consumer<T> | 打印People的年龄 | (People p)->System.out.print(p.getAge) |
传入一个对象返回另一种类型对象 | T->R | Function<T, R> | 将int转为String | (int i) -> i+"" |
合并两个值 | (T,U)->R | IntBinaryOperator | 返回两个值的乘积 | (int a, int b) -> a * b |
比较两个对象 | (T,T)->boolean | Comparator<T>或 BiFunction<T, T, R> | 比较两个People谁年龄大 | (People p1, People p2) -> p1.getAge().compareTo(p2.getAge()) |
注:由于JDK1.8中接口支持默认方法,但即使一个接口中有很多默认方法,只要接口只定义了一个抽象方法,它就仍然是一个函数式接口。
类型检查、类型推断和限制
类型检查
Lambda 表达式本身并不包含它是实现哪个函数式接口的信息,编译器会根据 Lambda 表达式所处的上下文(context)环境来推断 Lambda 表达式的目标类型(target type),例如对于下面的代码:
Runnable a = ()->System.out.println("Runnable");
Lambda 表达式会赋值给 Runnable 对象,那么该 Lambda 表达式对应的目标类型就是 Runnable 接口,该接口中的 a方法对应的函数描述符为 () -> void
,这个和 ()->System.out.println("Runnable")
可以匹配,这样就完成了类型检查。下图是一个完整的例子,概述了代码的类型检查过程,这个是《Java8实战》上的一个例子,用来筛选苹果集合中重量大于150g的苹果:
有了目标类型的接口,因此,只要函数签名相同,Lambda就可以混合使用。例如Callable接口和PrivilegedActionj接口的都是函数式接口,函数描述都是()->T,代表什么都不接受并返回一个泛型T的实例。那么,下面两个赋值都是有效的:
Callable<String> callable = () -> "a";
PrivilegedAction<String> privilegedAction = () -> "a";
第一个Lambda的赋值的目标类型是Callable<String>,第二个幅值的目标类型是PrivilegedAction<String>。
特殊的void兼容规则
如果一个Lambda的主体是一个语句表达式,他就和一个返回void的函数描述符兼容(当然需要参数列表也兼容)。例如,以下两行都是合法的,尽管List的add方法返回的是一个boolean,而不是Consumer上下文(T->void)所要求的viod:
//Predicate返回一个boolean
Predicate<People> p = people -> peopleList.add(new People("张三", 10));
//Consumer返回一个void
Consumer<People> a = people -> peopleList.add(new People("张三", 10));
类型推断
正如我之前说的,我们在写Lambda表达式的时候可以省略参数类型。这是因为Java编译器会从上下文自动帮我们推断出用什么函数式接口。有时候显式写出参数类型有助于提高代码可读性,有时候省略有助于代码可读性,具体要你自己选择。
使用局部变量
我们之前Lambda表达式中用的都是参数列表中传入的变量,但是Lambda表达式还允许使用自由变量(不是参数,而是在外层作用域中定义的变量)。它们被称为捕捉Lambda。例如,下面Lambda捕捉了number变量:
int number = 1;
Runnable runnable = () -> System.out.println(number);
Lambda表达式可以没有限制的实例变量 和静态变量,但是使用局部变量时,必须显式申明为final或者实际上是final。实例变量和静态变量好理解,局部变量?下面举个例子:
final unmber = 1;
Runnable runnable = () -> System.out.println(number);//这里编译器不会报错
unmber2 = 1;
Runnable runnable = () -> System.out.println(number2);//这里编译器不会报错
int number3 = 1;
numbe3r=3;
Runnable runnable = () -> System.out.println(number3);//这里编译器会报错
unmber4 = 1;
Runnable runnable = () -> {
number4 = number4+1;
System.out.println(number4);//这里编译器会报错
};
对于局部变量,我们要想在Lambda中使用,必须对这个变量必须是final或者是effectively final。第二个例子中,number2在赋值过后没有再次修改,JDK1.8就在前面默认他是effectively final的。
为什么会这么要求呢?个人理解是,因为实例变量和静态变量是保存在堆中,局部变量是保存在栈里,当Lambda在一个线程运行时,想要访问自由变量(局部变量),可能会出现局部变量已经被销毁了。所以,为了保证Lambda可以访问到这个自由变量,Lambda访问的是这个自由变量的副本,即便以前的变量销毁了,Lambda还是可以通过这个副本正常访问到以前的值。如果这个副本不是final或者effectively final,可能会导致安全问题。
方法引用
方法引用可以被看做仅仅调用特定方法的Lambda表达式的简单写法(如果一个Lambda主体只是调用这个方法(主体没有{}),就可以用方法引用来替代这个Lambda表达式),方法引用可以重复使用现有的方法定义,并像Lambda一样传递他们,同时更加简洁。例如下面这两行代码是等效的:
FilterPeople filterPeople = (People people) -> people.getName();
FilterPeople filterPeople = People::getName;
那要如何使用它呢?
目标引用放在分隔符::前,方法的名称放在后面。例如People::getName代表引用People类中的getName方法,这是上面Lambda表达式的快捷写法。
方法引用可以看做单一方法的Lambda的语法糖,主要有三类:
(1)指向静态方法的方法引用,例如方法主体是调用Integer的parseInt方法(String s)->Integer.parseInt(s) 可以用Integer::parseInt 来代替
Function<String, Integer> stringIntegerFunction0 = (String string) -> Integer.parseInt(string);
Function<String, Integer> stringIntegerFunction1 = Integer::parseInt;
(2)指向任意类型实例方法的方法引用,例如方法主体是调用String的length方法(String s)->s.length() 可以用 String::length 来代替。
Function<String, Integer> stringIntegerFunction = (String string) -> string.length();
Function<String, Integer> stringIntegerFunction1 = String::length;
(3)指向现有对象的实例方法的方法引用,例如方法主体是调用People的getName方法,写作People::getName。
//people是自由变量
Runnable runnable = () -> people.getName();
Runnable runnable1 = People::getName;
构造函数引用
对于一个现有构造函数,你可以利用它的名称和关键字new来创建它的一个引用。例如之前我们提到Supplier<Apple> 函数式接口可以用来创建对象实例。例如:
Supplier<People> peopleFunction = () -> new People();
People people = peopleFunction.get();
通过构造函数引用,可以将代码写作:
Supplier<People> peopleFunction = People::new;
People people = peopleFunction.get();
对于多参构造器,Java8提供了很多多参的函数式接口,让我们也可以用构造引用来创建。
到目前为止,Lambda表达式的内容基本结束了,事实上,Lambda更适合和JDK提供Stream配合使用。下面一部分将会介绍流的使用方式,以及如何和Lambda组合使用。
更多与JDK1.8相关的文章请看:Java JDK1.8 核心特性详解----(总目录篇)