3

I have a set of derived classes (mathematical values, such as Length, Angle, Value) and I'm defining calculation functions for them. The overloaded functions aren't being called as I expected. Problem boiled down to simple form...

public abstract class Operand {

    public abstract Operand multiply(Operand other);

    public static void main(String[] args) {
        try {
            Operand x = new Value(5);
            Value y = new Value(6);
            Operand z = x.multiply(y);
            System.out.println(z);
        } catch (Throwable e) {
            e.printStackTrace(System.out);
        }
    }
}

public class Value extends Operand {
    final double value;

    Value(double arg) { value = arg; }
    @Override
    public Operand multiply(Operand other) {  // type of other not known
        System.out.println("dispatch of " + getClass().getSimpleName() +
                ".multiply(" + other.getClass().getSimpleName()+")");
        return other.multiply(this);  // dispatch to handler through polymorphism.
    }

    public Operand multiply(Value other) { // this and other are specific type
        System.out.println("calculation of " + getClass().getSimpleName() +
                ".multiply(" + other.getClass().getSimpleName()+")");
        return new Value(value*other.value);
    }

    @Override
    public String toString() {
        return Double.toString(value);
    }
}

Hopefully you can see that I basically want multiple type derived from "Operand" and I want to be able to do:

Operand a;
Operand b;
Operand a.function(b)

And have the right function eventually called for the underlying specific type.

However, Java is not picking of the type of this in the dispatch and applying it on the subcall. I thought java was supposed to pick the most specific method prototype when the class is know. Instead I'm getting infinite recursive calls to the dispatch function:

dispatch of Value.multiply(Value)
dispatch of Value.multiply(Value)
dispatch of Value.multiply(Value)
dispatch of Value.multiply(Value)
...

this is certainly known in this case to be Class==Value so why isn't other.multiply(this) resulting in Value multiply(Value) being the chosen prototype?

What understanding of Java am I missing here?

Alexander Ivanchenko
  • 25,667
  • 5
  • 22
  • 46
  • 3
    Java performs overload resolution at *compile-time*, so based on the compile-time type of the argument, not on the *execution-time* type - that's what you're missing. – Jon Skeet Jul 24 '22 at 18:29

1 Answers1

1

You've created an infinite recursion in multiply(Operand) implementation.

While line return other.multiply(this); is executed, method multiply(Operand) repeatedly calls itself because it's the only method accessible for the type Operand. It happens because variable other is of type Operand and it doesn't know the method multiply(Value), hence the multiply call is mapped by the Compiler to multiply(Operand).

That's how you might fix this problem:

@Override
public Operand multiply(Operand other) {  // type of other not known
    System.out.println("dispatch of " + getClass().getSimpleName() +
        ".multiply(" + other.getClass().getSimpleName()+")");
    
    if (other instanceof Value otherValue) {
        return otherValue.multiply(this);  // note that's not a polymorphic call
    }
    
    throw new RuntimeException(); // todo
}

In this case, compiler will be sure that multiply() call should be mapped to the multiply(Value), since it knows the type of the variable otherValue and would resolve the method call to the most specific overloaded version which is multiply(Value) because this is of type Value, hence this method is more specific than multiply(Operand) which would require performing widening conversion.

Here's a link to the part of Java Language Specification that describes how method resolution works.

In short, the compiler needs to find potentially applicable methods based on the type and method name. Then it analyzes method signatures if there's a method applicable by so-called strict invocation (i.e. provided arguments and signature match exactly) no further action is needed (which is the case in all situations described above). Otherwise, compiler would try to apply widening reference conversion (in case if argument is of reference type).

Also note multiply(Operand) and multiply(Value) are overloaded methods, i.e. they absolutely independent and might have different return types, access modifiers and sets of parameters (which totally unacceptable for overridden methods).

Alexander Ivanchenko
  • 25,667
  • 5
  • 22
  • 46
  • I understand the answer now. But I would still love to know why overridden methods can't also be specifically prototype target. It seems to me the parental class could define a method and the child should be able to define multiple polymorphic candidates for it so long as the arguments are more specific than the parent's definition. Is there a fundamental reason why this couldn't be part of the language? – Jeffrey Wiegley Jul 25 '22 at 19:07
  • @JeffreyWiegley Your background is JavaScript? Terms like prototype are alien to Java) OK, I'll try to explain. Firstly, in the Java World, we have static types and mechanisms of type safety. The compiler inspects each method as I've described: as the first step, it figures out all the applicable methods for this particular type. If there are several overloaded methods, it needs to resort to analyzing the method signatures based on provided parameters to find the *most specific* method. If there's method which types of parameters match exactly - we are done, it is so-call *strict invocation*. – Alexander Ivanchenko Jul 25 '22 at 20:39
  • @JeffreyWiegley Otherwise, compiler tries to apply [*conversions*](https://docs.oracle.com/javase/specs/jls/se17/html/jls-5.html). If my guess is correct you might not realize that **not** every conversion is safe. In case of reference type `Compiler` never does *Narrowing reference conversions*, i.e. it would never try to cast `Operand` to `Value`. – Alexander Ivanchenko Jul 25 '22 at 20:39
  • The second important thing is that *Polymorphism* has nothing to do with method **overloading**. Polymorphic calls are about method **overriding**, not overloading. Overloaded methods like `multiply(Operand)` and `multiply(Value)` basically are two **independent** methods, they can have completely different return types, access modifier and sets of parameters. They only share the same method name and that's it. Sure, another developer would expect that is you've them the same name they must be connected somehow, but compiler would not assume that. – Alexander Ivanchenko Jul 25 '22 at 21:00