Java Binary Compatibility Part 3: The Technical Details
To understand in detail how binary compatibility works, it helps to understand exactly how Java programs are represented in class files, and what class files meansince the class files are the actual implementation of your Java program. This installment looks at how Java class files work, and in particular how late binding is implemented.
Late Binding by Example
Here's a piece of code to figure out what wine I should drink with dinner tonight, and at what temperature the wine should be served (so that I can begin chilling it on time). Let's start by assuming some wine-oriented classes:
class Sommelier { Wine recommend(String meal) { ... } } abstract class Wine { // Make a recommendation for appropriate wine serving temperature abstract float temperature(); } class RedWine extends Wine { // Reds in general are served warmer than whites float temperature() { return 63; } } class WhiteWine extends Wine { float temperature() { return 47; } } // And so on for a variety of wines class Bordeaux extends RedWine { float temperature() { return 64; } } class Riesling extends WhiteWine { // Inherit the temperature from the WhiteWine class } We'll use these classes to make some recommendations for dinner: void example1() { Wine wine = sommelier.recommend("duck"); float temp = wine.temperature(); }
In the second call in example1, the only thing we know about the wine object is that it's a Wine. It could be a Bordeaux or a Riesling or something else. We know it can't be an instance Wine itself, since that class is abstract.
When you compile the call to wine.temperature(), the class file contains a line that looks like this:
invokevirtual Wine/temperature ()F
NOTE
Notice that the class file contains a binary representation of this code, rather than this actual text. There is no one standard textual representation of Java class files. I'm using one called Oolong. You can read more about it in my book Programming for the Java Virtual Machine.
This is a method calla regular (virtual) method call, as opposed to a static method callthat calls the temperature on a Wine object. The argument on the right, ()F, is called the signature. This signature indicates a method with no arguments (that's what the empty parentheses mean) and returns a floating-point value (the F).
When the Java Virtual Machine (JVM) reaches this statement, it won't necessarily invoke the definition of temperature in Wine. In this case, it couldn't anyway, because the method is abstract. Rather, the JVM looks at the class of the object and seeks a method with the exact name and signature given in the invokevirtual statement. If none exists, it looks at the superclass, the super-superclass, and so on until an implementation is found.
Mostly this is just like Java method calls. However, this is somewhat simpler, in that it's looking for a simple string match on the name and signature. It doesn't consider subtypes and supertypes of the classes mentioned; only an exact match will do. In this example, the method signature only mentions the built-in type float, so there wouldn't be subclasses to consider, but we'll get to a more complicated example shortly.
In this case, if the object is a Bordeaux, the JVM calls the temperature()F method in that class, which returns 64. If the object is a Riesling, it seeks the method and doesn't find one, so it looks in the WhiteWine class. It finds a temperature()F method there and invokes it, which returns 47.