在Java中的嵌套函数

67

Java编程语言是否存在可以创建嵌套函数的扩展程序?

在许多情况下,我需要创建仅在另一个方法或for循环的上下文中使用一次的方法。尽管JavaScript可以轻松实现此操作,但我到目前为止未能在Java中完成。

例如,在标准Java中无法完成以下操作:

for(int i = 1; i < 100; i++){
    times(2); // Multiply i by 2 and print i
    times(i); // Square i and then print the result

    public void times(int num){

        i *= num;
        System.out.println(i);
    }
}
8个回答

77

Java 8引入了lambda表达式。

java.util.function.BiConsumer<Integer, Integer> times = (i, num) -> {
    i *= num;
    System.out.println(i);
};
for (int i = 1; i < 100; i++) {
    times.accept(i, 2); //multiply i by 2 and print i
    times.accept(i, i); //square i and then print the result
}
() ->语法适用于只定义了一个方法的任何接口。因此,您可以将其与Runnable一起使用,但不能将其用于ListBiConsumer是由java.util.function提供的许多功能接口之一。
值得注意的是,在幕后,这定义了一个匿名类并实例化它。 times是对该实例的引用。

这需要以某种方式纳入顶部答案,因为在我看来,这是现在的惯用解决方案。有趣的是它是如何通过内部类声明实现的。 - Kamuela Franco
1
注意:这个Java程序与JavaScript程序不同。我不确定这是否是有意的,但JavaScript中的 times 访问并修改了 i。输出结果是 2、4、10 和 100。在Java中,您只能访问final变量,这意味着您不能修改它们。 - Florian F
@FlorianF:非常正确。Lambda不能修改它们的作用域。引用的局部变量必须是final或“有效地final”。更多信息请参见https://dev59.com/bF8f5IYBdhLWcg3wFfT_ - Neal Ehardt
1
也许我误解了《Java核心技术》中的内容,() -> 语法适用于任何“单一抽象方法(SAM)”接口,比如 Comparator 定义了抽象的 compare() 方法,但为其他方法提供了默认实现,例如 reverse() - Alonso del Arte

31

下面的答案讨论的是在Java 8之前,最接近嵌套函数的方法。这并不一定是我处理与JavaScript中使用嵌套函数完成的相同任务的方式。通常一个私有的辅助方法就可以胜任 - 甚至可能是一个私有的辅助类型,你在方法内创建一个实例,但它对所有方法都可用。

当然,在Java 8中,lambda表达式是一个更简单的解决方案。


最接近的方法是使用匿名内部类。这是Java目前最接近闭包的方法,尽管希望在Java 8中有更多的支持。

匿名内部类有各种限制 - 它们显然与您的JavaScript示例(或使用lambda的任何内容)相比非常冗长,并且它们对封闭环境的访问仅限于final变量。

所以(可怕地)变异您的示例:

interface Foo {
    void bar(int x);
}

public class Test {
    public static void main(String[] args) {
        // Hack to give us a mutable variable we can
        // change from the closure.
        final int[] mutableWrapper = { 0 };

        Foo times = new Foo() {
            @Override public void bar(int num) {
                mutableWrapper[0] *= num;
                System.out.println(mutableWrapper[0]);
            }
        };

        for (int i = 1; i < 100; i++) {
            mutableWrapper[0] = i;
            times.bar(2);
            i = mutableWrapper[0];

            times.bar(i);
            i = mutableWrapper[0];
        }
    }
}

输出:

2
4
10
100

这是你从 JavaScript 代码得到的输出吗?


匿名内部类?真的吗?我会说惯用方法就是只使用“private”帮助方法... - Matt Ball
@Matt:但是你如何在另一个方法中声明它呢?我并不是说我不会使用私有辅助方法 - 我是说它们不是嵌套函数。不过我会编辑一下来谈论这个问题。 - Jon Skeet
12
私有助手方法(与内部函数相比)唯一的问题在于它们通过使函数可在其预期范围之外使用而使源代码变得混乱。 - Anderson Green
15
@Andy:不仅如此——它们无法访问你感兴趣的方法中的变量,而嵌套函数可以。闭包非常方便…… - Jon Skeet

29

我认为在Java 7中最接近嵌套函数的方法不是使用匿名内部类(Jon Skeet的回答),而是使用很少使用的本地类。这种方式,甚至嵌套类的接口也不会在其预定范围之外可见,并且更加简洁。

使用本地类实现Jon Skeet的示例如下:

public class Test {
    public static void main(String[] args) {
        // Hack to give us a mutable variable we can
        // change from the closure.
        final int[] mutableWrapper = { 0 };

        class Foo {
            public void bar(int num) {
                mutableWrapper[0] *= num;
                System.out.println(mutableWrapper[0]);
            }
        };

        Foo times = new Foo();

        for (int i = 1; i < 100; i++) {
            mutableWrapper[0] = i;
            times.bar(2);
            i = mutableWrapper[0];

            times.bar(i);
            i = mutableWrapper[0];
        }
    }
}

输出:

2
4
10
100

在我看来,这更具表现力和惯用性,适用于Java 7。 - Olaseni
通过在内部类中添加更多的方法,可以很容易地将这种方式扩展到几个内部函数。 - Adam Gawne-Cain
1
非常聪明地使用内部类来实现这个。 - Luc-Olivier

2

对于无参数方法,您可以创建一个Runnable对象。

private static void methodInsideMethod(){
    Runnable runnable = new Runnable(){
        @Override
        public void run(){
            System.out.println("Execute something");
        }
    };

    for(int i = 0; i < 10; i++){
        runnable.run();
    }
}

2
这些方法有时被称为闭包。看看 Groovy – 也许你会更喜欢它而不是Java。在Java 8中,可能也会有闭包(请参见JSR335延迟列表)。

我猜Java 7要使用闭包有点晚了 :) - yozh
我同意(在此处检查):) 有太多期望被提起。 - dma_k
那么,对于这个问题可以做些什么呢?也许有人可以编写一个Java的预处理器来“模拟”它,但这超出了我的技能水平。 - Anderson Green

1

我不知道是否有其他人已经发现了这一点,但显然你可以通过Java 10及以上版本中提供的var关键字来实现魔法。如果你没有适合自己的接口,这将使你免于声明一个。

public class MyClass {
    public static void main(String args[]) {
        var magic = new Object(){
            public void magic(){
                System.out.println("Hello World!");
            }
        };
        magic.magic();
    }
}

我已经测试过它,它能够正常工作。但我还没有找到一款Java编译器可以让您轻松地进行共享。


你应该明确指出这是仅适用于JAVA 10且相当新的版本。 - Louis Caron

0
考虑创建一个匿名本地类,并使用其初始化块来完成工作:
public class LocalFunctionExample {
    public static void main(final String[] args) {
        for (final int i[] = new int[] { 1 }; i[0] < 100; i[0]++) {
            new Object() {
                {
                    times(2); //multiply i by 2 and print i
                    times(i[0]); //square i and then print the result
                }

                public void times(final int num) {
                    i[0] *= num;
                    System.out.println(i[0]);
                }
            };
        }
    }
}

输出:

2
4
10
100

使用这种技术不一定需要自动使用“最终包装器技巧”,但在处理变异要求时需要。

这几乎与lambda版本一样简洁,但您可以使用任何方法签名,它们可以具有真实的参数名称,并且可以直接通过它们的名称调用方法-无需使用.apply()或其他什么。 (这种情况有时也使IDE工具更好地工作。)


-9

我不喜欢使用被禁用的词,但你可以使用一个goto语句在方法内创建一个有效的子程序。虽然它很丑陋和危险,但比之前的答案要容易得多。虽然调用第一个方法内部的私有方法更好,而且完全满足您的需求。我不知道为什么您想将嵌套方法用于这么简单的事情。


2
抱歉打字有点错误,这个键盘不太正常,有些按键卡住了,我无法使用大写字母。 - michael
5
这个问题关于Java,它也没有goto语句。 - Display Name
3
推荐一个在Java中不存在的关键字,干得好。 - Stefan Reich

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