lambda详解

Posted by 胡奚冰 on May 9, 2019

lambda介绍

lambda是JDK8新增的功能,它增加了新的语法元素,是Java语言的表达能力得以提升,并流线化了一些常见结构的实现方式。其次,lambda表达式的加入也导致API库中增加了新的功能,包括利用多核环境的并行处理功能变得更加容易,以及支持对数据执行管道操作的新的流API。

lambda表达式本质上就是一个匿名方法。这个方法不是独立执行的,而是用于实现由函数式接口定义的另一个方法。

lambda表达式语法

我们先来看一个简单的lambda表达式:

 () -> 1.5

()表示方法的参数,这里表示没有参数,->表示lambda要执行的动作,也就是方法体中的内容,1.5表示返回值,省略了return。这个lambda表达式等同于下面这个方法:

double getValue() {
    return 1.5;
}

再来看一个稍微复杂一点的lambda表达式:

(int n) -> (n % 2) == 0

这里(double n)表示需要一个double类型的参数,方法体内判断n是否一个偶数 (n % 2) == 0 的执行结果是一个boolean类型的值,也作为lambda表达式的返回值。等同于下面这个方法:

boolean test(int n) {
    return (n % 2) == 0;
}

函数式接口

函数式接口是仅包含一个抽象方法的接口。一般来说,这个方法指明了接口的目标用途。因此,函数式接口通常表示单个动作,例如Runnable是一个函数式接口。

下面是函数式接口的一个例子:

interface MyNumber {
    double getValue();
}

上面讲到lambda表达式实际上就是对应一个方法实现,我们知道在Java中不能单独定义方法,方法一定要定义在类(或接口)中。所以lambda表达式需要关联一个函数式接口,而lambda表达式等同于一个实现该函数式接口的内部类的方法。

我们先来看下要实现一个函数式接口的内部类如何做:

MyNumber myNum = new MyNumber() {
    @Override
    public double getValue() {
        return 1.5;
    }
};

上面这个内部类可以用lambda表达式代替:

MyNumber myNum = () -> 1.5;

完整的lambda表达式

事实上完整的lambda是这样的:

(int n) -> { 
    return (n % 2) == 0; 
}

这样来看的是不是就与方法特别像了?但是这样写起来与定义方法并没有多大的区别,一样的繁琐。于是lambda表达式有一些重要特性来简化:

  • 可选类型声明 :不需要声明参数类型,编译器可以统一识别参数值。
  • 可选的参数圆括号 :一个参数无需定义圆括号,但多个参数需要定义圆括号。
  • 可选的大括号 :如果主体包含了一个语句,就不需要使用大括号。
  • 可选的返回关键字 :如果主体只有一个表达式返回值则编译器会自动返回值;但是如果存在大括号,则需要指定明表达式返回了一个数值。

简化最终也就是上面看到的两个例子的样子。我们来多看几个例子:

//不需要参数,返回值为 5  
() -> 5  
  
//接收一个参数(数字类型),返回其2倍的值  
x -> 2 * x  
  
//接受2个参数(数字),并返回他们的差值  
(x, y) -> x – y  
  
//接收2个int型整数,返回他们的和  
(int x, int y) -> x + y  
  
//接受一个 string 对象,并在控制台打印,不返回任何值
(String s) -> System.out.print(s)

泛型函数式接口

lambda表达式自身不支持泛型,但可以通过关联泛型的函数式接口表现出泛型的特性。

我们先声明一个泛型接口:

    interface SomeFunc<T> {
        T func(T t);
    }

接着使用lambda表达式定义两个实现:

public class Test {

    public static void main(String[] args) {
        SomeFunc<String> reverse = str -> {
            StringBuilder sb = new StringBuilder();
            for (int i = str.length() - 1; i >= 0; i--) {
                sb.append(str.charAt(i));
            }
            return sb.toString();
        };
        System.out.println(reverse.func("abcdefg"));

        SomeFunc<Integer> factorial = n -> {
            int result = 1;
            for (int i = 1; i <= n; i++) {
                result = i * result;
            }
            return result;
        };
        System.out.println(factorial.func(5));
    }
}

SomeFunc接口的func()方法的参数和返回值都是泛型T,表示它可以接收任意参数。reverse和factorial实际上是两个不同的lambda表达式,一个String类型,另一个是Integer类型,他们都可以关联到SomeFunc接口。

作为参数传递lambda表达式

作为参数传递lambda表达式是一种常见且强大的用途,着极大地增强了Java的表达能力。

为了将lambda表达式作为参数传递,我们在接收lambda表达式的方法参数声明成函数式接口的类型:

    public static <T> T transfer(T original, SomeFunc<T> func) {
        return func.func(original);
    }

这个方法接收两个参数,一个任意对象,一个函数式接口用于接收lambda表达式,这个函数式接口就是上文的SomeFunc接口,func方法接收一个任意对象,返回该对象。通过传入不同的lambda,我们可以实现对该对象的的任意变形操作。

    public static void main(String[] args) {
        String s = "abcdefg";
        System.out.println(transfer(s, str -> {
            StringBuilder sb = new StringBuilder();
            for (int i = str.length() - 1; i >= 0; i--) {
                sb.append(str.charAt(i));
            }
            return sb.toString();
        }));

        System.out.println(transfer(s, str -> str.toUpperCase()));
    }

这里我们传入两个不同的lambda实现对同一个String的不同变形操作。

方法引用

有时候我们作为参数传递的lambda表达式中可能仅仅是调用其他类的方法,例如这样:

public class Test {

    public interface SomeFunc<T> {
        T func(T t);
    }

    public static String func(String str) {
        StringBuilder sb = new StringBuilder();
        for (int i = str.length() - 1; i >= 0; i--) {
            sb.append(str.charAt(i));
        }
        return sb.toString();
    }

    public static <T> T transfer(T original, SomeFunc<T> func) {
        return func.func(original);
    }

    public static void main(String[] args) {
        String s = "abcdefg";
        System.out.println(transfer(s, str -> Test.func(str)));
    }
}

这种情况下,transfer(s, str -> Test.func(str)) 可以简写成transfer(s, Test::func) 表示直接把Test的静态方法func作为lambda表达式传递。:: 是JDK8新增的分隔符,专门用于此目的。

也就是说要创建静态方法引用,使用如下语法:

ClassName::methodName

如果是实例方法引用

objRef::methodName

还有一个特殊的情况,前文中我们有一个这样的使用 transfer(s, str -> str.toUpperCase()),其实可以用 transfer(s, String::toUpperCase)代替。

toUpperCase是String的实例方法,并且没有参数:

    public String toUpperCase() {
        return toUpperCase(Locale.getDefault());
    }

但在这里却写成了类似静态方法的引用。使用这种形式时,函数式接口的第一个参数匹配调用对象,第二个参数(如果有)匹配方法指定的参数

这里的函数式接口是:

    interface SomeFunc<T> {
        T func(T t);
    }

参数t对应toUpperCase方式的实例也就是str,toUpperCase方法返回值是String,满足上面的规则。

当然如果记不住这样方法引用也没有关系,按照原本的写法就可以正常运行。IDEA编译器也会提示你转换成方法引用的写法。

预定义的函数式接口

前面的例子我们都是自己定义了函数式接口,可能你也注意到通过泛型定义的函数式接口可以承担很多功能。考虑到这个,很多时候我们并不需要自己定义函数式接口,JDK8的新包java.util.function中预定义了很多有用的函数式接口。

Function

Function表示接受一个参数并生成结果的函数。

public interface Function<T, R> {
    R apply(T t);
}

针对基本数据类型,为了避免产生冗余的包装类,定义了一系列基本数据类型的Function:

  • DoubleFunction
  • IntFunction
  • LongFunction
  • IntToDoubleFunction
  • IntToLongFunction
  • LongToDoubleFunction
  • LongToIntFunction
  • ToDoubleFunction
  • ToIntFunction
  • ToLongFunction

还有BiFunction,与Function类似,接受两个参数并生成结果的函数:

public interface BiFunction<T, U, R> {
    R apply(T t, U u);
}

Operator

Operator是特殊的一种Function,表示参数和返回是同一种类型:

public interface UnaryOperator<T> extends Function<T, T> {}

同样的也有针对基本数据类型扩展类,请自行查看。

Consumer

Consumer表示接受单个输入参数而不返回结果的操作,主要用于对参数t的处理

public interface Consumer<T> {
    void accept(T t);
}

同样的也有针对基本数据类型,以及Bi的扩展类,请自行查看。

Supplier

Supplier表示内容的提供者,即不需要参数有一个返回值。

public interface Supplier<T> {
    T get();
}

同样的也有针对基本数据类型扩展类,请自行查看。

Predicate

Predicate接收一个参数并返回boolean:

public interface Predicate<T> {
    boolean test(T t);
}

同样的也有针对基本数据类型扩展类,请自行查看。