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.