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.