1 Lambda表达式

Lambda表达式是Java8中的新特性,编码时,我们一般尽可能轻量级的将代码封装为数据,传统的解决方案是通过接口和实现类(匿名内部类)实现,这种方式存在语法冗余,this关键字,变量捕捉,数据控制等问题。而Lambda表达式则更为简化,它强调做什么,而不是以什么形式做

1.1 Lambda表达式标准格式

Lambda省去面向对象的条条框框,格式由3个部分组成:

⼀些参数、⼀个箭头、⼀段代码。

Lambda表达式的标准格式为:

(参数类型 参数名称) -> { 代码语句 }

格式说明:

小括号内的语法与传统方法参数列表⼀致:无参数则留空;多个参数则用逗号分隔。

-> 是新引入的语法格式,代表指向动作。

大括号内的语法与传统方法体要求基本⼀致。 

1.2 Lambda表达式的使用前提

Lambda的语法非常简洁,完全没有面向对象复杂的束缚。

但是使用时有几个问题需要特别注意:

1. 使用Lambda必须具有接口,且要求接口中有且仅有一个抽象方法。无论是JDK内置的Runnable 、 Comparator 接口还是自定义的接⼝,只有当接口中的抽象方法存在且唯⼀时,才可以使用Lambda。

2. 使用Lambda必须具有上下文推断。也就是方法的参数或局部变量类型必须为Lambda对应的接口类型,才能使用Lambda作为该接口的实例。

备注:有且仅有⼀个抽象方法的接口,称为“函数式接口”。

1.3 Lambda表达式的使用举例

下面举例演示 java.util.Comparator 接口的使用场景代码,其中的抽象方法定义为:

public abstract int compare(T o1, T o2);

当需要对一个对象数组进行排序时,Arrays.sort 方法需要⼀个 Comparator 接口实例来指定排序的规则。假设有一个 Person 类,含有 String name 和 int age 两个成员变量:

public class Person {
    private String name;
    private int age;
 
    // 省略构造器、toString⽅法与Getter Setter
}

传统写法:

如果使用传统的代码对 Person[] 数组进行排序,写法如下:

import java.util.Arrays;
import java.util.Comparator;
public class Demo06Comparator {
    public static void main(String[] args) {
        // 本来年龄乱序的对象数组
        Person[] array = {
            new Person("古⼒娜扎", 19),
            new Person("迪丽热巴", 18),
            new Person("⻢尔扎哈", 20) };
 
        // 匿名内部类
        Comparator<Person> comp = new Comparator<Person>() {
            @Override
            public int compare(Person o1, Person o2) {
                return o1.getAge() - o2.getAge();
            }
        };
        Arrays.sort(array, comp); // 第⼆个参数为排序规则,即Comparator接⼝实例
 
        for (Person person : array) {
            System.out.println(person);
        }
    }
}

这种做法在面向对象的思想中,似乎也是“理所当然”的。其中 Comparator 接口的实例(使用了匿名内部类)代表了“按照年龄从小到大”的排序规则。

代码分析:

下面我们来搞清楚上述代码真正要做什么事情。

为了排序, Arrays.sort 方法需要排序规则,即 Comparator 接口的实例,抽象方法 compare 是关键; 为了指定 compare 的方法体,不得不需要 Comparator 接口的实现类;为了省去定义⼀个 ComparatorImpl 实现类的麻烦,不得不使用匿名内部类;必须覆盖重写抽象 compare 方法,所以方法名称、方法参数、方法返回值不得不再写⼀遍,且不能写错;实际上,只有参数和方法体才是关键

Lambda写法:

import java.util.Arrays;
public class Demo07ComparatorLambda {
    public static void main(String[] args) {
        Person[] array = {
            new Person("古⼒娜扎", 19),
            new Person("迪丽热巴", 18),
            new Person("⻢尔扎哈", 20) };
 
            Arrays.sort(array, (Person a, Person b) -> {
            return a.getAge() - b.getAge();
        });
 
        for (Person person : array) {
            System.out.println(person);
        }
    }
}

2 方法引用

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

2.1 方法引用符

双冒号 :: 为引用运算符,而它所在的表达式被称为方法引用

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

具体例子下面会进行说明。

2.2 冗余的Lambda场景

来看一个简单的函数式接口以应用Lambda表达式:

@FunctionalInterface

public interface Printable {

    void print(String str);

}

Printable 接口当中唯一的抽象方法 print 接收一个字符串参数,目的就是为了打印显示它。那么通过Lambda来使用它的代码很简单:

public class Demo01PrintSimple {
    private static void printString(Printable data) {
        data.print("Hello, World!");
    }
 
    public static void main(String[] args) {
        printString(s -> System.out.println(s));
    }
}

其中 printString 方法只管调用 Printable 接口的 print 方法,而并不管 print 方法的具体实现逻辑会将字符串打印到什么地方去。而 main 方法通过Lambda表达式指定了函数式接口 Printable 的具体操作方案为:拿到String(类型可推导,所以可省略)数据后,在控制台中输出它

问题分析:

这段代码的问题在于,对字符串进行控制台打印输出的操作方案,明明已经有了现成的实现, 即System.out 对象中的 println(String) 方法。

既然Lambda希望做的事情就是调用 println(String) 方法,那何必自己手动调用呢?

2.3 用方法引用改进代码

能否省去Lambda的语法格式(尽管它已经相当简洁)呢?

只要“引用”过去就好了:

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

再次注意:请关注其中的双冒号 :: 写法,这被称为“方法引用”,而双冒号是⼀种新的语法。

语义分析:

上例中,System.out 对象中有⼀个重载的 println(String) 方法恰好就是我们所需要的。

那么对于 printString 方法的函数式接口参数,对比下面两种写法,完全等效:

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

方法引用写法: System.out::println

第⼀种语义是指:拿到参数之后经Lambda之⼿,继而传递给 System.out.println 方法去处理。

第⼆种等效写法的语义是指:直接让 System.out 中的 println 方法来取代Lambda。

两种写法的执行效果完全⼀样,而第⼆种方法引用的写法复用了已有方案,更加简洁。

:Lambda 中传递的参数一定是方法引用中的那个方法可以接收的类型,否则会抛出异常。

推导与省略:

如果使用Lambda,那么根据“可推导就是可省略”的原则,无需指定参数类型,也无需指定的重载形式 -- 它们都将被自动推导。而如果使用方法引用,也是同样可以根据上下文进行推导。

函数式接口是Lambda的基础,而方法引用是Lambda的孪生兄弟。

下面这段代码将会调用 println 方法的不同重载形式,将函数式接口改为int类型的参数:

@FunctionalInterface
public interface PrintableInteger {
    void print(int str);
}

由于上下文变了之后可以自动推导出唯⼀对应的匹配重载,所以方法引用没有任何变化:

public class Demo03PrintOverload {
    private static void printInteger(PrintableInteger data) {
        data.print(1024);
    }
 
    public static void main(String[] args) {
        printInteger(System.out::println);
    }
}

这次方法引用将会自动匹配到 println(int) 的重载形式。

2.4 方法引用的各种用法

1.对象引用成员方法

2.类引用静态方法

3.引用构造方法

4.引用数组的构造器: int[] arr = new int[10] 。

5.this引用成员方法: this是使用在lambda表达式中, 必须出现类的成员方法中。

6.super引用父类的方法: super, 必须出现在子类的成员方法里。