By Amanda Waite
We want to hear from you! Please send us your
FEEDBACK.
Method Invocation Narrowing
One of the jobs of the Java[tm] platform compiler javac is to determine which method
should be applied to a specific method invocation. This is a very simple process if there
is only one method that can be applied to the invocation but becomes a lot more complicated when the compiler is faced with a complex inheritance hierarchy and a number of overloaded methods.
The process used by the compiler to determine which method should be applied to an
invocation, is fairly well documented. Many of these articles give the impression that
the only factor taken into consideration when evaluating methods is the types of the
method's input parameters - but this isn't the complete story as we will shortly see.
I've now gotten into the habit of referring to the process used by the compiler as
Method Invocation Narrowing. I will use this term throughout this article.
A Simple View of the Process
Let's look briefly at how the process of Method Invocation Narrowing works.
- Step 1: When faced with a method invocation the compiler will first determine the
name of the method that is being invoked, as well as which classes or interfaces should be
searched for matching methods.
- Step 2: The compiler tries to determine which of these methods are both accessible
and applicable to the invocation. A method is deemed to be accessible if it is visible at
the point at which the invocation was made. Accessibility is determined by the access
modifiers of both the method and the class that contains it.
A method is deemed to be applicable if the parameters of the method invocation can be
assigned to the arguments of the method invocation without the need for an explicit cast
(see Note 1 at the end of this article).
If only one method is found to be accessible and applicable then the compiler will use
that method. If more than one method is found to be accessible and applicable, then the
compiler tries to determine which of these remaining methods is the most specific.
- Step 3. The compiler now looks at the parameters of each method in turn and attempts
to assign these parameters to the parameters of each of the other methods. If all of the
parameters of a method can be assigned to the parameters of another, then that first
method is considered to be more specific and the second method can be discarded. This
process is repeated until only one method remains.
Let us now have a look at Step 3 in a little more detail, as this is where things can
become a little unclear.
Let's say that after evaluating a method invocation the compiler finds that two
methods are both accessible and applicable and that the signatures of these methods are:
Method 1: multiply(float, long)
Method 2: multiply(float, int)
When the compiler tries to assign the parameters of Method 1 to those of Method 2
it fails, because while a float can be assigned to a float, a
long cannot be assigned to an int without using an explicit cast.
This means that Method 1 is not more specific than Method 2. When the compiler then tries
to assign the parameters of Method 2 to those of Method 1, it succeeds because a float
can be assigned to a float and an int can be assigned to a
long. Therefore Method 2 is more specific than Method 1, so Method 1 can
be discarded.
Note that this process can only succeed for one method. An ambiguity and therefore
a compiler error can occur if, at the end of this process, more than one method remains.
This situation can arise when for each of the methods that remain, the attempt to assign
the parameters of that method to those of another fails. Consider the following example.
After evaluating a method invocation the compiler finds that two methods are both
accessible and applicable and that the signatures of these methods are:
Method 1: multiply(float, double)
Method 2: multiply(double, float)
Now, when the compiler tries to assign the parameters of Method 1 to those of Method 2,
it fails because while a float can be assigned to a double, a
double cannot be assigned to a float. The attempt to assign the
parameters of Method 2 to those of Method 1 also fails because once again, a
double cannot be assigned to a float. Since the compiler
cannot determine which method to use it throws the following error:
Test.java:12: reference to multiply is ambiguous, both method multiply(float,double) in AccountUtilsand method multiply(double,float) in AccountUtils match
multiply(6, 3);
^
Going Deeper
As an explanation of the process of Method Invocation Narrowing this seems to fit all
of the facts. Unfortunately, it falls short of telling us the whole story. In order to
demonstrate this, following is a sample program whose sole task is to feed fish.
class AngelFish {
public void giveFood(GreenAlgae ga) {
}
}
class ColdWaterAngelFish extends AngelFish {
public void giveFood(Algae a) {
}
}
abstract class Algae {
}
class GreenAlgae extends Algae {
}
class BlueGreenAlgae extends Algae {
}
public class FishFood {
public static void main(String[] args) {
ColdWaterAngelFish caf = new ColdWaterAngelFish();
GreenAlgae ga = new GreenAlgae();
caf.giveFood(ga);
}
}
It looks simple enough, we have two inheritance hierarchies;
ColdWaterAngelFish is a subclass of AngelFish and
both BlueGreenAlgae and GreenAlgae are subclasses of the
abstract class Algae. The AngelFish class has
a giveFood() method that takes an instance of GreenAlgae as an
argument. ColdWaterAngelFish is a specialization of the AngelFish class and has an overloaded giveFood() method that takes any kind of Algae as an argument.
Finally, in the main() method of the FishFood class we create
an instance of ColdWaterAngelFish and attempt to give it some
GreenAlgae using the giveFood() method.
Surprisingly, if we attempt to compile this code it fails with a similar error to
the one that we saw in our earlier example, and it appears to be the invocation of the
giveFood() method that the compiler is complaining about. This seems odd
as we have tried to make it clear that an AngelFish can be given only
GreenAlgae and a ColdWaterAngelFish can be given anything that
is a concrete implementation of the abstract Algae class. This includes both
BlueGreenAlgae and GreenAlgae.
So let's revisit the Method Invocation Narrowing process but this time we will apply it to this last example.
After Step 1 and Step 2 of the narrowing process it is found that the method
AngelFish.giveFood(GreenAlgae ga) is both accessible and applicable, as
is the method ColdWaterAngelFish.giveFood(Algae a). Now the compiler has to
determine which of these methods is the most specific.
Here are the signatures of the remaining methods:
Method 1: giveFood(GreenAlgae)
Method 2: giveFood(Algae)
The parameters of Method 1 can be assigned to Method 2 so therefore Method 1 is more
specific. But if this were really the case then no ambiguity exists and the code should
compile.
So What Are We Missing?
In order to understand what is happening here we need to look more closely at
what the compiler sees as being the parameters of a method. When evaluating methods
the compiler considers not only the types of the input arguments but also the type
of the object on which the method is being called. Given these considerations let's look
at this in more detail by expanding an earlier example:
Consider that while narrowing a method invocation the compiler finds that two
methods are both accessible and applicable and that both methods are in the
AccountUtils class. The signatures of these methods are as follows:
Method 1: AccountUtils.multiply(float, long)
Method 2: AccountUtils.multiply(float, int)
We know from earlier that the compiler attempts to assign the types of the input
arguments of one method to those of the other method. But what we didn't establish was
that the compiler also does the same with the type of class or interface in which the
methods are declared. In this case both methods are in the AccountUtils
class, so the result is the same as that which we saw earlier. When the compiler
tries to assign the parameters of Method 1 to those of Method 2, it fails because while
an AccountUtils can be assigned to an AccountUtils and
a float can be assigned to a float, a long cannot
be assigned to an int without an explicit cast. If we then try to assign
the parameters of Method 2 to those of Method 1, it succeeds because an
AccountUtils can be assigned to an AccountUtils, a
float can be assigned to a float and an int can
be assigned to an long. Therefore Method 2 is more specific than Method 1, so Method 1 can be discarded.
With this in mind let's look at the FishFood example again. We have already stated that:
The method AngelFish.giveFood(GreenAlgae ga) is both accessible and
applicable as is the method ColdWaterAngelFish.giveFood(Algae a). So
which of these is the most specific? Here are the signatures of the remaining methods:
Method 1: AngelFish.giveFood(GreenAlgae ga)
Method 2: ColdWaterAngelFish.giveFood(Algae a)
If we try to assign the parameters of Method 1 to those of Method 2 it fails,
because while GreenAlgae can be assigned to
Algae, AngelFish cannot be assigned to
ColdWaterAngelFish. If we try to assign the parameters of Method 2 to
those of Method 1, it fails because while ColdWaterAngelFish can be assigned
to AngelFish, Algae cannot be assigned to
GreenAlgae. Neither of the two methods is more specific than the other
and the compiler cannot determine which method it should use and will therefore throw
the "ambiguous" error that we saw earlier.
Looking for Solutions
So having clarified the process used by the compiler to select a method we now need
to look at why this is a problem and what steps we can take to address it.
The first thing to understand is why this is a problem for the compiler. In this case
the compiler has been able to narrow the method invocation down to two methods, each of
which could be applied to the method invocation but neither of which is more specific
than the other. The compiler believes that the intent of the programmer is unclear. Does
he or she want to call the super class method with the exact argument or does he or she
want to call the subclass method with the less specific argument? In these circumstances
if the compiler selected the wrong method there may be unwanted and unexpected results.
This is more of a programming issue than a design issue although having said that there
has been much debate about the inappropriate use of method overloading in Object-Oriented
languages. Even though this is primarily a programming issue it may need some redesign
in order to find a suitable solution.
One possible solution is to provide an implementation of the
giveFood(GreenAlgae ga) method in the ColdWaterAngelFish class.
For instances of ColdWaterAngelFish this will override the same method in
the AngelFish superclass. Inside this new method we have the choice of
making a call to the superclass method or of invoking the less specific giveFood(Algae a) method.
To invoke the superclass method we would implement the method in
ColdWaterAngelFish as follows:
public void giveFood(GreenAlgae ga) {
super.giveFood(ga);
}
To invoke the less specific method in the ColdWaterAngelFish class you would implement the method as follows:
public void giveFood(GreenAlgae ga) {
giveFood((Algae)ga);
}
This assumes that the existing methods already implement the behaviour that we need.
If this were not the case we could write a complete implementation of this method that
would give us the desired behaviour.
In this case the changes described above would help to enforce the specialization
relationship between the two classes.
The problem that we have been looking at has most likely come about either because
of design changes made to existing code or because of refactoring. It's quite possible
that the cause of the problem isn't a change to the method invocation, but is actually
caused by a change to the classes or interfaces in which the method is being declared
and/or implemented. This really highlights the fact that developers need to be aware of
the potential problems of overloading methods in subclasses. In this case the compiler
has picked up the fact that there is an ambiguity, but if the changes to the code had not
required the recompilation of the class that contained that actual method invocation, then
the compiler wouldn't have picked up the problem and the code would potentially no longer
perform as intended.
Conclusion
The intent of this article has been to help programmers and designers to better
understand the reasons why the compiler finds ambiguities, and to describe the ways
in which we can resolve them. A highly detailed
reference on the operation of the Java platform compiler is the Java Language
Specification.
Notes
Note 1: The assignment or 'conversion' of one type to another when evaluating
methods is described as "Method Invocation Conversion" in the Java Language Specification,
in sections 5.3 and 5.2. What follows is an extract from the start of section 5.3:
"Method Invocation Conversion is applied to each argument value in a method
or constructor invocation: the type of the argument expression must be converted to the
type of the corresponding parameter. Method invocation contexts allow the use of an
identity conversion, a widening primitive conversion, or a widening reference
conversion."