什么是闭包?Java 有闭包吗?

41

我正在阅读有关面向对象JavaScript的内容,发现了闭包的概念。我并不太理解它何时以及为什么要使用。其他像Java这样的语言是否也具有闭包?基本上,我想了解如何通过了解闭包的概念来帮助我改进编码。


相关:https://dev59.com/MEfRa4cB1Zd3GeqP8V7j - Josh Lee
1
如果有人和我一样蠢,并且你正在为了知道这个“Closure”是什么而苦苦挣扎,那么请看这个视频:https://www.youtube.com/watch?v=Nj3_DMUXEbE - Piyush Kukadiya
5个回答

37
一个闭包是一个带有绑定变量的一等函数。
大致意思是:
  • 你可以将闭包作为参数传递给其他函数
  • 闭包存储了创建它时词法作用域中某些变量的值
Java最初没有对闭包提供语法支持(这是在Java 8中引入的),虽然使用匿名内部类模拟它们是相当普遍的做法。以下是一个示例:
import java.util.Arrays;
import java.util.Comparator;

public class StupidComparator { 
    public static void main(String[] args) {
        // this is a value used (bound) by the inner class
        // note that it needs to be "final"
        final int numberToCompareTo=10;

        // this is an inner class that acts like a closure and uses one bound value
        Comparator<Integer> comp=new Comparator<Integer>() {
            public int compare(Integer a, Integer b) {
                int result=0;
                if (a<numberToCompareTo) result=result-1;
                if (b<numberToCompareTo) result=result+1;
                return result;
            }
        };

        Integer[] array=new Integer[] {1,10, 5 , 15, 6 , 20, 21, 3, 7};

        // this is a function call that takes the inner class "closure" as a parameter
        Arrays.sort(array,comp);

        for (int i:array) System.out.println(i);
    }
}

3
一等函数意味着您可以像处理语言中的任何其他“对象”一样处理函数,即将其作为参数传递,存储在集合中等。由于匿名内部类表示Java对象就像任何其他对象一样,因此在Java中它是“一等”的 - 您可以对其执行与任何其他普通对象相同的操作。 - mikera
1
上面给出了好处的例子 - 你有一个通用的排序函数(例如Arrays.sort(..)),因为你可以通过参数传递任何比较函数,所以它变得更加强大。例如,你可以按照相反的顺序、按数字位数排序、按数字是否为质数排序等。这基本上为你创建算法提供了额外的抽象层次,这可以非常强大。如果你想了解更多信息,阅读"函数式编程"是值得的。 - mikera
闭包和“模拟”闭包有什么区别呢?如果我们已经在处理抽象概念,那么原始和模拟的区别真的很重要吗?声称它们有实质性的不同应该证明使用“真正”的闭包可以做到什么,在Java的实现中是不可能的。 - DavidS
2
@DavidS 我认为区别在于语言语法自然支持闭包与寻找实现相同效果的另一种方式(这在任何图灵完备的语言中最终都是可能的)。截至Java 8,Java实际上已经具有相当不错的闭包语法支持,作为Lambda表达式支持的一部分引入。 - mikera
那么,这是否意味着闭包与我们在C ++,C#,JavaScript,Java,Delphi(Pascal)中称之为委托,Lambda或匿名函数的东西是相同的?这是博士级别的名称,用于我们BSc级别每天使用的东西吗? - AaA
显示剩余5条评论

16

在不同语言中,闭包有着不同的名称,但其本质为以下几点:

要创建闭包,需要使用支持将函数类型绑定到变量并像字符串、整数或布尔值一样传递它的编程语言。

还需要能够内联声明函数。在JavaScript中,可以这样实现:

foo("bar", "baz" function(x){alert("x")});

我们可以将匿名函数作为参数传递给foo函数。我们可以使用这种方法来创建闭包。

闭包可以"捕获"变量,因此可以用于传递范围内的变量。考虑下面的示例:

function foo(){
    var spam = " and eggs";
    return function(food){alert(food + spam)};
}
var sideOfEggs = foo();

现在,eggs的一侧包含一个函数,它将“ and eggs”附加到传递给它的任何食物。 spam变量是foo函数作用域的一部分,并且本应该在函数退出时丢失,但是闭包"闭合"了命名空间,只要闭包保持在内存中,就会保留它。

所以,我们清楚了闭包可以访问其父级私有作用域变量,对吗?那么如何使用它们来模拟JavaScript中的私有访问修饰符?

var module = (function() {   
    var constant = "I can not be changed";

     return {
         getConstant    :    function() {  //This is the closure
            return constant;               //We're exposing an otherwise hidden variable here
         }
    };
}());                                     //note the function is being defined then called straight away

module.getConstant();                     //returns "I can not be changed"
module.constant = "I change you!";
module.getConstant();                     //still returns "I can not be changed" 

这里发生的是我们正在创建并立即调用一个匿名函数。函数中有一个私有变量。它返回一个具有单个方法的对象,该方法引用此变量。一旦函数退出,getConstant方法就是访问变量的唯一方式。即使删除或替换此方法,也不会泄露其秘密。我们使用闭包实现了封装和变量隐藏。有关更详细的说明,请参见http://javascript.crockford.com/private.html

Java目前还没有闭包的概念。最接近的是匿名内部类。但是要以内联方式实例化其中之一,您必须实例化整个对象(通常来自现有接口)。闭包的美在于它们封装简单、表达性强的语句,而这在匿名内部类的噪音中有些遗失。


从维基百科中得知:“闭包”一词经常被错误地用来表示匿名函数。这可能是因为大多数实现匿名函数的语言允许它们形成闭包,程序员通常同时介绍这两个概念。然而,这些是不同的概念。 - Kirk Woll
@Kirk Woll 公正的观点,尽管我的意图并不是将两者视为完全相同。我会尝试让这一点更加清晰明了。 - Ollie Edwards
我喜欢你的解释。你能给我一个更实用的例子吗?这个例子可以完美地说明使用闭包是解决问题的最佳方式。 - sushil bharwani
@sushil bharwani 确定,看看我的新部分,介绍如何使用闭包在JavaScript中模拟私有成员。 - Ollie Edwards
尽管您对constant变量的作用域是正确的,但我认为module.constant与函数内的var constant不同。您所做的是将另一个常量添加到模块作为属性。您可以通过再次阅读module.constant来验证这一点,它仍将返回“我改变了你!” - AaA

12

虽然Java没有一流函数(first-class functions),但实际上它确实具有词法闭包(lexical closures)。

例如,下面这个来自Paul Graham的书《On Lisp》的Lisp函数返回一个加数字的函数:

(defun make-adder (n)
  (lambda (x) (+ x n))

这可以在Java中完成。然而,由于它没有一级函数,我们需要定义一个接口(让我们称其为Adder)和一个匿名内部类,其中包含实现此接口的函数。

public interface Adder {
    int add(int x);
}

public static Adder makeAdder(final int n) {
    return new Adder() {
        public int add(int x) {
            return x + n;
        }
    };
}

内部的add()函数是一个词法闭包,因为它使用了外部词法作用域中的n变量。

为了实现这一点,该变量必须声明为final,这意味着该变量不能更改。但是,即使它们是final,也可以更改引用变量中的值。例如,考虑以下来自On Lisp的Lisp函数:

(defun make-adderb (n)
  (lambda (x &optional change)
    (if change
        (setq n x)
        (+ n n))))

可以通过将外部变量封装在一个引用类型变量中(例如数组或对象)来在Java中实现此操作。

public interface MutableAdder {
    int add(int x, boolean change);
}

public static MutableAdder makeAdderB(int n) {
    final int[] intHolder = new int[] { n };
    return new MutableAdder() {
        public int add(int x, boolean change) {
            if (change) {
                intHolder[0] = x;
                return x;
            }
            else {
                return intHolder[0] + x;
            }
        }
    };
}

我会声称这是真正的词法闭包,而不是模拟。但我不会声称它很漂亮。


4

闭包是一种作用域技术。Java没有闭包。

在JavaScript中,您可以像以下示例那样实现:

var scope = this;

var f = function() {
    scope.somethingOnScope //scope is 'closed in' 
}

如果你将f传递给一个函数,那么它的作用域是定义时所处的作用域。

如果您引用在匿名类作用域之外声明的变量,您会如何称呼它? - Kirk Woll
我已经了解到闭包是一种作用域技术。谢谢,但我想了解它如何帮助处理特定的编码场景。 - sushil bharwani
@sushil 它很有用,因为它允许你将作用域与某些东西(例如函数)关联起来,然后传递该函数。 - hvgotcodes

3

闭包是一种非常自然的功能,它允许自由变量被其词法环境所捕获。

以下是javascript中的一个示例:

function x() {

    var y = "apple";

    return (function() {
         return y;
    });
}

函数x返回一个函数。请注意,当创建函数时,不会像返回表达式时那样计算在此函数中使用的变量。函数被创建时,它会查看哪些变量不是函数本地变量(自由变量)。然后,它定位这些自由变量并确保它们不会被垃圾回收,以便在实际调用函数时可以使用。

为了支持此功能,您需要具有一级函数,而Java不支持此功能。

请注意,这是在JavaScript等语言中拥有私有变量的一种方式。


3
FSVO“very natural”。这个问题本身就表明对许多人来说它是深奥的。 - Adriano Varoli Piazza
有些应用程序确实非常自然。例如,在传递回调函数时,以及在使用函数式语言时。仅仅因为它很神秘并不意味着它不是一个好主意。:P - Ken Struys
哦,我并不怀疑它们的有用性,相反,只是对于我来说需要一些时间来理解它们。 - Adriano Varoli Piazza
当你必须解释发生了什么,并且已经使用命令式语言编程很长时间时,这可能会很困难。我曾见过初学者的函数式程序员仅是不加思索地使用它们,然后认为“噢,当然会起作用”。 - Ken Struys
我喜欢术语“允许自由变量被其词法环境捕获”。 - Tim

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