CHAPTER 15: Expressions Previous
Previous
Java Language
Java Language
Index
Index
Next
Next

15.11 Method Invocation Expressions

15.11.1 Compile-Time Step 1: Determine Class or Interface to Search , 15.11.2 Compile-Time Step 2: Determine Method Signature , 15.11.3 Compile-Time Step 3: Is the Chosen Method Appropriate? , 15.11.4 Runtime Evaluation of Method Invocation

A method invocation expression is used to invoke a class or instance method.


MethodInvocation:

	MethodName ( ArgumentListopt )

	Primary . Identifier ( ArgumentListopt )

	super . Identifier ( ArgumentListopt )

The definition of ArgumentList from S15.8 is repeated here for convenience:


ArgumentList:

	Expression

	ArgumentList , Expression

Resolving a method name at compile time is more complicated than resolving a field name because of the possibility of method overloading. Invoking a method at run time is also more complicated than accessing a field because of the possibility of instance method overriding.

Determining the method that will be invoked by a method invocation expression involves several steps. The following three sections describe the compile-time processing of a method invocation; the determination of the type of the method invocation expression is described in S15.11.3.


15.11.1 Compile-Time Step 1: Determine Class or Interface to Search

The first step in processing a method invocation at compile time is to figure out the name of the method to be invoked and which class or interface to check for definitions of methods of that name. There are several cases to consider, depending on the form that precedes the left parenthesis, as follows:


15.11.2 Compile-Time Step 2: Determine Method Signature

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 S15.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 S15.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 Chapter 13 for a detailed discussion of binary compatibility and type evolution.


15.11.3 Compile-Time Step 3: Is the Chosen Method Appropriate?

If there is a most specific method declaration for a method invocation, it is called the compile-time declaration for the method invocation. Two further checks must be made on the compile-time declaration:

The following compile-time information is then associated with the method invocation for use at run time:

If the compile-time declaration for the method invocation is not void , then the type of the method invocation expression is the result type specified in the compile-time declaration.


15.11.4 Runtime Evaluation of Method Invocation

At run time, method invocation requires five steps. First, a target reference may be computed. Second, the argument expressions are evaluated. Third, the accessibility of the method to be invoked is checked. Fourth, the actual code for the method to be executed is located. Fifth, a new activation frame is created, synchronization is performed if necessary, and control is transferred to the method code.

15.11.4.1 Compute Target Reference (If Necessary)

There are several cases to consider, depending on which of the three productions for MethodInvocation (S15.11) is involved:

In either case, if the evaluation of the Primary expression completes abruptly, then no part of any argument expression appears to have been evaluated, and the method invocation completes abruptly for the same reason.


15.11.4.2 Evaluate Arguments

The argument expressions are evaluated in order, from left to right. If the evaluation of any argument expression completes abruptly, then no part of any argument expression to its right appears to have been evaluated, and the method invocation completes abruptly for the same reason.

15.11.4.3 Check Accessibility of Type and Method

Let C be the class containing the method invocation, and let T be the class or interface that contained the method being invoked, and m be the name of the method, as determined at compile time (S15.11.3).

A Java Virtual Machine must insure, as part of linkage, that the method m still exists in the type T. If this is not true, then a NoSuchMethodError (which is a subclass of IncompatibleClassChangeError ) occurs. If the invocation mode is interface , then the virtual machine must also check that the target reference type still implements the specified interface. If the target reference type does not still implement the interface, then an IncompatibleClassChangeError occurs.

The virtual machine must also insure, during linkage, that the type T and the method m are accessible. For the type T:

For the method m:

If either T or m is not accessible, then an IllegalAccessError occurs (S12.3).

15.11.4.4 Locate Method to Invoke

The strategy for method lookup depends on the invocation mode.

If the invocation mode is static , no target reference is needed and overriding is not allowed. Method m of class T is the one to be invoked.

Otherwise, an instance method is to be invoked and there is a target reference. If the target reference is null , a NullPointerException is thrown at this point. Otherwise, the target reference is said to refer to a target object and will be used as the value of the keyword this in the invoked method. The other four possibilities for the invocation mode are then considered.

If the invocation mode is nonvirtual , overriding is not allowed. Method m of class T is the one to be invoked.

Otherwise, the invocation mode is interface , virtual , or super , and overriding may occur. A dynamic method lookup is used. The dynamic lookup process starts from a class S, determined as follows:

The dynamic method lookup uses the following procedure to search class S, and then the superclasses of class S, as necessary, for method m.

  1. If class S contains a declaration for a method named m with the same descriptor (same number of parameters, the same parameter types, and the same return type) required by the method invocation as determined at compile time (S15.11.3), then this is the method to be invoked, and the procedure terminates. (We note that as part of the loading and linking process that the virtual machine checks that an overriding method is at least as accessible as the overridden method; an IncompatibleClassChangeError occurs if this is not the case.)
  2. Otherwise, if S is not T, this same lookup procedure is performed using the superclass of S; whatever it comes up with is the result of this lookup.

This procedure will find a suitable method when it reaches class T , because otherwise an IllegalAccessError would have been thrown by the checks of the previous section S15.11.4.3.

We note that the dynamic lookup process, while described here explicitly, will often be implemented implicitly, for example as a side-effect of the construction and use of per-class method dispatch tables, or the construction of other per-class structures used for efficient dispatch.

15.11.4.5 Create Frame, Synchronize, Transfer Control

A method m in some class S has been identified as the one to be invoked.

Now a new activation frame is created, containing the target reference (if any) and the argument values (if any), as well as enough space for the local variables and stack for the method to be invoked and any other bookkeeping information that may be required by the implementation (stack pointer, program counter, reference to previous activation frame, and the like). If there is not sufficient memory available to create such an activation frame, an OutOfMemoryError is thrown.

The newly created activation frame becomes the current activation frame. The effect of this is to assign the argument values to corresponding freshly created parameter variables of the method, and to make the target reference available as this , if there is a target reference.

If the method m is a native method but the necessary native, implementation-dependent binary code has not been loaded (S20.16.13, S20.16.14) or otherwise cannot be dynamically linked, then an UnsatisfiedLinkError is thrown.

If the method m is not synchronized , control is transferred to the body of the method m to be invoked.

If the method m is synchronized , then an object must be locked before the transfer of control. No further progress can be made until the current thread can obtain the lock. If there is a target reference, then the target must be locked; otherwise the Class object for class S, the class of the method m, must be locked. Control is then transferred to the body of the method m to be invoked. The object is automatically unlocked when execution of the body of the method has completed, whether normally or abruptly. The locking and unlocking behavior is exactly as if the body of the method were embedded in a synchronized statement (S14.17).

15.11.4.6 Implementation Note: Combining Frames

In order to allow certain kinds of code optimization, implementations are permitted some freedom to combine activation frames. Suppose that a method invocation within class C is to invoke a method m within class S. Then the current activation frame may be used to provide space for S instead of creating a new activation frame only if one of the following conditions is true:


15.11.4.7 Example: Target Reference and Static Methods

When a target reference is computed and then discarded because the invocation mode is static , the reference is not examined to see whether it is null :


class Test {
	static void mountain() {

		System.out.println("Monadnock");

	}

	static Test favorite(){
		System.out.print("Mount ");
		return null;
	}

	public static void main(String[] args) {
		favorite().mountain();
	}
}

which prints:

Mount Monadnock

Here favorite returns null , yet no NullPointerException is thrown.

15.11.4.8 Example: Evaluation Order

As part of an instance method invocation (S15.11), there is an expression that denotes the object to be invoked. This expression appears to be fully evaluated before any part of any argument expression to the method invocation is evaluated.

So, for example, in:


class Test {
	public static void main(String[] args) {
		String s = "one";
		if (s.startsWith(s = "two"))
			System.out.println("oops");
	}
}

the occurrence of s before ".startsWith " is evaluated first, before the argument expression s="two" . Therefore, a reference to the string "one" is remembered as the target reference before the local variable s is changed to refer to the string "two" . As a result, the startsWith method (S20.12.20) is invoked for target object "one" with argument "two" , so the result of the invocation is false , as the string "one" does not start with "two" . It follows that the test program does not print "oops ".

15.11.4.9 Example: Overriding

In the example:


class Point {

	final int EDGE = 20;
	int x, y;

	void move(int dx, int dy) {
		x += dx; y += dy;
		if (Math.abs(x) >= EDGE || Math.abs(y) >= EDGE)
			clear();
	}

	void clear() {
		System.out.println("\tPoint clear");
		x = 0; y = 0;
	}
}

class ColoredPoint extends Point {
	int color;


	void clear() {
		System.out.println("\tColoredPoint clear");
		super.clear();
		color = 0;
	}
}

the subclass ColoredPoint extends the clear abstraction defined by its superclass Point . It does so by overriding the clear method with its own method, which invokes the clear method of its superclass, using the form super.clear .

This method is then invoked whenever the target object for an invocation of clear is a ColoredPoint . Even the method move in Point invokes the clear method of class ColoredPoint when the class of this is ColoredPoint , as shown by the output of this test program:


class Test {
	public static void main(String[] args) {
		Point p = new Point();
		System.out.println("p.move(20,20):");
		p.move(20, 20);
		ColoredPoint cp = new ColoredPoint();
		System.out.println("cp.move(20,20):");
		cp.move(20, 20);
		p = new ColoredPoint();
		System.out.println("p.move(20,20), p colored:");
		p.move(20, 20);
	}
}

which is:


p.move(20,20):
	Point clear
cp.move(20,20):
	ColoredPoint clear
	Point clear
p.move(20,20), p colored:
	ColoredPoint clear
	Point clear

Overriding is sometimes called "late-bound self-reference"; in this example it means that the reference to clear in the body of Point.move (which is really syntactic shorthand for this.clear ) invokes a method chosen "late" (at run time, based on the run-time class of the object referenced by this ) rather than a method chosen "early" (at compile time, based only on the type of this ). This provides the Java programmer a powerful way of extending abstractions and is a key idea in object-oriented programming.

15.11.4.10 Example: Method Invocation using super

An overridden instance method of a superclass may be accessed by using the keyword super to access the members of the immediate superclass, bypassing any overriding declaration in the class that contains the method invocation.

When accessing an instance variable, super means the same as a cast of this (S15.10.2), but this equivalence does not hold true for method invocation. This is demonstrated by the example:


class T1 {
	String s() { return "1"; }
}

class T2 extends T1 {
	String s() { return "2"; }
}

class T3 extends T2 {
	String s() { return "3"; }

	void test() {
		System.out.println("s()=\t\t"+s());
		System.out.println("super.s()=\t"+super.s());
		System.out.print("((T2)this).s()=\t");
			System.out.println(((T2)this).s());
		System.out.print("((T1)this).s()=\t");
			System.out.println(((T1)this).s());
	}
}

class Test {
	public static void main(String[] args) {
		T3 t3 = new T3();
		t3.test();
	}
}

which produces the output:


s()=					3
super.s()=					2
((T2)this).s()=					3
((T1)this).s()=					3

The casts to types T1 and T2 do not change the method that is invoked, because the instance method to be invoked is chosen according to the run-time class of the object referred to be this . A cast does not change the class of an object; it only checks that the class is compatible with the specified type.

Top© 1996 Sun Microsystems, Inc. All rights reserved.