15.11.2 Compile-Time Step 2: Determine Method Signature

The hand-writing experts were called upon for their opinion of the signature . . .
—Agatha Christie, The Mysterious Affair at Styles (1920), Chapter 11

The second step searches the class or interface determined in the previous step for method declarations. This step uses the name of the method and the types of the argument expressions to locate method declarations that are both applicable and accessible, that is, declarations that can be correctly invoked on the given arguments. There may be more than one such method declaration, in which case the most specific one is chosen. The descriptor (signature plus return type) of the most specific method declaration is one used at run time to do the method dispatch.

15.11.2.1 Find Methods that are Applicable and Accessible

A method declaration is applicable to a method invocation if and only if both of the following are true:

The class or interface determined by the process described in §15.11.1 is searched for all method declarations applicable to this method invocation; method definitions inherited from superclasses and superinterfaces are included in this search.

Whether a method declaration is accessible to a method invocation depends on the access modifier (public, none, protected, or private) in the method declaration and on where the method invocation appears.

If the class or interface has no method declaration that is both applicable and accessible, then a compile-time error occurs.

In the example program:


public class Doubler {
	static int two() { return two(1); }
	private static int two(int i) { return 2*i; }
}

class Test extends Doubler {	
	public static long two(long j) {return j+j; }

	public static void main(String[] args) {
		System.out.println(two(3));
		System.out.println(Doubler.two(3));	// compile-time error
	}
}

for the method invocation two(1) within class Doubler, there are two accessible methods named two, but only the second one is applicable, and so that is the one invoked at run time. For the method invocation two(3) within class Test, there are two applicable methods, but only the one in class Test is accessible, and so that is the one to be invoked at run time (the argument 3 is converted to type long). For the method invocation Doubler.two(3), the class Doubler, not class Test, is searched for methods named two; the only applicable method is not accessible, and so this method invocation causes a compile-time error.

Another example is:


class ColoredPoint {
	int x, y;
	byte color;
	void setColor(byte color) { this.color = color; }
}

class Test {
	public static void main(String[] args) {
		ColoredPoint cp = new ColoredPoint();
		byte color = 37;
		cp.setColor(color);
		cp.setColor(37);											// compile-time error
	}
}

Here, a compile-time error occurs for the second invocation of setColor, because no applicable method can be found at compile time. The type of the literal 37 is int, and int cannot be converted to byte by method invocation conversion. Assignment conversion, which is used in the initialization of the variable color, performs an implicit conversion of the constant from type int to byte, which is permitted because the value 37 is small enough to be represented in type byte; but such a conversion is not allowed for method invocation conversion.

If the method setColor had, however, been declared to take an int instead of a byte, then both method invocations would be correct; the first invocation would be allowed because method invocation conversion does permit a widening conversion from byte to int. However, a narrowing cast would then be required in the body of setColor:

	void setColor(int color) { this.color = (byte)color; }

15.11.2.2 Choose the Most Specific Method

If more than one method is both accessible and applicable to a method invocation, it is necessary to choose one to provide the descriptor for the run-time method dispatch. Java uses the rule that the most specific method is chosen.

The informal intuition is that one method declaration is more specific than another if any invocation handled by the first method could be passed on to the other one without a compile-time type error.

The precise definition is as follows. Let m be a name and suppose that there are two declarations of methods named m, each having n parameters. Suppose that one declaration appears within a class or interface T and that the types of the parameters are T1, . . . , Tn; suppose moreover that the other declaration appears within a class or interface U and that the types of the parameters are U1, . . . , Un. Then the method m declared in T is more specific than the method m declared in U if and only if both of the following are true:

A method is said to be maximally specific for a method invocation if it is applicable and accessible and there is no other applicable and accessible method that is more specific.

If there is exactly one maximally specific method, then it is in fact the most specific method; it is necessarily more specific than any other method that is applicable and accessible. It is then subjected to some further compile-time checks as described in §15.11.3.

It is possible that no method is the most specific, because there are two or more maximally specific method declarations. In this case, we say that the method invocation is ambiguous, and a compile-time error occurs.

15.11.2.3 Example: Overloading Ambiguity

Consider the example:

class Point { int x, y; }
class ColoredPoint extends Point { int color; }


class Test {

	static void test(ColoredPoint p, Point q) {
		System.out.println("(ColoredPoint, Point)");
	}

	static void test(Point p, ColoredPoint q) {
		System.out.println("(Point, ColoredPoint)");
	}

	public static void main(String[] args) {
		ColoredPoint cp = new ColoredPoint();
		test(cp, cp);											// compile-time error
	}
}

This example produces an error at compile time. The problem is that there are two declarations of test that are applicable and accessible, and neither is more specific than the other. Therefore, the method invocation is ambiguous.

If a third definition of test were added:


	static void test(ColoredPoint p, ColoredPoint q) {
		System.out.println("(ColoredPoint, ColoredPoint)");
	}

then it would be more specific than the other two, and the method invocation would no longer be ambiguous.

15.11.2.4 Example: Return Type Not Considered

As another example, consider:

class Point { int x, y; }
class ColoredPoint extends Point { int color; }
class Test {


	static int test(ColoredPoint p) {
		return color;
	}

	static String test(Point p) {
		return "Point";
	}

	public static void main(String[] args) {
		ColoredPoint cp = new ColoredPoint();
		String s = test(cp); // compile-time error
	}
}

Here the most specific declaration of method test is the one taking a parameter of type ColoredPoint. Because the result type of the method is int, a compile- time error occurs because an int cannot be converted to a String by assignment conversion. This example shows that, in Java, the result types of methods do not participate in resolving overloaded methods, so that the second test method, which returns a String, is not chosen, even though it has a result type that would allow the example program to compile without error.

15.11.2.5 Example: Compile-Time Resolution

The most applicable method is chosen at compile time; its descriptor determines what method is actually executed at run time. If a new method is added to a class, then Java code that was compiled with the old definition of the class might not use the new method, even if a recompilation would cause this method to be chosen.

So, for example, consider two compilation units, one for class Point:

package points;
public class Point {
	public int x, y;
	public Point(int x, int y) { this.x = x; this.y = y; }
	public String toString() { return toString(""); }


	public String toString(String s) {
		return "(" + x + "," + y + s + ")";
	}
}

and one for class ColoredPoint:

package points;
public class ColoredPoint extends Point {


	public static final int
		RED = 0, GREEN = 1, BLUE = 2;

	public static String[] COLORS =
		{ "red", "green", "blue" };
	public byte color;

	public ColoredPoint(int x, int y, int color) {
		super(x, y); this.color = (byte)color;
	}

	/** Copy all relevant fields of the argument into
		    this ColoredPoint object. */
	public void adopt(Point p) { x = p.x; y = p.y; }

	public String toString() {
		String s = "," + COLORS[color];
		return super.toString(s);
	}
}

Now consider a third compilation unit that uses ColoredPoint:

import points.*;
class Test {
	public static void main(String[] args) {
		ColoredPoint cp =
			new ColoredPoint(6, 6, ColoredPoint.RED);
		ColoredPoint cp2 =
			new ColoredPoint(3, 3, ColoredPoint.GREEN);
		cp.adopt(cp2);
		System.out.println("cp: " + cp);
	}
}

The output is:

cp: (3,3,red)

The application programmer who coded class Test has expected to see the word green, because the actual argument, a ColoredPoint, has a color field, and color would seem to be a "relevant field" (of course, the documentation for the package Points ought to have been much more precise!).

Notice, by the way, that the most specific method (indeed, the only applicable method) for the method invocation of adopt has a signature that indicates a method of one parameter, and the parameter is of type Point. This signature becomes part of the binary representation of class Test produced by the compiler and is used by the method invocation at run time.

Suppose the programmer reported this software error and the maintainer of the points package decided, after due deliberation, to correct it by adding a method to class ColoredPoint:


public void adopt(ColoredPoint p) {
	adopt((Point)p); color = p.color;
}

If the application programmer then runs the old binary file for Test with the new binary file for ColoredPoint, the output is still:

cp: (3,3,red)

because the old binary file for Test still has the descriptor "one parameter, whose type is Point; void" associated with the method call cp.adopt(cp2). If the source code for Test is recompiled, the compiler will then discover that there are now two applicable adopt methods, and that the signature for the more specific one is "one parameter, whose type is ColoredPoint; void"; running the program will then produce the desired output:

cp: (3,3,green)

With forethought about such problems, the maintainer of the points package could fix the ColoredPoint class to work with both newly compiled and old code, by adding defensive code to the old adopt method for the sake of old code that still invokes it on ColoredPoint arguments:


public void adopt(Point p) {
	if (p instanceof ColoredPoint)
		color = ((ColoredPoint)p).color;
	x = p.x; y = p.y;
}

A similar consideration applies if a method is to be moved from a class to a superclass. In this case a forwarding method can be left behind for the sake of old code. The maintainer of the points package might choose to move the adopt method that takes a Point argument up to class Point, so that all Point objects may enjoy the adopt functionality. To avoid compatibility problems with old binary code, the maintainer should leave a forwarding method behind in class ColoredPoint:


public void adopt(Point p) {
	if (p instanceof ColoredPoint)
		color = ((ColoredPoint)p).color;
	super.adopt(p);
}

Ideally, Java code should be recompiled whenever code that it depends on is changed. However, in an environment where different Java classes are maintained by different organizations, this is not always feasible. Defensive programming with careful attention to the problems of class evolution can make upgraded code much more robust. See §13 for a detailed discussion of binary compatibility and type evolution.