Search code examples
javapolymorphismoverloadingmethod-call

Isn't java suppose to match overloaded functions to most specific type?


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?


Solution

  • 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).