使用 varargs 参数调用重载方法时的 Nashorn 错误

2022-09-02 12:02:18

假设以下 API:

package nashorn.test;

public class API {

    public static void test(String string) {
        throw new RuntimeException("Don't call this");
    }

    public static void test(Integer... args) {
        System.out.println("OK");
    }
}

以下 Nashorn JavaScript 代码段将失败:

var API = Java.type("nashorn.test.API");
API.test(1);

将调用第一个方法,而不是第二个方法。这是Nashorn引擎中的一个错误吗?

为了记录在案,此问题之前已在 jOOQ 用户组上报告过,其中大量使用方法重载和 varargs,并且此问题可能会导致很多麻烦。

关于拳击

有人可能会怀疑这可能与拳击有关。事实并非如此。当我这样做时,问题也出现了

public class API {

    public static void test(String string) {
        throw new RuntimeException("Don't call this");
    }

    public static void test(Integer... args) {
        System.out.println("OK");
    }

    public static void test(MyType... args) {
        System.out.println("OK");
    }
}

和:

public class MyType {
}

然后:

var API = Java.type("nashorn.test.API");
var MyType = Java.type("nashorn.test.MyType");

API.test(new MyType());

答案 1

作为为Nashorn编写过载解决机制的人,我总是对人们遇到的角落案例着迷。无论好坏,以下是最终调用它的方式:

Nashorn 的重载方法解析尽可能地模仿 Java 语言规范 (JLS),但也允许特定于 JavaScript 的转换。JLS表示,当为重载名称选择要调用的方法时,只有在没有适用的固定arity方法时,才能考虑调用变量arity方法。通常,当从 Java 调用时,不适用于使用 的调用,因此该方法将被调用。但是,由于JavaScript实际上允许数字到字符串的隐式转换,因此它是适用的,并且在任何变量arity方法之前考虑。因此,观察到的行为。Arity胜过不皈依。如果添加了一个方法,则会在 String 方法之前调用它,因为它是固定的 arity,并且比 String 方法更具体。test(String)inttest(Integer...)test(int)

你可以争辩说我们应该改变选择方法的算法。甚至在Nashorn项目之前(甚至在我独立开发Dynalink的时候),就已经对此进行了很多思考。当前的代码(体现在Dynalink库中,Nashorn实际上是基于该库构建的)遵循JLS,并且在没有特定于语言的类型转换的情况下,将选择与Java相同的方法。然而,一旦你开始放松你的类型系统,事情就开始微妙地改变,你越放松它,它们就会改变得越多(JavaScript放松了很多),对选择算法的任何改变都会有一些其他人会遇到的其他奇怪的行为......恐怕它只是带有宽松的类型系统。例如:

  • 如果我们允许将 varargs 与 fixargs 一起考虑,我们需要在不同的 arity 方法之间发明一种“比”更具体的关系,这种关系在 JLS 中不存在,因此与它不兼容,并且会导致 varargs 有时被调用,否则 JLS 会规定 fixargs 调用。
  • 如果我们不允许JS允许的转换(从而强制认为不适用于参数),一些JS开发人员会感到需要扭曲他们的程序来调用String方法(例如,做以确保是一个字符串等)。test(String)inttest(String(x))x

正如你所看到的,无论我们做什么,其他事情都会受到影响。重载方法选择在 Java 和 JS 类型系统之间处于紧要关头,并且对逻辑中的微小变化也非常敏感。

最后,当您在重载中手动选择时,也可以坚持使用非限定类型名称,只要参数位置中包名称的潜在方法签名中没有歧义,即

API["test(Integer[])"](1);

也应该工作,不需要前缀。这可能会稍微缓解语法噪音,除非你可以重新设计API。java.lang.

HTH,阿提拉。


答案 2

以下是有效的解决方法:

使用数组参数显式调用该方法:test(Integer[])

var API = Java.type("nashorn.test.API");
API.test([1]);

消除过载:

public class AlternativeAPI1 {
    public static void test(Integer... args) {
        System.out.println("OK");
    }
}

去除变音符:

public class AlternativeAPI3 {
    public static void test(String string) {
        throw new RuntimeException("Don't call this");
    }

    public static void test(Integer args) {
        System.out.println("OK");
    }
}

替换为(或任何其他“类似类型”):StringCharSequence

public class AlternativeAPI2 {
    public static void test(CharSequence string) {
        throw new RuntimeException("Don't call this");
    }

    public static void test(Integer args) {
        System.out.println("OK");
    }
}