Java 8支持闭包吗?

52

我很困惑。我以为Java 8将从石器时代走出来,并开始支持lambda/闭包。但是当我尝试这样做时:

public static void main(String[] args) {
    int number = 5;

    ObjectCallback callback = () -> {
        return (number = number + 1);
    };

    Object result = callback.Callback();
    System.out.println(result);
}

它说数字应该是有效的终极。我认为这不是闭包。这听起来只是通过值传递而不是通过引用复制环境。

奖励问题!

Android是否支持Java 8功能?


9
为什么,为什么是Java。为什么啊,为什么。 - sircodesalot
2
他们可以自己保持疯狂,我也不想念 var 参数。 - BevynQ
3
Lambda并不是闭包,它只是一个匿名函数。闭包是使用创建时的上下文的函数(匿名或非匿名)。请参见https://dev59.com/1nVC5IYBdhLWcg3wqzDV。如果想在Java中受益于它们,可以使用Scala或Groovy。Java 8支持Lambda http://docs.oracle.com/javase/tutorial/java/javaOO/lambdaexpressions.html,但不支持闭包。 - unludo
1
那就要看你认为闭包是什么了。在解释型、实际上是单线程的语言中,这种可变捕获方式是完全合理的,因为你实际存储的是指向相关环境/符号表的指针。但在高度多线程编译语言中,这并不奏效。请问 - 在JVM字节码术语中,对可变变量的捕获意味着什么?你又该如何表示它? - kittylyst
1
Scala和C#似乎都能毫无问题地完成这个任务。 - sircodesalot
显示剩余6条评论
4个回答

43
为什么Java没有真正的闭包?可能是因为向后兼容性和项目资源限制的组合。实现过程上下文作为一级对象(即闭包)需要确保某些本地变量的生命周期超出了声明方法调用的返回,这意味着你不能简单地将它们放在堆栈中,而是必须使一些本地变量成为堆对象的字段。要支持Java中的“真正闭包”需要进行基本的JVM架构更改或使用新类型的隐藏类。但是,由于Java语言已经存在,并且变化的性质会对数百万现有的Java应用程序造成影响,所以从Oracle和第三方实施者的角度来看需要做出巨大努力才能更新所有工具链。此外,这种变化可能会对其他语言产生影响,例如,Android依赖于JVM体系结构/字节码文件作为其Davlik工具链的“输入语言”,还有为Python、Ruby等语言编写的代码生成器。总之,在Java中实现“真正的闭包”对于所有相关人员来说都是一个巨大而可怕的提议,而“final变量闭包”是一个实用的妥协方案,它能够胜任实际工作。

最后,有可能在未来的版本中删除final限制。 (我不过于抱有希望...)


安卓将支持Java-8特性吗?

除非有人拥有可信的内部知识,否则无法回答这个问题。如果他们确实知道,他们会疯狂地在这里透露出来。显然,谷歌尚未宣布支持Java 8。

但好消息是,Java 7语法扩展现在已经在KitKat和相应版本的Android Studio或Eclipse ADT上得到支持。


9
当支持闭包时,C# / CLR语言/平台仍然是一个相对较新的领域。如果不必担心破坏现有代码,拥有丰富的资源并能从他人的错误中学习,那么您可以做出惊人的事情。 "C# can do it ..." - Stephen C
1
关于绿色场地语言和断更变化,这就是为什么C#有具体化的泛型而Java没有。;-) - C. K. Young
1
@ChrisJester-Young - 是的。还有对尾调用优化和其他一些功能的支持。 - Stephen C
1
@StephenC 很好的帖子。但有趣的是,像Groovy和Scala这样的JVM语言提供了scala。 - More Than Five
1
@MoreThanFive - 嗯,是的...但它们没有像Java一样的(商业)限制。 - Stephen C

14
您需要说明您对“闭包”的定义。
对我来说,“闭包”是指一些东西(如函数、对象或其他可以以某种方式运行的东西,就像具有方法一样),它从其封闭范围中捕获(“关闭”)一个局部变量,并且可以在其代码中使用该变量,即使在稍后运行函数或对象的方法时,包括封闭范围不再存在的情况下。在不同的语言中,可以通过值或引用来捕获变量,也可能两者都可以。
按照这个定义,Java匿名类(自Java 1.1以来就一直存在)是闭包,因为它们可以引用其封闭范围中的局部变量。
Java 8中的Lambda基本上是匿名类的一种特殊情况(即实现仅有一个方法(“函数接口”)的接口的匿名类,没有实例变量,也不显式或隐式地引用自身(使用“this”)。任何Lambda都可以重写成等效的匿名类表达式。所以上面所说的也适用于Lambda。

哦,那我想不是闭包了。

嗯,您先生,您对“闭包”的定义有误。

5
根据定义,一个可以接受命令行参数的编译程序将会是一个闭包... - Ale Morales
"...一直以来都是从Java的第一个版本开始"这种说法是不正确的,因为Java的第一个版本并没有内部类。内部类是在Java 1.1中引入的,因此在这方面,闭包自Java 1.1以来就存在于Java中..." - Holger
你对“闭包”(close over)的定义是“在匿名类构造函数中按值复制,从未在闭包中访问它”? - doug65536
@doug65536:匿名类不能声明构造函数。此外,我不知道你所说的“在闭包中永远不要访问它”的意思——只有实际在匿名类/lambda中使用的外部变量才会被捕获。捕获的变量不会传递给任何构造函数。是的,当创建匿名类/lambda时,捕获的变量按值复制。 - newacct
@doug65536:我不知道你如何定义你的闭包,但对我来说,一个包含代码(无论是函数还是对象)的东西可以透明地使用外部局部作用域中的局部变量,即使该外部局部作用域已经结束,这意味着它是一个闭包。我不明白在内部或外部作用域中是否可以分配变量与此有何关系。如果您需要能够进行分配,则似乎像Haskell、OCaml和Standard ML这样的语言也不能具有闭包,因为您根本无法分配任何变量。 - newacct
显示剩余2条评论

13

我认为final限制有技术原因。Lambda表达式仅仅是从周围方法的上下文中取值,因为引用生存于栈上,并且不会在方法结束后继续存在。

如果你将上下文的值放入一个引用中,你就可以构建一个“真正的”闭包:

import java.util.function.Supplier;

public class CreatingAClosure {

    public static void main(String[] args) {
        Supplier<Supplier<String>> mutterfunktion = () -> {
            int container[] = {0};
            return () -> {
                container[0]++;
                return "Ich esse " + container[0] + " Kuchen.";
            };
        };
        Supplier<String> essen = mutterfunktion.get();
        System.out.println(essen.get());
        System.out.println(essen.get());
        System.out.println(essen.get());
    }
}

输出:

Ich esse 1 Kuchen.
Ich esse 2 Kuchen.
Ich esse 3 Kuchen.

你可以选择任何适当的对象实例代替数组,因为它位于堆上,lambda表达式只保留对此实例的引用(final)。

在这种情况下,container的值被封装到mutterfunktion中。每次调用mutterfunktion都会创建一个新的引用实例。

该值无法从函数外部访问(在Java 7及以前版本中非常难以构建)。由于lambda表达式是作为方法引用实现的,在此示例中不涉及内部类。

你也可以在方法的上下文中定义container,并且可以在lambda外进行更改:

public static void main(String[] args) {
    int container[] = {0};
    Supplier<String> essen = () -> {
        container[0]++;
        return "Ich esse " + container[0] + " Kuchen.";
    };
    System.out.println(essen.get());
    System.out.println(essen.get());
    container[0]++;
    System.out.println(essen.get());
}

输出:

Ich esse 1 Kuchen.
Ich esse 2 Kuchen.
Ich esse 4 Kuchen.

所以你的问题的答案将是“是”。


1
使用java.util.concurrent.atomic.AtomicIntegerint container[]更为合适。 - Volker Seibt

1
你可以使用final引用来避免更改外部作用域中声明的变量的状态,但结果仍然相同,闭包外部作用域的状态不会保留,并且对所引用的对象(通过最终引用)进行进一步更改将在闭包中看到。
@Test
public void clojureStateSnapshotTest() {
    Function wrapperFunc;
    wrapperFunc = (a) -> {
        // final reference
        final WrapLong outerScopeState = new WrapLong();

        outerScopeState.aLong = System.currentTimeMillis();
        System.out.println("outer scope state BEFORE: " + outerScopeState.aLong);

        Function closure = (b) -> {
            System.out.println("closure: " + outerScopeState.aLong);
            return b;
        };

        outerScopeState.aLong = System.currentTimeMillis();
        System.out.println("outer scope state AFTER: " + outerScopeState.aLong);

        // show correct snapshot state
        closure.apply(new Object());

        return a;
    };
    // init clojure
    wrapperFunc.apply(new Object());
}

public class WrapLong {
    public long aLong = 0;
}

但仍然有趣...

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接