Lambda 表达式是 JDK8 的一个新特性,它可以定义大部分的匿名内部类,从而让程序员能写出更优雅的Java代码,尤其在集合的各种操作中可以极大地优化代码结构。
(资料图)
一个接口的实现类可以被定义为匿名类。经过大量实践,人们发现定义一个接口的匿名实现类需要做很多“复制名称”的工作,如图8-50所示。
图8-50 定义接口的匿名实现类
图8-50展现了一段代码,这段代码中定义了一个接口MyInterface,并且在main()方法中定义了这个接口的匿名实现类。通过观察图片不难发现:图片中方框里面的内容实际上都是“复制”而来的,不仅仅复制了接口的名称,还复制了接口所定义的抽象方法的名称。由此可以看出:定义一个接口的匿名实现类时,程序员要做很多简单的复制工作,并把复制而来的内容按一定的规则粘贴到特定位置。这些工作都是没有任何创造性的,真正有创造性的工作只有编写匿名类中method()方法的代码。
那么,能否减少复制性的工作,以更简洁的方式来定义匿名类呢?从JDK1.8开始,Java语言提供了一种新的定义匿名类的方式,它被称为Lambda表达式。使用Lambda表达式创建MyInterface定义匿名实现类的代码如下。
()->{System.out.println("method方法被执行");};
在这条语句中,首先出现了一对小括号,这对小括号其实就是匿名类中method()方法的那对小括号,是用来传递参数的。因为method()方法没有参数,所以小括号中什么都没写。小括号的右边是一个减号和一个大于号,像箭头一样,这是 Lambda表达式语法规定的符号,读者只要记住它必须出现在小括号后面就可以了。箭头后面是一对大括号,我们都知道:任何方法都有一对大括号,语句中的这对大括号就是method()方法的那对大括号,程序员在大括号中写上实现抽象方法的代码就可以。以上代码中的输出语句就是抽象方法的实现代码。表达式中没有出现接口的名称,也没有出现抽象方法的名称,所以“复制名称”的工作全部被去除掉了,在Lambda表达式中只有用于实现抽象方法的语句代码。我们知道:匿名类在被定义时就必须同时创建出它的对象,所以一个Lambda表达式既表示匿名类的定义,也表示这个匿名类的对象。
在8.6小节中曾经讲过:匿名类都必须是某个接口的实现类或某个父类的子类,但Lambda表达式中没有接口的名称,所以编译器根本不知道这个Lambda表达式所定义的匿名类是哪一个接口的实现类。为了让编译器明确的知道Lambda表达式代表哪一个接口的匿名实现类,Java语言规定:由Lambda表达式所创建匿名类对象必须被一个引用所指向,这样编译器根据引用的类型就能判断出这个一个Lambda表达式所定义匿名类是哪一个接口的实现类。
MyInterface myInterface = ()->{System.out.println("method方法被执行");};
在这条语句中,Lambda表达式所创建的匿名实现类对象被一个引用所指向,这个引用的类型是MyInterface,编译器由引用的类型就能判断出这个Lambda表达式所定义的匿名类是MyInterface这个接口的匿名实现类。
如果用Lambda表达式充当某个方法的参数,编译器根据参数的类型也能判断出Lambda表达式所定义的匿名类是哪一个接口的实现类。
【例08_32 Lambda表达式当作方法参数】
MyInterface.java
public interface MyInterface { void method();}
Exam08_32.java
public class Exam08_32 { public static void call(MyInterface m) { m.method(); } public static void main(String[] args) { call(()->{System.out.println("method方法被执行");}); }}
【例08_32】的Exam08_32类定义了一个静态方法call()。这个方法的参数类型是MyInterface,在main()方法中调用call()方法时,用一个Lambda表达式所创建的匿名类对象作为方法的参数,编译器根据call()方法参数的类型就能判断出这个匿名类是MyInterface的实现类。
Lambda表达式中不仅没有接口的名称,甚至连方法的名称也没有出现。既然没有方法的名称,那么编译器如何判断Lambda表达式中所定义的方法到底实现了接口中的哪一个抽象方法呢?为了解决这个问题,Java语言规定:Lambda表达式只能用来定义仅有且仅有一个抽象方法的接口的匿名实现类。如果某个接口仅有一个抽象方法,那么Lambda表达式中的方法所实现的就必然是接口中唯一的那个抽象方法。专业上,把这种只有一个抽象方法的接口称为“函数式接口”。函数式接口的英文名称是“Single Abstract Method interfaces”,名称前三个单词的首字母是“SAM”,因此专业上也把函数式接口称为SAM接口。
无论一个接口中是否定义了有实现过程的默认方法和静态方法,也无论这个接口中是否定义了属性,只要这个接口中仅有一个抽象方法,它就是一个函数式接口,哪怕它唯一的抽象方法是从父接口那里继承而来的也可以。JDK1.8定义了一个新的注解,叫做 FunctionalInterface,只要把这个注解添加到一个接口的前面,就能检验出这个接口是不是函数式接口。如果这个接口不是函数式接口,就会出现语法错误,如图8-51所示。
图 检验函数式接口
图的接口MyInterface上面添加了FunctionalInterface注解,但它实际上定义了两个抽象方法,所以被检查出不是函数式接口并因此导致编译器报错。
函数式接口中只能定义一个抽象方法,但这条规定有一个例外情况,那就是:如果接口中的某个抽象方法能够在Object类中找到它的实现过程,那么这个抽象方法在接口中不占用一个抽象方法名额,如同8-52所示。
图函数式接口的例外情况
图8-52中的MyInterface接口定义了method()和hashCode()两个抽象方法,并且接口上面还添加了FunctionalInterface注解来检查它是不函数式接口,但编译器并没有因接口定义了两个抽象方法而报错,这是因为Object类中定义了一个hashCode()方法,这个hashCode()方法能够实现MyInterface接口中定义的hashCode()抽象方法。很多读者都会问:为什么函数式接口中一个抽象方法在Object类中能够找到实现过程,这个抽象方法就不占用抽象方法的名额呢?因为Java语言所有的类都是Object的子类,Lambda表达式定义的匿名类也不例外,所以这个匿名类能够继承Object类中的方法。虽然Lambda表达式只能实现1个抽象方法,但从Object类中继承来的方法替Lambda表达式实现了接口中的其他抽象方法,这样的话,在Object类中能够找到实现过程的抽象方法不需要由Lambda表达式去实现,因此它们不占用抽象方法的名额。
以上讲解的所有例子中,Lambda表达式定义的匿名类都被当作了接口的实现类,那么,Lambda表达式定义的匿名类能不能被当作抽象类的子类呢?Java语言规定:Lambda表达式只能用来定义函数式接口的匿名实现类,因此由Lambda表达式所定义的匿名类不能被当作抽象类的子类。
同样,使用引用来指向用Lambda表达式创建的匿名类对象时,引用的类型只能是函数式接口,否则会引起语法错误,如图8-53所示。
图8-53 引用类型不匹配导致语法错误
按理说Object是所有类的父类,使用Object类型的引用可以指向任何对象,包括匿名类对象,那么为什么图片中的语句为什么会出错呢?前文讲过,Java语言中Lambda表达式只能用来定义函数式接口的匿名实现类,而编译器就是根据引用的类型来判断Lambda表达式定义的匿名类是哪一个接口的实现类,图中用Object类型的引用指向Lambda表达式创建的匿名类对象,这会让编译器认为这个Lambda表达式定义的是Object的匿名子类而不是某个函数式接口的实现类,因此编译器会报错。
如果函数式接口中的抽象方法使用throws关键字声明了异常,用Lambda表达式定义这个接口的匿名实现类时不需要在小括号后面再次用throws关键字声明异常,否则就会出现语法错误。之所以不用在Lambda表达式中再次声明异常,是因为Lambda表达式被发明出来就是为了提供一种最简洁的定义匿名实现类的语法格式。这种语法格式遵循一个基本原则:如果能从函数式接口中找到的信息,都可以不出现在Lambda表达式中。因此,函数式接口中的抽象方法已经声明了异常,Lambda表达式中就不需要再次声明这些异常。
Lambda表达式的本质其实就是用来定义函数式接口匿名实现类的一种语法格式,这种语法格式相比传统的创建匿名类对象的语法格式要简洁很多。在某些情况下,Lambda表达式还可以写的更简洁,本小节就来梳理一下Lambda表达式的各种简化形式的写法。
1. 省略参数类型
如果函数式接口的抽象方法中定义了参数,那么在用Lambda表达式定义匿名实现类时可以省略定义方法的参数类型。
【例08_33 省略定义参数类型】
NewInterface.java
public interface NewInterface { void method(int x);}
Exam08_33.java
public class Exam08_33 { public static void main(String[] args) { NewInterface n1 = (int x)->{System.out.println("定义了参数类型");}; NewInterface n2 = (x)->{System.out.println("没有定义参数类型");}; n1.method(1); n2.method(2); }}
【例08_33】中,函数式接口NewInterface的抽象方法method()定义了1个参数,在main()方法中由Lambda表达式定义的匿名实现类在实现method()抽象方法时采用了两种书写形式,其中第二种书写形式只是定义出了参数的名称,并没有定义参数的类型。【例08_33】的运行结果如图8-54所示。
图8-54【例08_33】运行结果
从8-54可以看出,即使Lambda表达式中没有定义参数的类型,方法也依然能够运行成功。那么,编译器是如何判断调用方法时传入的参数是否合法呢?在调用方法时,编译器会检查每一个传入方法的实际参数能否与函数式接口中抽象方法定义的参数类型相匹配,以此来判断实际参数的合法性。需要注意:书写Lambda表达式时,要省略参数类型就全部省略,要不就干脆都不省略,不能只省略其中的一部分。
2. 省略小括号
Lambda表达式中有一对小括号,这对小括号中书写了方法的参数。如果函数式接口中的抽象方法有且仅有一个参数,在这种情况下,书写Lambda表达式时如果省略了参数类型,也可以同时省略小括号。因此,如果使用Lambda表达式定义NewInterface的匿名实现类可以采用如下形式:
NewInterface n = x->{System.out.println("省略小括号");};
以上代码中的Lambda表达式省略了参数x外围的小括号。需要注意:省略小括号的两个条件是:一、抽象方法本身有且仅有一个参数。二、书写Lambda表达式时省略了参数的类型。如果没有省略参数类型,那么小括号也不能被省略。
3. 省略大括号及return关键字
在Lambda表达式中,如果大括号中只有一条语句,那么这对大括号以及大括号外面的分号是可以省略的,因此可以用以下形式的Lambda表达式来定义NewInterface的匿名实现类。
NewInterface n = x->System.out.println("省略大括号");
如果Lambda表达式所实现的函数式接口中的抽象方法是一个有返回值的方法,那么在省略大括号时必须把return关键字一同省略掉。例如NewInterface中的method()抽象方法被定义为如下形式:
void method(int x);
在这种情况下,可以把Lambda表达式定义为:
NewInterface n = x->1;
这个Lambda表达式表示method()方法的返回值为1,它等同于:
NewInterface n = x->{return 1;};
本例中方法的返回值是一个常量,实际上,方法的返回值还可以是一个变量,也可以是一个表达式,甚至可以是一个方法的运行结果。总之,只要是能被当作方法的返回值的东西,都可以写在“->”的右边。但是要注意:如果Lambda表达式在实现抽象方法时需要用到多条语句,那么大括号以及return关键字都不能省略。读者可以仿照以上几个例子尝试用各种简化形式来书写Lambda表达式。
有些Lambda表达式所定义的匿名类在实现抽象方法的过程中只是简单的调用了另一个方法,除此之外并没有做其他操作。凡是具有这种特征的匿名类,都可以用一种更简洁的Lambda表达式来定义,这种简洁的定义形式被称为“方法引用”。请看下面的【例08_34】:
【例08_34 静态方法引用】
QuoteInterface1.java
public interface QuoteInterface1 { int method(int x);}
Exam08_34.java
public class Exam08_34 { public static void main(String[] args) { //传统方式定义匿名实现类 QuoteInterface1 q1 = new QuoteInterface1() { @Override public int method(int x) { return Math.abs(x); } }; //Lambda表达式定义匿名实现类 QuoteInterface1 q2 = x->Math.abs(x); //静态方法引用定义匿名实现类 QuoteInterface1 q3 = Math::abs; System.out.println(q1.method(-1)); System.out.println(q2.method(-2)); System.out.println(q3.method(-3));//① }}
【例08_34】中定义了一个函数式接口QuoteInterface1,接口中定义了抽象方法method()。在main()方法中,以不同的形式创建了3个QuoteInterface1接口的匿名实现类。匿名类没有名称,为了方便表述每个匿名类,此处暂时以指向匿名类对象的引用名称来指代这3个匿名类,所以在下面的表述中,定义在main()方法中的这3个匿名类分别被称为q1,q2和q3。
q1是以传统方式定义的匿名类,q1在实现method()方法过程中,只是调用了Math类的静态方法abs(),并把它的运算结果作为方法返回值,除此之外再无其他操作。q1的定义过程可以被简化为q2,因此,q2与q1这两个类实现抽象方法的过程实际上是相同的,只是写法不同而已。既然匿名类在实现抽象方法的过程只是调用其他方法,没有其他操作,那么只要能够明确的指出被调用的方法是什么,就已经完成了对抽象方法的实现。按照这种思想,Java语言提供了一种Lambda表达式的新写法,这种写法能够以最简洁的方式指出被调用的方法是什么,所以这种简洁的写法被称为“方法引用”。本例中的q1和q2这两个匿名类实现抽象方法的方式决定了它们都可以简化为方法引用的形式。由于被调用的方法又可以分为普通方法、静态方法和构造方法,所以方法引用也可以分为很多种类。本例中,实现抽象方法过程中调用的abs()是静态方法,所以要按“静态方法引用”的形式来书写Lambda表达式。本例中,以方法引用定义的匿名类是q3,它是q1和q2的简化版本,q3的定义代码是:
Math::abs;
q3的定义代码展现了静态方法引用的书写格式,这种格式与静态方法的调用格式非常相似,只是把调用方法时的点号(.)改成了双冒号(::)。双冒号左边的“Math”表示实现抽象方法时调用的静态方法属于Math类,而双冒号右边的“abs”则表明方法的名称是abs。但q3的定义代码没有体现出在调用abs()方法时要向它传递什么参数。那么虚拟机在执行abs()方法时应该给把什么参数传递给abs()方法呢?Java语言规定:执行代码时要“原封不动”的把匿名类方法的所有参数传递给表达式中的方法。
这条规定有些不好理解,此处以【例08_34】中的代码为例进行解释。在【例08_34】中,q3定义了QuoteInterface1的匿名实现类,在匿名类中实现了method()抽象方法,匿名类所实现的method()方法就是规定中所说的“匿名类方法”。在实现抽象方法的过程中,又调用了Math类的abs()方法,这个abs()方法当然会出现在Lambda表达式中,因此abs()方法就是规定中所说的“表达式中的方法”。语句①在执行method()方法时,给method()方法传递的参数是-3,虚拟机就会把这个参数原封不动的再传递给abs()方法。这条规定使得表达式中的方法有了明确的参数来源以及接收方式,所以在q3的定义代码中只需要指明被调用的方法属于哪一个类以及它的名称是什么就可以,无需指明要给方法传递哪些参数。此外,还需要强调:“原封不动”的传递参数,不仅仅是不能改变参数的值,还不能改变顺序。假设传递给某个匿名类方法的参数是x、y、z,那么把这些参数传递给表达式方法时也要按照x、y、z的顺序传递。各位读者可以自行运行【例08_34】,以此来体验以方法引用形式定义的匿名类对象是如何接收并传递参数的。
如果Lambda表达式定义的匿名类在实现抽象方法时仅调用了一个构造方法,这种情况下也可以把匿名类的定义简化为方法引用形式。因为被调用的是一个构造方法,所以这种方法引用被称为“构造方法引用”,构造方法引用的格式为:
类名::new |
下面的【例08_35】展示了如何使用构造方法引用定义函数式接口的匿名实现类。
【例08_35 构造方法引用】
QuoteInterface2.java
public interface QuoteInterface2 { Integer method(int x);}
Exam08_35.java
public class Exam08_35 { public static void main(String[] args) { //传统方式定义匿名实现类 QuoteInterface2 q1 = new QuoteInterface2() { @Override public Integer method(int x) { return new Integer(x); } }; //Lambda表达式定义匿名实现类 QuoteInterface2 q2 = x ->new Integer(x) ; //构造方法引用定义匿名实现类 QuoteInterface2 q3 = Integer::new; System.out.println(q1.method(1)); System.out.println(q2.method(2)); System.out.println(q3.method(3));//① }}
【例08_35】也是用3种不同方式定义了函数式接口QuoteInterface2的匿名实现类。q1和q2这两个匿名类的结构特征决定了它们都可以简化为方法引用的形式。本例中的q3就是用方法引用的形式定义的匿名类,它是q1和q2的简化版本,q3的定义代码是:
Integer::new
双冒号右边的“new”表示要调用构造方法,而双冒号左边的“Integer”表示method()方法中调用的是Integer类的构造方法。按照规定,语句①在执行method()方法时,会把传递给method()方法的参数原封不动的传递给Integer类的构造方法,这样构造方法就能创建出一个Integer类的对象,这个对象正是method()方法的返回值。
如果匿名类在实现抽象方法时调用的是一个普通方法,并且没有做其他操作,这种情况下也可以用方法引用的形式定义匿名类,这种方法引用被称为“实例方法引用”,实例方法引用的格式为:
对象名::方法名 |
下面的【例08_36】展示了如何使用实例方法引用定义函数式接口的匿名实现类。
【例08_36 实例方法引用】
QuoteInterface3.java
public interface QuoteInterface3 { int method(String substr,int start);}
Exam08_36.java
public class Exam08_36 { public static void main(String[] args) { String str = new String("abcdefabc"); // 传统方式定义匿名实现类 QuoteInterface3 q1 = new QuoteInterface3() { @Override public int method(String substr,int start) { return str.indexOf(substr, start); } }; // Lambda表达式定义匿名实现类 QuoteInterface3 q2 = (substr,start)->str.indexOf(substr, start); //实例方法引用定义匿名实现类 QuoteInterface3 q3 = str::indexOf; System.out.println(q1.method("abc",5)); System.out.println(q2.method("abc",5)); System.out.println(q3.method("abc",5)); }}
与前两个例子一样,【例08_36】也是用三种不同方式定义了函数式接口QuoteInterface3的匿名实现类。在匿名类实现抽象方法时都只调用了字符串对象str的indexOf()方法,并把方法的计算结果作为返回值。此处需要讲解一下indexOf()方法,这个方法的作用是寻找当前字符串对中参数所指定的子字符串处于什么位置。第一个参数就是要寻找的那个子字符串,第二个参数用来指定从哪个位置开始寻找。例如:
str.indexOf("abc",5);
这条语句表示在str这个字符串对象中寻找“abc”的位置,并且是从下标为5的位置开始寻找。需要注意:子字符串在整个字符串中的位置是从0而不是1开始计数的。字符串str的值是“abcdefabc”,所以“str.indexOf("abc",5);”的运行结果为6。
本例中在以方法引用形式定义的匿名实现类是q3,它的定义代码“str::indexOf”就表示调用str这个对象的indexOf()方法。当然,在执行method()方法时,还是会把传递给method()方法的参数原封不动的传递给indexOf()方法。
仔细观察【例08_36】的程序代码可以发现:无论q1、q2、q3在执行method()方法时传递了怎样的参数,都是在“abcdefabc”这个特定的字符串中寻找子字符串的位置,如果希望实现从任意字符串中寻找子字符串该怎么办呢?这种情况下就需要把字符串当作方法的参数传入到method()方法中,因此实现method()方法时要按照如下方式编写代码:
public int method(String str,String substr,int start) { return str.indexOf(substr, start);}
以这种方式定义的method()方法把字符串str当作参数,在方法中寻找str的某个子字符串的位置。在调用method()方法时,可以任意指定str参数的值,这样就能实现在任意字符串中寻找子字符串的位置。实现了这种形式的method()方法的匿名类也可以用方法引用来定义,这种方法引用称为“对象方法引用”。对象方法引用的格式为:
类名::方法名 |
下面的【例08_37】展示了如何用对象方法引用定义函数式接口的匿名实现类:
【例08_37 对象方法引用】
QuoteInterface4.java
public interface QuoteInterface4 { int method(String str,String substr,int start);}
Exam08_37.java
public class Exam08_37 { public static void main(String[] args) { // 传统方式定义匿名实现类 QuoteInterface4 q1 = new QuoteInterface4() { @Override public int method(String str,String substr,int start) { return str.indexOf(substr, start); } }; // Lambda表达式定义匿名实现类 QuoteInterface4 q2 = (str,substr,start)->str.indexOf(substr, start); //对象方法引用定义匿名实现类 QuoteInterface4 q3 = String::indexOf; System.out.println(q1.method("abcdefabc","abc",5)); System.out.println(q2.method("abcdefabc","abc",5)); System.out.println(q3.method("abcdefabc","abc",5)); }}
【例08_37】也是用三种不同方式定义了函数式接口QuoteInterface4的匿名实现类。q1和q2这两个匿名类的定义代码都很容易理解,下面重点解释一下q3的定义代码是什么意思。q3的定义代码是:
String::indexOf
双冒号的左边是“String”,这表示匿名类在实现抽象方法过程中要调用String类中定义的一个方法。双冒号的右边是“indexOf”,表示要调用String类中定义的indexOf()方法,但indexOf()方法并不是一个静态方法,必须通过一个String类的对象才能调用到这个方法,而q3的定义代码中并没有指明这个对象是谁。Java语言规定:如果使用对象方法引用定义匿名类,那么这个匿名类在实现抽象方法时,会通过传递给匿名类方法的第一个参数对象去调用方法,并把匿名类方法的剩余参数原封不动的传递给表达式中的方法。这条规定也不太好理解,此处以q3调用method()方法的语句为例进行讲解。q3调用method()方法的语句为:
q3.method("abcdefabc","abc",5);
以上语句中,method()方法就是匿名类方法。可以看到,语句中传递给匿名类方法的第一个参数是“abcdefabc”,这个参数就是一个字符串对象。按照规定,在method()方法中会通过“abcdefabc”这个字符串对象去调用方法。那么会调用哪一个方法呢?q3的定义代码已经明确指出要调用的是indexOf()方法。在调用indexOf()方法时,会把匿名类方法的剩余参数传递给它,匿名类方法的剩余参数是“abc”和5,所以传递给indexOf()方法的两个参数就是“abc”和5。这样的话,method()方法中所执行的语句实际上就是:
"abcdefabc".indexOf("abc",5);
经过分析可以看出:对象方法引用所定义的匿名类,在实现抽象方法的过程中,并不是“原封不动”的把匿名类方法的所有参数传递给表达式中的方法,而只是把除第一个参数以外的剩余参数传递给表达式中的方法。
对象方法引用和静态方法引用的写法有点类似,格式都是“类名::方法名”,编译器区分这两种方法引用的依据是检查双冒号后面的方法是一个普通方法还是一个静态方法。判断出方法引用的类别后就能采取不同的策略对方法引用定义的匿名类进行编译。
很多读者都会问:在执行method()方法时会调用它第一个参数对象的方法,而第一参数的类型已经被定义为String,为什么还要在定义q3时在双冒号的左边写上String这个类的名称呢?原因就是第一个参数有可能使用到泛型,而泛型在编译阶段都会被擦拭掉,在这种情况下编译器必须明确第一个参数到底是什么类型的对象,所以必须要在双冒号的左边写上类名。请看下面的【例08_38】
【例08_38 方法引用定义泛型接口实现类】
QuoteInterface5.java
public interface QuoteInterface5{ int method(T str,String substr,int start);}
Exam08_38.java
public class Exam08_38 { public static void main(String[] args) { QuoteInterface5q = String::indexOf; System.out.println(q.method("abcdefabc","abc",5)); }}
在【例08_38】中,函数式接口是一个泛型接口,接口中抽象方法第一个参数的类型被定义为泛型。虽然泛型在编译阶段会被擦拭,但对象方法引用已经明确的指出了indexOf()方法被定义在String中,所以无需通过第一个参数的类型去判断indexOf()方法的来源。
本小节讲解了方法引用的相关知识。方法引用本质上也是一种Lambda表达式,它以最简洁的形式指出了匿名类在实现抽象方法时要调用哪一个方法。但并非所有的Lambda表达式都能简化为方法引用的形式,读者在把一个普通的Lambda表达式简化为方法引用的时候一定要仔细观察它是否符合简化条件。
本文字版教程还配有更详细的视频讲解,小伙伴们可以点击这里观看。