Java8的新特性之一:lambda表达式

在Java8中,添加了函数式接口、lambda表达式和方法引用,以便更容易地创建函数对象。同时Stream API也为处理数据序列提供支持。

使用单一抽象方法的接口被用作函数类型。它们的实例被称作函数对象,而创建函数对象最常见的方法就是利用匿名类。

匿名类

匿名类是不能有名字的类,它们不能被引用,只能在创建时用New语句来声明它们。匿名类的声明是在编译时进行的,实例化在运行时进行,这意味着for循环中的一个new语句会创建相同匿名类的几个实例。
匿名类的目的是在某个地方需要特殊的实现,因此在该处编写其实现,并获取它的实例,调用它的方法。不要在匿名内部类编写其他的方法,是不可见的。形式为:new <类或接口> <类的主体>

匿名类有两种创建方式,一种是使用单一抽象方法的接口;其次是使用具有单一抽象方法的抽象类
下面是一段代码片段,按照字符串长度顺序对列表进行排序,使用匿名类创建排序的比较方法(强制排序顺序):

// Anonymous class instance as a function object - obsolete!
Collections.sort(words, new Comparator<String>() {
    public int compare(String s1, String s2) {
        return Integer.compare(s1.length(), s2.length());
    }
});

不足之处很明显,匿名类很冗长,所以函数式编程应运而生。

在 Java 8 中有这样的概念:使用单个抽象方法的接口是特别的,应该得到特别的对待。 这些接口现在称为函数式接口,并且该语言允许你使用 lambda 表达式或简称 lambda 来创建这些接口的实例。 Lambdas 在功能上与匿名类相似,但更为简洁。 下面的代码使用 lambdas 替换上面的匿名类:

// Lambda expression as function object (replaces anonymous class)
Collections.sort(words,
        (s1, s2) -> Integer.compare(s1.length(), s2.length()));

s1和s2是参数,“->“箭头后面跟着的就是实现;编译器可以根据上下文推出s1、s2的类型,有些时候可能无法推断出来,必须指定它们的类型。
顺便提一句,如果使用比较器构造方法代替 lambda,则代码中的比较器可以变得更加简洁:

Collections.sort(words, comparingInt(String::length));

实际上,通过利用添加到 Java 8 中的 List 接口的 sort 方法,可以使片段变得更简短:

words.sort(comparingInt(String::length));

不得不说,代码变得简洁了,但是对于不熟悉的人来说可读性其实并不好,所以还是要根据实际情况使用,并且注意合理添加注释。

lambdas 添加到该语言中,使得使用函数对象在以前没有意义的地方非常实用。例如,考虑条目 34 中的 Operation 枚举类型。由于每个枚举都需要不同的应用程序行为,所以我们使用了特定于常量的类主体,并在每个枚举常量中重写了 apply 方法。下面是之前的代码:

// Enum type with constant-specific class bodies & data 
public enum Operation {
    PLUS("+") {
        public double apply(double x, double y) { return x + y; }
    },

    MINUS("-") {
        public double apply(double x, double y) { return x - y; }
    },

    TIMES("*") {
        public double apply(double x, double y) { return x * y; }
    },

    DIVIDE("/") {
        public double apply(double x, double y) { return x / y; }
    };

    private final String symbol;

    Operation(String symbol) { this.symbol = symbol; }

    @Override 
    public String toString() { return symbol; }

    public abstract double apply(double x, double y);
}

如果使用lambda表达式:

public enum Operation {
    PLUS  ("+", (x, y) -> x + y),
    MINUS ("-", (x, y) -> x - y),
    TIMES ("*", (x, y) -> x * y),
    DIVIDE("/", (x, y) -> x / y);

    private final String symbol;
    private final DoubleBinaryOperator op;

    Operation(String symbol, DoubleBinaryOperator op) {
        this.symbol = symbol;
        this.op = op;
    }

    @Override 
    public String toString() { return symbol; }
    
    public double apply(double x, double y) {
        return op.applyAsDouble(x, y);
    }
}

你可能会认为匿名类在 lambda 时代已经过时了。 这更接近事实,但有些事情你可以用匿名类来做,而却不能用 lambdas 做。 Lambda 仅限于函数式接口。 如果你想创建一个抽象类的实例,你可以使用匿名类来实现,但不能使用 lambda。 同样,你可以使用匿名类来创建具有多个抽象方法的接口实例。 最后,lambda 不能获得对自身的引用。

Lambdas 与匿名类共享无法可靠地序列化和反序列化实现的属性。因此,应该很少 (如果有的话) 序列化一个 lambda(或一个匿名类实例)。 如果有一个想要进行序列化的函数对象,比如一个 Comparator,那么使用一个私有静态嵌套类的实例(详见第 24 条)。

总结

综上所述,从 Java 8 开始,lambda 是迄今为止表示小函数对象的最佳方式。 除非必须创建非函数式接口类型的实例,否则不要使用匿名类作为函数对象。 另外,请记住,lambda 表达式使代表小函数对象变得如此简单,以至于它为功能性编程技术打开了一扇门,这些技术在 Java 中以前并不实用。