【Java】Lambda表达式


概述

之前在自学的时候接触过 Lambda 表达式,但那会其实用的并不多,对于其也是大概有个了解,但在阅读公司代码的时候,发现在对于集合的处理时都是转成stream流再通过foreach结合 Lambda 表达式进行处理的,所以打算再重新学学这部分的内容

先来看看网上对于它的定义

Lambda 表达式是 Java 8 的重要更新,它支持将代码块作为方法参数、允许使用更简洁的代码来创建只有一个抽象方法的接口的实例。 Lambda 表达式的主要作用就是可以用于简化创建匿名内部类对象,Lambda 表达式的代码块将会用于实现抽象方法的方法体,Lambda 表达式就相当于一个匿名方法

结合上面的描述可以得到一个结论,Lambda 表达式在匿名函数以及集合的 stream 操作有着重要的作用

先来看一段简单的代码

public class TestLambda {
       public static void main(String[] args) {
       Thread thread = new Thread(new MyRunnable());
       thread.start();
       thread.close();
    }
}
class MyRunnable implements Runnable{
	@Override
        public void run() {
            System.out.println("Hello");
        }
}

现在将代码简化一下,使用匿名内部类来实现 Runnable 接口

public class TestLambda {
    public static void main(String[] args) {
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("Hello");
            }
        });
        thread.start();
    }
}

然而上面这段代码还不是最简单的,再看下面这个

public class TestLambda {
    public static void main(String[] args) {
        new Thread(() -> System.out.println("Hello")).start();
    }
}

这样对比下来,发现使用 Lambda 表达式确实能极大的让代码更简洁

语法

Lambda 表达式由三部分构成

  • 形参列表:形参列表允许省略类型,如果形参列表中只有一个参数,形参列表的圆括号也可以省略
  • 箭头(->)
  • 代码块:可以是表达式也可以代码块,是函数式接口里方法的实现。代码块可返回一个值或者什么都不反回,这里的代码块块等同于方法的方法体。如果是表达式,也可以返回一个值或者什么都不返回

总结一下就是:实现的这个接口中的抽象方法中的形参列表 -> 抽象方法的处理

无返回值有形参的方法

public interface MyInterface {
    public abstract void show(int a,int b);
}
public class MyTest {
    public static void main(String[] args) {
        MyInterface myInterface = new MyInterface() {
            @Override
            public void show(int a, int b) {
                System.out.println(a + b);
            }
        };

        //简写1:方法名可以自己推断出来
        MyInterface myInterface1 = (int a, int b) -> {
            System.out.println(a + b);
        };

        //简写2:可以省略形参列表中的形参类型
        MyInterface myInterface2 = (a, b) -> {
            System.out.println(a + b);
        };

        //简写3:如果抽象方法中只有一行代码,可以省略方法体的大括号,当然,如果不止一行,就不能省略
        MyInterface myInterface3 = (a, b) -> System.out.println(a + b);
    }
}

有返回值的抽象方法

public interface MyInterface {
    public abstract int test(int a,int b);
}
public class MyTest {
    public static void main(String[] args) {
        MyInterface test1 = new MyInterface() {
            @Override
            public int test(int a, int b) {
                return a - b;
            }

        //简写1:
        MyInterface test3 = (a, b) -> {return a - b;};

        //简写3:这个有返回值的方法,不能直接去掉大括号,还需要去掉return关键字
        MyInterface test4 = (a, b) -> a - b;
    }
}

只有一个形参的抽象方法

public interface MyInterface {
    public abstract int show(int a);
}
public class MyTest {
    public static void main(String[] args) {
        //形参列表中只有一个参数,可以去掉形参的括号
        MyInterface myInterface = a -> a-20;
    }
}

Lambda表达式作为参数

public class Main {
  public static void main(String[] argv) {
    engine((x,y)-> x + y);
    engine((x,y)-> x * y);
    engine((x,y)-> x / y);
    engine((x,y)-> x % y);
  }
  private static void engine(Calculator calculator){
    int x = 2, y = 4;
    int result = calculator.calculate(x,y);
    System.out.println(result);
  }
}

@FunctionalInterface
interface Calculator{
  int calculate(int x, int y);
}

Lambda 表达式与函数式接口

在以往的资料上,他们大多爱说这么一句话

Lambda 表达式的类型,也被称为目标类型(target type)。Lambda 表达式的目标类型必须是函数式接口(functional interface)

我相信很多想要了解Lambda表达式的人一查资料,发现这么一句话,肯定是一头雾水的。暂且先不讨论这句话的含义,但有一点是可以达成共识的,那就是:Lambda表达式和函数接口一定存在着千丝万缕的关联

事实上确实是这样的,函数式接口和Lambda表达式同为Java8的新特性,它大致可以用以下的文字来形容

函数式接口就是一个有且仅有一个抽象方法,但是可以有多个非抽象方法的接口。可以通过 Lambda 表达式来创建该接口的对象

通过查询 Java 8 的 API 文档,可以发现大量的函数式接口,例如熟知的 Runnable 接口就是一个函数式接口(其中只有一个抽象的run()方法)。Java 8 还提供了@FunctionalInterface注解,该注解用于告诉编译器校验接口必须是函数式接口,否则就报错

由于 Lambda 表达式的结果就是被当做对象/实例,因此,可以使用 Lambda 表达式进行赋值

但是下面这个是个错误的实例

Object obj = () -> {
    for (int i = 0; i < 100; i++) {
        System.out.println(i);
    }
};

其实如果已经了解了 Lambda 表达式与函数式接口的关系,大概都不会犯这个错,但考虑到之前有提到过“Lambda 表达式的结果就是被当成对象/实例的”,所以还是把它拿出来说说。这个程序执行之后会报Target type of a lambda conversion must be an interface,将 Lambda 表达式赋值给 Object 类型的变量,编译器只能推断出它的表达类型为 Object,而 Object 并不是函数式接口,因此就报错了

为了保证 Lambda 表达式的目标类型是明确的函数式接口,有如下三种常见方式:

  • 将 Lambda 表达式赋值给函数式接口类型的变量
  • 将 Lambda 表达式作为函数式接口类型的参数传给某个方法
  • 使用函数式接口对 Lambda 表达式进行强制类型转换

那么以上的错误就可以更改成

//这里就使用函数式接口 Runnable 对 Lambda 表达式进行了强制转换
Object obj = (Runnable)() -> {
    for (int i = 0; i < 100; i++) {
        System.out.println(i);
    }
};

奇奇怪怪的运算符——“::”

一个简单问题的思考

在使用 Lambda 表达式的时候,我们实际上传递进去的代码就是一种解决方案:拿什么参数做什么操作。那么考虑一种情况:如果我们在 Lambda 中所指定的操作方案,已经有地方存在相同方案,那是否还有必要再写重复逻辑?

来看例子:冗余的 Lambda 场景

@FunctionalInterface
public interface Printable {
    /**
     * 接收一个字符串参数,打印显示它
     * @param str 字符串
     */
    public abstract void print(String str);
}
-------------------------------------------------------
public class Demo01 {
 
    public static void main(String[] args) {
        printString(s -> System.out.println(s));
    }
 
    private static void printString(Printable printable) {
        printable.print("Hello, World!");
    }
 
}

这段代码的问题在于,对字符串进行控制台打印输出的操作方案,明明已经有了现成的实现,那就是 System.out 对象中的println(String)方法。既然 Lambda 希望做的事情就是调用println(String)方法,那何必自己手动调用呢?

解决方式

public class Demo02 {
 
    public static void main(String[] args) {
        printString(System.out::println);
    }
    
    private static void printString(Printable printable) {
        printable.print("Hello, World!");
    }
 
}

方法引用

双冒号::为引用运算符,而它所在的表达式被称为方法引用。如果 Lambda 要表达的函数方案已经存在于某个方法的实现中,那么则可以通过双冒号来引用该方法作为 Lambda 的替代者

例如上例中,System.out对象中有一个重载的println(String)方法恰好就是我们所需要的。那么对于printString方法的函数式接口参数,对比下面两种写法,完全等效

// Lambda表达式写法
s -> System.out.println(s);
// 方法引用写法
System.out::println

Lambda 中传递的参数,一定是方法引用中的那个方法可以接收的类型,否则会出现编译错误。上例中 println 方法可以接收 String 类型的参数,所以才有两种等价的写法

通过对象名引用成员方法

假如我们已经在一个类中定义了一个方法的具体实现

public class MethodRefObject {
 
    public void printUpperCase(String str) {
        System.out.println(str.toUpperCase());
    }
 
}

然后函数式接口定义如下

@FunctionalInterface
public interface Printable {
    public abstract void print(String str);
}

拿到参数之后经 Lambda 之手,继而传递给toUpperCase()方法去处理

public class Demo {
 
    public static void main(String[] args) {
        printString(s -> s.toUpperCase());
    }
 
    private static void printString(Printable lambda) {
        lambda.print("Hello");
    }
 
}

折腾了一大圈发现,其实已经有方法实现了 Lambda 表达式想要实现的功能

这个时候,当需要使用这个 printUpperCase 成员方法来替代 Printable 接口的 Lambda 的时候,已经具有了 MethodRefObject 类的对象实例,则可以通过对象名引用成员方法

public class Demo {
    
    public static void main(String[] args) {
        MethodRefObject obj = new MethodRefObject();
        printString(obj::printUpperCase);
    }
    
    private static void printString(Printable lambda) {
        lambda.print("Hello");
    }
    
}

通过类名引用静态方法

Math 类中已经存在了静态方法abs(),现在需要通过 Lambda 去调用这个方法,有两种表示方式

第一种,使用函数式接口

@FunctionalInterface
public interface CalculationAble {
    int calculation(int num);
}
public class Demo {
 
    public static void main(String[] args) {
        method(-666, n -> Math.abs(n));
    }
 
    private static void method(int num, CalculationAble lambda) {
        System.out.println(lambda.calculation(num));
    }
    
}

第二种,可以使用类名来引用静态方法

public class Demo {
 
    public static void main(String[] args) {
        method(-666, Math::abs);
    }
 
    private static void method(int num, CalculationAble reference) {
        System.out.println(reference.calculation(num));
    }
 
}

使用 super 引用成员方法

函数式接口

@FunctionalInterface
public interface GreetAble {
    void greet();
}

父类

public class Human {
 
    public void sayHello() {
        System.out.println("Hello!");
    }
 
}

子类

public class Man extends Human {
    @Override
    public void sayHello() {
        System.out.println("hey,bro!");
    }
 
    /**
     * 定义方法method,参数传递GreetAble接口
     * @param g 这里传入的是Lambda表达式
     */
    public void method(GreetAble g) {
        g.greet();
    }
 
    /**
     * 调用method方法,使用Lambda表达式
     */
    public void show(){
 
        // 创建Human对象,调用sayHello方法
        method(() -> { new Human().sayHello(); });
        
        // 简化Lambda
        method(() -> new Human().sayHello());
 
        // 使用super关键字代替父类对象
        method(() -> super.sayHello());

        // 再简化
        method(supper::sayHello);
        
    }
    
}

使用 this 引用成员方法

函数式接口

@FunctionalInterface
public interface RichAble {
    void buy();
}

一个方法要以函数式接口为参数

public class Husband {
    /**
     * 结婚
     * @param lambda 函数式接口,买东西
     */
    private void marry(RichAble lambda) {
        lambda.buy();
    }
 
    /**
     * 要开心
     */
    public void beHappy() {
        marry(() -> System.out.println("买套房子"));
    }
}

开心方法 beHappy 调用了结婚方法 marry ,后者的参数为函数式接口 Richable ,所以需要一个 Lambda 表达式。 但是如果这个 Lambda 表达式的内容已经在本类当中存在了,则可以对 Husband 丈夫类进行修改

public class Husband {
    /**
     * 买房子
     */
    private void buyHouse() {
        System.out.println("买套房子");
    }
 
    /**
     * 结婚
     * @param lambda 函数式接口,买东西
     */
    private void marry(RichAble lambda) {
        lambda.buy();
    }
 
    /**
     * 要开心
     */
    public void beHappy() {
        marry(() -> this.buyHouse());
    }
}

如果希望去掉 Lambda 表达式,可以使用 this 去引用成员方法

public class Husband03 {
    /**
     * 买房子
     */
    private void buyHouse() {
        System.out.println("买套房子");
    }
 
    /**
     * 结婚
     * @param lambda 函数式接口,买东西
     */
    private void marry(RichAble lambda) {
        lambda.buy();
    }
 
    /**
     * 要开心
     */
    public void beHappy() {
        marry(this::buyHouse);
    }
}

类构造器引用

方法引用其实还好,至少方法名是固定的。但是构造器的名字与类名相同,根本不固定,所以可以通过类名称::new的方式引用

一个简单的类

public class Person {
 
    private String name;
 
    public Person(String name) {
        this.name = name;
    }
 
    public String getName() {
        return this.name;
    }
 
    public void setName(String name) {
        this.name = name;
    }
 
}

函数式接口

@FunctionalInterface
public interface PersonBuilder {
    /**
     * 创建 Person 对象
     * @param name Person对象名
     * @return Person对象
     */
    Person buildPerson(String name);
}

要使用这个接口就需要通过 Lambda 表达式

public class Demo {
 
    public static void main(String[] args) {
        printName("Tom", (name) -> new Person(name));
    }
 
    public static void printName(String name, PersonBuilder builder) {
        System.out.println(builder.buildPerson(name).getName());
    }
 
}

如果换成类构造器引用,就得这么写

public class Demo {
 
    public static void main(String[] args) {
        printName("Tom", Person::new);
    }
 
    public static void printName(String name, PersonBuilder builder) {
        System.out.println(builder.buildPerson(name).getName());
    }
 
}

如果是数组,那写法就有些区别了

函数式接口

@FunctionalInterface
public interface ArrayBuilder {
    /**
     * 创建数组的函数式接口
     * @param length 数组长度
     * @return 存储int类型的数组
     */
    int[] buildArray(int length);
}

使用 Lambda 表达式应用接口

public class Demo {
 
    public static void main(String[] args) {
        int[] array = initArray(10, length -> new int[length]);
    }
 
    private static int[] initArray(int length, ArrayBuilder builder) {
        return builder.buildArray(length);
    }
 
}

使用构造器引用应用接口

public class Demo {
 
    public static void main(String[] args) {
        int[] array = initArray(10, int[]::new);
    }
 
    private static int[] initArray(int length, ArrayBuilder builder) {
        return builder.buildArray(length);
    }
 
}

很显然,数组的构造器引用就不能通过类名了,这个需要注意

写在

看到过其他人的文章中有一句话形容 Lambda 表达式非常贴切

Lambda 表达式的原则是“可推导就是可省略”

关于 Lambda 表达式我认为我学习到的内容才是冰山一角,很多东西可能还需要在以后的实践过程中再去熟悉再去体会,后续有其他关于 Lambda 的想要分享的内容还是会在博客中分享

参考内容
Java基础——Lambda表达式
理解 Java 方法引用(方法引用符:“双冒号 :: ”)

原创文章,作者:wdmbts,如若转载,请注明出处:https://blog.ytso.com/tech/pnotes/272553.html

(0)
上一篇 2022年7月9日 07:24
下一篇 2022年7月9日 07:28

相关推荐

发表回复

登录后才能评论