Java8新增之Lambda表达式
前言
Lambda表达式是Java 8的重要更新,也是一个被广大开发者期待已久的新特性。Lambda表达式支持将代码块作为方法参数, Lambda表达式允许使用更简洁的代码来创建只有一个抽象方法的接口(这种接口被称为函数式接口)的实例。
一、Lambda表达式入门
下面先使用匿名内部类实现一个自定义的Command接口,通过重写其方法来处理一个整型数组。代码如下:
//Command接口,Command.java
public interface Command {
void sumArray(int[] arr);
}
//CommandTest类,CommandTest.java
public class CommandTest {
public static void main(String[] args) {
//现在要求处理数组,求所有整数的和
int[] array = new int[]{1,2,56,38,99,12,56,2};
CommandTest commandTest = new CommandTest();
//处理数组,具体的处理方法由匿名内部类决定
commandTest.testSumArray(array, new Command() {
@Override
public void sumArray(int[] arr) {
int sum = 0;
for ( int i : arr ) {
sum += i;
}
System.out.println("该数组的和为: " + sum);
}
});
}
//传入一个Command变量
public void testSumArray(int[] arr1 , Command com){
com.sumArray(arr1);
}
}
如果希望 CommanTest类的testSumArray()方法处理数组时,可以动态传入一段代码作为具体的处理行为,那么可以创建一个匿名内部类实例来封装处理行为。从上面代码可以看出,用于封装处理行为的关键就是实现程序中new Command(){…}中花括号{}里面的代码。但为了向testSumArray()方法传入这段粗体字代码,程序不得不使用匿名内部类的语法来创建对象。
而Lambda表达式完全可用于简化创建匿名内部类对象,因此可将上面调用testSumArray()方法即14~24行代码改为如下形式:
commandTest.testSumArray(array, (int[] arr) -> {
int sum = 0;
for ( int i : arr ) {
sum += i;
}
System.out.println("该数组的和为: " + sum);
});
从上面程序代码可以看出,这段代码与创建匿名内部类时需要实现的sumArray()(Command接口的唯一方法)方法完全相同,只是不需要new Xxx(){}这种烦琐的代码,不需要指出重写的方法名字,也不需要给出重写的方法的返回值类型—只要给出重写的方法括号以及括号里的形参列表即可。从上面可以看出,当使用Lambda表达式代替匿名内部类创建对象时, Lambda表达式的代码块将会代替实现抽象方法的方法体, Lambda表达式就相当一个匿名方法。
从上面语法格式可以看出, Lambda表达式的主要作用就是代替匿名内部类的烦琐语法。它由三部分组成:
(1)形参列表。形参列表允许省略形参类型。如果形参列表中只有一个参数,甚至连形参列表的圆
括号也可以省略。
(2)箭头(->)。必须通过英文中画线和大于符号组成。
(3)代码块。如果代码块只包含一条语句, Lambda表达式允许省略代码块的花括号,那么这条语句
就不要用花括号表示语句结束。Lambda代码块只有一条return语句,甚至可以省略return关键字。Lambda表达式需要返回值,而它的代码块中仅有一条省略了return的语句, Lambda表达式会自动返回这条语句的值。
下面程序示范了Lambda表达式的几种简化写法:
//定义三个接口
interface Eatable{
void eat();
}
interface Flyable{
void fly(String s1);
}
interface Addable{
int add(int a,int b);
}
public class LambdaTest {
//定义三个方法分别调用三个接口
public void testEat(Eatable eatable){
eatable.eat();
}
public void testFly(String s,Flyable flyable){
flyable.fly(s);
}
public void testAdd(Addable addable){
System.out.println("求和46,23: " + addable.add(46,23));
}
//测试
public static void main(String[] args) {
LambdaTest lambdaTest = new LambdaTest();
//Lambda表达式的代码块只有一条语句,可以省略花括号
lambdaTest.testEat(() -> System.out.println("我正在吃火锅"));
//Lambda表达式的形参列表只有一个形参,可以省略圆括号
lambdaTest.testFly("晴天",str -> {
System.out.print("今天天气是" + str);
System.out.println(",飞机安稳落地");
});
//Lambda表达式的代码块只有一条语句,可以省略花括号
//代码块中只有一条语句,即使该表达式需要返回值,也可以省略return关键字
lambdaTest.testAdd((a,b) -> a+b);
}
}
从上面可以看出Lambda表达式实际上将会被当成一个“任意类型”的对象,到底需要当成何种类型的对象,这取决于运行环境的需要。
二、Lambda表达式与函数式接口
1.引入
Lambda表达式的类型,也被称为“目标类型(target type)", Lambda表达式的目标类型必须是“函数式接口(functional interface)",函数式接口代表只包含一个抽象方法的接口。函数式接口可以包含多个默认方法、类方法,但只能声明一个抽象方法。
如果采用匿名内部类语法来创建函数式接口的实例,则只需要实现一个抽象方法,在这种情况下即可采用Lambda表达式来创建对象,该表达式创建出来的对象的目标类型就是这个函数式接口。查询Java 8的API文档,可以发现大量的函数式接口,例如: Runnable, ActionListener等接口都是函数式接口。
Java 8专门为函数式接口提供了@Functionallnterface注解,该注解通常放在接口定义前面,该注解对程序功能没有任何作用,它用于告诉编译器执行更严格检查—检查该接口必须是函数式接口,否则编译器就会报错。
由于Lambda表达式的结果就是被当成对象,因此程序中完全可以使用Lambda表达式进行赋值,例如如下代码:
public class LambdaTest1 {
public static void main(String[] args) {
Runnable run1 = () -> {
for (int i = 0; i < 5; i++) {
System.out.print(i +" ");
}
};
run1.run();
}
}
Runnable是Java本身提供的一个函数式接口,只有一个抽象方法,源代码如下:
从上面代码可以看出, Lambda表达式实现的是匿名方法—因此它只能实现特定函数式接口中的唯一方法。这意味着Lambda表达式有如下两个限制:
(1)Lambda表达式的目标类型必须是明确的函数式接口。
(2)Lambda表达式只能为函数式接口创建对象。Lambda表达式只能实现一个方法,因此它只能为只有一个抽象方法的接口(函数式接口)创建对象。
2.扩展
关于上面的第一点限制,改变上段代码如下:
package com.man.man6;
public class LambdaTest1 {
public static void main(String[] args) {
Object run1 = () -> {
for (int i = 0; i < 5; i++) {
System.out.print(i +" ");
}
};
run1.run();
}
}
运行代码可以看到,会报如下错误:
从该错误信息可以看出, Lambda表达式的目标类型必须是明确的函数式接口。上面代码将Lambda表达式赋值给Object变量,编译器只能确定该Lambda表达式的类型为Object,而Object并不是函数式接口,因此上面代码报错。
为了保证Lambda表达式的目标类型是一个明确的函数式接口,可以有如下三种常见方式。
(1)将Lambda表达式赋值给函数式接口类型的变量。如 Runnable run1 = () ->{…}
(2)将Lambda表达式作为函数式接口类型的参数传给某个方法。如commandTest.testSumArray(array, (int[] arr) -> {});
(3)使用函数式接口对Lambda表达式进行强制类型转换。如Object run1 = (Runnable)() ->{…}
需要说明的是,同样的Lambda表达式的目标类型完全可能是变化的—唯一的要求是, Lambda表达式实现的匿名方法与目标类型(函数式接口)中唯一的抽象方法有相同的形参列表。
Java 8在java.util.function包下预定义了大量函数式接口,典型地包含如下4类接口:
(1)XxxFunction:这类接口中通常包含一个apply()抽象方法,该方法对参数进行处理、转换(apply()方法的处理逻辑由Lambda表达式来实现),然后返回一个新的值。该函数式接口通常用于对指
定数据进行转换处理。
(2)XxxConsumer:这类接口中通常包含一个accept()抽象方法,该方法与XxxFunction接口中的apply()方法基本相似,也负责对参数进行处理,只是该方法不会返回处理结果。
(3)XxxxPredicate:这类接口中通常包含一个test()抽象方法,该方法通常用来对参数进行某种判断
(test()方法的判断逻辑由Lambda表达式来实现),然后返回一个boolean值。该接口通常用于判断参数是否满足特定条件,经常用于进行筛滤数据。
(4)XxxSupplier:这类接口中通常包含一个getAsXxx()抽象方法,该方法不需要输入参数,该方法会按某种逻辑算法(getAsXxx ()方法的逻辑算法由Lambda表达式来实现)返回一个数据。
三、引用方法与引用构造器
前面说过,如果Lambda表达式的代码块只有一条代码,程序就可以省略Lambda表达式中代码块的花括号。不仅如此,如果Lambda表达式的代码块只有一条代码,还可以在代码块中使用方法引用和构造器引用。
方法引用和构造器引用可以让Lambda表达式的代码块更加简洁。方法引用和构造器引用都需要使用两个英文冒号。Lambda表达式支持如如下表所示的几种引用方式:
1.引用类方法
//Converter 接口,Converter.java
@FunctionalInterface
public interface Converter {
Integer conver(String from);
}
//LambdaTest1类,LambdaTest1.java
public class LambdaTest1 {
public static void main(String[] args) {
Converter con1 = from -> Integer.valueOf(from);//valueOf()为Integer包装类的类方法
System.out.println(con1.conver("12345"));
//1.引用类方法
Converter con2 = Integer :: valueOf;//::前面写类方法所在类,::后写方法名
System.out.println(con2.conver("559"));
}
}
对于上面的类方法引用,也就是调用Integer类的valueOf)类方法来实现Converter函数式接口中唯
一的抽象方法,当调用Converter接口中的唯一的抽象方法时,调用参数将会传给Integer类的valueof()
类方法。
2.引用特定对象的实例方法
//Converter 接口,Converter.java
@FunctionalInterface
public interface Converter {
Integer conver(String from);
}
//LambdaTest2类,LambdaTest2.java
public class LambdaTest2 {
public static void main(String[] args) {
Converter con3 = from -> "sjkcidh".indexOf(from);//特定对象为String类对象:"sjkcidh"
System.out.println(con3.conver("k"));
//2.引用特定对象的实例方法
Converter con4 = "sjkcidh" :: indexOf;
System.out.println(con4.conver("k"));
}
}
上面代码调用对象的convert()方法时—由于对象是Lambda表达式创建的,convert()方法执行体就是Lambda表达式的代码块部分。
3.引用某类对象的实例方法
//ConverterTwo接口,ConverterTwo.java
@FunctionalInterface
public interface ConverterTwo {
String test(String a,int b,int c);
}
//LambdaTest3类,LambdaTest3.java
public class LambdaTest3 {
public static void main(String[] args) {
ConverterTwo converterTwo1 = ( a, e, c) -> a.substring(e, c);
System.out.println(converterTwo1.test("askkkkkd",2,4));
//3.引用某类对象的实例方法
ConverterTwo converterTwo2 = String :: substring;
System.out.println(converterTwo2.test("sssssssdwc",2,5));
}
}
方法引用代替Lambda表达式:引用某类对象的实例方法;函数式接口中被实现方法的第一个参数作为调用者;后面的参数全部传给该方法作为参数。
4.引用构造器
//ConverterConstructor接口,ConverterConstructor.java
@FunctionalInterface
public interface ConverterConstructor {
Test1 constructor(String a);
}
//Test1类,Test1.java
public class Test1 {
private String s1;
public Test1() {
}
public Test1(String s1) {
this.s1 = s1;
}
}
//LambdaTest4类,LambdaTest4.java
public class LambdaTest4 {
public static void main(String[] args) {
ConverterConstructor converterConstructor1 = a -> new Test1(a);
System.out.println(converterConstructor1.constructor("s"));
//4.引用构造器
ConverterConstructor converterConstructor2 = Test1 :: new;
System.out.println(converterConstructor2.constructor("dd"));
}
}
四、Lambda表达式与匿名内部类的联系和区别
1.联系
从前面可以看出, Lambda表达式是匿名内部类的一种简化,因此它可以部分取代匿名内部类的作用, Lambda表达式与匿名内部类存在如下相同点:
(1)Lambda表达式与匿名内部类一样,都可以直接访问"effectively final"的局部变量,以及外部类的成员变量(包括实例变量和类变量)。
(2)Lambda表达式创建的对象与匿名内部类生成的对象一样,都可以直接调用从接口中继承的默认方法。
Java 中局部内部类和匿名内部类访问的局部变量必须由 final 修饰,以保证内部类和外部类的数据一致性。但从 Java 8 开始,我们可以不加 final 修饰符,由系统默认添加,当然这在 Java 8 以前的版本是不允许的。Java 将这个功能称为 Effectively final 功能。从 Java 8 开始,它不要求程序员必须将访问的局部变量显式的声明为 final 的。只要该变量不被重新赋值就可以。
一个非 final 的局部变量或方法参数,其值在初始化后就从未更改,那么该变量就是 effectively final。在 Lambda 表达式中,使用局部变量的时候,也要求该变量必须是 final 的,所以 effectively final 在 Lambda 表达式上下文中非常有用。Lambda 表达式在编程中是经常使用的,而匿名内部类是很少使用的
2.区别
Lambda表达式与匿名内部类主要存在如下区别:
(1)匿名内部类可以为任意接口创建实例—不管接口包含多少个抽象方法,只要匿名内部类实现所有的抽象方法即可;但Lambda表达式只能为函数式接口创建实例。
(2)匿名内部类可以为抽象类甚至普通类创建实例;但Lambda表达式只能为函数式接口创建实例。
(3)匿名内部类实现的抽象方法的方法体允许调用接口中定义的默认方法;但Lambda表达式的代码块不允许调用接口中定义的默认方法。
仅供参考