文档结构  
原作者:Christoph Nahr    来源:news.kynosarges.org [英文]
CY2    计算机    2016-10-24    0评/703阅
翻译进度:已翻译   参与翻译: KeYIKeYI (4), abel533 (3), 拥抱阳光の雪 (1)

除了 lambda 表达式,Java SE 8 引入了方法引用作为简写符号。 这些主要用于引用静态方法(例如 Double :: toString)或构造函数(egString [] :: new),这些用法是直接的。 然而,对实例方法的方法引用会以令人惊讶的方式产生与lambda表达式不同的结果。 这是因为方法引用的调用目标(在 :: 之前的部分)在首次遇到它的声明时被求值,而 lambda 表达式仅在实际调用时被求值。

第 1 段(可获 1.01 积分)

以下程序以各种方式调用实例方法,使用方法引用和 lambda 表达式来演示这种不同的行为。下面我们将通过输出案例来看看发生了什么。

class MethodRefTest {
 
    public static void main(String[] args) {
        System.out.println("\nConstructor in method reference");
        final Runnable newRef = new Counter()::show;
        System.out.println("Running...");
        newRef.run(); newRef.run();
 
        System.out.println("\nConstructor in lambda expression");
        final Runnable newLambda = () -> new Counter().show();
        System.out.println("Running...");
        newLambda.run(); newLambda.run();
 
        System.out.println("\nFactory in method reference");
        final Runnable createRef = Counter.create()::show;
        System.out.println("Running...");
        createRef.run(); createRef.run();
 
        System.out.println("\nFactory in lambda expression");
        final Runnable createLambda = () -> Counter.create().show();
        System.out.println("Running...");
        createLambda.run(); createLambda.run();
 
        System.out.println("\nVariable in method reference");
        obj = new Counter(); // NPE if after method reference declaration! 
        final Runnable varRef = obj::show;
        System.out.println("Running...");
        varRef.run(); obj = new Counter(); varRef.run();
 
        System.out.println("\nVariable in lambda expression");
        obj = null; // no NPE, lambda expression declaration not evaluated
        final Runnable varLambda = () -> obj.show();
        System.out.println("Running...");
        obj = new Counter(); varLambda.run();
        obj = new Counter(); varLambda.run();
 
        System.out.println("\nGetter in method reference");
        final Runnable getRef = get()::show;
        System.out.println("Running...");
        getRef.run(); obj = new Counter(); getRef.run();
 
        System.out.println("\nGetter in lambda expression");
        final Runnable getLambda = () -> get().show();
        System.out.println("Running...");
        getLambda.run(); obj = new Counter(); getLambda.run();
    }
 
    static Counter obj;
 
    static Counter get() {
        System.out.print("get: ");
        return obj;
    }
 
    static class Counter {
        static int count;
        final int myCount;
 
        Counter() {
            myCount = count++;
            System.out.println(String.format("new Counter(%d)", myCount));
        }
 
        static Counter create() {
            System.out.print("create: ");
            return new Counter();
        }
 
        void show() {
            System.out.println(String.format("Counter(%d).show()", myCount));
        }
    }
}
第 2 段(可获 0.45 积分)

构造方法

第一个块的代码在直接创建的 Counter 类的新实例上调用方法。 此类跟踪在当前运行期间创建的实例数,并且通过其创建索引标识每个实例。 下面是输出结果:

Constructor in method reference
new Counter(0)
Running...
Counter(0).show()
Counter(0).show()

Constructor in lambda expression
Running...
new Counter(1)
Counter(1).show()
new Counter(2)
Counter(2).show()

方法引用和 lambda 表达式调用一样都被调用了两次,正确的输出了两次 show 方法。 但是,在方法引用中,指定的构造方法只在声明时调用了一次。 然后重用创建好的对象。该 lambda 表达式在声明时不执行任何操作,而是在每次运行时调用构造函数。

第 3 段(可获 1.23 积分)

工厂方法

第二段代码实际上等同于第一段,但使用工厂方法而不是构造函数来获取newCounter对象。 结果与以前相同,表明方法表达式的不同顺序与在调用目标表达式中直接新建对象的顺序无关。

通过方法引用来调用工厂方法
create: new Counter(3)
Running...
Counter(3).show()
Counter(3).show()

通过lambda 表达式调用工厂方法
Running...
create: new Counter(4)
Counter(4).show()
create: new Counter(5)
Counter(5).show()
第 4 段(可获 0.6 积分)

变量访问

第三段代码测试了(不同方式下的)变量访问,这里由于lambda表达式不接受可变的本地变量而使用了静态字段.

使用方法引用的变量访问
new Counter(6)
Running...
Counter(6).show()
new Counter(7)
Counter(6).show()

使用lambda表达式的变量访问
Running...
new Counter(8)
Counter(8).show()
new Counter(9)
Counter(9).show()

方法引用对它的调用目标立即求值会造成两个结果.一是,字段初始化必须在(方法引用)声明前,否则会发生NullPointerException.lambda表达式却不是这种情况:我们可以在(lambda表达式)声明前将字段重置为null---只要我们在调用时该字段有有效值即可.

第 5 段(可获 0.95 积分)

二是,由于目标对象的引用保存的是字段的即时值,所以当该字段接下来被赋值为一个新建的Counter,目标对象的引用也不会变.于此不同地是,lambda表达式每次运行时都会去取字段当前的值.

取值器(Getter)方法

最后一段代码与变量访问的例子等效,只是使用了getter方法来读取字段当前的值.又一次,在后面这个例子中,这个辅助字符再两次调用之间发生了改变.

使用方法引用的Getter
get: Running...
Counter(9).show()
new Counter(10)
Counter(9).show()

使用lambda表达式的Getter
Running...
get: Counter(10).show()
new Counter(11)
get: Counter(11).show()
第 6 段(可获 0.86 积分)

在方法引用的例子中,get:在Running...之前只出现了一次,说明getter在声明的时候只调用了一次.因此,返回的字段值被用于两次show方法调用.lambda表达式调用了两次getter,在第二次调用时获取到了更新的字段值.

分析

上述行为在Java SE 8 语言规范的§15.13.3 “方法引用的运行时求值”的最后注释中被描述:

方法引用表达式求值的时机比lambda表达式更复杂(§15.27.4).当一个方法引用表达式在:: 分隔符前有一个表达式(而不是一个类型)时,这个子表达式将会被立即求值.求值的结果会被一直保存,直到相关的函数接口类型被调用;那个时候,求值的结果将会被用作调用的目标引用.这表明在::分隔符之前的表达式只在程序进入方法引用表达式时求值,并且不会在接下来的函数接口类型调用中重新求值.

 

第 7 段(可获 2.1 积分)

我第一次注意到方法引用与lambda表达式的不同是当我像在构造函数测试例子中那样尝试使用方法引用在一个MenuItem 处理器中创建对话框实例时.我惊讶地发现每次调用会打开带着上次调所有控制内容的完全一样的实例.使用lambda表达式替换方法引用后才产生期望的行为----每次调用创建一个新的对话框.

方法引用的立即对目标求值很有可能不是用户想要的行为,在大多数情况下,我们期望目标在调用间改变.你可以考虑只对静态方法和构造函数(X::new)使用方法引用,或者和那些对所有调用都确定不会改变的实例引用一起使用.如果目标引用有任何需要动态重新求值的可能,你就必须使用lambda表达式.

注意:原版的文章在测试程序中有一个严重错误使得我认为方法引用还有一些其他的奇怪行为,但是这些行为其实并不存在.感谢Jwolfe和Tom迅速地之指出了这个错误!

第 8 段(可获 2.29 积分)

文章评论