Java 8带来了"久违"了的lambda表达式、函数式编程接口。为什么是“久违”了呢,也许是因为Scala在许久之前就有这些个特性吧。

定义lambda表达式

Java 8中如何定义lambda表达式呢?直接来看代码,

Function<Integer, String> lambda = x -> String.valueOf(x);

这里定义了一个lambda函数,参数类型以及返回类型在Function<Integer, String>中进行声明。Function是java 8引入的新接口,定义在java.util.function这个包中,

package java.util.function;

@FunctionalInterface
public interface Function<T, R> {

    R apply(T t);

    default <V> Function<V, R> compose(Function<? super V, ? extends T> before) {
        Objects.requireNonNull(before);
        return (V v) -> apply(before.apply(v));
    }

    default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {
        Objects.requireNonNull(after);
        return (T t) -> after.apply(apply(t));
    }

    static <T> Function<T, T> identity() {
        return t -> t;
    }
}

这段代码出现了java 8引入的几个新东西,

  • default method

java 8中在接口内可以定义函数实现了,通过在函数之前添加default关键字,标记此函数为接口的默认函数。一个类实现多个含有相同函数签名的接口时会产生编译错误。

  • @FunctionInterface

FunctionInterface是新增加的一个annotation,用于标记接口是否是函数接口。不过FunctionInterface是可选的,来看下The Java® Language Specification中的说明,

A functional interface is an interface that has just one abstract method (aside from the methods of Object), and thus represents a single function contract. 

所以实际上不加这个anotation也不会出错,不过实际编码中是建议添加的,这样如何不符合FunctionInterface的定义可以获取到编译器错误提示。

更多的lambda表达式定义

上面展示了带有一个参数的lambda表达式如何定义,那么一个参数、或多个参数如何定义呢?

翻了下java.util.function中定义的接口,发现默认的接口中,

java.util.function.Function
java.util.function.BiFunction

这两个是用来定义带一个参数、两个参数的lambda表达式。更多参数以及无参数的就需要自己定义了,标准库内并未提供,

@FunctionalInterface
interface ZeroFunc<R> {
    R apply();
}

@FunctionalInterface
interface MultiFunc<F, S, T, R> {
    R apply(F f, S s, T t);
}

上面定义了两个接口用于声明0个参数、3个参数的lambda表达式,可以这么来用,

ZeroFunc<String> zero = () -> "";
MultiFunc<Integer, Integer, Integer, String> multi = (a, b, c) -> String.format("%s, %s, %s", a, b, c);

zero.apply();
multi.apply(1, 2, 3);

将lambda表达式作为参数传递

java 8中lambda表达式也可以进行参数传递,直接来看代码,

public class Lambda {

    public static void main(String[] args) throws Exception {
        execute(a -> String.valueOf(a + 1), 10);
    }

    public static void execute(Function<Integer, String> func, int num) {
        System.out.println(func.apply(num));
    }
}

上面可以看到在进行参数传递的时候,lambda表达式可以不先声明而直接写在参数当中,编译器会进行必要检测来判定传入的lambda是否符合函数参数类型定义。

再来看一段代码,

@FunctionalInterface
interface CustomFunction<T, R> {
    R apply(T t);
}

public class Lambda {

    public static void main(String[] args) throws Exception {
        execute(a -> "", 10);
    }

    public static void execute(Function<Integer, String> func, int num) {
        System.out.println(func.apply(num));
    }

    public static void execute(CustomFunction<Integer, String> func, int num) {
        System.out.println(func.apply(num));
    }
}

这里定义了一个与Function接受同样参数类型与返回类型的CustomFunction,上面的代码并不能正常运行,

execute(a -> "", 10);

这一句会产生混淆,两个execute都满足条件,所以会遭遇编译错误。在实际编码过程中,lambda更多的是直接当参数进行调用,这个时候就需要注意这种容易产生编译错误的地方了。

如果像文章最开始那样先声明lambda表达式,再进行调用,那么也是正确的。通过javap命令,可以看到具体的函数签名,

public class Lambda {
  public Lambda();
    descriptor: ()V

  public static void main(java.lang.String[]) throws java.lang.Exception;
    descriptor: ([Ljava/lang/String;)V

  public static void execute(java.util.function.Function<java.lang.Integer, java.lang.String>, int);
    descriptor: (Ljava/util/function/Function;I)V

  public static void execute(CustomFunction<java.lang.Integer, java.lang.String>, int);
    descriptor: (LCustomFunction;I)V
}

两个execute的descriptor并不相同,只是编译器在直接传递lambda时无法判定进行怎样的转换。

lambda表达式是如何实现的

Lambda表达式的判定是编译期就做出的,可以通过反射来看看是否有什么玄机,

public class Lambda {

    public static void main(String[] args) throws Exception {
        execute(x -> String.valueOf(x), 10);
        for (Method method : Lambda.class.getDeclaredMethods()) {
            System.out.println(method);
        }
    }

    public static void execute(Function<Integer, String> func, int num) {
        System.out.println(func.apply(num));
    }
}
public static void Lambda.main(java.lang.String[]) throws java.lang.Exception
public static void Lambda.execute(java.util.function.Function,int)
private static java.lang.String Lambda.lambda$main$0(java.lang.Integer)
10

上面代码输出了一句,

private static java.lang.String Lambda.lambda$main$0(java.lang.Integer)

编译器添加了上面这个函数。Translation of Lambda Expressions这篇文章里介绍了lambda转化的种种规则,可以去继续了解下。

与Python、Scala中的lambda表达式简单对比

最后再对比下Python、Scala两门语言中lambda表达式的定义与使用,

Python是动态语言,所以声明lambda的时候无需定义类型,可以直接将其进行传递,可以直接像函数那样进行调用,

func = lambda x: x + 1
map(func, [1, 2, 3])
func(1)

Scala是静态语言,在声明lambda的时候如果能动态推导出类型那么就无需声明,

val func = (x: Int) => x + 1
Array(1, 2, 3).map(func)
func(1)

同样的逻辑在Java中实现则是,

Function<Integer, Integer> func = x -> x + 1;
Arrays.stream(new int[] {1, 2, 3}).boxed().map(func);
func.apply(1);

Java版看着也有了函数式的样子,虽然略微稍稍显得繁琐,但相比较以前也是进步了许多。

lambda表达式使用的不便利处

实际使用lambda函数进行操作后,主要发现了两个问题,

  • 标准库内没有提供支持更多参数的函数式接口
  • lambda表达式中引用到的变量需要实际是final

第一点在开始之初已经提及,第二点具体来看代码,

// 编译通过
public static void main(String[] args) throws Exception {
    int num = 10;
    Function<Integer, Integer> lambda = x -> x + num;
}

// 编译错误
public static void main(String[] args) throws Exception {
    int num = 10;
    num += 1;
    Function<Integer, Integer> lambda = x -> x + num;
}

上面代码的num均没有声明成final,但第一段代码num没有任何修改操作,所以编译通过,而第二段代码修改了num,立马就编译失败了。

method reference

Java 8另外还包含了method refence,可以将定义好的函数像lambda表达式那样进行传递,

public class Lambda {

    public static void main(String[] args) throws Exception {
        execute(String::valueOf, 10);
    }

    public static void execute(Function<Integer, String> func, int num) {
        System.out.println(func.apply(num));
    }
}

在上面的代码中可以看到,java引入了一个新的操作符“::”,C++中的这个操作符又回来了..

总结

Java 8带来了“新”的语言特性,虽说函数式的概念自己也不是第一次接触,在Python、Scala早就实际应用过,不过看着Java“慢吞吞”的升级也挺有趣。lambda表达式是进行函数式操作的基础之一,后续再来慢慢看其它相关的特性吧。