Search code examples
javaoopoverloadingjava-17

Method dispatch in Java: why can't it differentiate between interface and class arguments at runtime?


I'm working on a Java program that has an interface I and classes A and B that implement it. I also have another class implementing C that has 2 method with one argument: one that accepts an object of A and another that accepts an object of B.

  interface I {}
  static class A implements I {}
  static class B implements I {}
  interface C {
    String map(A a);
    String map(B b);
  }

  public static void main(String[] args) {
    // Injected at runtime;
    C mapper = getInstance();
    I a = new A();
    I b = new B();
    // This fail at compilation 
    String result = mapper.map(a);
  }   
private static C getInstance() {
    return new C() {
      @Override
      public String map(A a) {
        return "a";
      }
      @Override
      public String map(B b) {
        return "b";
      }
    };
  }

When I try to call the method of C with an object of A or B I get the following error:

java: no suitable method found for map(Main.I)
    method Main.C.map(Main.A) is not applicable
      (argument mismatch; Main.I cannot be converted to Main.A)
    method Main.C.map(Main.B) is not applicable
      (argument mismatch; Main.I cannot be converted to Main.B)

Even if we say that other instances of type I can be passed to mapper this should be fixed with sealed interface I permits A, B {}

Why there is no method dispatch at runtime in Java ? My question is why it's not possible in java more then "Why it didn't work"


Solution

  • There have been several answers to the effect of "because overload selection is done at compile time based on the static types of the arguments, not at runtime based on the dynamic type of the arguments." These statements are 100% correct; this is how overloading works, and how it was designed to work. Multiple dispatch ("multimethods") were well known at the time Java selected single (receiver-based) dispatch.

    A reasonable next question is why was it done this way? There are several reasons why this was a slam-dunk in 1995:

    • Predictability. It is a simplicity benefit if a given call site always corresponds to exactly the same (virtual) method.
    • Safety. There are cases when there is no "best" overload. Determining this at compile time, with a clear error message, is preferable to having the program fail at runtime only when an unexpected combination of inputs is passed. Having every method dispatch potentially throw an "no most specific overload" exception would make the language less reliable.
    • Performance. Overload selection is potentially expensive; better to perform this up-front, at compile time, and not burden the runtime with it.
    • Simplicity. The interaction of overloads and overrides would be significantly more complicated than they are today, because, for example, we'd need more rules to break ties between "more specific method in superclass and overridden candidate in subclass". This would lead to more surprising interactions.
    • Practicality. True multiple dispatch is rare; embracing multi-parameter dispatch would likely complicate the language and runtime in excess of its expressive value.

    In 2023, compiler and runtime technology have improved quite a bit, so the balance between compile-time overload selection and runtime dispatch might have shifted a bit, but overall this remains a sensible and pragmatic tradeoff.