Java
Java
目录
函数传递
“函数指针传递” 的方式(Java)
有几种方法
① 接口方法
例如通过实现接口的方式,本质是传递一个实现了接口的类
举两个例子:
① 接口实现:事件监听器
class Worker implements ActionsListener
{
public void actionPerformed(AxtionEvent event)
{
// do some work
}
}
② 接口实现:比较器 compare方法不是立即调用。实际上,在数组完成排序之前,sort方法会一直调用compare方法
class LengthComparator implements Comparator<String>
{
public int compar(String first, String second)
{
return first.length() - second.length();
}
}
...
Array.sort(Strings, new LengthComparator());
这两个例子有一些共同点,
都是将一个代码块传递到某个对象(一个定时器,或者一个sort方法)。这个代码块会在将来某个时间调用。
② Lambda表达式
Lambda表达式也可以做到类似的作用,并且方式更加简单
Q:思考:Java为什么引入lambda表达式
A:lambda表达式是一个可传递的代码块,可以在以后执行一次或多次。 先来看看没有Lambda表达式时,Java是怎么传递 “函数指针的”。一般通过类似于接口+观察者模式来实现
③ 传递方法引用
普通方法引用
构造器引用
与C++不同(传递代码段的方式)
- Java
- Java很难传递代码段
- 在Java中传递一个代码段并不容易,不能直接传递代码段。 Java是一种纯面向对象语言,所以必须构造一个对象,这个对象的类需要有一个方法能包含所需的代码。
- 问题已经不是是否增强Java来支持函数式编程,而是要如何做到这一点。 设计者们做了多年的尝试,终于找到一种适合Java的设计 —— Lambda表达式(Java SE 8引入)
- 其他语言
- 在其他语言中可以直接处理代码块。
- Java设计者很长时间以来一直拒绝增加这个特性。Java非常重视其简单性和一致性。 他们认为如果只要一个特性能够让代码稍简洁一些,就把这个特性增加到语言中,这个语言很快就会变得一团糟,无法管理。
Lambda表达式(Java SE 8)
Lambda主要学习目的:
- 如何生成lambda表达式
- 如何把lambda表达式传递到需要一个函数式接口的方法
- 如何编写方法处理lambda表达式
定义方法
定义方法
(String first, String second)
-> first.length() - second.length()
(String first, String second) ->
{
if (first.length() < second.length()) return -1;
else if (first.length() > second.length()) return 1;
else return 0;
}
// 写法上有点像js的箭头函数,即使lambda表达式没有参数,仍然要提供空括号,就像无参数方法一样
类型自动推导
Comparator<String> comp
= (first, second) // 相当于 (String first, String second)
-> first.length() - second.length();
// 如果可以推导出一个lambda表达式的参数类型,则可以忽略其类型。
// 在这里,编译器可以推导出first和second必然是字符串,因为这个lambda表达式将赋给一个字符串比较器。
省略小括号
ActionListener listener = event -> System.out.println("The time is "+new Date());
// 相当于 (event) -> ... or (ActionEvent event) -> ...
// 如果方法只有一个参数,而且这个参数的类型可以推导得出,那么甚至还可以省略小括号
自动推导返回类型
// 无需指定lambda表达式的返回类型。lambda表达式的返回类型总是会由上下文推导得出
// 例如下面这句可以在需要int类型结果的上下文中使用
(String first, String second) -> first.length() - second.length()
自动推倒返回类型时,要确保返回唯一性
// 如果一个lambda表达式只在某些分支返回一个值,而在另外一些分支不返回值,这是不合法的
// 例如下面这句是不合法的
(int x) -> {if (x>=0) return 1;}
使用举例
如何在一个比较器和一个动作监听器中使用lambda表达式
package lambda;
import java.util.*;
import javax.swing.*;
import javax.swing.Timer;
/**
* 此程序演示如何使用lambda表达式
* @version 1.0 2015-05-12
* @author Cay Horstmann
*/
public class LambdaTest
{
public static void main(String[] args)
{
// Lambda作为比较器参数传入
String[] planets = new Sting[] {"水星", "金星", "地球", "火星", "木星", "土星", "天王星", "海王星"};
System.out.println(Attays.toString(Planets));
System.out.println("按字典名称排序:");
Array.sort(planets);
System.out.println(Arrays.toString(planets));
System.out.println("按长度排序:");
Arrays.sort(planets, (first,second) -> first.length() - second.length()); // Lambda的使用1
System.out.println(Arrays.toString(planets));
// Lambda表达式作为监听器参数传入
Timer t = new Timer(
1000,
event -> System.out.println("The time is " + new Date()) // Lambda的使用2
);
t.start();
// 保持程序运行,直到用户选择 "Ok"
J0ptionPane. showMessageDialog(nul1,"是否要退出程序?");
System.exit(O);
}
}
与C++不同(概念上)
Lambda表达式【区别还挺大的】
- Java
- lambda表达式所能做的也只是能转换为函数式接口。函数式接口这个概念其他语言也似乎是没有的
- 其他语言
- (这里指其他支持函数字面量的程序设计语言)
- 可以声明函数类型(如
(String, String) -> int
)、声明这些类型的变量,还可以使用变量保存函数表达式。 不过,Java设计者还是决定保持我们熟悉的接口概念,没有为Java语言增加函数类型。
与C++不同(写法上)
Java(区别极大)
// 这里用了类型自动推导。另外,最重要的一点是: // 这里表示可以用来代替函数式接口来使用,而不是真的赋值、保存Lambda表达式 Comparator<String> comp = (first, second) -> { first.length() - second.length(); } // 用BiFunction倒是可以保存一个Lambda表达式到BiFunction变量中,但这没有什么用 // 例如:没有哪个Arrays.sort方法想要接收一个BiFunction BiFunction<String, Sting, Integer> comp = (first, second) -> first.length() - second.length(); // Predicate接口才是专门用来传递lambda表达式
C++
// 语法 [捕获列表] (形参列表) mutable 异常列表-> 返回类型 { 函数体 } /* 捕获列表:捕获外部变量,捕获的变量可以在函数体中使用,可以省略,即不捕获外部变量。 形参列表:和普通函数的形参列表一样。可省略,即无参数列表 mutable:mutable 关键字,如果有,则表示在函数体中可以修改捕获变量,根据具体需求决定是否需要省略。 异常列表:noexcept / throw(...),和普通函数的异常列表一样,可省略,即代表可能抛出任何类型的异常。 返回类型:和函数的返回类型一样。可省略,如省略,编译器将自动推导返回类型。 函数体:代码实现。可省略,但是没意义。 */ // 举例 auto l = [](int x) -> bool { // ... }; auto lambda = [a, b](int x, int y)mutable throw() -> bool { return a + b > x + y; };
Python
self.aboutAct = QAction("关于", self, statusTip="关于界面" , triggered=lambda: QMessageBox.about(self, "About MDI", "WWWWWWWWWWWWW") ) # def about(self);
JavaScript
let max = (a,b) => a>b?a:b; // 也可用es6的箭头写 let max = function (a,b){ return a>b?a:b; }
函数式接口 (Functional Interface)
概念
函数式接口 (Functional Interface)
- 概念
- 就是一个有且仅有一个抽象方法,但是可以有多个非抽象方法的接口(Runoob的解释)
- 特点
- 函数式接口可以被隐式转换为 lambda 表达式
- 举例
- Java中已经有很多封装代码块的接口,如ActionListener或Comparator。 lambda表达式与这些接口是兼容的。
- 不能把lambda表达式赋给类型为Object的变量,Object不是一个函数式接口
几个概念的区别
三者区别(接口、函数式接口、Lambda表达式、函数、内联函数)
- 函数式接口 & 接口 & Lambda
- 函数式接口是一种特殊的接口
- 函数式接口可以隐式地转换为Lambda表达式(普通接口则不行)
- Lambda & 函数
- 其他语言:Lambda是一种特殊的函数、类似于匿名闭包函数的概念
- Java:没有函数这个概念,Lambda是一种可以用来代替函数式接口的东西
- 原理上
- C++:lambda表达式可以理解为一个匿名的内联函数
使用举例
展示如何转换为函数式接口
举例:Arrays.sort()方法
Array.sort(
words,
(first, second) -> first.length() - second.length()
);
// Arrays.sort 简明原理
// 原本它的第二个参数需要一个Comparator实例,方法会接收实现Comparator<String>的某个类的对象
// 而Comparator只有一个方法的接口,属于函数式借口,所以可以提供一个lambda表达式
// 在这个对象上调用compare方法会自动执行这个lambda表达式。这也是为什么函数式接口要求有且只有一个抽象方法
// 最好把lambda表达式看作是一个函数,而不是一个对象,另外要接受lambda表达式可以传递到函数式接口。
举例:Timer定时器
Timer t = new Timer(1000, event ->
{
System.out.println("At the tone, the time is " + new Date());
Toolkit.getDefaultToolkit().beep();
}
);
// 原本它的第二个参数要传入一个实现了ActionListener接口的类
// 而这个类只有一个方法的接口
这些对象和类的管理完全取决于具体实现,与使用传统的内联类相比,这样可能要高效得多。???(这句话不太懂)
Java库举例
后面 “处理Lambda” 表达式一章会详细讲
BiFunction,保存Lambda表达式到变量
Java API在java.util.function包中定义了很多非常通用的函数式接口
其中一个接口BiFunction<T,U,R>
描述了参数类型为T和U而且返回类型为R的函数。可以把我们的字符串比较lambda表达式保存在这个类型的变量中
BiFunction<String, Sting, Integer> comp
= (first, second) -> first.length() - second.length();
不过,这对于排序并没有帮助。没有哪个Arrays.sort方法想要接收一个BiFunction
这与其他函数式程序语言不同。如果你之前用过某种函数式程序设计语言,可能会发现这很奇怪
Predicate
java.util.function包中有一个尤其有用的接口Predicate
public interface Predicate<T>
{
boolean test(T t);
// 附加默认项和静态方法
}
ArrayList类有一个removeIf方法,它的参数就是一个Predicate。这个接口专门用来传递lambda表达式。
例如,下面的语句将从一个数组列表删除所有null值:
list.removeIf(e -> e == null);
方法引用(Method Reference)
- 普通方法引用(Method Reference)
- 构造器引用(Construct Reference)
使用举例
举例:Timer定时器
例如,假设你希望只要出现一个定时器事件就打印这个事件对象
// 方案一:传入Lambda表达式
Timer t = new Timer(1000, event -> System.out.println(event));
// 方案二:直接把println方法传递到Timer构造器
Timer t = new Timer(1000, System.out::println);
举例:Arrays.sort()方法
假设你想对字符串排序,而不考虑字母的大小写。可以传递以下方法表达式
Arrays.sort(strings, String::compareToIgnoreCase)
其他补充
等价的Lambda表达式
上面的 “举例:Timer定时器“ 中
System.out::println
是一个方法引用
(method reference), 它等价于lambda表达式:x->System.out.println(x)
函数重载问题
如果有多个同名的重载方法,编译器就会尝试从上下文中找出你指的那一个方法。
例如,Math.max方法有两个版本,一个用于整数,另一个用于double值。选择哪一个版本取决于 Math::max 转换为哪个函数式接口的方法参数。
::
操作符小总结
要用::
操作符分隔方法名与对象或类名。主要有3种情况
- object::instanceMethod
- Class::staticMethod
- Class::instanceMethod
在前2种情况中,方法引用等价于提供方法参数的lambda表达式
// 情况一。例如:
System.out::println /*等价于*/ x->System.out.println(x)
// 情况二。例如:
Math::pow /*等价于*/ (x,y)->Math.pow(x,y)
// 情况三。例如:
String::compareToIgnoreCase /*等同于*/ (x,y)->x.compareToIgnoreCase(y) // 其中第1个参数会成为方法的实例
结合this
/super
参数
可以在方法引用中使用this参数。例如,this::equals
等同于x->this.equals(x)
super::instanceMethod
例子
class Greeter
{
public void greet()
{
System.out.println("Hello, world!");
}
}
class TimedGreeter extends Greeter
{
public void greet()
{
// TimedGreeter.greet方法开始执行时,会构造一个Timer,它会在每次定时器滴答时执行super::greet方法
Timer t = new Timer(1000, super::greet);
t.start();
}
}
构造器引用(Construct Reference)
构造器引用与方法引用很类似,只不过方法名为new
例如:Person::new是Person构造器的一个引用
使用举例
假设你有一个字符串列表。可以把它转换为一个Person对象数组 为此要在各个字符串上调用构造器,调用如下:
ArrayList<String> names = ...;
Stream<Person> stream = names.stream().map(Person::new); // 传入构造器引用。
List<Person> people = stream.collect(Collectors.toList());
// map方法会为各个列表元素调用Person(String)构造器
其他补充
等价的Lambda表达式
可以用数组类型建立构造器引用。
例如,int[]::new
是一个构造器引用,它有一个参数:即数组的长度。这等价于lambda表达式x->new int[x]
函数重载问题
例如在上面的例子中
如果有多个Person构造器,编译器会选择有一个String参数的构造器,因为它从上下文推导出这是在对一个字符串调用构造器。
妙用:构造泛型类型T的数组
Java有一个限制,无法构造泛型类型T的数组。数组构造器引用对于克服这个限制很有用。表达式new T[n]会产生错误,因为这会改为new Object[n]
例如:
// 假设我们需要一个Person对象数组。Stream接口有一个toArray方法可以返回Object数组:
Object[] people = stream.toArray();
// 不过,这并不让人满意。用户希望得到一个Person引用数组,而不是Object引用数组
// 流库利用构造器引用解决了这个问题。可以把Person[]::new传入toArray方法
Person[] people = steam.toArray(Person[]::new);
// toArray方法调用这个构造器来得到一个正确类型的数组。然后填充这个数组并返回。
闭包问题(Lambda变量作用域)
写法 - 访问外部方法或变量
通常,你可能希望能够在lambda表达式中访问外围方法或类中的变量 其他编程语言的Lambda基本都有这个功能
public static void repeatMessage(String text, int delay)
{
ActionListener listener = event ->
{
// 注意这里的text变量
// 不是在这个lambda表达式中定义的,是repeatMessage方法的一个参数变量
// 这里会有问题:
// lambda表达式的代码可能会在repeatMessage调用返回很久以后才运行,而那时这个参数变量已经不存在了。
// 如何保留text变量呢?
System.out.println(text);
Toolkit.getDefaultToolkit().beep();
};
new Timer(delay, listener).start();
}
// 调用
repeatMessage("Hello", 1000); // 打印:Hello every 1,000 milliseconds
原理 - Lambda如何保留变量?
下面来巩固我们对lambda表达式的理解。lambda表达式有3个部分:
- 一个代码块
- 参数
- 自由变量的值。这是指非参数而且不在代码中定义的变量
我们说它被lambda表达式捕获(captured)
_
关于代码块以及自由变量值有一个术语:闭包(closure)。在Java中,lambda表达式就是闭包。
写法限制
lambda表达式可以捕获外围作用域中变量的值,但是会有一些限制
① 只能引用值不会改变的变量
原因:因为如果在lambda表达式中改变变量,并发执行多个动作时就会不安全
// 例如,下面的写法是不合法的
public static void countDown(int start, int delay)
{
ActionListener listener = event ->
{
start--; // 错误: 不能修改捕获的变量
System.out.println(start);
};
new Timer(delay, lestener).start();
}
② 不能引用可能在外部被改变的变量
如果在lambda表达式中引用变量,而这个变量可能在外部改变,这也是不合法的
lambda表达式中捕获的变量必须实际上是最终变量(effectively final)。最终变量是指:这个变量初始化之后就不会再为它赋新值
// 例如,下面的写法是不合法的
public static void repeat(String text, int count)
{
for(int i=1; i<=count; i++)
{
ActionListener listener = event ->
{
System.out.println(i+": "+text); // 报错:不能引用可能被在外部改变的变量
};
new Timer(1000, listener).start();
}
}
③ 不能声明同名变量
在lambda表达式中声明与一个局部变量同名的参数或局部变量是不合法的
这里同样适用命名冲突和遮蔽的有关规则
// 例如,下面的写法是不合法的
Path first = Paths.get("/usr/bin");
Comparator<String> comp =
(first, second) -> first.length() - second.length(); // 报错:变量“first”已经被定义过了
④ this的含义
在一个lambda表达式中使用this关键字时,是指创建这个lambda表达式的方法的this参数 这点有点像 js 箭头函数中 this 的使用
public class Application()
{
public void init()
{
ActionListener listener = event ->
{
// 这里的this.toString()
// 会调用Application对象的toString方法,而不是ActionListener实例的方法
System.out.println(this.toString());
...
}
...
}
}
与C++不同(闭包与变量捕获)
首先,**闭包 (closure)**是什么?
参考:
百度百科解释:
**(功能上.)**闭包就是能够读取其他函数内部变量的函数。
**(使用上.)**例如在javascript中,只有函数内部的子函数才能读取局部变量,所以闭包可以理解成“定义在一个函数内部的函数“。
**(本质上.)**在本质上,闭包是将函数内部和函数外部连接起来的桥梁。
mozilla解释:
**(组成上.)**闭包(closure)是一个函数以及其捆绑的周边环境状态(lexical environment,词法环境)的引用的组合。
**(功能上.)**换而言之,闭包让开发者可以从内部函数访问外部函数的作用域。
**(使用上.)**在 JavaScript 中,闭包会随着函数的创建而被同时创建。
Wiki:
(概念上.)在计算机科学中,闭包(Closure),又称词法闭包(Lexical Closure)或函数闭包(function closures)。 是在支持头等函数的编程语言中实现词法绑定的一种技术。
(组成/实现上.)闭包是一个结构体,它存储了一个函数(通常是其入口地址)和一个关联的环境(相当于一个符号查找表)。 环境里是若干对符号和值的对应关系,它既要包括约束变量(该函数内部绑定的符号),也要包括自由变量(在函数外部定义但在函数内被引用),有些函数也可能没有自由变量。
**(语言上.)**最早实现闭包的程序语言是Scheme。 之后,闭包被广泛使用于函数式编程语言 (如ML语言和LISP),很多命令式程序语言也开始支持闭包。
**(区分闭包和 函数 .)**它们最大的不同在于,当捕捉闭包的时候,它的自由变量会在捕捉时被确定,这样即便脱离了捕捉时的上下文,它也能照常运行。 捕捉时对于值的处理可以是值拷贝,也可以是名称引用,这通常由语言设计者决定,也可能由用户自行指定(如C++)。
**(区分闭包和匿名函数.)**它们经常被用作同义词。但严格来说,匿名函数就是字面意义上没有被赋予名称的函数,而闭包则实际上是一个函数的实例,也就是说它是存在于内存里的某个结构体。
各种语言中(类似)闭包的结构
C语言
C语言的回调函数
在C语言中,支持回调函数的库有时在注册时需要两个参数:一个函数指针,一个独立的
void*
指针用以保存用户数据。 这样的做法允许回调函数恢复其调用时的状态。这样的惯用法在功能上类似于闭包,但语法上有所不同。gcc对C语言的扩展
gcc编译器对C语言实现了一种闭包的程序特性。
C语言扩展:Blocks
略
C++函数对象
早期标准
早期标准允许通过重载
operator()
来定义函数对象。这种对象的行为在某种程度上与函数式编程语言中的函数类似。它们可以在运行时动态创建、保存状态,但是不能如闭包一般方便地隐式获取局部变量,并且有“专物专用”的繁琐问题——对于每一段闭包代码都要单独写一个函数对象类。
C++11
// C++11标准已经支持了闭包,这是一种特殊的函数对象,由特殊的语言结构——lambda表达式自动构建 void foo(string myname) { typedef vector<string> names; int y; names n; // ... names::iterator i = find_if(n.begin(), n.end(), [&](const string& s){return s != myname && s.size() > y;}); // 'i' 现在是'n.end()'或指向'n'中第一个 // 不等于'myname'且长度大于'y'的字符串 }
Java
Java SE 8 引入Lambda,可以用Lambda来实现闭包
注意事项
- ① 只能引用值不会改变的变量
- ② 不能引用可能在外部被改变的变量
- ③ 不能声明同名变量
- ④ this的含义
代码举例
public static void repeatMessage(String text, int delay) { ActionListener listener = event -> { // 这里捕获了text变量。不怕函数结束时该变量被销毁 System.out.println(text); Toolkit.getDefaultToolkit().beep(); }; new Timer(delay, listener).start(); } // 调用 repeatMessage("Hello", 1000); // 打印:Hello every 1,000 milliseconds
JavaScript
JavaScript 在闭包上要简单许多 Javascript 语言的特殊之处,就在于函数内部可以直接读取全局变量 JavaScript 变量可以是局部变量或全局变量。私有变量可以用到闭包。
// 实例1,a 是一个 局部 变量 function myFunction() { var a = 4; return a * a; } // 实例2,a 是一个 全局 变量 var a = 4; function myFunction() { return a * a; }
处理Lambda表达式
概述
使用lambda表达式的重点是延迟执行(deferred execution) 毕竟,如果想要立即执行代码,完全可以直接执行,而无需把它包装在一个lambda表达式中
写法
① 写法(使用默认的函数式接口:Runnable)
// 【目的】
// 假设你想要重复一个动作n次
// 【方法定义】
// 要接受这个lambda表达式,需要选择(偶尔可能需要提供)一个函数式接口
public static void repeat(int n, Runnable action)
{
for(int i = 0; i<n; i++) action.run(); // 调用 action.run() 时会执行这个lambda
}
// 【调用】
// 将这个动作和重复次数传递到一个repeat方法:
repeat(10, () -> System.out.println("Hello, World!"));
② 改进版本(使用自定义的函数式接口:IntConsumer)
// 【目的】
// 现在让这个例子更复杂一些。我们希望告诉这个动作它出现在哪一次迭代中。
// 【函数式接口定义】
public interface IntConsumer
{
void accept(int value);
}
// 【方法定义】
// 这里使用函数式接口:IntConsumer
public static void repeat(int n, IntConsumer action)
{
for (int i=0; i<0; i++) action.accept(i);
}
// 【调用】
repeat(10, i -> System.out.println("Countdown: " + (9-i)));
常用函数式接口
常用函数式接口(表一)
函数式接口 | 参数类型 | 返回类型 | 抽象方法名 | 描述 | 其他方法 |
---|---|---|---|---|---|
Runnable | none | void | run | 作为无参数或返回值的动作运行 | |
Supplier<T> | none | T | get | 提供一个T类型的值 | |
Consumer<T> | T | void | accept | 处理一个T类型的值 | andThen |
BiConsumer<T, U> | T, U | void | accept | 处理T和U类型的值 | andThen |
Function<T, R> | T | R | apply | 有一个T类型参数的函数 | compose, andThen, identity |
BiFunction<T, U, R> | T, U | R | apply | 有T和U类型参数的函数 | andThen |
UnaryOperator<T> | T | T | apply | 类型T上的一元操作符 | compose, andThen, identity |
BinaryOperator<T> | T, T | T | apply | 类型T上的二元操作符 | andThen, maxBy, minBy |
Predicate<T> | T | boolean | test | 布尔值函数 | and, or ,negate, isEqual |
BiPredicate<T, U> | T, U | boolean | test | 有两个参数的布尔值函数 | and, or, negate |
基本类型的函数式接口(表二)
函数式接口 | 参数类型 | 返回类型 | 抽象方法名 | 描述------------------------------------------------------------------------------------- |
---|---|---|---|---|
BooleanSupplier | none | boolean | getAsBoolean | |
PSupplier | none | p | getAsP | |
PConsumer | p | void | accept | |
ObjPConsumer<T> | T, p | void | accept | |
PFunction<T> | p | T | apply | |
PToQFunction | p | q | applyAsQ | |
ToPFunction<T> | T | p | applyAsP | |
ToPBiFunction<T, U> | T, U | P | applyAsP | |
PUnaryOperator | p | P | applyAsP | |
PBinaryOperator | p, p | P | applyAsP | |
PPredicate | p | boolean | test |
注:p,q为int,long,double;P,Q为Int,Long,Double
常用函数式接口的注意要点
使用优先级
① 最好优先使用上面表中的接口
例如:假设要编写一个方法来处理满足某个特定条件的文件。 虽然有一个遗留接口java.io.FileFilter,不过最好使用标准的 Predicate<File>
。 只有一种情况下可以不这么做,那就是你已经有很多有用的方法可以生成FileFilter实例
② 最好优先使用表二的接口
使用特殊化规范来减少自动装箱(即使用下面的表二而不是表一) 出于这个原因,前面的例子中使用了IntConsumer
而不是Consumer<Integer>
非抽象方法来生成或合并函数
大多数标准函数式接口都提供了非抽象方法来生成或合并函数
生成函数,例如:
Predicate.isEqual(a)
// 等同于
a::equals
// 不过如果a为null也能正常工作。
合并函数,例如:
// 提供了默认方法 and、or和negate 来合并谓词
Predicate.isEqual(a).or(Predicate.isEqual(b))
// 等同于
x->a.equals(x)||b.equals(x)
@FunctionalInterface 注解
如果设计你自己的接口,其中只有一个抽象方法,可以用@FunctionalInterface
注解来标记这个接口。
这样做有两个优点
① 如果你无意中增加了另一个非抽象方法,编译器会产生一个错误消息
② 另外javadoc页里会指出你的接口是一个函数式接口。
并不是必须使用注解。根据定义,任何有一个抽象方法的接口都是函数式接口。不过使用@FunctionalInterface注解确实是一个很好的做法
实例 - 再谈Comparator
Comparator接口包含很多方便的静态方法来创建比较器。这些方法可以用于lambda表达式或方法引用。
静态comparing方法
可以取一个“键提取器”函数,它将类型T映射为一个可比较的类型(如String)。
对要比较的对象应用这个函数,然后对返回的键完成比较。
代码举例
假设有一个Person对象数组
可以按名字对这些对象排序:
Arrays.sort(
people,
Comparator.comparing(Person::getName) // 与手动实现一个Comparator相比,这当然要容易得多。
// 另外,代码也更为清晰,因为显然我们都希望按人名来进行比较。
);
可以先比较姓再比较名:(把比较器与thenComparing方法串起来实现)
Array.sort
(
people,
Comparator.comparing(Person::getLastName).thenComparing(Person::getFirstName)
);
// 如果两个人的姓相同,就会使用第二个比较器
可以根据人名长度完成排序:
Array.sort
(
people,
Comparator.comparing(
Person::getName,
(s,t) -> Integer.compare(s.length(), t.length())
)
);
上面的代码还有一种更容易的做法:
Array.sort
(
people,
Comparator.comparingInt(p -> p.getName().length())
);
按可能为null的中名进行排序:
// 如果键函数可以返回null,可能就要用到nullsFirst和nullsLast适配器
// 原理:这些静态方法会修改现有的比较器,从而在遇到null值时不会抛出异常,而是将这个值标记为小于或大于正常值。
// 例如,假设一个人没有中名时getMiddleName会返回一个null,就可以使用
Comparator.comparing(Person::getMiddleName(), Comparator.nullsFirst(...))
// 下面是一个完整的调用,可以按可能为null的中名进行排序。
// 这里使用了一个静态导入java.util.Comparator.*,以便理解这个表达式
Array.sort
(
people,
Comparator.comparing(Person::getMiddleName, nullsFirst(naturalOrder()))
);
其他
静态reverseOrder方法会提供自然顺序的逆序。要让比较器逆序比较,可以使用reversed实例方法。
例如 naturalOrder().reversed()
等同于 reverseOrder()