Lambda表达式是什么?

Lambda 表达式(lambda expression)是一个匿名函数,Lambda表达式基于数学中的λ演算得名,直接对应于其中的lambda抽象(lambda abstraction),是一个匿名函数,即没有函数名的函数。

Lambda表达式的本质是“匿名方法”,即当编译我们的程序代码时,“编译器”会自动将“Lambda表达式”转换为“匿名方法”。

Lambda表达式是Java8的重要更新,它支持将代码块作为方法参数,允许使用更简洁的代码来创建只有一个抽象方法的接口,这种接口称为函数式接口。


一起来使用Lambda表达式吧

首先,我们先看一个关于匿名内部类的程序~

interface Command {
    void process(int[] target);
}
class ProcessArray {
    public void process(int[] target, Command cmd) {
        cmd.process(target);
    }
}
public class CommandTest {
    public static void main(String[] args) {
        ProcessArray pa = new ProcessArray();
        int[] target = {3, -4, 6, 4};
        pa.process(target, new Command() {
            @Override
            public void process(int[] target) {
                int sum = 0;
                for(int tmp : target) {
                    sum += tmp;
                }
                System.out.println("数组元素总和为:" + sum);
            }
        });
    }
}

这段代码是说,ProcessArray类的process()方法处理数组时,希望可以动态传入一段代码作为具体的处理行为,因此程序创建了一个匿名内部类实例来封装处理行为。

但是现在有了Lambda表达式,完全可用于简化创建匿名内部类对象,因此我们改写代码:

public class CommandTest {
    public static void main(String[] args) {
        ProcessArray pa = new ProcessArray();
        int[] array = {3, -4, 6, 4};
        pa.process(array , (int[] target) -> {
            int sum = 0;
            for(int tmp : target) {
                sum += tmp;
            }
            System.out.println("数组元素总和为:" + sum);
        });
    }
}

不难发现,改写后的代码和创建匿名内部类时需要实现的process(int[] target)方法完全相同。当使用Lambda表达式代替匿名内部类创建对象时,Lambda表达式的代码块将会代替实现抽象方法的方法体,所以Lambda表达式就相当于一个匿名方法。那这两种实现方式的区别在哪里呢?

  • 不需要new …()这种烦琐的代码
  • 不需要指出重写方法的名字
  • 不需要给出重写方法的返回类型
  • 只需要给出重写的方法括号以及括号里的形参列表

所以我们总结出Lambda表达式的组成部分:
1. 形参列表。允许省略形参类型;如果形参列表中只有一个参数,甚至连圆括号也可以省略。
2. 箭头 ->。
3. 代码块。如果代码块中只包含一条语句,Lambda代码块允许省略花括号;如果只有一条return语句,甚至可以省略return关键字;如果该Lambda表达式需要返回值,并且代码块只有一条省略了return的语句,就会自动返回这条语句的值。

我们来看一个具体的例子:

interface Eatable {
    void taste();
}
interface Flyable {
    void fly(String weather);
}
interface Addable {
    int add(int a, int b);
}

public class LambdaQS {
    public void eat(Eatable e) {
        System.out.println("我正在吃:" + e);
        e.taste();
    }
    public void drive(Flyable f) {
        System.out.println("我正在驾驶:" + f);
        f.fly("碧空如洗的日子");
    }
    public void test(Addable add) {
        System.out.println("我正在做加法:" + add);
        System.out.println("5和3的和为:" + add.add(5, 3));
    }

    public static void main(String[] args) {
        LambdaQS lq = new LambdaQS();

        //Lambda表达式的代码块只有一条语句,可以省略花括号
        lq.eat(() -> System.out.println("苹果味道不错!"));

        //Lambda表达式的形参列表只有一个形参,可以省略圆括号
        lq.drive(weather -> {
            System.out.println("今天天气是:" + weather);
            System.out.println("直升机飞行平稳");
        });

        //Lambda表达式的代码块只有一条语句,可以省略花括号
        //代码块中只有一条语句,即该表达式需要返回值,可省略return
        lq.test((a, b) -> a+b);
    }
}

运行结果如下:

java 匿名类详解 java匿名表达式_实例方法

在调用三个方法时,本应需要分别传入实现了对应接口的对象,但是我们将对象打印,都是类名$$Lambda$数字/hashcode。可以看出实际上传入的是Lambda表达式(其中的数字应该表示这是该类使用的第几个Lambda表达式),并且可以正常编译、运行,说明Lambda表达式实际上会被当成一个“任意类型”的对象,那么到底需要当成何种类型的对象,这取决于运行环境的需要。

下面详细介绍Lambda表达式被当成何种对象。


Lambda表达式与函数式接口

Lambda表达式的类型,也被称为“目标类型”,Lambda表达式的目标类型必须是“函数式接口”。函数式接口是只包含一个抽象方法的接口,可以包含多个默认方法、类方法。如Runnable、Comparator等接口都是函数式接口。

@FunctionalInterface注解,检查该接口必须是函数式接口,否则编译器报错。

Lambda表达式有两个限制:
1. 目标类型必须是明确的函数式接口
2. 只能实现一个方法

Object obj = () -> {
    for(int i = 0; i < 100; i++) {
        System.out.println();
    }
};
//编译错误:Object不是函数接口
***************************************************
改写:
Object obj = (Runnable)() -> {
    for(int i = 0; i < 100; i++) {
        System.out.println();
    }
};

上面的例子,Lambda表达式执行了强制类型转换,这样就确定该表达式的目标类型为Runnable函数式接口。只要Lambda表达式实现的匿名方法与目标类型中唯一的抽象方法有相同的形参列表,同样的Lambda表达式就可以被当成不同的目标类型。所以上面被强制转型为Runnable的Lambda表达式也可以被强转为FkTest类型(FkTest接口中唯一的抽象方法也不带参数)。

Java8在java.util.function包下预定义了大量函数式接口,典型包含如下4类:

  • XxxFuntion:apply(),该方法对参数进行处理、转换,返回一个新的值
  • XxxConsumer:accept(),和apply()类似,只是不返回处理结果
  • XxxxPredicate:test(),对参数进行某种判断,返回boolean值。经常用于进行筛滤数据。
  • XxxxSupplier:getAsXxx(),不需输入参数,会按某种逻辑算法返回一个数据。

方法引用和构造器引用

如果Lambda表达式的代码块还有一条语句,还可以在代码块中使用方法引用和构造器引用。可以时Lambda表达式的代码块更加简洁。

Lambda表达式支持如下四种引用方式:

种类

示例

说明

对应Lambda表达式

引用类方法

类名::类方法

函数式接口中被实现方法的全部参数传给还类方法作为参数

(a,b,…) -> 类名.类方法(a,b,…)

引用特定对象的实例方法

特定对象::实例方法

函数式接口中被实现方法的全部参数传给该方法作为参数

(a,b,…) -> 特定对象.实例方法(a,b,…)

引用某类对象的实例方法

类名::实例方法

函数式接口中被实现方法的第一个参数作为调用者,后面的参数全部传给该方法作为参数

(a,b,…) -> a.实例方法(b,…)

引用构造器

类名::new

函数式接口中被实现方法的全部参数传给该构造器作为参数

(a,b,…) -> new 类名(a,b,…)

1.引用类方法

//Converter converter1 = from -> Integer.valueOf(from);
//使用Lambda表达式创建Converter对象

Converter converter1 = Integer::valueOf;
//方法引用代替Lambda表达式——引用类方法
//函数式接口中被实现方法的全部参数传给该类方法作为参数
//调用Integer类的valueOf()类方法来实现Converter函数式接口中唯一的抽象方法

2.引用特定对象的实例方法

//Converter converter2 = from -> "abc".indexOf(from);
//使用Lambda表达式创建Converter对象

Converter converter2 = “abc”.indexOf;
//方法引用代替Lambda表达式——引用特定对象的实例方法
//函数式接口中被实现方法的全部参数传给该实例方法作为参数
//调用"abc"对象的indexOf()实例方法来实现Converter函数式接口中唯一的抽象方法

3.引用某类对象的实例方法

//MyTest mt = (a, b, c) -> a.substring(b, c);
//使用Lambda表达式创建MyTest对象

MyTest mt = String::substring;
//方法引用代替Lambda表达式——引用某类对象的实例方法
//函数式接口中被实现方法的第一个参数作为调用者,后面参数全部传给该方法作为参数
//调用某个String对象的substring()实例方法来实现MyTest函数式接口中唯一的抽象方法

4.引用构造器

//YourTest yt = (String a) -> new JFrame(a);
//使用Lambda表达式创建YourTest对象

YourTest yt = JFrame::new;
//构造器引用代替Lambda表达式
//函数是接口中被实现方法的全部参数传给该构造器作为参数
//调用某个JFrame类的构造器来实现YourTest函数式接口中唯一的抽象方法

Lambda表达式与匿名内部类的联系和区别

联系:

  • 都可以直接访问“effectively final”的局部变量,以及外部类的成员变量
  • 都可以直接调用从接口中继承的默认方法

区别:

  • 匿名内部类可以为任意接口创建实例,只要实现所有抽象方法即可,但Lambda表达式只能为函数式接口创建实例
  • 匿名类可以为抽象类甚至普通类创建实例
  • 匿名内部类实现的抽象方法体允许调用接口中定义的默认方法,但Lambda表达式的代码块不允许调用接口中定义的默认方法

使用Lambda表达式调用Arrays的类方法

import java.util.Arrays;

public class LambdaArrays {
    public static void main(String[] args) {
        String[] arr1 = new String[]{"java", "fkava", "fkit", "ios", "android"};
        Arrays.parallelSort(arr1, (o1, o2) -> o1.length() - o2.length());
        //目标类型是Comparator,指定了判断字符串大小的标准:字符串越长,则认为该字符串越大
        System.out.println(Arrays.toString(arr1));

        int[] arr2 = new int[]{3, -4, 25, 16, 30, 18};
        Arrays.parallelPrefix(arr2, (left, right) -> left * right);
        //目标类型是IntBinaryOperator,根据前一个元素和当前元素的值来计算当前元素的值
        System.out.println(Arrays.toString(arr2));

        long[] arr3 = new long[5];
        Arrays.parallelSetAll(arr3, opereand -> opereand * 5);
        //目标类型是IntToLongFunction,根据当前元素的索引来计算当前元素的值
        System.out.println(Arrays.toString(arr3));
    }
}

通过程序,不难看出,Lambda表达式让程序更简洁。