java lambda深度研究

目录 [−]

  1. Java Lambda语法
  2. Java Lambda递归
  3. 捕获变量
  4. 序列化
    1. 一般序列化/反序列化
    2. 带捕获变量的序列化/序列化
  • 方法引用
    1. static method reference
    2. instance method reference
    3. arbitrary instance reference
    4. constructor reference
  • 多继承
    1. 三角继承
    2. 菱形继承
    3. 叉型继承
  • Stream接口的lazy方法和eager方法
  • Functional interface只能有一个方法吗?
  • Lambda的性能
  • Java 8发布有一段日子, 大家关注Java 8中的lambda可能更早, 对Java 8中这一最重要的语言变化也基本熟悉了。这篇文章将深入研究Java 8中的lambda特性以及Stream接口等, 讨论一些深层次的技术细节。
    比如, 一个lambda表达式序列化反序列化后, 对捕获的上下文变量的引用的情况。 Lambda表达式递归。 类方法的引用和实例方法的引用的区别。 菱形继承的问题。 Stream接口的Lazy和eager模式。 Lambda的性能。

    Java Lambda语法

    尽管你已经很熟悉了, 我们还是先回顾一下lambda表达式的语法。

    “A lambda expression is like a method: it provides a list of formal parameters and a body—an expression or block—expressed in terms of those parameters,”
    JSR 335

    1
    Function<Integer, Integer> fun = (Integer x, Integer y) -> {return x + y;}

    如果body只有一个表达式,可以省略body的·大括号 和 return

    1
    Function<Integer, Integer> fun = (Integer x, Integer y) -> x + y

    参数可以声明类型,也可以根据类型推断而省略。

    1
    Function<Integer, Integer> fun = (x, y) -> x + y

    但是不能部分省略。

    1
    Function<Integer, Integer> fun = (x, Integer y) -> x + y //wrong

    单个的参数可以省略括号。

    1
    2
    Function<Integer, Integer> fun = (x) -> x+1
    Function<Integer, Integer> fun = x -> x+1

    但是不能加上类型声明。

    1
    Function<Integer, Integer> fun = Integer x -> x+1 //wrong

    如果没有参数, 括号是必须的。

    1
    2
    () -> 1995
    () -> { System.gc(); }

    Java Lambda递归

    匿名函数是没有名字的, 但是Lambda表达式可以赋值给一个变量或者作为参数传递, 这意味着它有"名字"。 那么可以利用这个名字进行递归吗?
    lambdafaq网站说可以。

    1
    2
    Function<Long, Long> fib = x -> {if (x ==1 || x == 2) return 1L; else return fib.apply(x -1) + x;};
    System.out.println(fib.apply(3L));

    实际你并不能编译这段代码, 因为编译器认为fib可能没有初始化。

    1
    The local variable fib may not have been initialized

    没办法递归了吗?
    有一些hacked方法, 如

    1
    2
    IntToDoubleFunction[] foo = { null };
    foo[0] = x -> { return ( x == 0)?1:x* foo[0].applyAsDouble(x-1);};

    或者 (泛型数组的创建有些麻烦)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    @SuppressWarnings("unchecked")
    private static <E> E[] newArray(Class clazz, int size)
    {
    return (E[]) Array.newInstance(clazz, size);
    }
    public static void main(String[] args) throws InstantiationException, IllegalAccessException, SecurityException, NoSuchMethodException {
    //Function<Long, Long> fib = x -> {if (x ==1 || x == 2) return 1L; else return fib.apply(x -1) + x;};
    Function<Long, Long>[] funs = newArray(Function.class, 1);
    funs[0] = x -> {if (x ==1 || x == 2) return 1L; else return funs[0].apply(x -1) + x;};
    System.out.println(funs[0].apply(10L));
    }

    或者使用一个helper类。

    1
    2
    BiFunction<BiFunction, Long, Long> factHelper = (f, x) -> {if (x ==1 || x == 2) return 1L; else return x + (long)f.apply(f,x-1);};
    Function<Long, Long> fib = x -> factHelper.apply(factHelper, x);

    捕获变量

    就像本地类和匿名类一样, Lambda表达式可以捕获变量(capture variable)。

    In addition, a local class has access to local variables. However, a local class can only access local variables that are declared final. When a local class accesses a local variable or parameter of the enclosing block, it captures that variable or parameter

    但是Lambda表达式不强迫你将变量声明为final, 只要它的行为和final 变量一样即可,也就是等价final.
    下面的例子s不必声明为final,实际加上final也不会编译出错。

    1
    2
    3
    String s = "smallnest";
    Runnable r = () -> System.out.println("hello " + s);
    r.run();

    但是下面的例子s实际已经不是final了,编译会出错。,

    1
    2
    3
    4
    String s = "smallnest";
    Runnable r = () -> System.out.println("hello " + s);
    s = "colobu";
    r.run();

    下面的代码一样也会编译不成功:

    1
    2
    3
    String s = "smallnest";
    Runnable r = () -> {s = "abc"; System.out.println("hello " + s);};
    r.run();

    注意final仅仅是变量不能再被赋值, 而变量字段的值是可以改变的。

    1
    2
    3
    4
    5
    Sample s = new Sample();
    s.setStr("smallnest");
    Runnable r = () -> System.out.println("hello " + s.getStr());
    s.setStr("colobu");
    r.run();

    这里我们可以更改s的str字段的值。

    序列化

    一般序列化/反序列化

    Lambda表达式可以被序列化。下面是一个简单的例子。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    Runnable r = (Runnable & Serializable)() -> System.out.println("hello serialization");
    FileOutputStream fos = new FileOutputStream("Runnable.lambda");
    ObjectOutputStream os = new ObjectOutputStream(fos);
    os.writeObject(r);
    FileInputStream fis = new FileInputStream("Runnable.lambda");
    ObjectInputStream is = new ObjectInputStream(fis);
    r = (Runnable) is.readObject();
    r.run();

    注意(Runnable & Serializable)是Java 8中新的语法。 cast an object to an intersection of types by adding multiple bounds.
    一个Lambda能否序列化, 要以它捕获的参数以及target type能否序列化为准。当然,不鼓励在实践中使用序列化。上面的例子r实现了Serializable接口,而且没有captured argument,所以可以序列化。

    带捕获变量的序列化/序列化

    再看一个带captured argument的例子。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    class Sample implements Serializable {
    private String str;
    public String getStr() {
    return str;
    }
    public void setStr(String str) {
    this.str = str;
    }
    }
    public static void serializeLambda() throws Exception {
    Sample s = new Sample();
    s.setStr("smallnest");
    SampleSerializableInterface r = () -> System.out.println("hello " + s.getStr());
    FileOutputStream fos = new FileOutputStream("Runnable.lambda");
    ObjectOutputStream os = new ObjectOutputStream(fos);
    os.writeObject(r);
    s.setStr("colobu");
    }
    public static void deserializeLambda() throws Exception {
    FileInputStream fis = new FileInputStream("Runnable.lambda");
    ObjectInputStream is = new ObjectInputStream(fis);
    SampleSerializableInterface r = (SampleSerializableInterface) is.readObject();
    r.run();
    }

    可以看到连同captured argument s一同序列化了。 即使反序列化出来,captured argument也不是原来的s了。
    结果输出hello smallnest

    方法引用

    方法引用是一个有趣的特性, 方法类似指针一样可以被直接引用。 新的操作符"::"用来引用类或者实例的方法。

    static method reference

    1
    2
    3
    4
    BiFunction<Long,Long,Integer> bf = Long::compare;
    Long a = 10L;
    Long b = 11L;
    System.out.println(bf.apply(a, b));

    instance method reference

    1
    2
    Consumer<String> c = System.out::println;
    c.accept("hello colobu");

    以上两种情况引用的方法签名应和 target type的方法签名一样,方法的名字不一定相同。

    arbitrary instance reference

    1
    2
    String[] stringArray = { "Barbara", "James", "Mary", "John", "Patricia", "Robert", "Michael", "Linda" };
    Arrays.sort(stringArray, String::compareToIgnoreCase);

    这是一个很有趣的使用方法。 可以引用任意的一个类型的实例。等价的lambda表达式的参数列表为(String a, String b),方法引用会调用a.compareToIgnoreCase(b)。

    constructor reference

    另一种特殊的方法引用是对构造函数的引用。
    对构造函数的引用类似对静态方法的引用,只不过方法名是new。 一个类有多个构造函数, 会根据target type选择最合适的构造函数。

    多继承

    由于Java 8引入了缺省方法(default method)的特性,Java也想其它语言如C++一样遇到了多继承的问题。这里列出两个典型的多继承的情况。

    三角继承

    三角继承如下图所示。
    A
    |\
    | \
    | B
    | /
    C

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    class A {
    public A () {
    System.out.println("A()");
    }
    public A(int x) {
    System.out.println("A(int x)");
    }
    }
    1
    2
    C c = new C();
    c.say(); //B says

    菱形继承

    菱形继承如下图所示
    A
    /\
    / \
    B C
    \ /
    D

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    interface A {
    default void say() {
    System.out.println("A says");
    }
    }
    interface B extends A{
    default void say() {
    System.out.println("B says");
    }
    }
    interface C extends A{
    default void say() {
    System.out.println("C says");
    }
    }
    class D implements A, B, C{
    }

    直接编译出错。原因是Duplicate default methods.
    你需要在D中重载say方法, 自定义或者使用父类/接口的方法。 注意其写法接口.super.default_method_name

    1
    2
    3
    4
    5
    class D implements A, B, C{
    public void say() {
    B.super.say();
    }
    }

    叉型继承

    叉型继承如下图所示

    B C
    \ /
    D

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    interface B {
    default void say() {
    System.out.println("B says");
    }
    }
    class C {
    public void say() {
    System.out.println("C says");
    }
    }
    class D extends C implements B{
    }

    上面的代码输出C says

    原则:
    基本上,你可以根据以下三条原则判断多继承的实现规则。

    1. 类优先于接口。 如果一个子类继承的父类和接口有相同的方法实现。 那么接口中的定义会被忽略。 如第三个例子。
    2. 子类型中的方法优先于府类型中的方法。 如第一个例子。
    3. 如果以上条件都不满足, 如第二个例子,则必须显示覆盖/实现其方法,或者声明成abstract。

    Stream接口的lazy方法和eager方法

    新增加的Stream接口有很多方法。

    lazy例子:

    1
    allStudents.stream().filter(s -> as.age> 16);

    filter并不会马上对列表进行遍历筛选, 它只是为stream加上一些"秘方"。当前它的方法实现不会被执行。直到遇到eager类型的方法它才会执行。

    eager例子:

    1
    allStudents.stream().filter(s -> as.age> 16).count();

    原则:
    看方法的返回值。 如果返回Stream对象,那么它是lazy的, 如果返回其它类型或者void,那它是eager的,会立即执行。

    Functional interface只能有一个方法吗?

    Functional interface又被称作Single Abstract Method (SAM)或者Role Interface
    那么接口中只能声明一个方法吗?
    上面的例子也表明, 你可以在functional interface中定义多个default method。 事实上java.util.function下好多functional interface都定义default method.
    那么除去default method, functional interface可以声明多个的方法吗?看个例子

    1
    2
    3
    4
    5
    interface MyI {
    void apply(int i);
    String toString();
    }

    MyI声明了两个方法apply和toString()。 它能作为一个lambda 表达式的target type吗?

    1
    2
    MyI m = x -> System.out.println(x);
    m.apply(10);

    没问题, 代码可以正常编译, 程序正常运行。
    但是, 等等, 不是functional interface只能声明一个abstract的方法吗?
    事实上你看第二个方法比较特殊,它和Object的方法签名相同。它是对象隐性实现的一个方法,所以可以忽略它。

    同样,interface Foo { boolean equals(Object obj); }也不是一个functional interface,因为没有声明一个方法。
    ` java
    interface Foo {
    int m();
    Object clone();
    }
    也不是一个functional interface, 因为Object.clone不是public类型的。

    Lambda的性能

    Oracle公司的性能工程师Sergey Kuksenko有一篇很好的性能比较的文档: JDK 8: Lambda Performance study, 详细而全面的比较了lambda表达式和匿名函数之间的性能差别。这里是视频。 16页讲到最差(capture)也和inner class一样, non-capture好的情况是inner class的5倍。

    lambda开发组也有一篇ppt, 其中也讲到了lambda的性能(包括capture和非capture的情况)。看起来lambda最差的情况性能内部类一样, 好的情况会更好。

    Java 8 Lambdas - they are fast, very fast也有篇文章 (可能需要翻墙),表明lambda表达式也一样快。
    Java 8 Lambda Performance Comparison