Java Lambda表达式及方法引用

时间:2022-05-04 19:04:59

Lambda

Lambda表达式是Java SE 8中一个重要的新特性。允许你通过表达式来代替功能接口,其几乎解决了匿名内部类带来的所有问题。

其实Lambda表达式的本质是一个”语法糖”,由编译器推断并帮你转换包装为常规的代码,因此你可以使用更少的代码来实现同样的功能。

语法糖(Syntactic sugar),也译为糖衣语法,是由英国计算机科学家彼得·兰丁发明的一个术语,指计算机语言中添加的某种语法,这种语法对语言的功能并没有影响,但是更方便程序员使用。语法糖让程序更加简洁,有更高的可读性。


匿名内部类的问题

java闭包中的匿名内部类使用的非常广泛。匿名内部类使用的最常见的场景就是事件处理器了。其次匿名内部类还常被用在多线程的程序中,我们通常写匿名内部类,而不是创建 Runnable/Callable 接口的实现类。

虽然匿名类到处都在使用,但是他们还是有很多问题。

  1. 第一个主要问题是复杂。这些类让代码的层级看起来很乱很复杂,也称作 Vertical Problem 。
  2. 第二,他们不能访问封装类的非 final 成员,this 这个关键字将变得很有迷惑性。

如果一个匿名类有一个与其封装类相同的成员名称,内部变量将会覆盖外部的成员变量,在这种情况下,外部的成员在匿名类内部将是不可见的,甚至不能通过 this 关键字来访问。因为 this 关键字值得是匿名类对象本身而不是他的封装类的对象。例子如下:

public void anonymousExample() {
String nonFinalVariable = "Non Final Example";
String variable = "Outer Method Variable";
new Thread(new Runnable() {
String variable = "Runnable Class Member";
public void run() {
String variable = "Run Method Variable";
//下面注释句子编译会出错
//System.out.println("->" + nonFinalVariable);
System.out.println("->" + variable);
System.out.println("->" + this.variable);
}
}).start();
}

//输出
->Run Method Variable
->Runnable Class Member

Functional Interfaces

在我们进一步探讨 lambda 表达式之前,让我们来看一看 Functional Interfaces。
简单来说,Functional Interfaces 是一个只有单个抽象方法的接口。(但是可以有其他方法,例如有其他继承Object而来的方法)。

大多数回调接口都是 Functional Interfaces。例如 Runnable,Callable,Comparator 等等。以前被称作 SAM(Single Abstract Method)

官方原文如下:
(The‘Single’method can exist in the form of multiple abstract methods that are inherited from superinterfaces. But in that case the inherited methods should logically represent a single method or it might redundantly declare a public method that is provided by classes like Object, e.g. toString.)

interface Runnable { void run(); }
// 1.Functional
interface Foo { boolean equals(Object obj); }
//2. Not functional; equals是Object类的方法
interface Bar extends Foo {int compare(String o1, String o2); }
//3. Functional; Bar有一个抽象的不是object类的方法
interface Comparator {
boolean equals(Object obj);
int compare(T o1, T o2);
}
//4. Functional; Comparator有一个抽象的不是object类的方法,且equals是object的方法
interface Foo {int m(); Object clone(); }
//5. Not functional; 方法Object.clone并不是public的
interface X { int m(Iterable arg); }
interface Y { int m(Iterable arg); }
interface Z extends X, Y {}
//6. Functional: 虽然有两个方法,但是这两个方法的声明是完全一样的,在Z中只当做一个方法

PS:Object中的clone()是protected的,而接口中的方法只能是public,所以在接口中声明Object clone();会被当做一个新的方法,在上面的第4个接口便包含了两个自己声明的方法,所以不是functional interface。


Lambda表达式

lambda表达式允许你通过表达式来代替上面的Functonal接口。 lambda表达式就和方法一样,它提供了一个正常的参数列表和一个使用这些参数的主体(body,可以是一个表达式或一个代码块)。

基本语法:
(parameters) -> expression 或 (parameters) ->{ statements; }
例如 :
(String s)-> s.lengh;
() ->{ return true; }
(int x, int y) -> { x + y; }

我们上边说过,匿名类的一个主要问题是是代码的层级看起来很乱,也就是 Vertical Problem 了,Lamdba 表达式实际上就是匿名类,只不过他们的结构更轻量,更短。Lambda 表达式看起来像方法。他们有一个正式的参数列表和这些参数的块体表达。

public class FirstLambdaExpression {
public String variable = "Class Level Variable";
public static void main(String[] arg) {
new FirstLambdaExpression().anonymousExample();
}
public void anonymousExample() {
String nonFinalVariable = "Non Final Example";
String variable = "Outer Method Variable";
new Thread(() -> {
//下面注释句子编译会出错
//String variable = "Run Method Variable"
System.out.println("->" + variable);
System.out.println("->" + this.variable);
}).start();

}
}

//输出
->Outer Method Variable
->Class Level Variable

你可以比较一些使用 Lambda 表达式和使用匿名内部类的区别。我们可以清楚的说,使用 Lambda 表达式的方式写匿名类解决了变量可见性的问题。Lambda 表达式不允许创建覆盖变量。

通常的 Lambda 表达式的语法包括一个参数列表,箭头关键字”->”最后是主体。主体可以是表达式(单行语句)也可以是多行语句块。如果是表达式,将被计算后返回,如果是多行的语句块,就看起来跟方法的语句块很相似了,可以使用 return 来指定返回值。

为什么选择这个特殊的语法形式呢,因为目前 C# 和 Scala 中通常都是这种样式,也算是 Lambda 表达式的通用写法。这样的语法设计基本上解决了匿名类的复杂性。但是与此同时他也是非常灵活的,例如,如果方法体是单个表达式,大括号和 return 语句都是不需要的。表达式的结果就是作为他自己的返回值。这种灵活性可以保持代码简洁。


lambda例子

接下来我们再看看一些lambda的例子:

//首先定义一个人物列表players
String[] atp = {"Rafael Nadal", "Novak Djokovic",
"David Ferrer", "Roger Federer",
"Andy Murray", "Tomas Berdych"};
List<String> players = Arrays.asList(atp);
//***要循环逐个输出列表中的名字***//
// 1、可以使用List的foreach函数,里面的参数是Comsunmer类型,foreach有一个循环会逐个调用Comsumer的accept方法。可以可以自己去看源码得知。
players.forEach(new Consumer<String>() {
@Override
public void accept(String player) {
System.out.print(player + "; ");
}
});

//2、 因为Comsuer是一个Fuctional Interface,所以可以使用 lambda 表达式调用,下面式子功能跟上面完全一致。
players.forEach((player) -> System.out.print(player + "; "));

//3、Comsunmer也可以单独使用lambda表达式新建实例再传进foreach函数
Consumer<String> consumer = (player) ->
System.out.print(player + "; ");
players.forEach(consumer);
//***根据 name首字母 排序 players***//
//1、使用Arrays的sort方法排序,有个Comparator类型的参数作为排序规则。
Arrays.sort(players, new Comparator<String>() {
@Override
public int compare(String s1, String s2) {
return (s1.compareTo(s2));
}
});

//2、同样的Compartor也是一个Functional Interface,所以可以使用Lambda代替
Arrays.sort(players, (String s1, String s2) -> (s1.compareTo(s2)));

//3、同样的Compartor可以单独使用lambda表达式新建实例传进sort函数
Comparator compatator = (String s1, String s2) -> (s1.compareTo(s2));
Arrays.sort(players, compatator);

仔细看看 lambda 表达式,目标接口类型不在表达式中完全没有声明到。

一个很明显的问题来了,为什么 Lambda 表达式不需要一个指定的方法名呢?

Lambda 表达式只能用于 functional interface ,而 functional interface 只有一个方法。
而且编译器会根据所需类型、参数个数、函数返回类型帮助推断 lambda 表达式的类型。

下面例子有点繁杂,大家要耐心看:

//下面定义两个functional interface
interface intTypeInterface { int test(); }
interface StringTypeInterface { String test(); }
interface paramInterface { String test(String param); }

class Test {
//两个同名不同参数不同返回类型的函数
//函数返回int类型的接口
void invoke(intTypeInterface a) { System.out.println("intType"); }
//函数返回String类型的接口
void invoke(StringTypeInterface b) { System.out.println("StringType");}
//函数返回String类型,但是有两个参数的接口
void invoke(paramInterface b) { System.out.println("param");}


//***如果调用invoke函数会调用哪一个?***//

public static void main(String[] args) {
Test test = new Test();

//根据 1 返回类型得知应实例化intTypeInterface接口
test.invoke( () -> 1 );
//根据 "String" 返回类型得知应实例化StringTypeInterface接口
test.invoke( () -> "String" );
//根据函数参数得知应实例化paramInterface接口
test.invoke( (String s) -> "String" );
}

可以看出,编译器是根据invoke里面的参数个数和类型去调用符合的invoke函数,而invoke函数里面的参数是什么类型的是根据lambda匹配到了什么而确定。
很明显,这里的lambda表达式是根据参数个数,函数返回类型进行匹配确定需要实例化哪个接口。

List list = Arrays.asList(
(Callable) ()->"callable 1",
(Callable) ()->"callable 2",
(Callable) ()->"callable 3");

Lambda表达式可以显式的转换为指定的目标类型,只要跟所需的类型兼容。看一下下面的程序,我实现了三种Callable,而且都将其转换为Callable类型。

匹配总结:

Lambda 表达式必须有一个目标类型,而他们可以适配任意可能的目标类型。当目标类型是一个接口的时候,下面的条件必须满足,才能编译正确:

  1. 接口应该是一个 functional interface
  2. 表达式的类型和参数数量必须与 functional interface 中声明的一致
  3. 返回值类型必须兼容 functional interface 中方法的返回值类型
  4. 抛出的异常表达式必须兼容 functional interface 中方法的抛出异常声明

PS :
一、由于编译器可以通过目标类型的声明中得知参数类型和个数,所以在 Lambda 表达式中,可以省略参数类型声明。

Comparator c = (s1, s2) -> s1.compareToIgnoreCase(s2);

二、如果目标类型中声明的方法只接收一个参数(很多时候都是这样的),那么参数的小括号也是可以不写的,例如:

players.forEach(player -> System.out.print(player + "; "));

方法引用

Lambda 表达式允许我们定义一个匿名的方法,并将它作为 Functional interface 的一个实例。方法引用跟 Lambda 表达式很像,他们都需要一个目标类型,但是不同的是方法引用不提供方法的实现,他们引用一个已经存在的类或者对象的方法。
类似如下结构:

System::getProperty
String::length
Integer::compareTo
ArrayList::new

上面的语句展示了方法和构造函数的引用的通用语法。::符号叫做双冒号操作符(double colon operator),用来表示对某个类或对象的某个方法的引用。

class Dog {
//定义了一个类方法speak
static void speak() {
System.out.println("我就是一条狗");
}
public static void main(String[] args) {
//用一个线程去运行Dog的speak方法
new Thread(new Runnable() {
@Override
public void run() {
Dog.speak();
}
});
//1、使用lambda表达式
new Thread(() -> Dog.speak()).start();
//2、使用方法引用
new Thread(Dog::speak()).start;
}
}

new Thread()需要一个Runnable对象,我们用lambda表达式() -> Dog.speak() 实例化了Runnable对象。
而这里方法引用只用了一个Dog::speak(),为什么可以这样呢?同样的原因,方法引也是用在Functional Interface里面,其只有一个方法,所以我们可以连方法的结构都不写直接让编译器去识别实例化需要类型的对象。

//前面例子我们用sort函数和Comparator对players按照名字进行排序
Arrays.sort(players, new Comparator<String>() {
@Override
public int compare(String s1, String s2) {
return (s1.compareTo(s2));
}
});

//这里我们建立一个类包含了一个比较函数
class Util {
public static int mycompare(String s1, String s2) {
return (s1.compareTo(s2));
}
}

//我们用自己写的mycompare函数排序
Arrays.sort(players, new Comparator<String>() {
@Override
public int compare(String s1, String s2) {
return Util.mycompare(s1, s2);
}
});

//**这里便可以用方法引用的方式,功能同上**//
Arrays.sort(players, Util::mycompare);

其实上面的

sort 方法的第二个参数是 Comparator 类型的,但是我们却传递了 Util 的一个静态方法引用。重要的问题就在这了,我既没有让 Util 实现 Comparable 接口,也没有写一个独立的 Comparator 类,但是输出确实没有任何问题。

让我们来看一看这是为什么。

Arrays.sort 方法期望一个 Comparator 的实例,而这个 Comparator 是一个 functional interface ,这就意味着他只有一个方法 compare 了。compare方法有两个都是String类型的参数,而且返回类型是int。而这里与Util的mycompare函数参数的类型、数量,返回值都是相同的,只有他们的名字是不一样的。这时候我们就可以创建一个方法引用,并将它传递给 sort 作为第二个参数。

当有多个相同的名称的方法的时候,编译器会根据目标类型选择最佳的匹配。为了搞明白,来看一个例子:

class MyComparator {
//同样的名字,不同参数
public static int myCompare(String s1, String s2) {
return s1.compareTo(s2);
}
public static int myCompare(Integer int1, Integer int2) {
return int1 - int2;
}
}
//建立两个数组
String[] strings= {"Rafael Nadal", "Novak Djokovic", "Roger Federer"};
Integer[] ints = {1 , 4, 8, 2, 3, 8, 6};

//使用myComparator的函数排序
MyComparator comparator = new MyComparator();
Arrays.sort(strings, comparator::myCompare);
Arrays.sort(ints, comparator::myCompare);

这里,两行代码中的方法引用声明都是相同的(comparator::myCompare),唯一不同的是我们传入的数组,这里编译器会帮助我们检查第一个参数,并且智能的找到合适的引用方法。

所以我们知道对于方法引用,匹配的是函数参数的类型、数量,返回值,程序会进行匹配以调用合适的方法。

方法应用 :
静态方法 : 类名::方法名
实例方法 : 对象::方法名

上面ints数组的排序也可以写成 :

Arrays.sort(ints, Integer::compareTo);

是不是很他妈神奇,明明Integer中的compareTo()函数不是static的,我们却直接使用类名引用了。

答案是:这种类型的语句允许使用在一些特定的类型中。Integer和String是某种数据类型,而对于数据类型来说,这种语句是允许的。

如果我们将 Util 的方法 myCompare 变成非静态的,然后这样使用:Util::myCompare,就会出编译错误:No Suitable Method Found。


构造方法引用

构造方法引用是 JavaSE 8 的一个新的特性。我们可以构造一个构造方法的引用,并且将它作为参数传递给目标类型。

当我们使用方法引用的时候,我们引用一个已有的方法使用他们。同样的,在使用构造方法引用的时候,我们创建一个已有的构造方法的引用。

结构是 : 构造方法引用的语法类名::new,这看起来很像方法引用。这种构造方法的引用可以分配给目标 functional interface 的实例。一个类可能有多个构造方法,在这种情况下,编译器会检查 functional interfaces 的类型,最终找到最好的匹配。

public class ConstructorReference {
public static void main(String[] ar){
//会把 Myclass的 实例传递给 MyInterface 的实例
MyInterface in = MyClass::new;
System.out.println("->"+in.getMeMyObject());
}
}
//Functional Interface
interface MyInterface{
MyClass getMeMyObject();
}
class MyClass{
MyClass(){}
}

//输出 MyClass@6d311334

那怎样实例化一个带参数的构造方法引用?

public class ConstructorReference {
public static void main(String[] ar){
EmlpoyeeProvider provider = Employee::new;
Employee emp = provider.getMeEmployee("John", 30);
System.out.println("->Employee Name: "+emp.name);
System.out.println("->Employee Age: "+emp.age);
}
}
interface EmlpoyeeProvider{
Employee getMeEmployee(String s, Integer i);
}
class Employee{
String name;
Integer age;
Employee (String name, Integer age){
this.name = name;
this.age = age;
}
}

//输出:
->Employee Name: John
->Employee Age: 30