利用行为参数化来传递代码有助于应对不断变化的需求。它允许你定义一段代码块来表示一个行为,然后传递它。
采用匿名类来表示多种行为并不令人满意:代码十分啰唆,这会影响程序员在实践中使用行为参数化的积极性。
可以把 Lambda 表达式理解为一种简洁的可传递匿名函数:它没有名称,但它有参数列表、函数主体、返回类型,可能还有一个可以抛出的异常列表。这个定义够大的,让我们慢慢道来。
Lambda 解决了这个问题:它可以让你十分简明地传递代码。理论上来说,你在 Java 8 之前做不了的事情,Lambda 也做不了.
利用 Lambda 表达式,你可以更为简洁地自定义一个 Comparator 对象。
之前
Comparator byWeight = new Comparator() {
public int compare(Apple a1, Apple a2){
return a1.getWeight().compareTo(a2.getWeight());
}
};
现在 lambda方式
Comparator byWeight = (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());
例子:
Lambda 表达式具有一个 String 类型的参数并返回一个 int。Lambda 没有 return 语句,因为已经隐含了 return
(String s) -> s.length()
Lambda表达式有一个Apple类型的参数并返回一个 boolean(苹果的重量是否超过 150 克)
(Apple a) -> a.getWeight() > 150
Lambda 表达式具有两个 int 类型的参数而没有返回值(void返回)。注意 Lambda 表达式可以包含多行语句,这里是两行
(int x, int y) -> {
System.out.println("Result:");
System.out.println(x + y);
}
Lambda 表达式没有参数,返回一个 int
() -> 42
Lambda表达式具有两个 Apple 类型的参数,返回一个 int:比较两个 Apple 的重量
(Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight())
基本语法
(parameters) -> expression
块风格
(parameters) -> { statements; }
测验 3.1:Lambda 语法
以下哪个不是有效的 Lambda 表达式?
(1) () -> {}
(2) () -> “Raoul”
(3) () -> {return “Mario”;}
(4) (Integer i) -> return “Alan” + i;
(5) (String s) -> {“Iron Man”;}
答案:只有(4)和(5)是无效的 Lambda,其余都是有效的。详细解释如下。
(1) 这个 Lambda 没有参数,并返回 void。它类似于主体为空的方法:public void run() {}。一个有趣的事实:这种 Lambda 也经常被叫作“汉堡型 Lambda”。 如果只从一边看,它的形状就像是两块圆面包组成的汉堡。
(2) 这个 Lambda 没有参数,并返回 String 作为表达式。
(3) 这个 Lambda 没有参数,并返回 String(利用显式返回语句)。
(4) return 是一个控制流语句。要使此 Lambda 有效,需要使用花括号,如下所示:(Integer i) -> {return “Alan” + i;}
(5)“Iron Man”是一个表达式,不是一个语句。要使此 Lambda 有效,可以去除花括号和
分号,如下所示:
(String s) -> “Iron Man”
或者如果你喜欢,可以使用显式返回语句,如下所示:
(String s) -> {return “Iron Man”;}
可以在函数式接口上使用 Lambda 表达式。但是方法参数需要是这个Predicate类型的
Predicate 它就是一个函数式接口
函数式接口就是只定义一个抽象方法的接口。 Java API 中的一些其他函数式接口
下面哪些接口是函数式接口?
public interface Adder {
int add(int a, int b);
}
public interface SmartAdder extends Adder {
int add(double a, double b);
}
public interface Nothing {
}
答案:只有 Adder 是函数式接口。
SmartAdder 不是函数式接口,因为它定义了两个叫作 add 的抽象方法(其中一个是从Adder 那里继承来的)。
Nothing 也不是函数式接口,因为它没有声明抽象方法
用函数式接口可以干什么呢?Lambda 表达式允许你直接以内联的形式为函数式接口的抽象方法提供实现,并把整个表达式作为函数式接口的实例(具体说来,是函数式接口一个具体实现的实例)
函数式接口的抽象方法的签名基本上就是 Lambda 表达式的签名。我们将这种抽象方法叫作函数描述符。
Runnable 接口可以看作一个什么也不接受什么也不返回(void)的函数的签名,因为它只有一个叫作 run 的抽象方法,这个方法什么也不接受,什么也不返回(void)。
Lambda 及空方法调用
虽然下面这种 Lambda 表达式调用看起来很奇怪,但是合法的:
process(() -> System.out.println(“This is awesome”));
System.out.println 返回 void,所以很明显这不是一个表达式!为什么不像下面这样用花括号环绕方法体呢?
process(() -> { System.out.println(“This is awesome”); });
结果表明,方法调用的返回值为空时,Java 语言规范有一条特殊的规定。这种情况下,你不需要使用括号环绕返回值为空的单行方法调用。以下哪些是使用 Lambda 表达式的有效方式?
(1) execute(() -> {});
public void execute(Runnable r){ r.run(); }
(2) public Callable fetch() { return () -> “Tricky example ”; }
(3) Predicate p = (Apple a) -> a.getWeight();
答案:只有(1)和(2)是有效的。
第(1)个例子有效,是因为 Lambda() -> {}具有签名() -> void,这和 Runnable 中的抽象方法 run 的签名相匹配。请注意,此代码运行后什么都不会做,因为 Lambda 是空的!
第(2)个例子也是有效的。事实上,fetch 方法的返回类型是 Callable。Callable基本上就定义了一个方法,签名是() -> String,其中 T 被 String代替了。因为 Lambda() -> "Trickyexample;-)"的签名是() -> String,所以在这个上下文中可以使用 Lambda。
第(3)个例子无效,因为 Lambda 表达式(Apple a) -> a.getWeight()的签名是(Apple) -> Integer,这和 Predicate: (Apple) -> boolean 中定义的 test 方法的签名不同。
在进行文件读取的时候,基本上就是 打开-处理-关闭 这几个模式。大多情况下 打开和关闭都是类似的操作,所以我们把这两个抽取出来
在以下代码中,加粗显示的就是从一个文件中读取一行所需的模板代码
但是这段代码的是有局限的,如果我想读取两行,或者是拼接的返回,那我我可能需要重写一个方法,也是复制在修改一下。
那么我们是不是可以把processFile方法的行为参数化呢?
如果想读取两行,写法可以是这样的:String result = processFile((BufferedReader br) -> br.readLine() + br.readLine());
Lambda 仅可用于上下文是函数式接口的情况。你需要创建一个能匹配BufferedReader -> String,还可以抛出 IOException 异常的接口
@FunctionalInterface
public interface BufferedReaderProcessor {
String process(BufferedReader b) throws IOException;
}
processFile 方法的参数public String processFile(BufferedReaderProcessor p) throws IOException {}
任何 BufferedReader -> String 形式的 Lambda 都可以作为参数来传递,因为它们符合BufferedReaderProcessor 接口中定义的 process 方法的签名。
Lambda 表达式允许你直接内联,为函数式接口的抽象方法提供实现,并且将整个表达式作为函数式接口的一个实例。
public String processFile(BufferedReaderProcessor p) throws IOException {
try (BufferedReader br = new BufferedReader(new FileReader("data.txt"))) {
// 处理 BufferedReader 对象
return p.process(br);
}
}
现在通过传递Lambda 来重用 processFile 方法。
处理一行:
String oneLine = processFile((BufferedReader br) -> br.readLine());
处理两行:
String twoLines = processFile((BufferedReader br) -> br.readLine() + br.readLine());
总结了所采取的使 pocessFile 方法更灵活的四个步骤
函数式接口定义且只定义了一个抽象方法。函数式接口很有用,因为抽象方法的签名可以描述 Lambda 表达式的签名。函数式接口的抽象方法的签名称为函数描述符。
java.util.function.Predicate接口定义了一个名叫 test 的抽象方法,它接受泛型 T 对象,并返回一个 boolean
@FunctionalInterface
public interface Predicate {
boolean test(T t);
}
public List filter(List list, Predicate p) {
List results = new ArrayList<>();
for(T t: list) {
if(p.test(t)) {
results.add(t);
}
}
return results;
}
Predicate nonEmptyStringPredicate = (String s) -> !s.isEmpty();
List nonEmpty = filter(listOfStrings, nonEmptyStringPredicate);
java.util.function.Consumer接口定义了一个名叫 accept 的抽象方法,它接受泛型 T 的对象,没有返回(void)。你如果需要访问类型 T 的对象,并对其执行某些操作,就可以使用这个接口。
@FunctionalInterface
public interface Consumer{
void accept(T t);
}
public void forEach(List list, Consumer c){
for(T i: list){
c.accept(i);
}
}
forEach(
Arrays.asList(1,2,3,4,5),
(Integer i) -> System.out.println(i) // Lambda 是 Consumer中 accept 方法的实现
);
java.util.function.Function
@FunctionalInterface
public interface Function {
R apply(T t);
}
public List map(List list, Function f) {
List result = new ArrayList<>();
for(T t: list) {
result.add(f.apply(t));
}
return result;
}
// [7, 2, 6]
List l = map(
Arrays.asList("lambdas", "in", "action"),
(String s) -> s.length()); //Lambda是Function接口的 apply 方法的实现
Java 类型要么是引用类型要么是基本类型,但是泛型(比如 Consumer中的 T)只能绑定到引用类型。这是由泛型内部的实现方式造成的。
把基本类型转化成引用类型,这个方法叫做装箱(boxing)。相反,把引用类型转成基本类型,就叫拆箱,
装箱是有性能代价的,装箱后的值本质上就是把基本类型包裹起来,并保存在堆里。因此,装箱后的值需要更多的内存,并需要额外的内存搜索来获取被包裹的基本值。
在下面的代码中,使用 IntPredicate 就避免了对值 1000 进行装箱操作,但要是用 Predicate就会把参数 1000 装箱到一个 Integer 对象中:
public interface IntPredicate {
boolean test(int t);
}
// true(无装箱)
IntPredicate evenNumbers = (int i) -> i % 2 == 0;
evenNumbers.test(1000);
// false(装箱)
Predicate oddNumbers = (Integer i) -> i % 2 != 0;
oddNumbers.test(1000);
一般来说,针对专门的输入参数类型的函数式接口的名称都要加上对应的基本类型前缀,比如 DoublePredicate、IntConsumer、LongBinaryOperator、IntFunction 等。Function接口还有针对输出参数类型的变种:ToIntFunction、IntToDoubleFunction 等。
Lambda 的类型是从使用 Lambda 的上下文推断出来的。上下文(比如,接受它传递的方法的参数,或接受它的值的局部变量)中 Lambda 表达式需要的类型称为目标类型。
例子:List
类型检查过程分解如下。
有了目标类型的概念,同一个 Lambda 表达式就可以与不同的函数式接口联系起来,只要它们的抽象方法签名能够兼容。Callable
PrivilegedAction
Java 编译器会从上下文(目标类型)推断出用什么函数式接口来配合 Lambda 表达式,这意味着它也可以推断出适合 Lambda 的签名,
参数 apple 没有显式类型List
可以这样来创建一个Comparator 对象:
没有类型推断Comparator
有类型推断Comparator
我们迄今为止所介绍的所有 Lambda 表达式都只用到了其主体里面的参数。但 Lambda 表达式也允许使用自由变量(不是参数,而是在外层作用域中定义的变量),就像匿名类一样。int portNumber = 1337;
Runnable r = () -> System.out.println(portNumber);
Lambda 可以没有限制地捕获(也就是在其主体中引用)实例变量和静态变量。但局部变量必须显式声明为 final,或事实上是 final。
下面这个例子是错误的
int portNumber = 1337;
Runnable r = () -> System.out.println(portNumber);
portNumber = 31337;
因为Lambda 表达式引用的局部变量必须是最终的(final)或事实上最终的
对局部变量的限制
实例变量和局部变量背后的实现有一个关键不同。实例变量都存储在堆中,局部变量则保存在栈上。
这一限制不鼓励你使用改变外部变量的典型命令式编程模式。
方法引用。可以把它们视为某些Lambda 的快捷写法。
方法引用让你可以重复使用现有的方法定义,并像 Lambda 一样传递它们。
先前:inventory.sort((Apple a1, Apple a2) a1.getWeight().compareTo(a2.getWeight()));
之后(使用方法引用和 java.util.Comparator.comparing):inventory.sort(comparing(Apple::getWeight));
如果一个 Lambda 代表的只是“直接调用这个方法”,那最好还是用名称来调用它,而不是去描述如何调用它。
方法引用就是让你根据已有的方法实现来创建Lambda 表达式。但是,显式地指明方法的名称,你的代码的可读性会更好。当你需要使用方法引用时,目标引用放在分隔符::前,方法的名称放在后面。例如,Apple::getWeight 就是引用了 Apple 类中定义的方法 getWeight。getWeight 后面不需要括号,因为你没有实际调用这个方法,只是引用了它的名称。
List str = Arrays.asList("a","b","A","B");
str.sort((s1, s2) -> s1.compareToIgnoreCase(s2));
Lambda 表达式的签名与 Comparator 的函数描述符兼容 可以修改成这样的
str.sort(String::compareToIgnoreCase);
测验 3.6:方法引用
下列 Lambda 表达式的等效方法引用是什么?
(1) ToIntFunction stringToInt = (String s) -> Integer.parseInt(s);
(2) BiPredicatecontains = (list, element) -> list.contains(element);
(3) Predicate startsWithNumber = (String string) -> this .startsWithNumber(string);
答案:(1) 这个 Lambda 表达式将其参数传给了 Integer 的静态方法 parseInt。这种方法接受一个需要解析的 String,并返回一个 Integer。因此,可以使用图 3-5 中的办法➊(Lambda 表达式调用静态方法)来重写 Lambda 表达式,如下所示:**ToIntFunction stringToInt = Integer::parseInt; **
(2) 这个 Lambda 使用其第一个参数,调用其 contains 方法。由于第一个参数是 List类型的,因此你可以使用图 3-5 中的办法➋,如下所示:BiPredicatecontains = List::contains
; 这是因为,目标类型描述的函数描述符是(List,String) -> boolean,而List::contains 可以被解包成这个函数描述符。
(3) 这种“表达式–风格”的 Lambda 会调用一个私有方法。你可以使用图 3-5 中的办法❸,如下所示:
Predicate startsWithNumber = this::startsWithNumber
对于一个现有构造函数,你可以利用它的名称和关键字 new 来创建它的一个引用:ClassName::new。它的功能与指向静态方法的引用类似。
例如,假设有一个构造函数没有参数。它适合 Supplier 的签名() -> Apple。你可以这样做:
List weights = Arrays.asList(7, 3, 4, 10);
List apples = map(weights, Apple::new); // 将构造函数引用传递给 map 方法
public List map(List list, Function f) {
List result = new ArrayList<>();
for(Integer i: list) {
result.add(f.apply(i));
}
return result;
}
如果你有一个具有两个参数的构造函数 Apple(String color, Integer weight),那么它就适合 BiFunction 接口的签名,于是你可以这样写:
如果有构造参数 ,那么Apple::new;这样返回一个对象,怎么知道我的构造参数呢?
测验 3.7:构造函数引用
你已经看到了如何将有零个、一个、两个参数的构造函数转变为构造函数引用。那要怎么样才能对具有三个参数的构造函数,比如 RGB(int, int, int),使用构造函数引用呢?
答案:你看,构造函数引用的语法是 ClassName::new,那么在这个例子里面就是 RGB::new。但是你需要与构造函数引用的签名匹配的函数式接口。由于语言本身并没有提供这样的函数式接口,因此你可以自己创建一个:
public interface TriFunction{
R apply(T t, U u, V v);
}
现在你可以像下面这样使用构造函数引用了:
TriFunctioncolorFactory = RGB::new;
用不同的排序策略给一个 Apple 列表排序,并需要展示如何把一个原始粗暴的解决方案转变得更为简明。这会用到书中迄今讲到的所有概念和功能:行为参数化、匿名类、Lambda表达式和方法引用。我们想要实现的最终解决方案是这样的:inventory.sort(comparing(Apple::getWeight));
Java 8 API 已经为你提供了一个 List 可用的 sort 方法。那么最困难的部分已经搞定了!但是,如何把排序策略传递给 sort 方法呢?sort 方法的签名是这样的:void sort(Comparator super E> c)
sort 的行为被参数化了:传递给它的排序策略不同,其行为也会不同。
方案:
public class AppleComparator implements Comparator {
public int compare(Apple a1, Apple a2){
return a1.getWeight().compareTo(a2.getWeight());
}
}
inventory.sort(new AppleComparator());
使用匿名内部类修改
inventory.sort(new Comparator() {
public int compare(Apple a1, Apple a2){
return a1.getWeight().compareTo(a2.getWeight());
}
});
lambda传递代码方式,函数式接口inventory.sort((Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()));
Java 编译器可以根据 Lambda 出现的上下文来推断 Lambda 表达式参数的类型。那么可以重写成这样:inventory.sort((a1, a2) -> a1.getWeight().compareTo(a2.getWeight()));
Comparator 具有一个叫作 comparing 的静态辅助方法,它可以接受一个 Function 来提取 Comparable 键值,并生成一个 Comparator 对象Comparator
可以改成这样import static java.util.Comparator.comparing;
// 静态导入inventory.sort(comparing(apple -> apple.getWeight()));
假设你静态导入了 java.util.Comparator.comparing
inventory.sort(comparing(Apple::getWeight));
Java 8 的好几个函数式接口都有为方便而设计的方法。具体而言,许多函数式接口,比如用于传递 Lambda 表达式的 Comparator、Function 和 Predicate 都提供了允许你进行复合的方法。
我们前面看到,你可以使用静态方法 Comparator.comparing,根据提取用于比较的键值的 Function 来返回一个 Comparator,如下所示:Comparator
用不着去建立另一个 Comparator 的实例。接口有一个默认方法 reversed 可以使给定的比较器逆序inventory.sort(comparing(Apple::getWeight).**reversed**());
// 按重量递减排序
如果苹果一样重呢?需要进一步的按照其他属性排序
如果两个对象用第一个 Comparator 比较之后是一样的,就提供第二个 Comparator。你又可以优雅地解决这个问题了:
inventory.sort(comparing(Apple::getWeight)
.reversed() // 按重量递减排序
.thenComparing(Apple::getCountry)); // 两个苹果一样重时,进一步按国家排序
谓词接口包括三个方法:negate、and 和 or,让你可以重用已有的 Predicate 来创建更复杂的谓词。比如,你可以使用 negate 方法来返回一个 Predicate 的非,比如苹果不是红的:
// 产生现有 Predicate对象 redApple 的非**Predicate
想要把两个 Lambda 用 and 方法组合起来,比如一个苹果既是红色又比较重:
// 链接两个谓词来生成另一个 Predicate 对象Predicate
进一步组合谓词,表达要么是重(150 克以上)的红苹果,要么是绿苹果:
Predicate redAndHeavyAppleOrGreen =
redApple.and(apple -> apple.getWeight() > 150)
.or(apple -> GREEN.equals(a.getColor()));
and 和 or 方法是按照在表达式链中的位置,从左向右确定优先级的。因此,a.or(b).and©可以看作(a || b) && c。同样,a.and(b).or© 可以看作(a && b) || c。
还可以把 Function 接口所代表的 Lambda 表达式复合起来。Function 接口为此配了 andThen 和 compose 两个默认方法,它们都会返回 Function 的一个实例。
andThen 方法会返回一个函数,它先对输入应用一个给定函数,再对输出应用另一个函数。
Function f = x -> x + 1; // 函数 f 给数字加 1 (x -> x + 1)
Function g = x -> x * 2; // 函数 g 给数字乘 2
// 数学上会写作 g(f(x))或(g o f)(x)
Function h = f.andThen(g); // 组合成一个函数 h,先给数字加 1,再给结果乘 2
int result = h.apply(1); // 返回 4
也可以类似地使用 compose 方法,先把给定的函数用作 compose 的参数里面给的那个函数,然后再把函数本身用于结果
Function f = x -> x + 1;
Function g = x -> x * 2;
// 数学上会写作 f(g(x))或(f o g)(x)
Function h = f.compose(g);
int result = h.apply(1); // 返回 3
实际使用
比方说你有一系列工具方法,对用 String 表示的一封信做文本转换:
public class Letter{
public static String addHeader(String text){
return "From Raoul, Mario and Alan: " + text;
}
public static String addFooter(String text){
return text + " Kind regards";
}
public static String checkSpelling(String text){
return text.replaceAll("labda", "lambda");
}
}
可以通过复合这些工具方法来创建各种转型流水线了,比如创建一个流水线:先加上抬头,然后进行拼写检查,最后加上一个落款,如图 3-7 所示。
Function addHeader = Letter::addHeader;
Function transformationPipeline
= addHeader.andThen(Letter::checkSpelling)
.andThen(Letter::addFooter);
第二个流水线可能只加抬头、落款,而不做拼写检查:
Function addHeader = Letter::addHeader;
Function transformationPipeline = addHeader.andThen(Letter::addFooter);
Java 8 的表示法(double x) -> x + 10(一个 Lambda 表达式)恰恰就是为此设计的,因此你可以写:
**integrate((double x) -> x + 10, 3, 7) **
或者
**integrate((double x) -> f(x), 3, 7) **
或者使用方法引用的方式
**integrate(C::f, 3, 7) **// 这里 C 是包含静态方法 f 的一个类。理念就是把 f 背后的代码传给 integrate 方法。
integrate 方法本身,数学的形式。(Java函数的写法不能像数学里那样)
public double integrate((double -> double) f, double a, double b) {
return (f(a) + f(b)) * (b - a) / 2.0
}
或者用 DoubleUnaryOperator,这样也可以避免对结果进行装箱:
public double integrate(DoubleUnaryOperator f, double a, double b) {
return (f.applyAsDouble(a) + f.applyAsDouble(b)) * (b - a) / 2.0;
}
有点可惜的是你必须写 f.apply(a),而不是像数学里面写 f(a),但 Java 无法摆脱“一切都是对象”的思想——它不能让函数完全独立!