CHAPTER 13: Binary Compatibility Previous
Previous
Java Language
Java Language
Index
Index
Next
Next

13.4 Evolution of Classes

13.4.1 abstract Classes , 13.4.2 final Classes , 13.4.3 public Classes , 13.4.4 Superclasses and Superinterfaces , 13.4.5 Class Body and Member Declarations , 13.4.6 Access to Members and Constructors , 13.4.7 Field Declarations , 13.4.8 final Fields and Constants , 13.4.9 static Fields , 13.4.10 transient Fields , 13.4.11 volatile Fields , 13.4.12 Method and Constructor Declarations , 13.4.13 Method and Constructor Parameters , 13.4.14 Method Result Type , 13.4.15 abstract Methods , 13.4.16 final Methods , 13.4.17 native Methods , 13.4.18 static Methods , 13.4.19 synchronized Methods , 13.4.20 Method and Constructor Throws , 13.4.21 Method and Constructor Body , 13.4.22 Method and Constructor Overloading , 13.4.23 Method Overriding , 13.4.24 Static Initializers

This section describes the effects of changes to the declaration of a class and its members and constructors on pre-existing binaries.


13.4.1 abstract Classes

If a class that was not abstract is changed to be declared abstract , then pre- existing binaries that attempt to create new instances of that class will throw either an InstantiationError at link time, or an InstantiationException at run time (if the method newInstance (S20.3.6) of class Class is used); such a change is therefore not recommended for widely distributed classes.

Changing a class that was declared abstract to no longer be declared abstract does not break compatibility with pre-existing binaries.


13.4.2 final Classes

If a class that was not declared final is changed to be declared final , then a VerifyError is thrown if a binary of a pre-existing subclass of this class is loaded, because final classes can have no subclasses; such a change is not recommended for widely distributed classes.

Changing a class that was declared final to no longer be declared final does not break compatibility with pre-existing binaries.


13.4.3 public Classes

Changing a class that was not declared public to be declared public does not break compatibility with pre-existing binaries.

If a class that was declared public is changed to not be declared public , then an IllegalAccessError is thrown if a pre-existing binary is linked that needs but no longer has access to the class type; such a change is not recommended for widely distributed classes.


13.4.4 Superclasses and Superinterfaces

A ClassCircularityError is thrown at load time if a class would be a superclass of itself. Changes to the class hierarchy that could result in such a circularity when newly compiled binaries are loaded with pre-existing binaries are not recommended for widely distributed classes.

Changing the direct superclass or the set of direct superinterfaces of a class type will not break compatibility with pre-existing binaries, provided that the total set of superclasses or superinterfaces, respectively, of the class type loses no members.

Changes to the set of superclasses of a class will not break compatibility with pre-existing binaries simply because of uses of class variables and class methods. This is because uses of class variables and class methods are resolved at compile time to symbolic references to the name of the class that declares them. Such uses therefore depend only on the continuing existence of the class declaring the variable or method, not on the shape of the class hierarchy.

If a change to the direct superclass or the set of direct superinterfaces results in any class or interface no longer being a superclass or superinterface, respectively, then link-time errors may result if pre-existing binaries are loaded with the binary of the modified class. Such changes are not recommended for widely distributed classes. The resulting errors are detected by the verifier of the Java Virtual Machine when an operation that previously compiled would violate the type system. For example, suppose that the following test program:

class Hyper { char h = 'h'; } 
class Super extends Hyper { char s = 's'; }
class Test extends Super {
    public static void main(String[] args) {
        Hyper h = new Super();
        System.out.println(h.h);
    }
}

is compiled and executed, producing the output:

h

Suppose that a new version of class Super is then compiled:

class Super { char s = 's'; }

This version of class Super is not a subclass of Hyper . If we then run the existing binaries of Hyper and Test with the new version of Super , then a VerifyError is thrown at link time. The verifier objects because the result of new Super() cannot be assigned to a variable of type Hyper , because Super is not a subclass of Hyper .

It is instructive to consider what might happen without the verification step: the program might run and print:

s

This demonstrates that without the verifier the type system could be defeated by linking inconsistent binary files, even though each was produced by a correct Java compiler.

As a further example, here is an implementation of a cast from a reference type to int , which could be made to run in certain implementations of Java if they failed to perform the verification process. Assume an implementation that uses method dispatch tables and whose linker assigns offsets into those tables in a sequential and straightforward manner. Then suppose that the following Java code is compiled:

class Hyper { int zero(Object o) { return 0; } }
class Super extends Hyper { int peek(int i) { return i; }  }


class Test extends Super {
	public static void main(String[] args) throws Throwable {
		Super as = new Super();
		System.out.println(as);
		System.out.println(Integer.toHexString(as.zero(as)));
	}
}

The assumed implementation determines that the class Super has two methods: the first is method zero inherited from class Hyper , and the second is the method peek . Any subclass of Super would also have these same two methods in the first two entries of its method table. (Actually, all these methods would be preceded in the method tables by all the methods inherited from class Object but, to simplify the discussion, we ignore that here.) For the method invocation as.zero(as) , the compiler specifies that the first method of the method table should be invoked; this is always correct if type safety is preserved.

If the compiled code is then executed, it prints something like:


Super@ee300858
0

which is the correct output. But if a new version of Super is compiled, which is the same except for the extends clause:

class Super { int peek(int i) { return i; }  }

then the first method in the method table for Super will now be peek , not zero . Using the new binary code for Super with the old binary code for Hyper and Test will cause the method invocation as.zero(as) to dispatch to the method peek in Super , rather than the method zero in Hyper . This is a type violation, of course; the argument is of type Super but the parameter is of type int . With a few plausible assumptions about internal data representations and the consequences of the type violation, execution of this incorrect program might produce the output:


Super@ee300848
ee300848

A poke method, capable of altering any location in memory, could be concocted in a similar manner. This is left as an exercise for the reader.

The lesson is that a implementation of Java that lacks a verifier or fails to use it will not maintain type safety and is, therefore, not a valid Java implementation.


13.4.5 Class Body and Member Declarations

No incompatibility with pre-existing binaries is caused by adding a class member that has the same name (for fields) or same name, signature, and return type (for methods) as a member of a superclass or subclass. References to the original field or method were resolved at compile time to a symbolic reference containing the name of the class in which they were declared. This makes compiled Java code more robust against changes than it might otherwise be. No error occurs even if the set of classes being linked would encounter a compile-time error. As an example, if the program:

class Hyper { String h = "Hyper"; }
class Super extends Hyper { }
class Test extends Super {
	public static void main(String[] args) {
		String s = new Test().h;
		System.out.println(s);
	}
}

is compiled and executed, it produces the output:

Hyper

Suppose that a new version of class Super is then compiled:

class Super extends Hyper { char h = 'h'; }

If the resulting binary is used with the existing binaries for Hyper and Test , then the output is still:

Hyper

even though compiling the source for these binaries:

class Hyper { String h = "Hyper"; }
class Super extends Hyper { char h = 'h'; }
class Test extends Super {
	public static void main(String[] args) {
		String s = new Test().h;
		System.out.println(s);
	}
}

would result in a compile-time error, because the h in the source code for main would now be construed as referring to the char field declared in Super , and a char value can't be assigned to a String .

Deleting a class member or constructor that is not declared private may cause a linkage error if the member or constructor is used by a pre-existing binary, even if the member was an instance method that was overriding a superclass method. This is because, during resolution, the linker looks only in the class that was identified at compile time. Thus, if the program:


class Hyper {
	void hello() { System.out.println("hello from Hyper"); }
}

class Super extends Hyper {
	void hello() { System.out.println("hello from Super"); }
}

class Test {
	public static void main(String[] args) {
		new Super().hello();
	}
}

is compiled and executed, it produces the output:

hello from Super

Suppose that a new version of class Super is produced:

class Super extends Hyper { }

If Super and Hyper are recompiled but not Test , then a NoSuchMethodError will result at link time, because the method hello is no longer declared in class Super .

To preserve binary compatibility, methods should not be deleted; instead, "forwarding methods" should be used. In our example, replacing the declaration of Super with:


class Super extends Hyper {
	void hello() { super.hello(); }
}

then recompiling Super and Hyper and executing these new binaries with the original binary for Test , produces the output:

hello from Hyper

as might have naively been expected from the previous example.

The super keyword can be used to access a method declared in a superclass, bypassing any methods declared in the current class. The expression:

super.Identifier

is resolved, at compile time, to a method M declared in a particular superclass S. The method M must still be declared in that class at run time or a linkage error will result. If the method M is an instance method, then the method MR invoked at run time is the method with the same signature as M that is a member of the direct superclass of the class containing the expression involving super . Thus, if the program:


class Hyper {
	void hello() { System.out.println("hello from Hyper"); }
}
class Super extends Hyper { }
class Test extends Super {

	public static void main(String[] args) {
		new Test().hello();
	}

	void hello() {
		super.hello();
	}
}

is compiled and executed, it produces the output:

hello from Hyper

Suppose that a new version of class Super is produced:


class Super extends Hyper {
	void hello() { System.out.println("hello from Super"); }
}

If Super and Hyper are recompiled but not Test , then running the new binaries with the existing binary of Test produces the output:

hello from Super

as you might expect. (A flaw in some early versions of Java caused them to print:

hello from Hyper

incorrectly.)


13.4.6 Access to Members and Constructors

Changing the declared access of a member or constructor to permit less access may break compatibility with pre-existing binaries, causing a linkage error to be thrown when these binaries are resolved. Less access is permitted if the access modifier is changed from default access to private access; from protected access to default or private access; or from public access to protected , default, or private access. Changing a member or constructor to permit less access is therefore not recommended for widely distributed classes.

Perhaps surprisingly, Java is defined so that changing a member or constructor to be more accessible does not cause a linkage error when a subclass (already) defines a method to have less access. So, for example, if the package points defines the class Point :

package points;


public class Point {
	public int x, y;
	protected void print() {
		System.out.println("(" + x + "," + y + ")");
	}
}

used by the Test program:


class Test extends points.Point {
	protected void print() { System.out.println("Test"); }
	public static void main(String[] args) {
		Test t = new Test();
		t.print();
	}
}

then these classes compile and Test executes to produce the output:

Test

If the method print in class Point is changed to be public , and then only the Point class is recompiled, and then executed with the previously existing binary for Test then no linkage error occurs, even though it is improper, at compile time, for a public method to be overridden by a protected method (as shown by the fact that the class Test could not be recompiled using this new Point class unless print were changed to be public .)

Allowing superclasses to change protected methods to be public without breaking binaries of preexisting subclasses helps make Java binaries less fragile. The alternative, where such a change would cause a linkage error, would create additional binary incompatibilities with no apparent benefit.


13.4.7 Field Declarations

Adding a field to a class will not break compatibility with any pre-existing binaries that are not recompiled, even in the case where a class could no longer be recompiled because a field access previously referenced a field of a superclass with an incompatible type. The previously compiled class with such a reference will continue to reference the field declared in a superclass. Thus compiling and executing the code:

class Hyper { String h = "hyper"; }
class Super extends Hyper { String s = "super"; }
class Test {
	public static void main(String[] args) {
		System.out.println(new Super().h);
	}
}

produces the output:

hyper

Changing Super to be defined as:


class Super extends Hyper {
	String s = "super";
	int h = 0;
}

recompiling Hyper and Super , and executing the resulting new binaries with the old binary of Test produces the output:

hyper

The field h of Hyper is output by the original binary of main no matter what type field h is declared in Super . While this may seem surprising at first, it serves to reduce the number of incompatibilities that occur at run time. (In an ideal world, all source files that needed recompilation would be recompiled whenever any one of them changed, eliminating such surprises. But such a mass recompilation is often impractical or impossible, especially in the Internet. And, as was previously noted, such recompilation would sometimes require further changes to the source code.)

Deleting a field from a class will break compatibility with any pre-existing binaries that reference this field, and a NoSuchFieldError will be thrown when such a reference from a pre-existing binary is linked. Only private fields may be safely deleted from a widely distributed class.


13.4.8 final Fields and Constants

If a field that was not final is changed to be final , then it can break compatibility with pre-existing binaries that attempt to assign new values to the field. For example, if the program:

class Super { static char s; }


class Test extends Super {
	public static void main(String[] args) {
		s = 'a';
		System.out.println(s);
	}
}

is compiled and executed, it produces the output:

a

Suppose that a new version of class Super is produced:

class Super { static char s; }

If Super is recompiled but not Test , then running the new binary with the existing binary of Test results in a IncompatibleClassChangeError . (In certain early implementations of Java this example would run without error, because of a flaw in the implementation.)

We call a field that is static , final , and initialized with a compile-time constant expression a primitive constant. Note that all fields in interfaces are implicitly static and final , and they are often, but not always, constants.

If a field is not a primitive constant, then deleting the keyword final or changing the value to which the field is initialized does not break compatibility with existing binaries.

If a field is a primitive constant, then deleting the keyword final or changing its value will not break compatibility with pre-existing binaries by causing them not to run, but they will not see any new value for the constant unless they are recompiled. If the example:

class Flags { final static boolean debug = true; }


class Test {
	public static void main(String[] args) {
		if (Flags.debug)
			System.out.println("debug is true");
	}
}

is compiled and executed, it produces the output:

debug is true

Suppose that a new version of class Flags is produced:

class Flags { final static boolean debug = false; }

If Flags is recompiled but not Test , then running the new binary with the existing binary of Test produces the output:

debug is true

because the value of debug was a compile-time primitive constant, and could have been used in compiling Test without making a reference to the class Flags .

This behavior would not change if Flags were changed to be an interface, as in the modified example:

interface Flags { boolean debug = true; }
class Test {
	public static void main(String[] args) {
		if (Flags.debug)
			System.out.println("debug is true");
	}
}

(One reason for requiring inlining of primitive constants is that Java switch statements require constants on each case , and no two such constant values may be the same. Java checks for duplicate constant values in a switch statement at compile time; the class file format does not do symbolic linkage of case values.)

The best way to avoid problems with "inconstant constants" in widely-distributed code is to declare as primitive constants only values which truly are unlikely ever to change. Many primitive constants in interfaces are small integer values replacing enumerated types, which Java does not support; these small values can be chosen arbitrarily, and should not need to be changed. Other than for true mathematical constants, we recommend that Java code make very sparing use of class variables that are declared static and final . If the read-only nature of final is required, a better choice is to declare a private static variable and a suitable accessor method to get its value. Thus we recommend:

private static int N;
public static int getN() { return N; }

rather than:

public static final int N = ...;

There is no problem with:

public static int N = ...;

if N need not be read-only. We also recommend, as a general rule, that only truly constant values be declared in interfaces. We note, but do not recommend, that if a field of primitive type of an interface may change, its value may be expressed idiomatically as in:


interface Flags {
	boolean debug = new Boolean(true).booleanValue();
}

insuring that this value is not a constant. Similar idioms exist for the other primitive types.

One other thing to note is that static final fields that have constant values (whether of primitive or String type) must never appear to have the default initial value for their type (S4.5.4). This means that all such fields appear to be initialized first during class initialization (S8.3.2.1, S9.3.1, S12.4.2).


13.4.9 static Fields

If a field that is not declared private was not declared static and is changed to be declared static , or vice versa, then a linkage time error, specifically an IncompatibleClassChangeError , will result if the field is used by a preexisting binary which expected a field of the other kind. Such changes are not recommended in code that has been widely distributed.


13.4.10 transient Fields

Adding or deleting a transient modifier of a field does not break compatibility with pre-existing binaries.


13.4.11 volatile Fields

If a field that is not declared private was not declared volatile and is changed to be declared volatile , or vice versa, then a linkage time error, specifically an IncompatibleClassChangeError , may result if the field is used by a preexisting binary that expected a field of the opposite volatility. Such changes are not recommended in code that has been widely distributed.


13.4.12 Method and Constructor Declarations

Adding a method or constructor declaration to a class will not break compatibility with any pre-existing binaries, even in the case where a type could no longer be recompiled because a method invocation previously referenced a method of a superclass with an incompatible type. The previously compiled class with such a reference will continue to reference the method declared in a superclass.

Deleting a method or constructor from a class will break compatibility with any pre-existing binary that referenced this method or constructor; a NoSuchMethodError will be thrown when such a reference from a pre-existing binary is linked. Only private methods or constructors may be safely deleted from a widely distributed class.

If the source code for a class contains no declared constructors, the Java compiler automatically supplies a constructor with no parameters. Adding one or more constructor declarations to the source code of such a class will prevent this default constructor from being supplied automatically, effectively deleting a constructor, unless one of the new constructors also has no parameters, thus replacing the default constructor. The automatically supplied constructor with no parameters is given the same access modifier as the class of its declaration, so any replacement should have as much or more access if compatibility with pre-existing binaries is to be preserved.


13.4.13 Method and Constructor Parameters

Changing the name of a formal parameter of a method or constructor does not impact pre-existing binaries. Changing the name of a method, the type of a formal parameter to a method or constructor, or adding a parameter to or deleting a parameter from a method or constructor declaration creates a method or constructor with a new signature, and has the combined effect of deleting the method or constructor with the old signature and adding a method or constructor with the new signature (see S13.4.12).


13.4.14 Method Result Type

Changing the result type of a method, replacing a result type with void , or replacing void with a result type has the combined effect of deleting the old method or constructor and adding a new method or constructor with the new result type or newly void result (see S13.4.12).


13.4.15 abstract Methods

Changing a method that is declared abstract to no longer be declared abstract does not break compatibility with pre-existing binaries.

Changing a method that is not declared abstract to be declared abstract will break compatibility with pre-existing binaries that previously invoked the method, causing an AbstractMethodError . If the example program:


class Super { void out() { System.out.println("Out"); } }

class Test extends Super {
	public static void main(String[] args) {
		Test t = new Test();
		System.out.println("Way ");
		t.out();
	}
}

is compiled and executed, it produces the output:


Way
Out

Suppose that a new version of class Super is produced:


abstract class Super {
	abstract void out();
}

If Super is recompiled but not Test , then running the new binary with the existing binary of Test results in a AbstractMethodError , because class Test has no implementation of the method out , and is therefore is (or should be) abstract. (An early version of Java incorrectly produced the output:

Way

before encountering an AbstractMethodError while invoking the method out , incorrectly allowing the class Test to be prepared even though it has an abstract method and is not declared abstract .)


13.4.16 final Methods

Changing an instance method that is not final to be final may break compatibility with existing binaries that depend on the ability to override the method. If the test program:

class Super { void out() { System.out.println("out"); } }
class Test extends Super {


	public static void main(String[] args) {
		Test t = new Test();
		t.out();
	}
	void out() { super.out(); }
}

is compiled and executed, it produces the output:

out

Suppose that a new version of class Super is produced:

class Super { final void out() { System.out.println("!"); } }

If Super is recompiled but not Test , then running the new binary with the existing binary of Test results in a VerifyError because the class Test improperly tries to override the instance method out .

Changing a class (static ) method that is not final to be final does not break compatibility with existing binaries, because the class of the actual method to be invoked is resolved at compile time.

Removing the final modifier from a method does not break compatibility with pre-existing binaries.


13.4.17 native Methods

Adding or deleting a native modifier of a method does not break compatibility with pre-existing binaries.

The impact of changes to Java types on preexisting native methods that are not recompiled is beyond the scope of this specification and should be provided with the description of an implementation of Java. Implementations are encouraged, but not required, to implement native methods in a way that limits such impact.


13.4.18 static Methods

If a method that is not declared private was declared static (that is, a class method) and is changed to not be declared static (that is, to an instance method), or vice versa, then compatibility with pre-existing binaries may be broken, resulting in a linkage time error, namely an IncompatibleClassChangeError , if these methods are used by the pre-existing binaries. Such changes are not recommended in code that has been widely distributed.


13.4.19 synchronized Methods

Adding or deleting a synchronized modifier of a method does not break compatibility with existing binaries.


13.4.20 Method and Constructor Throws

Changes to the throws clause of methods or constructors do not break compatibility with existing binaries; these clauses are checked only at compile time.

We are considering whether a future version of the Java language should require more rigorous checking of throws clauses when classes are verified.


13.4.21 Method and Constructor Body

Changes to the body of a method or constructor do not break compatibility with pre-existing binaries.

We note that a compiler cannot inline expand a method at compile time unless, for example, either:

The keyword final on a method does not mean that the method can be safely inlined; it only means that the method cannot be overridden. Unless the compiler has extraordinary knowledge, it is still possible that a new version of that method will be provided at link time.

In general we suggest that Java implementations use late-bound (run-time) code generation and optimization.


13.4.22 Method and Constructor Overloading

Adding new methods that overload existing method names does not break compatibility with pre-existing binaries. The method signature to be used for each method invocation was determined when these existing binaries were compiled; therefore newly added methods will not be used, even if their signatures are both applicable and more specific than the method signature originally chosen.

While adding a new overloaded method or constructor may cause a compile-time error the next time a class or interface is compiled because there is no method or constructor that is most specific (S15.11.2.2), no such error occurs when a Java program is executed, because no overload resolution is done at execution time.

If the example program:


class Super {
	static void out(float f) { System.out.println("float"); }
}

class Test {
	public static void main(String[] args) {
		Super.out(2);
	}
}

is compiled and executed, it produces the output:

float

Suppose that a new version of class Super is produced:


class Super {
	static void out(float f) { System.out.println("float"); }
	static void out(int i) { System.out.println("int"); }
}

If Super is recompiled but not Test , then running the new binary with the existing binary of Test still produces the output:

float

However, if Test is then recompiled, using this new Super , the output is then:

int

as might have been naively expected in the previous case.


13.4.23 Method Overriding

If an instance method is added to a subclass and it overrides a method in a superclass, then the subclass method will be found by method invocations in pre-existing binaries, and these binaries are not impacted. If a class method is added to a class, then this method will not be found, because the invocation of a class method is resolved at compile time to use the fully qualified name of the class where the method is declared. Thus if the example:


class Hyper {
	void hello() { System.out.print("Hello, "); }
	static void world() { System.out.println("world!"); }
}
class Super extends Hyper { }

class Test {
	public static void main(String[] args) {
		Super s = new Super();
		s.hello();
		s.world();
	}
}

is compiled and executed, it produces the output:

Hello, world!

Suppose that a new version of class Super is produced:


class Super extends Hyper {
	void hello() { System.out.print("Goodbye, cruel "); }
	static void world() { System.out.println("earth!"); }
}

If Super is recompiled but not Hyper or Test , then running the new binary with the existing binaries for Hyper and Test will produce the output:

Goodbye, cruel world!

This example demonstrates that the invocation in:

s.world();

in the method main is resolved, at compile time, to a symbolic reference to the class containing the class method world , as though it had been written:

Hyper.world();

This is why the world method of Hyper rather than Super is invoked in this example. Of course, recompiling all the classes to produce new binaries will allow the output:

Goodbye, cruel earth!

to be produced.


13.4.24 Static Initializers

Adding, deleting, or changing a static initializer (S8.5) of a class does not impact pre-existing binaries.

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