0202年了,你还不会lambda表达式?一文带你完全搞懂lambda!

前言

本文原载于我的博客,地址:https://blog.guoziyang.top/archives/55/

如果你像我一样使用IDEA作为Java开发的IDE,那么你一定也注意到了lambda表达式。

当你在IDEA中实现一个创建一个匿名接口实现类时,例如,在对一个list进行逆序排序:

Collections.sort(list, new Comparator() {
    @Override
    public int compare(Integer o1, Integer o2) {
        return o2.compareTo(o1);
    }
});

IDEA就会出现一个小提示,replace with lambda,选择后,代码就会变成下面这样:

Collections.sort(list, (o1, o2) -> o2.compareTo(o1));

这就是lambda表达式,Java 8新引入的一个语法糖,是Java进入函数式编程的第一步。

组成

lambda表达式的组成:

  • 一个逗号分隔的形参列表,被包裹在括号内,如果只有一个形参,则可以参略括号,如果没有参数,那么写一对空小括号即可

  • 箭头符号 ->

  • 函数体,由单条表达式或者一个方法体组成

    p -> p.getGender() == Person.Sex.MALE 
        && p.getAge() >= 18
        && p.getAge() <= 25
    

    如果使用的是单条表达式,那么在运行时,Java运行时就会计算表达式的值并返回。

    当然也可以使用一个return语句:

    p -> {
        return p.getGender() == Person.Sex.MALE
            && p.getAge() >= 18
            && p.getAge() <= 25;
    }
    

    return语句不是一个表达式,所以必须由一个大括号包裹。但是,如果方法体只调用了一个返回void的方法,那么也不需要包裹在大括号内,例如

    email -> System.out.println(email)
    

单一方法接口

例如,我们要实现一个事件监听器,在Java中,事件监听器通常被定义为具有单个方法的接口,如下:

interface StateChangeListener {
    public void onStateChange (State oldState,State newState);
}

假设你有一个类StateOwner可以注册状态监听器:

public class StateOwner {
    public void addStateListener (StateChangeListener listener) {...}
}

在Java 7中,我们可以使用匿名接口来添加事件监听器:

StateOwner stateOwner = new StateOwner();
stateOwner.addStateListener(new StateChangeListener(){
    public void onStateChange (State oldState, State newState) {
        ...
    }
});

在Java 8中,我们就可以使用Lambda表达式来添加事件监听器:

StateOwner stateOwner = new StateOwner();

stateOwner.addStateListener(
    (oldState,newState) -> System.out.println("状态已更改")
);

Lambda表达式只能在与它们匹配的类型是单个方法接口的地方使用。在上面的示例中,lambda表达式用作参数,其中参数类型为 StateChangeListener接口。该接口只有一个方法。因此,lambda表达式已与该接口成功匹配,该表达式就被转化为实现与该参数相同接口的函数。

注意,lambda表达式与匿名接口实现的一个很大的区别就是,匿名接口实现类可以有成员变量,而lambda表达式不能,因此lambda表达式也被称为无状态的。

lambda匹配规则

单个方法接口也被称为功能接口,lambda与功能接口的匹配主要有以下步骤:

  • 接口只有一个未实现的方法
  • lambda表达式的参数与该方法的参数匹配
  • lambda表达式的返回类型与该方法的返回类型匹配

如果三个条件都是真,那么lambda表达式即可与该接口匹配。

变量捕获

lambda表达式在某些情况下可以访问在lambda函数体外部声明的变量。

局部变量

当lambda使用一个外部的局部变量时,如果传入的是一个引用,那么lambda表达式不可以改变这个引用,否则会出现编译错误。

实例变量

注意这一段代码:

public class EventConsumerImpl {
    private String name = "MyConsumer";
    public void attach(MyEventProducer eventProducer){
        eventProducer.listen(e -> {
            System.out.println(this.name);
        });
    }
}

在lambda表达式中,使用了this关键字,访问到了EventConsumerImpl类中的name变量,即this指向的是EventConsumerImpl实例对象。

这和匿名接口实现是不同的,在匿名接口实现中,是可以拥有自己的实例变量的,this指向的也是匿名接口实现对象。

静态变量

当然,lambda可以访问静态变量,因为只要在该变量的访问域内,从Java程序的任何地方都可以访问到静态变量。

方法引用

Java 8中加入到一个特性,名为方法引用。如果你的lambda表达式的作用仅仅是使用传递给其的参数来调用另一个方法,那么我们就可以使用方法引用来简化其写法。

例如,我们有一个接口:

public interface MyPrinter { 
    public void print(String s); 
}

如果我们使用lambda来创建实现MyPrinter的实例,如下:

MyPrinter myPrinter = (s) -> {System.out.println(s);};

如果使用方法引用的话:

MyPrinter myPrinter = System.out::println;

双冒号::告诉编译器这是一个方法引用,引用的方法是双冒号之后的内容,引用的方法所在的类或对象在双冒号之前。

静态方法引用

最容易引用的应当就是静态方法了,例如前面我们举的MyPrinter的例子,由于MyPrinter.print()System.out.println()方法的参数相匹配,所以就可以创建实现MyPrinter.print()且引用System.out.println()方法的lambda表达式。

参数方法引用

我们还可以将参数之一的方法引用到lambda,例如接口:

public interface Finder { 
    public int find(String s1, String s2);
}

该接口用于查找s2在s1中首次出现的位置,如果我们直接使用lambda表达式:

Finder finder = (s1, s2) -> s1.indexOf(s2);

我们可以直接将s1的indexOf()方法引用到lambda中:

Finder finder = String::indexOf;

Java编译器将尝试将第二个参数作为引用方法的参数,将引用方法与第一个参数类型进行匹配。

实例方法引用

我们还可以从lambda中引用实例的方法,例如我们定义接口:

public interface Deserializer { 
    public int deserialize(String v1);
}

该接口用于将String转换为int。那么现在我们创建一个StringConverter类:

public class StringConverter { 
    public int convertToInt(String v1){
        return Integer.valueOf(v1);
    } 
}

我们注意到,StringConverter中的convertToInt方法与接口方法具有相同的参数和返回值,所以我们可以进行如下的方法引用:

StringConverter stringConverter = new StringConverter();
Deserializer des = stringConverter::convertToInt;

第二行的lambda表达式引用了第一行创建的stringConverter对象的convertToInt实例方法。

构造方法引用

引用一个构造方法很容易,你只需要将类名后面加上::new,像这样:

MyClass::new

例如我们有一个创建String的工厂接口:

public interface Factory {
    public String create(char[] val);
}

我们想要create方法调用new String(char[] chars)方法来创建String,直接使用lambda表达式如下:

Factory factory = chars -> new String(chars);

而使用构造器引用的话,即可简写为:

Factory factory = String::new;

你可能感兴趣的:(0202年了,你还不会lambda表达式?一文带你完全搞懂lambda!)