Java中的函数式编程(三)lambda表达式

658 阅读8分钟

写在前面

前面说过,判断一门语言是否支持函数式编程,一个重要的判断标准就是:它是否将函数看做是“第一等公民(first-class citizens)”。

函数是“第一等公民”,意味着函数和其它数据类型具备同等的地位——可以赋值给某个变量,可以作为另一个函数的参数,也可以作为另一个函数的返回值。

Java 8是通过函数式接口,赋予了函数“第一等公民”的特性。

本文将详细介绍Java 8中的函数式接口。

本文的示例代码可从gitee上获取:gitee.com/cnmemset/ja…

lambda表达式与匿名内部类

lambda表达式可以用来简化某些匿名内部类(Anonymous Inner Classes)的写法,但仅限于对函数式接口的简写。

无参的函数式接口

以最常用的Runnable接口为例:

在Java 7中,如果需要新建一个线程,使用匿名内部类的写法是这样:

public static void createThreadWithAnonymousClass() {
    // Runnable 是接口名。我们通过匿名内部类的方式,构造了一个 Runnable 的实例。
    Thread t = new Thread(new Runnable() {
        @Override
        public void run() {
            System.out.println("Thread is running");
        }
    });

    t.start();
}

使用匿名内部类的一个重要目的,就是为了减轻程序员的代码负担,不需要额外再定义一个类,而且这个类是一个一次性的类,没有太多的重用价值。但是,我们会发现,这个对象看起来也是多余的,因为我们实际上并不是要传入一个对象,而只是想传入一个方法。

在Java 8中,因为 Runnable 接口是一个函数式接口(只有一个抽象方法的接口都属于函数式接口),因此我们可以用lambda表达式来简化匿名内部类的写法:

public static void createThreadWithLambda() {
    // 在Java 8中,Runnable 是一个函数式接口,因此我们可以使用 lambda 表达式来实现它。
    Thread t = new Thread(() -> {
        System.out.println("Thread is running");
    });

    t.start();
}

带参的函数式接口

Runnable是一个无参的函数式接口,我们再来看一个典型的带参数的函数式接口 Comparator:

@FunctionalInterface
public interface Comparator {
    int compare(T o1, T o2);

    ....

}

假设一个场景:给定一个省份的拼音列表,需要对该列表中的省份进行排序,排序规则是字母长度最小的省份排在前面,如果两个省份字母长度一样,则按字母顺序排序。

使用匿名内部类的示例代码如下:

public static void sortProvincesWithAnonymousClass() {
    List list = Arrays.asList("Guangdong", "Zhejiang", "Jiangsu", "Xizang", "Fujian", "Hunan", "Guangxi");

    list.sort(new Comparator() {
        @Override
        public int compare(String first, String second) {
            int lenDiff = first.length() - second.length();
            return lenDiff == 0 ? first.compareTo(second) : lenDiff;
        }
    });

    list.forEach(s -> System.out.println(s));
}

上述代码输出为:

Hunan
Fujian
Xizang
Guangxi
Jiangsu
Zhejiang
Guangdong

 使用lambda表达式来简化Comparator的实现,示例代码如下:

public static void sortProvincesWithLambda() {
    List list = Arrays.asList("Guangdong", "Zhejiang", "Jiangsu", "Xizang", "Fujian", "Hunan", "Guangxi");

    // 下面的参数列表 first 和 second ,即方法 Comparator.compare 的参数列表
    list.sort((first, second) -> {
        int lenDiff = first.length() - second.length();
        return lenDiff == 0 ? first.compareTo(second) : lenDiff;
    });

    list.forEach(s -> System.out.println(s));
}

注意到,带参数的lambda表达式,甚至不需要声明类型,因为编译器可以通过上下文来推断出参数的类型。当然,我们也可以显式指定参数类型,尤其是在参数类型推断失败的时候:

(String first, String second) -> {
    int lenDiff = first.length() - second.length();
    return lenDiff == 0 ? first.compareTo(second) : lenDiff;
}

this关键字的作用域

前面提到过,匿名内部类和lambda表达式本质是不同的:匿名内部类本质是一个类,而lambda表达式本质是一个函数。在JVM层面,匿名内部类对应的是一个class文件,而lambda表达式对应的是它所在主类的一个私有方法。

这就导致了this关键字在匿名内部类和lambda表达式中是不一样的。在匿名内部类中,this关键字指向匿名内部类的实例,而在lambda表达式中,this关键字指向的是主类的实例。

我们用代码验证一下:

public class ThisScopeExample {
    public static void main(String[] args) {
        ThisScopeExample example = new ThisScopeExample();
        
        // 输出 "I am Anonymous Class."
        example.runWithAnonymousClass();
        
        // 输出 "I am ThisScopeExample Class."
        example.runWithLambda();
    }

    public void runWithAnonymousClass() {
        // 以匿名类的方式运行
        run(new Runnable() {
            @Override
            public void run() {
                // this 是实现了接口 Runnable 的匿名内部类的实例
                System.out.println(this);
            }

            @Override
            public String toString() {
                return "I am Anonymous Class.";
            }
        });
    }

    public void runWithLambda() {
        // 以lambda表达式的方式运行
        run(() -> {
            // this 是类 ThisScopeExample 的实例
            System.out.println(this);
        });
    }

    public void run(Runnable runnable) {
        runnable.run();
    }
    
    @Override
    public String toString() {
        return "I am ThisScopeExample Class.";
    }

}

上述代码输出为:

I am Anonymous Class.
I am ThisScopeExample Class.

lambda表达式的语法

lambda表达式的语法是:参数,箭头(->) 以及方法体。如果方法体无法用一个表达式来完成,就可以像写普通的方法一样,把代码放在大括号 { } 中。反之,如果方法体只有一个表达式,那么就可以省略大括号 { }。

例如,下面是一个典型的而且完整的lambda表达式。:

(String first, String second) -> {
    int lenDiff = first.length() - second.length();
    return lenDiff == 0 ? first.compareTo(second) : lenDiff;
}

对无参数的lambda表达式,参数部分也不能省略,需要提供空括号,例如:

Supplier supplier = () -> {
    return new Random().nextInt(100);
};

对于上面的lambda表达式,可以发现它的方法体只有一个表达式,所以,它可以省略大括号,甚至return关键字也省略了,因为编译器可以根据上下文推断是否需要返回值:如果需要,那么就返回该唯一表达式的返回值,如果不需要,则在该唯一表达式后直接return。例如:

// Supplier 是需要返回值的,所以下面的lambda表达式等同于:
// () -> { return new Random().nextInt(100); }
Supplier supplier = () -> new Random().nextInt(100);

// Runnable 是不需要返回值的,所以下面的lambda表达式等同于:
// () -> { new Random().nextInt(100); return; }
Runnable runnable = () -> new Random().nextInt(100);

如果编译器可以推断出lambda表达式的参数类型,则可以忽略其类型:

// 在这里,编译器可以推断出 first 和 second 的类型是 String。
Comparator comp = (first, second) -> {
    int lenDiff = first.length() - second.length();
    return lenDiff == 0 ? first.compareTo(second) : lenDiff;
};

如果lambda表达式只有一个参数,那么参数列表中的小括号也可以省略掉:

// 这里的 value ,等同于 (value)
Consumer consumer = value -> System.out.println(value);

与普通的函数不一样,lambda表达式不需要指定返回类型,它总是由编译器自行推断出返回类型。如果推断失败,则默认为Object类型。

lambda表达式与闭包

首先要理解lambda表达式和闭包(closure)是两个不同的概念,但两者有着紧密的联系。在不追求概念精确的场合,甚至可以说Java中的lambda表达式就是闭包。

闭包又称为函数闭包(function closure),是一种延长变量生命周期的技术,从这个意义上说,闭包和面向对象实现的功能是等价的。

闭包的定义是:在创建或定义一个函数的时候,除了记录函数本身以外,同时还记录了在创建函数时所能访问到的自由变量(自由变量 free variable,是指在函数外部定义的变量,它既不是函数的参数,也不是函数内的局部变量)。这样一来,闭包的变量作用域除了包含函数运行时的局部变量域外,还包含了函数定义时的外部变量域。

文字表达可能不够直观,我们来看一个代码示例:

public class ClosureExample {
    public static void main(String[] args) {
        // 平方
        IntUnaryOperator square = getPowOperator(2);
        
        // 立方
        IntUnaryOperator cube = getPowOperator(3);

        // 四次方
        IntUnaryOperator fourthPower = getPowOperator(4);

        // 5的平方
        System.out.println(square.applyAsInt(5));

        // 5的立方
        System.out.println(cube.applyAsInt(5));

        // 5的四次方
        System.out.println(fourthPower.applyAsInt(5));
    }

    public static IntUnaryOperator getPowOperator(int exp) {
        return base -> {
            // 变量 exp 是 getPowOperator 的参数,属于lambda 表达式定义时的自由变量,
            // 它的生命周期会延长到和返回的 lambda 表达式一样长。
            return (int) Math.pow(base, exp);
        };
    }
}

上述代码的输出是:

25
125
625

可以看到,exp是方法 getPowOperator 的参数,但通过闭包技术,它“逃逸”出 getPowOperator 的作用域了。 很显然,变量“逃逸”,在多线程环境下,容易导致线程安全问题,防不胜防。因此,Java规定了,在lambda表达式内部引用外部变量的话,必须是final的,即不可变对象,只能赋值一次,不可修改。(在这说句题外话,并不是所有的语言都这么要求闭包的,譬如Python和JavaScript,闭包中引用的外部变量是可以任意修改的。)

为了书写代码方便,Java 8不要求显式将变量声明为final,但如果你尝试修改变量的值,编译器将会报错。例如:

public static IntUnaryOperator getPowOperator(int exp) {
    // 尝试修改 exp 的值,但编译器会在lambda表达式中报错
    exp++;
    return base -> {
        // 如果尝试修改 exp 的值,会在此处报错:
        // Error: 从lambda 表达式引用的本地变量必须是final变量或实际上的final变量
        return (int) Math.pow(base, exp);
    };
}

但这种限制也是有限的,因为我们可以通过将变量声明为一个数组或一个类就可以修改其中的值。例如:

public static IntUnaryOperator getPowOperator(int[] exp) {
    // exp 是一个int数组:exp = new int[1];
    exp[0]++;
    return base -> {
        // 此时不会报错,可以正常运行
        return (int) Math.pow(base, exp[0]);
    };
}

结语

lambda表达式的出现,一方面为函数式编程提供了支持,另一方面也提升了Java程序员的生产力。我们要熟悉常见的函数式接口,灵活使用lambda表达式和闭包。