10.9 Method Calls

Java method calls compile into invokevirtual, invokespecial, invokeinterface, and invokestatic instructions. A Java programmer might not realize that although essentially the same syntax is used on every method call, there are actually four different kinds of method calls.

10.9.1 Virtual Method Calls

Consider this expression:

 System.out.println("Hello, world") 

This is a virtual method call: it executes with respect to a particular object, called the receiver of the method invocation. The receiver is the result of the expression to the left of the method name. In this case, the receiver expression is System.out, which returns the value of the out field of the java.lang.System class. The receiver must be an object.

To compile a virtual method call, first the receiver expression is evaluated, followed by code to evaluate the arguments. These are all left on the stack, where they are used by an invokevirtual instruction.

 ; This instruction evaluates the expression System.out getstatic java/lang/System/out Ljava/io/PrintStream; ; This instruction evaluates the expression "Hello, world" ldc "Hello, world" ; This is the method invocation invokevirtual java/io/PrintStream (Ljava/lang/String;)V 

10.9.2 Static Method Calls

Here is an example of a static method call:

 Math.max(10, 20) 

The method max in the class java.lang.Math is marked static. This means that instead of having a receiver expression, you use the name of the class.

To compile this expression, use an invokestatic instruction. As with virtual method invocations, the code for the argument expressions is written immediately before the invokestatic. The difference is that there is no need to compile code for the receiver. The above example compiles to

 bipush 10             ; Push the constant 10 bipush 20             ; Push the constant 20 invokestatic java/lang/Math/max(II)I 

10.9.3 Example of Method Calls

This class uses both static and virtual method invocations:

 class RightTriangle {    double a;                  // Two sides of a triangle    double b;    /** Print a debugging message */    void debug(double i, double j)    {         // Print out the value of i and j; code omitted    }    /** Compute the hypotenuse of the triangle */    double hypotenuse()    {         debug(a, b);         return Math.sqrt(a*a + b*b);    } } 

In the method hypotenuse, the call to debug is a virtual call with the receiver omitted. It is equivalent to

 this.debug(a, b); 

The receiver is this. There are two subexpressions, a and b. Using the search order described in section 10.5.1, the compiler finds that a and b are treated as fields of the current object. The statement compiles as

 aload_0                       ; Push this for the method call aload_0                       ; Push this.a getfield RightTriangle/a D aload_0                       ; Push this.b getfield RightTriangle/b D invokevirtual RightTriangle/debug(DD)V    ; Call the method 

The call to Math.sqrt is a static call with a single argument. This argument is the result of evaluating the expression a*a+b*b. We have already seen how the compiler finds a and b; the compiler must also generate code to do the rest of the arithmetic. Because * has a higher priority than +, both multiplications are done before the addition. The statement

 return Math.sqrt(a*a+b*b); 

compiles as

 aload_0                                 ; Push a getfield RightTriangle/a D dup dmul                                    ; Compute a*a aload_0                                 ; Push b getfield RightTriangle/b D dup dmul                                    ; Compute b*b dadd                                    ; Compute a*a+b*b ; Now there is a single argument on the stack: a*a+b*b. Use that ; as the argument to sqrt invokestatic java/lang/Math/sqrt(D)D    ; Call sqrt dreturn                                 ; Return the result 

10.9.4 Overriding

The rules of Java subtyping are defined so that any operation that can be applied to an instance of a class can also be applied to any instance of any subclass. For example, consider a class Rectangle and a class Square that extends it:

 class Rectangle {    boolean contains(Point p)    {         // Return true if this rectangle contains p; false            otherwise    }    boolean setLocation(int x, int y)    {         // Set the location of the rectangle to (x,y)    }    boolean setLocation(Point p)    {         // Set the location of the rectangle to p    } } class Square extends Rectangle {    boolean contains(Point p)    {         // Return true if this square contains p; false otherwise    } } 

Since Square is a subclass of Rectangle, any instance of Square is also an instance of Rectangle. This means that any method that can be applied to Rectangles can also be applied to Squares. By default, the implementation of the methods is the same for Squares as it is for Rectangles. We say that Square inherits the definitions of these methods from Rectangle.

If the Square class provides a definition of a method with the same name and the same arguments as one of the methods that it would inherit, then we say Square overrides that method. In the example, Square overrides the definition of contains, since there is an identical method declaration in the class Rectangle.

When the overridden method is invoked, the decision about which definition is executed is based on the runtime type of the object on which the method is invoked. For example, suppose r is a Rectangle and p is a Point; r may contain a Square or it may contain a plain old Rectangle. Consider:

 r.contains(p); 

At runtime, if r contains a non-Square Rectangle, then the Rectangle definition of contains will be used. If r contains a Square, then Square's definition will be used. Which definition will be used can be determined only by running the program.

Because the determination of which method will be used depends on the receiver of the method invocation, only nonstatic methods can be overridden. However, static methods can give the appearance of inheritance. For example, consider this code:

 class A {    static int compute() {/* Do something */ } } class B extends A { } 

You can use the Java expression B.compute(), which actually invokes the method compute in class A. However, B does not really inherit the method. Instead, method implementation in the nearest superclass is used in the invokestatic instruction. When B.compute() is compiled, it produces the Oolong

 invokestatic A/compute() I 

You can see this effect if you actually add a method compute to B. If you don't recompile the expression B.compute(), it will still invoke the definition from A.

10.9.5 Overloading

If two methods have the same name but different arguments, the methods are said to be overloaded. They are effectively two different methods that happen to share the same name. This is similar to the way the + operator means one thing when applied to ints and another applied to floats.

In the class java.awt.Rectangle there are two methods named setLocation: one takes two int values as arguments, the other takes a java.awt.Point. These two methods are not required to return the same type, though they happen to in this case. They have the same name, which implies to the programmer that the two operations are somehow related, but the Java language considers them to be completely different.

Unlike overriding, which is handled by the definition of the invokevirtual instruction, overloading requires the compiler to disambiguate which method is to be called at compile time. It does this by specifying both the name and the type of the method in the invokevirtual instruction.

The determination is based on the compile-time definitions of the arguments. For example, given that r is a Rectangle and p is a Point, here are two calls to setLocation:

 r.setLocation(10, 20) r.setLocation(p) 

Let's assign r to variable 1 and p to variable 0. The first call compiles as

 aload_1               ; Push r bipush 10             ; Push 10 bipush 20             ; Push 20                       ; Call setLocation using two ints invokevirtual java/awt/Rectangle/setLocation(II)V 

In this case, the two arguments are both ints. This matches the definition of setLocation, which takes two ints. The second case requires a Point, so it compiles to

 aload_1               ; Push r aload_2               ; Push p                       ; Call setLocation using a Point invokevirtual java/awt/Rectangle/setLocation(Ljava/awt/Point;)V 

There are more complicated situations. Sometimes arguments with numeric types must be promoted to make one of the arguments match. Sometimes arguments with object types must be treated as members of superclasses. If more than one argument is involved, it may be ambiguous which arguments should be promoted to make a type match. For more details, see section 15.11 of The Java Language Specification.

Note that overriding, as in the case of contains, is different from overloading, as in the example with setLocation. Overriding is declaring a method with the same name and same arguments declared in a subclass. Overloading is declaring two methods with the same name but different arguments; these are really two different methods.

If two methods are overloaded, the determination of which one will be used is made at compile time, based on the compile-time types of the arguments. If one method overrides another, the determination of which will be used will be made at runtime, based not on the arguments but only on the class of the receiver of the invocation.

10.9.6 Interfaces

An interface specifies a set of methods that a class must support. One example of an interface is java.util.Enumeration, which is used to give a collection of elements one at a time. Here is the definition of Enumeration, which is found in the package java.util:

 package java.util; interface Enumeration {    boolean hasMoreElements();    Object nextElement(); } 

Interfaces may be used as the declared type of a variable or field, and they may be used as the type of a method argument or return. For example, the declaration of the method elements in java.util.Vector is:

 Enumeration elements(); 

This means that elements returns only instances of classes that support the Enumeration interface. They may be of any class, as long as that class supports Enumeration.

The interface can also be used to declare variables and fields:

 Enumeration e; 

This declaration means that only instances of classes that support the Enumeration interface can be assigned to the variable e. For example, suppose vector is an instance of the class java.util.Vector:

 e = vector.elements(); 

The methods of Enumeration can be invoked on e:

 while(e.hasMoreElements())    Object o = e.nextElement(); 

Here, the compile-time type of the receiver of the method invocations of hasMoreElements and nextElement is an interface instead of a class as was shown in previous sections. This is called invoking through an interface, and it's compiled a bit differently from virtual method invocation (section 10.9.1).

The generated code is similar to virtual invocations. First the receiver expression is evaluated, followed by the argument expressions. At the end, instead of an invokevirtual instruction, an invokeinterface instruction is used. Assuming that e is stored in variable 1, the call to e.hasMoreElements() compiles to

 aload_1                        ; Push e invokeinterface java/util/Enumeration/hasMoreElements ()Z 1 

The invokeinterface instruction requires an additional argument, as described in section 4.9. That argument is the number of stack slots popped off the stack when the method is called. In this case, only a single slot is removed: the receiver of the instruction. If there were additional arguments to the method call, they would have to be included. As usual, longs and doubles count for two, all others count as one.



Programming for the Java Virtual Machine
Programming for the Javaв„ў Virtual Machine
ISBN: 0201309726
EAN: 2147483647
Year: 1998
Pages: 158
Authors: Joshua Engel

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net