CHAPTER 13
Java development tools should support automatic recompilation as necessary
whenever source code is available. Particular implementations of Java may also
store the source and binary of types in a versioning database and implement a
ClassLoader
(§20.14) that uses integrity mechanisms of the database to prevent
linkage errors by providing binary-compatible versions of types to clients.
Developers of packages and classes that are to be widely distributed face a different set of problems. In the Internet, which is our favorite example of a widely distributed system, it is often impractical or impossible to automatically recompile the pre-existing binaries that directly or indirectly depend on a type that is to be changed. Instead, Java defines a set of changes that developers are permitted to make to a package or to a class or interface type while preserving (not breaking) compatibility with existing binaries.
The paper quoted above appears in Proceedings of OOPSLA '95, published as ACM SIGPLAN Notices, Volume 30, Number 10, October 1995, pages 426-438. Within the framework of that paper, Java binaries are binary (release-to-release) compatible under all relevant transformations that the authors identify. Using their scheme, here is a list of some important binary compatible changes that Java supports:
private
fields, methods, or constructors of a class or interface.
We encourage Java development systems to provide facilities that alert developers to the impact of changes on pre-existing binaries that cannot be recompiled.
This chapter first specifies some properties that any Java binary format must have (§13.1). It next defines binary compatibility, explaining what it is and what it is not (§13.2). It finally enumerates a large set of possible changes to packages (§13.3), classes (§13.4) and interfaces (§13.5), specifying which changes are guaranteed to preserve binary compatibility and which are not.
class
file format specified
by the The Java Virtual Machine Specification, this specification does not
mandate the use of any specific binary file format. Rather, it specifies properties
that any binary format for compiled types must obey. A number of these properties
are specifically chosen to support source code transformations that preserve
binary compatibility.
static
, final
, and initialized with compile-time constant expressions are resolved at compile time to the constant value that is denoted. No reference to such a constant field should be present in the code in a binary file (except in the class or interface containing the constant field, which will have code to initialize it), and such constant fields must always appear to have been initialized; the default initial value for the type of such a field must never be observed. See §13.4.8 for a discussion.
void
and does not return a value. The signature of a method must include all of the following:
java.lang.Object
, then a symbolic reference to the direct superclass of this class
private
declared in the class or interface, given as the simple name of the field and a symbolic reference to the type of the field
private
declared in the class or interface, its signature and return type, as described above
The following sections specify the changes that may be made to class and interface type declarations without breaking compatibility with pre-existing binaries. The Java Virtual Machine and its standard class
file format support these changes; other Java binary formats are required to support these changes as well.
As described in §13.1, symbolic references to methods and fields name the exact class or interface in which the method or field is declared. This means that binaries are compiled to rely on the accessible members and constructors of other classes and interfaces. To preserve binary compatibility, a class or interface should treat these accessible members and constructors, their existence and behavior, as a contract with users of the class or interface.
Java is designed to prevent additions to contracts and accidental name collisions from breaking binary compatibility; specifically:
We hope to make some improvements to future versions of Java to better support both source and binary compatible evolution of types. In particular, we are considering a mechanism to allow a class to implement two interfaces that have methods with the same signature but are to be considered different or have different return types. We welcome suggestions and proposals that would help us to make additional improvements, either in managing name and signature conflicts or other sources of incompatibility.
Changes in class and interface types that are not public
and that are not a superclass or superinterface, respectively, of a public
type, affect only types within the package in which they are declared. Such types may be deleted or otherwise changed, even if incompatibilities are otherwise described here, provided that the affected binaries of that package are updated together.
abstract
Classesabstract
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
(§20.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.
final
Classesfinal
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.
public
Classespublic
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.
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:
hSuppose 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:
sThis 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 0which 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 ee300848A
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.
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:
HyperSuppose 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:
Hypereven 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 SuperSuppose 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 Hyperas 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 HyperSuppose 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 Superas you might expect. (A flaw in some early versions of Java caused them to print:
hello from Hyperincorrectly.)
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:
TestIf 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.
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:
hyperChanging
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:
hyperThe 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.
final
Fields and Constantsfinal
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:
aSuppose 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 trueSuppose 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 truebecause 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 result is a side-effect of the decision to support conditional compilation, as discussed at the end of §14.19.
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 (§4.5.4). This means that all such fields appear to be initialized first during class initialization (§8.3.2.1, §9.3.1, §12.4.2).
static
Fieldsprivate
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.
transient
Fieldstransient
modifier of a field does not break compatibility
with pre-existing binaries.
volatile
Fieldsprivate
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.
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.
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 §13.4.12).
abstract
Methodsabstract
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 OutSuppose 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:
Waybefore 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
.)
final
Methodsfinal
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:
outSuppose 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.
native
Methodsnative
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.
static
Methodsprivate
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.
synchronized
Methodssynchronized
modifier of a method does not break compatibility
with existing binaries.
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.
We note that a compiler cannot inline expand a method at compile time unless, for example, either:
private
to its class
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.
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 (§15.11.2.2), no such error occurs when a Java program is executed, because no overload resolution is done at execution time.
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:
floatSuppose 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:
floatHowever, if
Test
is then recompiled, using this new Super
, the output is then:
intas might have been naively expected in the previous case.
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.
public
Interfacespublic
to be declared public
does not
break compatibility with pre-existing binaries.
If an interface that is 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 interface type, so such a change is not recommended for widely distributed interfaces.
VerifyError
.
Deleting a member from an interface may cause linkage errors in pre-existing binaries. If the example program:
interface I { void hello(); } class Test implements I {
public static void main(String[] args) { I anI = new Test(); anI.hello(); } public void hello() { System.out.println("hello"); } }is compiled and executed, it produces the output:
helloSuppose that a new version of interface
I
is compiled:
interface I { }If
I
is recompiled but not Test
, then running the new binary with the existing
binary for Test
will result in a NoSuchMethodError
. (In some early implementations
of Java this program still executed; the fact that the method hello
no longer
exists in interface I
was not correctly detected.)
static
final
fields in classes, as described in §13.4.7 and §13.4.8.
abstract
methods in classes, as described in §13.4.13,
§13.4.14, §13.4.20, and §13.4.22.
Contents | Prev | Next | Index
Java Language Specification (HTML generated by Suzette Pelouch on February 24, 1998)
Copyright © 1996 Sun Microsystems, Inc.
All rights reserved
Please send any comments or corrections to [email protected]