- Late Binding by Example
- Implications for Binary Compatibility
- Overloading and Overriding
- Deleting Overridden and Overloaded Methods
- Fields
- Conclusion
Deleting Overridden and Overloaded Methods
If you delete fetchGlass(RedWine) and try to run example2 without recompiling, you'll get this error:
java.lang.NoSuchMethodError:
Sommelier.fetchGlass
(LRedWine;)LGlass;
This is the actual error message from the JDK 1.4 JVM. It's helpful to be able to read the JVM signature coding, because occasionally it prints out Java class file implementation details like this, even if you're writing Java and not messing around with the class files themselves.
With that method deleted, you could successfully recompile example2 without modification, in which case it will produce the same invokevirtual instruction for both calls:
invokevirtual Sommelier/fetchGlass (LWine;)LGlass;
But if you put back the fetchGlass(RedWine) method, it won't be used until you recompile example2. The JVM will use the fetchGlass(wine) example, for the same reason it didn't use the fetchGlass(WhiteWine) code when passed a Riesling: It didn't know about the existence of a method at compile time, so it selected the more general method. That method has a different signature, and to the JVM that's just as different as having a different name.
JVM ignored the Wine here:
invokevirtual Wine/temperature ()F
and looked up method implementation in the actual object, but cares very much about the RedWine here:
invokevirtual Sommelier/fetchGlass (LRedWine;)LGlass;
The difference is that the first Wine isn't actually part of the method signature. It's used for type checking before the invocation is made. RedWine is part of the type method signature, and the JVM looks up the methods based on the textual representation of the signature, along with the method name.
NOTE
In fact, the Wine argument of invokevirtual Wine/temperature ()F isn't completely ignored; it's used during type checking and verification of the file. But it's not relevant to the actual invocation.
Note that this principle applies to return types as well as to argument types. Suppose we add a method to Sommelier:
class RedWineGlass extends Glass { ... } RedWineGlass fetchGlass(RedWine wine) { ... }
Let's consider the original compilation of example2, where it calls fetchGlass with this:
invokevirtual Sommelier/fetchGlass (LRedWine;)LGlass;
The new method won't be used because RedWineGlass is not the same as Glass. However, if we compile example2 again, the invocation for the Bordeaux case will be as follows:
invokevirtual Sommelier/fetchGlass (LRedWine;)LRedWineGlass;
This is legal, since a RedWineGlass is a subclass of Glass, and the JVM will cast appropriately at runtime.
We can make things even more complicated, using both overriding and overloading with a subclass of the Sommelier class, but I think you get the idea by now. Here are the rules, in summary:
The Java compiler selects the method signature that best matches the classes that it knows about at compile time.
At runtime, the JVM seeks out a method with the exact method name and signature. Methods with similar names and signatures are ignored.
The JVM looks at the class of the actual object and the superclasses of that class. It picks the first one it finds, even if that's not the same one it would have found at compile time.
If no method is found, the JVM throws an exception and the class isn't loaded.
Even if additional methods are found that would have been better matches at runtime, they're ignored because they won't have the same signature.
Overloading is resolved at compile time. Overriding is resolved at runtime.