4.5 Invoking Methods on Objects

The usual way to invoke a method on an object is to use an invokevirtual instruction. When invokevirtual is executed, a new stack frame is created, with a new operand stack and a new local variable array. The stack is empty, and the slots in the local variable array are uninitialized.

Execution continues with the first instruction of the invoked method. The new stack and variable pool are the only ones which can be accessed until the method returns or throws an exception. For example, consider this class.

 .class RightTriangle .method sumOfSquares(FF)F ; Variable 0 contains the RightTriangle object ; Variable 1 contains the first argument (a) ; Variable 2 contains the second argument (b) fload_1          ; Compute a^2, leaving dup              ;    the result on the stack fmul fload_2          ; Compute b^2 dup fmul fadd             ; Add the squares together freturn          ; Return the result .end method 

This method takes two float values and returns the sum of their squares. In order to call this method, there must be three things on the stack, corresponding to the three initialized variables. The first thing must be a RightTriangle object, and the other two must be floats. The first is called the receiver of the method invocation; the other two are arguments to the method call.

This code fragment calls the sumOfSquares method:

 ; Assume that there's a RightTriangle object in variable 0 now aload_0          ; Push a RightTriangle object onto the stack                  ; as the receiver ldc 3.0          ; Push 3.0 and 4.0 as arguments ldc 4.0 invokevirtual RightTriangle/sumOfSquares (FF)F ; The result 25.0 is left on the stack 

At the moment the invokevirtual is executed, the new stack frame is created, and the program counter points to the first instruction of the sumOfSquares method (the fload_1).

The stack is empty. Local variable 0 contains the RightTriangle object itself. Local variables 1 and 2 contain the operands of the invokevirtual instruction; in this case, the float values 3.0 and 4.0.

The method continues executing the instructions of the sumOfSquares method until it reaches the freturn statement. At that point, the new stack frame is removed, and the original stack frame is now on top. The operand of the freturn instruction, the float 25.0, is pushed onto the stack.

Methods that are static are invoked differently, using the invokestatic instruction. Unlike non-static methods, the static methods don't use a receiver. They only have arguments. For example, here's another method in RightTriangle that uses both non-static and static method calls:

 .method hypotenuse(FF)F ; Variable 0 contains a RightTriangle object (this) ; Variable 1 contains a float (a) ; Variable 2 contains another float (b) aload_0                                          ; Push this fload_1                                          ; Push a fload_2                                          ; Push b invokevirtual RightTriangle/sumOfSquares(FF)F    ; Compute a^2+b^2 f2d                                              ; Convert to                                                  ;    double invokestatic java/lang/Math/sqrt(D)D             ; Compute square                                                  ;    root d2f                                              ; Back to float freturn .end method 

After invoking the sumOfSquares method just as in the earlier example, the method goes on to call the sqrt routine in the Math class, one of the standard Java platform classes. The sqrt method is static, which means that no receiver is used. It expects the stack to contain a double. The method takes the number and returns its square root as a double.

Because we were using float values, we have to convert the float result to a double before calling the method, then convert the result back to a float before returning it. These conversions are necessary to make the type of what's on the stack match the descriptor of sqrt and to match the return type of the hypotenuse.

Exercise 4.4

Modify the class RightTriangle to have two fields a and b. Modify hypotenuse to use the fields a and b instead of arguments.

4.5.1 Virtual Invocation

When you use invokevirtual, the method definition that's named in the arguments is not necessarily the one that's invoked. Instead, the JVM uses a procedure virtual dispatch[1] to select the method to call based on the method name, the method descriptor, and the type of the receiver at runtime.

[1] "Virtual dispatch" is unrelated to "virtual machine"; the word "virtual" is overused. The "virtual" in invokevirtual refers to virtual dispatch.

In virtual dispatch, the JVM looks in the class of the receiver to find a method implementation with the exact name and type given. If it exists, that's the method that's invoked. If not, it looks in the superclass of that class and so on until the method is found.

To illustrate, suppose that you have two classes like these:

 .class Hello .method greet()V getstatic java/lang/System/out Ljava/io/PrintStream; ldc "Hello, world" invokevirtual java/io/PrintStream/println (Ljava/lang/String;)V .end method .end class .class Hola .super Hello .method greet()V getstatic java/lang/System/out Ljava/io/PrintStream; ldc "Hola, mundo" invokevirtual java/io/PrintStream/println (Ljava/lang/String;)V .end method .end class 

If you had an Hola object on top of the stack and executed

 invokevirtual Hello/greet ()V 

the program would print

 Hola, mundo 

The class name given as an argument to invokevirtual is not completely ignored. When the code is loaded, the JVM verification algorithm checks to see that the class named in the argument actually exists and that it has a method with the appropriate name and descriptor. It also examines the code to ensure that receiver of the method invocation is going to be an instance of that class or some subclass of that class. By enforcing these requirements, the verification algorithm makes sure that there is always an appropriate method implementation to match the name and descriptor.

4.5.2 Method Inheritance and Overriding

A Java compiler uses the invokevirtual instruction for most non-static methods. (For more about static methods, see section 4.10). You probably already have a pretty good idea how virtual method invocation works. Let's see how the JVM treats it.

Method inheritance is different from field inheritance. As we said in section 4.4.2, when a subclass declares a field with the same name and descriptor as one of its superclasses, all instances of the subclass actually have two different fields. This doesn't confuse the JVM because the two fields are really named differently, since the full name of the field incorporates the class name as well.

With methods, when a subclass has a method with the same name and descriptor as a superclass, then the subclass ends up with only a single implementation of each method. The technical term for this is that the subclass overrides the method implementation in the superclass.

Figure 4.5 is a diagram of the memory in the JVM, showing an instance of the class Hola on top of the stack. If you go to invoke the method greet with descriptor ()V, the JVM will discover that the object is of class Hola. It begins its search in the class Hola, which does have an implementation of the method. This is the method that prints the string "Hola, mundo" and then returns.

Figure 4.5. Memory layout of class definitions

graphics/04fig05.gif

If you wanted to invoke the method toString() instead, using the descriptor ()Ljava/lang/String;, the method search would begin in Hola, but there is no matching method there. So the JVM tries Hello because that's the superclass of Hola, but there's no method there either. Finally it looks in java/lang/Object. There had better be a method there, because that's the last resort. Fortunately, there is a matching method implementation there, and that implementation is used.

If you don't like the idea of the JVM searching for a method implementation, another way to think of the class implementation space in the JVM is shown in Figure 4.6. There are a lot of arrows running around in this diagram, but here's what's going on: each class begins with a list of the methods of the superclass. The arrows point to the same place as the methods in the superclass do. This way, all methods are inherited from the superclass. The list is sometimes called a vtable. At the end of the list, you throw in any methods that are new to the subclass. This gives you the new methods. For any methods that are overridden, you replace the arrow to point to the new method implementation. This overrides the method implementation.

Figure 4.6. Overriding greet()V

graphics/04fig06.gif

Whenever you want to call a method, you go straight to the class definition for the receiver object and look up the name and descriptor. This gives you the exact method implementation to use, without having to look at any of the superclasses.

Suppose we added a new class that is a subclass of Hola:

 .class AlternateHola .super Hola .method hashCode()I ;; A new implementation of hashCode .end method .end class 

The vtable for this class would contain exactly the same elements as for Hola, except that the arrow from hashCode would point to the new implementation. The classes Hello and Hola would be unchanged. This is one of the key features of the JVM: you can do whatever you like in subclasses without having to make any changes to the implementations of the superclasses.

An advantage of looking at classes this way is that you know that the greet method will always appear in the same place on the list, whether it's an Hola or a Hello or even an AlternateHola. This means that the JVM can call the method without having to do any name comparisons at all. This advantage is purely internal to the JVM, because it's up to the JVM to organize its memory in a way that best suits it. You can continue to arrange the methods in your class in any order at all, without affecting the correctness of your class.

4.5.3 Overloading and Method Matching

When the JVM goes searching for the method implementation in superclasses, it is looking only at the superclasses of the receiver object. It does not consider the superclasses or subclasses of the arguments. It looks only at an exact match of the method descriptor. You may even have two or more methods with the same name but different descriptors.

For example, suppose you have the following class definition:

 .class Printer .method print(Ljava/lang/Object;)V getstatic java/lang/System/out Ljava/io/PrintStream; ldc "Inside Printer/print(Ljava/lang/Object;)V" invokevirtual java/io/PrintStream/println(Ljava/lang/String;)V return .end method .method print(Ljava/lang/String;)V getstatic java/lang/System/out Ljava/io/PrintStream; ldc "Inside Printer/print(Ljava/lang/String;)V" invokevirtual java/io/PrintStream/println(Ljava/lang/String;)V return .end method 

Suppose you have a Hello object that you want to print using a Printer object. You can't say:

 invokevirtual Printer/print(LHello;)V               ; FOUL! 

because there isn't any method implementation for print that takes a Hello object. Instead, you must write

 invokevirtual Printer/print(Ljava/lang/Object;)V    ; OK 

This can be the cause of some confusion. You might create a special subclass of Printer that can print Hello objects:

 .class HelloPrinter .super Printer .method print(LHello;)V getstatic java/lang/System/out Ljava/io/PrintStream; ldc "Inside HelloPrinter/print(LHello;)V" invokevirtual java/io/PrintStream/println(Ljava/lang/String;)V return .end method 

Suppose you have a HelloPrinter in local variable 0 and a Hello in variable 1. What do you think you get from

 aload_0                   ; Push the HelloPrinter aload_1                   ; Push the Hello invokevirtual Printer/print(Ljava/lang/Object;)V 

The answer is that it prints

 Inside Printer/print(Ljava/lang/Object;)V 

It did not find the special subclass implementation of print that we defined for Hello objects, because it was looking only within Printer (and the superclasses of Printer) for a method named print that takes an Object and returns void. In order to use the new method implementation, we'd have to write

 aload_0                   ; Push the HelloPrinter aload_1                   ; Push the Hello invokevirtual HelloPrinter/print(LHello;)V 

This code assumes that variable 0 contains a reference to a HelloPrinter object. That would be true if the instruction immediately previous to this code was a getfield whose type descriptor was LHelloPrinter;, followed by an astore_0.

If the verifier cannot prove that variable 0 contains a HelloPrinter, then the code is rejected. Section 9.5 describes a technique for tracing through the code and observing the effect each instruction has on the stack and the local variable array. The verification algorithm looks only at the types of objects, not their actual values, which is why you can verify the program without actually running it.

The Java compiler actually has to do a fair bit of work to figure out the appropriate arguments for the invokevirtual instruction. It must look at all the arguments and find out which of the available implementations best fits them all. How to do this is described in chapter 10.

4.5.4 Invoking a Method Directly

The invokespecial instruction can be used to bypass the virtual dispatch mechanism used by invokevirtual. Suppose you have an Hola object in local variable 0:

 aload_0               ; Push an Hola object invokespecial Hello/greet ()V aload_0               ; Push it again invokevirtual Hello/greet ()V 

The output reads

 Hello, world Hola, mundo 

In the first case the JVM invokes Hello/greet directly, without involving the virtual dispatch mechanism. The method implementation invoked is precisely the one you asked for. In the second case the virtual dispatch mechanism is involved, so it uses the implementation of Hola/greet instead.

This is something you can't usually do in Java. The Java compiler almost always uses invokevirtual. One exception to this rule is when you use the super keyword within the subclass:

 /** This method is part of Hola */ void greetInEnglish() {    super.greet();          // Call greet in Hello } 

When this method is compiled, it produces code equivalent to this in Oolong:

 .method greetInEnglish()V aload_0 invokespecial Hello/greet ()V return .end method 

The invokespecial instruction is used here to bypass the virtual dispatch mechanism, ensuring that the correct method implementation is called. If the code used invokevirtual, it would print the greeting in Spanish instead of English, since the virtual dispatch mechanism would be used.

The invokespecial instruction is also used by Java compilers to implement calls to private methods. Since private methods can be invoked only by the class itself, they are not inherited by subclasses. This means that there's no reason to go through the trouble of dispatching the invocation virtually. Consider this class:

 class Bonjour extends Hello {    private void saluent()    {        System.out.println("Bonjour, le monde");    }    void greet()    {        saluent();    } } 

Within the greet method, only this saluent method can be called. It isn't possible to override saluent in a subclass of Bonjour, because private methods aren't inherited the way public, protected, and package-access methods are. The private methods are truly private to the class that defines them, and they are seen nowhere else.

Because the Java compiler can tell exactly which method implementation will be used, it can use invokespecial to call it. The greet method compiles into code equivalent to the following code in Oolong:

 .method greet()V aload_0 invokespecial Bonjour/saluent()V return .end method 

4.5.5 invokespecial and super

Actually, the above description of invokespecial is a little too simplistic. In order to increase the robustness of Java programs whose base classes change, the meaning of invokespecial is a little different when invoked on an object whose class is marked super.

Here, super refers to the super keyword in the .class directive. In this code

 .class super Cheddar .super Cheese 

the class Cheddar is marked super. A Java compiler automatically marks all classes as super, so the behavior discussed in this section applies to all classes compiled from Java.

When invokespecial is used to invoke a method in a superclass of the current class and the receiver is an instance of a class which is super, then invocation actually occurs using a variant of the virtual dispatch mechanism. The JVM starts looking for the implementation of the method in the superclass of the current class.

Take a look at these three Oolong class definitions:

 .class super Bicycle .method tuneUp ()V getstatic java/lang/System/out Ljava/io/PrintStream; ldc "Tune up a bicycle" invokevirtual java/io/PrintStream/println(Ljava/lang/String;)V return .end method .end class .class super MountainBike .super Bicycle ; This method did not exist when DownhillMountainBike was written .method tuneUp ()V aload_0 invokespecial Bicycle/tuneUp()V getstatic java/lang/System/out Ljava/io/PrintStream; ldc "Tune up mountain bike features" invokevirtual java/io/PrintStream/println(Ljava/lang/String;)V return .end method .end class .class super DownhillMountainBike .super MountainBike .method tuneUp ()V aload_0 invokespecial Bicycle/tuneUp()V getstatic java/lang/System/out Ljava/io/PrintStream; ldc "Tune up downhill mountain bike features" invokevirtual java/io/PrintStream/println(Ljava/lang/String;)V return .end method .end class 

In each class, the tuneUp method begins with the generic Bicycle tune-up, then follows it up with special features for the specific kind of Bicycle.

When these classes were originally written, there was no tuneUp method in MountainBike, so the DownhillMountainBike tries to call the Bicycle implementation of tuneUp directly, missing out on the MountainBike tuneUp. The JVM recognizes that this sort of thing happens, so it doesn't actually invoke the Bicycle implementation of tuneUp, even though that's the method named in invokespecial. Instead, it does the equivalent of a virtual dispatch, starting from MountainBike, the immediate superclass of DownhillMountainBike. It finds an implementation of tuneUp with the appropriate parameters in MountainBike, so it calls that method, which proceeds to do a generic Bicycle tune-up before proceeding with its own tune-up.

Exercise 4.5

Using the results of exercise 4.1, add a field color to Dinosaur. The field stores a String, which is the name of the color. Add two methods called setColor. One takes a String and sets the color field to that string. The other takes a java.awt.Color and sets the color field to the result of calling toString on the argument.

Exercise 4.6

Given that there is a Dinosaur object in variable 0, set its color to java.awt.Color.green (a static field). Then set its color to the String "green".



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