19

Assume the following 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");
    }
}

The following Nashorn JavaScript snippet will fail:

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

The first method will be called instead of the second. Is this a bug in the Nashorn engine?

For the record, this issue was previously reported on the jOOQ User Group, where method overloading and varargs are used heavily, and where this issue may cause a lot of trouble.

About boxing

There might be a suspicion that this could have to do with boxing. It doesn't. The problem also appears when I do

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");
    }
}

And:

public class MyType {
}

And then:

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

API.test(new MyType());
Lukas Eder
  • 211,314
  • 129
  • 689
  • 1,509
  • Since varargs are syntactic sugar for an array I'd argue that Nashorn does the right thing. – a better oliver Sep 01 '14 at 16:38
  • @zeroflagL: But I don't even call the method with a string. There's no string type involved! If that's an ambiguous case, I'd prefer an exception to be thrown than the "wrong" (i.e. unexpected) method being called – Lukas Eder Sep 01 '14 at 17:11
  • 2
    You call a method with one argument, which is not an array. `API` has a method with a single non-array / non-varargs parameter. Match. From the docs: _If a Java method expects a String or a Boolean object, the values will be converted using all conversions allowed by the ToString and ToBoolean conversions defined by the JavaScript specification_. While I understand your position, it seems like a reasonable behavior to me. – a better oliver Sep 01 '14 at 18:17
  • @zeroflagL: [If you mean this documentation](http://docs.oracle.com/javase/8/docs/technotes/guides/scripting/prog_guide/javascript.html) I'd just say that the varargs case is not specified, at least not in the level of granularity that we'd expect from the JLS... But with [Attila's answer](http://stackoverflow.com/a/25610856/521799), I can now see how this behaviour makes sense from a JavaScript developer point of view. – Lukas Eder Sep 02 '14 at 05:54

3 Answers3

29

As the guy who wrote the overload resolution mechanism for Nashorn, I'm always fascinated with corner cases that people run into. For better or worse, here's how this ends up being invoked:

Nashorn's overload method resolution mimics Java Language Specification (JLS) as much as possible, but allows for JavaScript-specific conversions too. JLS says that when selecting a method to invoke for an overloaded name, variable arity methods can be considered for invocation only when there is no applicable fixed arity method. Normally, when invoking from Java test(String) would not be an applicable to an invocation with an int, so the test(Integer...) method would get invoked. However, since JavaScript actually allows number-to-string implicit conversion, it is applicable, and considered before any variable arity methods. Hence the observed behavior. Arity trumps non-conversion. If you added a test(int) method, it'd be invoked before the String method, as it's fixed arity and more specific than the String one.

You could argue that we should alter the algorithm for choosing the method. A lot of thought has been given to this since even before the Nashorn project (even back when I was developing Dynalink independently). Current code (as embodied in the Dynalink library, which Nashorn actually builds upon) follows JLS to the letter and in absence of language-specific type conversions will choose the same methods as Java would. However, as soon as you start relaxing your type system, things start to subtly change, and the more you relax it, the more they'll change (and JavaScript relaxes a lot), and any change to the choice algorithm will have some other weird behavior that someone else will run into… it just comes with the relaxed type system, I'm afraid. For example:

  • If we allowed varargs to be considered together with fixargs, we'd need to invent a "more specific than" relation among differing arity methods, something that doesn't exist in JLS and thus isn't compatible with it, and would cause varargs to sometimes be invoked when otherwise JLS would prescribe fixargs invocation.
  • If we disallowed JS-allowed conversions (thus forcing test(String) to not be considered applicable to an int parameter), some JS developers would feel encumbered by needing to contort their program into invoking the String method (e.g. doing test(String(x)) to ensure x is a string, etc.

As you can see, no matter what we do, something else would suffer; overloaded method selection is in a tight spot between Java and JS type systems and very sensitive to even small changes in the logic.

Finally, when you manually select among overloads, you can also stick to unqualified type names, as long as there's no ambiguity in potential methods signatures for the package name in the argument position, that is

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

should work too, no need for the java.lang. prefix. That might ease the syntactic noise a bit, unless you can rework the API.

HTH, Attila.

Attila Szegedi
  • 4,405
  • 26
  • 24
  • Thanks very much for your interesting answer! I can see how this is a corner case for most API consumers. Unfortunately, it isn't for [jOOQ](http://www.jooq.org) users, as jOOQ makes heavy use of overloading and varargs to mimick SQL as a language. I can see why you decided this way and you certainly went through a lot of thinking about alternatives. Prioritizing number-to-string conversion over varargs invocation may make sense from a JS perspective, but I've added another example where I'm using a `MyType` argument, in case of which I don't really see why the conversion is given priority. – Lukas Eder Sep 02 '14 at 06:01
  • Anyway, I think that my workaround where the argument is explicitly wrapped in an array at the call site is acceptable for users. In addition to that, we can probably rework the API to add one more overload in such cases: `test(String)`, `test(Integer...)`, `test(Integer)`. In any case, thanks again for this very interesting answer! – Lukas Eder Sep 02 '14 at 06:03
  • JS also allows object-to-string implicit conversion, so again the system will see `test(String)` as being matching arity, and applicable by invoking `myTypeInstance.toString()`. `java.lang.String` is somewhat special since it is used as the representation of the primitive JavaScript `string` type. – Attila Szegedi Sep 02 '14 at 08:11
  • I understand that, but this is very weird from an interoperability point of view. Imagine the method accepted a `CharSequence` instead of a `String`. It would work "as expected", then! So if an API is consumed by Nashorn clients, the risk of regressions in those clients when the API evolves is rather substantial, as the API designer will certainly not think of these rather unusual (from a Java perspective) caveats. – Lukas Eder Sep 02 '14 at 09:19
  • Hi Attila, thanks for the clarification. I have one more corner case about the overloading resolution from Nashorn, which you may be interested. It's about the overloading. Whenever there's overloading of a field instance and a method without parameters, the [] operator returns the method reference. I have described the issue properly at the following: http://stackoverflow.com/questions/9960560/java-instance-variable-and-method-having-same-name/39391819#39391819 http://stackoverflow.com/questions/9960560/java-instance-variable-and-method-having-same-name/39391819#39391819 – Pcgomes Sep 09 '16 at 10:15
  • Thanks. I was running into this with org.glassfish.jersey.client.JerseyWebTarget's request(...) method; and it was sporadically picking the wrong implementation. (It has 2 overloaded variable arity methods). Being able to specify the method is a great help. – fei0x Sep 26 '18 at 14:31
7

These are valid workarounds:

Explicitly calling the test(Integer[]) method using an array argument:

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

Removing the overload:

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

Removing the varargs:

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");
    }
}

Replacing String by CharSequence (or any other "similar type"):

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");
    }
}
Lukas Eder
  • 211,314
  • 129
  • 689
  • 1,509
4

This is an ambiguous situation. The second case it is looking for either an array of integers or more than one integer to discern from the first case. You can use method selection to tell Nashorn which case you mean.

API["test(java.lang.Integer[])"](1);
wickund
  • 1,077
  • 9
  • 13
  • Thank you for your response! But that's completely against any Java developer's intuition. If we assume *some* type checking, then the `Integer...` method should certainly be a better match than the `String` method, given that I'm actually passing an "`Integer`". Otherwise, if the call is ambiguous, I'd appreciate at least an error, rather than just picking the intuitively "less correct" one – Lukas Eder Sep 01 '14 at 12:21
  • Actually you're passing an int and expecting it to be autoboxed into an Integer. What happens if you actually explicitly pass an Integer? – Keilly Sep 01 '14 at 12:32
  • @Keilly: This happens with any type, not only with "boxable" ones. I'll update my question – Lukas Eder Sep 01 '14 at 12:34