java中的Lambda表达式小结结

1、概念

Lambda 表达式(Lambda expression)是一个匿名函数,基于数学中的λ演算得名,也可称为闭包(Closure)。现在很多语言都支持 Lambda 表达式,如 C++、C#、Java、 Python 和 JavaScript 等。

Lambda 表达式是推动 Java 8 发布的重要新特性,它允许把函数作为一个方法的参数(函数作为参数传递进方法中)
首先我们先来看一下一个程序演示:

使用匿名内部类来实现一个接口
1)接口Caculation

package Lambda表达式;

public interface Caculation{
    int caculationInt(int x,int y);//这是默认的抽象方法,如果不加声明,实现计算两个数的和,差
}


2)类Practice1

package Lambda表达式;

public class Practice1  {//我们是通过匿名内部类来实现该接口

    public Caculation caculation(char op) {
        Caculation result;
        if (op == '+') {
             result = new Caculation() {//使用匿名内部类来实现该接口
                @Override
                public int caculationInt(int x, int y) {

                    return x + y;
                }
            };

        } else{
            result = new Caculation() {//发现编译器会提示可以使用lambda表达式取代
                @Override
                public int caculationInt(int x, int y) {
                    return x - y;
                }

            };
        }
            return result;
    }
    public static void main(String[]args){
        //
        Practice1 pr = new Practice1();
        System.out.println(pr.caculation('+').caculationInt(3,4));      
    }
}

观察:使用匿名内部类的方法 calculate 代码很臃肿,Java 8 采用 Lambda 表达式可以替代匿名内部类。
修改如下:

package Lambda表达式;

public class Practice1  {//我们是通过匿名内部类来实现接口

    public  Caculation caculation(char op) {
        Caculation result;
        if (op == '+') {
            //使用匿名内部类来实现该接口
            result = (x, y) -> x + y;

        } else{
            //发现编译器会提示可以使用lambda表达式取代
            result = (x, y) -> x - y;
        }
            return result;
    }
    public static void main(String[]args){
        //
        Practice1 pr = new Practice1();
        System.out.println(pr.caculation('+').caculationInt(3,4));
    }
}

用 Lambda 表达式替代匿名内部类,可见代码变得简洁。通过以上示例我们发现,Lambda 表达式是一个匿名函数(方法)代码块,可以作为表达式、方法参数和方法返回值。

Java Lambda 表达式的优缺点
优点:

  1. 代码简洁,开发迅速
  2. 方便函数式编程
  3. 非常容易进行并行计算
  4. Java 引入 Lambda,改善了集合操作(引入 Stream API)

缺点:

  1. 代码可读性变差
  2. 在非并行计算中,很多计算未必有传统的 for 性能要高
  3. 不容易进行调试

2、函数式接口

Lambda 表达式实现的接口不是普通的接口,而是函数式接口。

函数式接口定义:

如果一个接口中,有且只有一个抽象的方法(Object 类中的方法不包括在内),那这个接口就可以被看做是函数式接口。

这种接口只能有一个抽象方法。如果接口中声明多个抽象方法,那么 Lambda 表达式会发生编译错误

The target type of this expression must be a functional interface

这说明该接口不是函数式接口.

为了防止在函数式接口中声明多个抽象方法,Java 8 提供了一个声明函数式接口注解@FunctionalInterface,示例代码如下。

// 可计算接口
@FunctionalInterface
public interface Calculable {
    // 计算两个int数值
    int calculateInt(int a, int b);
}

在接口之前使用 @FunctionalInterface 注解修饰,那么试图增加一个抽象方法时会发生编译错误。但可以添加默认方法和静态方法。

@FunctionalInterface 注解与 @Override 注解的作用类似。Java 8 中专门为函数式接口引入了一个新的注解 @FunctionalInterface。该注解可用于一个接口的定义上,一旦使用该注解来定义接口,编译器将会强制检查该接口是否确实有且仅有一个抽象方法,否则将会报错。需要注意的是,即使不使用该注解,只要满足函数式接口的定义,这仍然是一个函数式接口,使用起来都一样。

Lambda 表达式是一个匿名方法代码,Java 中的方法必须声明在类或接口中,那么 Lambda 表达式所实现的匿名方法是在函数式接口中声明的。

3、Lambda的三种简写形式

使用 Lambda 表达式是为了简化程序代码,Lambda 表达式本身也提供了多种简化形式,这些简化形式虽然简化了代码,但客观上使得代码可读性变差。

3.1、省略参数类型

Lambda 表达式可以根据上下文环境推断出参数类型。calculate 方法中 Lambda 表达式能推断出参数 a 和 b 是 int 类型,简化形式如下:

public static Calculable calculate(char opr) {
    Calculable result;
    if (opr == '+') {
        // Lambda表达式实现Calculable接口
        //标准形式
        result = (int a, int b) -> {
            return a + b;
        };
    } else {
        // Lambda表达式实现Calculable接口
        //省略形式1
        result = (a, b) -> {//不用写参数名字
            return a - b;
        };
    }
    return result;
}

3.2、省略参数小括号(只有一个参数的时候)

如果 Lambda 表达式中的参数只有一个,可以省略参数小括号。修改 Calculable 接口中的 calculateInt 方法,代码如下。

// 可计算接口
@FunctionalInterface
public interface Calculable {
    // 计算一个int数值
    int calculateInt(int a);
}

其中 calculateInt 方法只有一个 int 类型参数,返回值也是 int 类型。

public static Calculable calculate(int power) {
    Calculable result;
    if (power == 2) {
        // Lambda表达式实现Calculable接口
        // 标准形式
        result = (int a) -> {
            return a * a;
        };
    } else {
        // Lambda表达式实现Calculable接口
        // 省略形式2
        result = a -> {//参数类型省略,小括号也省略
            return a * a * a;
        };
    }
    return result;
}

3.3、省略return和大括号(Lambda的方法体只有一句时)

如果 Lambda 表达式体中只有一条语句,那么可以省略 return 和大括号,代码如下:

public static Calculable calculate(int power) {
    Calculable result;
    if (power == 2) {
        // Lambda表达式实现Calculable接口
        // 标准形式
        result = (int a) -> {
            return a * a;
        };
    } else {
        // Lambda表达式实现Calculable接口
        // 省略形式3
        result = a -> a * a * a;
    }
    return result;
}

这是最简化形式的 Lambda 表达式了,代码太简洁了!

4、Lambda的使用

4.1、作为函数的参数(可以接受接口的对象,Lambda表达式)

package Lambda表达式;

public class Practice  {
    public static void main(String[] args) {
        int n1 = 10;
        int n2 = 5;
        // 打印加法计算结果
        display((a, b) -> {
            return a + b;
        }, n1, n2);
        // 打印减法计算结果
        display((a, b) -> a - b, n1, n2);
    }
    /**
     * 打印计算结果
     *
     * @param calc Lambda表达式,因为时函数式接口,这个参数即可以接收实现 Calculable 接口的对象,也可以接收 Lambda 表达式
     * @param n1   操作数1
     * @param n2   操作数2
     */
    public static void display(Caculation calc, int n1, int n2) {
        System.out.println(calc.caculationInt(n1, n2));
    }
}

4.2、访问变量(局部,实例,静态变量)

Lambda 表达式可以访问所在外层作用域定义的变量,包括成员变量(实例变量、静态变量)和局部变量。

1.访问成员变量

public class LambdaDemo {
    // 实例成员变量
    private int value = 10;
    // 静态成员变量
    private static int staticValue = 5;
    // 静态方法,进行加法运算
    public static Calculable add() {//静态方法只能访问静态成员变量
        Calculable result = (int a, int b) -> {
            // 访问静态成员变量,不能访问实例成员变量
            staticValue++;
            int c = a + b + staticValue;
            // this.value;
            return c;
        };
        return result;
    }
    // 实例方法,进行减法运算
    public Calculable sub() {//实例方法都可以访问
        Calculable result = (int a, int b) -> {
            // 访问静态成员变量和实例成员变量
            staticValue++;
            this.value++;
            int c = a - b - staticValue - this.value;
            return c;
        };
        return result;
    }
}

当访问实例成员变量或实例方法时可以使用 this,如果不与局部变量发生冲突情况下可以省略 this。

2.访问局部变量
对于成员变量的访问 Lambda 表达式与普通方法没有区别,但是访问局部变量时,变量必须是 final 类型的(不可改变)注意不声明时java8开始默认局部变量为final,除非你重赋值。

package Lambda表达式;
public class LambdaDemo {
    // 实例成员变量
    private int value = 10;
    // 静态成员变量
    private static int staticValue = 5;
    // 静态方法,进行加法运算
    public static Caculation add() {
        // 局部变量
        int localValue = 20;
        Caculation result = (int a, int b) -> {
            //localValue++;重赋值编译器则认为不是final型
            // 编译错误
            int c = a + b + localValue;
            return c;
        };
        return result;
    }
    // 实例方法,进行减法运算
    public Caculation sub() {
        // final局部变量
        final int localValue = 20;
        Caculation result = (int a, int b) -> {
            int c = a - b - staticValue - this.value;
            // localValue = c;重赋值编译器则认为不是final型
            int l = localValue;//只有不重新赋值就没问题,当然这个量多余
            // 编译错误
            return c;
        };
        return result;
    }
}

不管这个变量是否显式地使用 final 修饰,它都不能在 Lambda 表达式中修改变量,Lambda 表达式只能访问局部变量而不能修改,否则会发生编译错误,但对静态变量和成员变量可读可写。

4.3、方法引用(::)

方法引用可以理解为 Lambda 表达式的快捷写法,它比 Lambda 表达式更加的简洁,可读性更高,有很好的重用性。如果实现比较简单,复用的地方又不多,推荐使用 Lambda 表达式,否则应该使用方法引用。

Java 8 之后增加了双冒号::运算符,该运算符用于“方法引用”,注意不是调用方法。“方法引用”虽然没有直接使用 Lambda 表达式,但也与 Lambda 表达式有关,与函数式接口有关。 方法引用的语法格式如下:

ObjectRef::methodName 

其中,ObjectRef 是类名或者实例名,methodName 是相应的方法名。

注意:被引用方法的参数列表和返回值类型,必须与函数式接口方法参数列表和方法返回值类型一致,示例代码如下。

public class LambdaDemo {
    // 静态方法,进行加法运算
    // 参数列表要与函数式接口方法calculateInt(int a, int b)兼容
    public static int add(int a, int b) {
        return a + b;
    }
    // 实例方法,进行减法运算
    // 参数列表要与函数式接口方法calculateInt(int a, int b)兼容
    public int sub(int a, int b) {
        return a - b;
    }
}

LambdaDemo 类中提供了一个静态方法 add,一个实例方法 sub。这两个方法必须与函数式接口方法参数列表一致,方法返回值类型也要保持一致。

public class HelloWorld {
    public static void main(String[] args) {
        int n1 = 10;
        int n2 = 5;
        // 打印加法计算结果
        display(LambdaDemo::add, n1, n2);
        LambdaDemo d = new LambdaDemo();
        // 打印减法计算结果 ,使用方法引用,因为sub方法与接口的方法实现相同
        display(d::sub, n1, n2);
    }
    /**
     * 打印计算结果
     *
     * @param calc Lambda表达式
     * @param n1   操作数1
     * @param n2   操作数2
     */
    public static void display(Calculable calc, int n1, int n2) {
        System.out.println(calc.calculateInt(n1, n2));
    }
}

代码第 18 行声明 display 方法,第一个参数 calc 是 Calculable 类型,它可以接受三种对象:Calculable 实现对象、Lambda 表达式和方法引用。代码第 6 行中第一个实际参数LambdaDemo::add是静态方法的方法引用。代码第 9 行中第一个实际参数d::sub,是实例方法的方法引用,d 是 LambdaDemo 实例。

提示:代码第 6 行的LambdaDemo::add和第 9 行的d::sub是方法引用,此时并没有调用方法,只是将引用传递给 display 方法,在 display 方法中才真正调用方法。

再来看一个Demo

package Lambda表达式;

public class Practice1  {//我们是通过匿名内部类来实现接口

    public  Caculation caculation(char op) {
        Caculation result;
        if (op == '+') {
            //使用匿名内部类来实现该接口,使用方法引用,引用Integer类的求和方法sum
            result = Integer::sum;

        } else{
            //发现编译器会提示可以使用lambda表达式取代
            result = (x, y) -> x - y;
        }

            return result;
    }
    public static void main(String[]args){
        //
        Practice1 pr = new Practice1();
        System.out.println(pr.caculation('+').caculationInt(3,4));

    }
}

5、Lambda表达式与匿名内部类的联系

Java Lambda 表达式的一个重要用法是简化某些匿名内部类的写法,因此它可以部分取代匿名内部类的作用。

5.1、相同点

  1. Lambda 表达式与匿名内部类一样,都可以直接访问 effectively final 的局部变量,以及外部类的成员变量(包括实例变量和类变量)。
  2. Lambda 表达式创建的对象与匿名内部类生成的对象一样,都可以直接调用从接口中继承的默认方法。

下面程序示范了 Lambda 表达式与匿名内部类的相似之处。

package Lambda表达式;

//定义一个函数式接口
@FunctionalInterface
interface Displayable {
    // 定义一个抽象方法和默认方法
    void display();
    default int add(int a, int b) {
        return a + b;
    }
}

public class LambdaAndNei {

        private int age = 12;
        private static String name = "演示Lambda与匿名内部类联系";
        public void test() {
            int local_value=4;//局部变量不会默认初始化,这里默认为final变量
            Displayable dis = () -> {//程序使用 Lambda 表达式创建了 Displayable 的对象之后,该对象不仅可调用接口中唯一的抽象方法,也可调用接口中的默认方法
                // 访问的局部变量
                System.out.println("local_value 局部变量为:" + local_value);
                // 访问外部类的实例变量和类变量
                System.out.println("外部类的 age 实例变量为:" + age);
                System.out.println("外部类的 name 类变量为:" + name);
            };
            dis.display();{//抽象方法的重写
            // 调用dis对象从接口中继承的add()方法
            System.out.println(dis.add(3, 5));
             }
        }
        public static void main(String[] args) {
            LambdaAndNei lambda = new LambdaAndNei();
            lambda.test();
        }

}

输出结果为:
在这里插入图片描述

上面程序使用 Lambda 表达式创建了一个 Displayable 的对象,Lambda 表达式的代码块中的代码第 19、21 和 22 行分别示范了访问“effectively final”的局部变量、外部类的实例变量和类变量。从这点来看, Lambda 表达式的代码块与匿名内部类的方法体是相同的。

当程序使用 Lambda 表达式创建了 Displayable 的对象之后,该对象不仅可调用接口中唯一的抽象方法,也可调用接口中的默认方法,如上面程序代码第 26 行所示。

5.2、区别

  1. 匿名内部类可以为任意接口创建实例——不管接口包含多少个抽象方法,只要匿名内部类实现所有的抽象方法即可;但 Lambda 表达式只能为函数式接口创建实例。
  2. 匿名内部类可以为抽象类甚至普通类创建实例;但 Lambda 表达式只能为函数式接口创建实例。
  3. 匿名内部类实现的抽象方法的方法体允许调用接口中定义的默认方法;但 Lambda 表达式的代码块不允许调用接口中定义的默认方法。

对于 Lambda 表达式的代码块不允许调用接口中定义的默认方法的限制,可以尝试对上面的程序稍做修改,在 Lambda 表达式的代码块中增加如下一行:

// 尝试调用接口中的默认方法,编译器会报错
System.out.println(add(3, 5));

在这里插入图片描述
虽然 Lambda 表达式的目标类型 Displayable 中包含了 add() 方法,但 Lambda 表达式的代码块不允许调用这个方法;如果将上面的 Lambda 表达式改为匿名内部类的写法,当匿名内部类实现 display() 抽象方法时,则完全可以调用这个 add() 方法,如下面代码所示。

package Lambda表达式;

//定义一个函数式接口
@FunctionalInterface
interface Displayable {
    // 定义一个抽象方法和默认方法
    void display();
    default int add1(int a, int b) {
        return a + b;
    }
}

public class LambdaAndNei {

        private int age = 12;
        private static String name = "演示Lambda与匿名内部类联系";
        public void test() {
            int local_value=4;//局部变量不会默认初始化,这里默认为final变量
            Displayable dis = new Displayable() {

                @Override
                public void display() {//使用匿名内部类实现接口,该抽象方法可以调用默认方法
                    // 访问的局部变量
                    System.out.println("local_value 局部变量为:" + local_value);
                    // 访问外部类的实例变量和类变量
                    System.out.println("外部类的 age 实例变量为:" + age);
                    System.out.println("外部类的 name 类变量为:" + name);

                    System.out.println(add1(1,2));

                }
            };
            dis.display();
        }
        public static void main(String[] args) {
            LambdaAndNei lambda = new LambdaAndNei();
            lambda.test();
        }

}

在这里插入图片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章