A primordial interface?

Investigate Java's elusive primordial interface

Section 8.1.3 of the Java Language Specification (JLS) states, "The class java.lang.Object ... is the primordial class and has no direct superclass." That means java.lang.Object serves as the root class for all Java class hierarchies. The single-rooted implementation inheritance hierarchy guarantees that every runtime object has a concrete implementation for all java.lang.Object type operations. Furthermore, the JLS clearly dismisses the existence of a similar primordial interface. Section 9.1.3 states, "There is no analogue of the class Object for interfaces; that is, while every class is an extension of class Object, there is no single interface of which all interfaces are extensions."

Nonetheless, this article investigates the fact that the Java language acts as if a primordial interface does indeed exist. To lay the foundation necessary for understanding the existence of a tacit primordial interface, I first present a conceptual, type-oriented view of Java objects.

Conceptual goggles

Figure 1 shows a UML class diagram for the simple class and type hierarchy used as this article's example. The diagram models two classes, one interface, and three type declarations. For brevity and clarity, the diagram and following discussion does not include the requisite root java.lang.Object class.

Figure 1. UML class diagram for the example code

I purposely keep simple the concrete implementation of each class. The following code implements the class Base:

public class Base
{
  public String m1()
  {
    return "Base.m1()";
  }
  public String m2( String s )
  {
    return "Base.m2( " + s + " )";
  }
  private String p()
  {
    return "Base.p()";
  }
}

As detailed in "Thanks Type and Gentle Class", every class declaration provides implementation code and declares a user-defined data type. Here class Base implements two public methods, m1() and m2(String), and a private method, p(). Type Base declares two operations, m1() and m2(String). Figure 2 depicts a conceptual view of the following Java code:

Base base = new Base();
System.out.println( base.m1() );          // Prints "Base.m1()"
System.out.println( base.m2( "base" ) );  // Prints "Base.m2( base )"
System.out.println( base.p() );           // Compile-time error
System.out.println( base.m() );           // Compile-time error
Figure 2. Base reference attached to Base object

The first statement in the code above attaches a type Base reference to an object instantiated with class Base. The Base type reference acts as conceptual goggles for viewing the underlying Base class object. The reference type effectively shields the Base object, permitting interaction only through messages adhering to declared Base type operations. The designer of class Base explicitly declared that type Base would only permit operations m1() and m2(String). So while the second and third lines of code pass compile-time type checking, the fourth and fifth lines result in compile-time errors. In declaring method p() private, the designer has hidden that implementation. At compile time, the type checker polices the code and censors the attempt to send a p() message to the Base object through a Base reference. Note that the Base object is fully capable of handling the message. As for calling method m(), the type checker also rejects that attempt, which is fortunate since the Base object is not prepared to receive that message. Importantly, when viewed through a Base reference, method p() is as invisible as a nonexistent method.

That conceptual view can be extended to include implementation inheritance as well. The code below declares type IType and implements class Derived:

public interface IType
{
  String m2( String s );
  String m3();
}
public class Derived
  extends Base
  implements IType
{
  public String m1()
  {
    return "Derived.m1()";
  }
  public String m3()
  {
    return "Derived.m3()";
  }
}

Class Derived provides implementation for methods m1() and m3(). As a subclass of Base, Derived inherits implementation for m1() and m2(String), though it overrides the m1() implementation. Since p() was declared private, Derived does not inherit that method's implementation.

Type Derived explicitly declares two operations, m1() and m3(). As a subtype of Base, Derived inherits operations m1() and m2(String) and, as a subtype of IType, it inherits operations m2(String) and m3(). So m1(), m2(String), and m3() make up the set of operations for type Derived. Note that inheriting operation m2(String) along multiple lines of the type hierarchy does not cause any difficulties. That is not true of multiple implementation inheritance, which Java does not allow.

This code attaches three separate reference types to a newly created Derived object:

Derived derived = new Derived();
Base base = derived;
IType iType = derived;

Figure 3 depicts the conceptual view of the first statement.

Figure 3. Derived reference attached to Derived object

The second and third statements in the code above attach Base and IType references to the existing Derived object. The type checker verifies that those attachments conform to the declared type hierarchy. Figure 4 depicts the resulting conceptual view of those statements.

Figure 4. Base and IType references attached to Derived object

Figures 3 and 4 reveal an important fact: the type of attached reference does not affect the mapping from the Derived object to the actual implementation code prescribed by the class inheritance hierarchy. The reference type does, however, significantly impact the accessibility to the methods of the underlying Derived object. The code below illustrates that impact:

// Call methods through Derived reference
System.out.println( derived.m1() );
System.out.println( derived.m2( "derived" ) );
System.out.println( derived.m3() );
// Call methods through Base reference
System.out.println( base.m1() );
System.out.println( base.m2( "base" ) );
System.out.println( base.m3() );       // Compile-time error
// Call methods through IType reference
System.out.println( iType.m1() );      // Compile-time error
System.out.println( iType.m2( "iType" ) );
System.out.println( iType.m3() );

Since the references are attached to the same object, each section of the above code attempts access to the exact same object methods. The access, however, is not uniform. The Base type reference restricts the call to m3(), whereas the IType type reference restricts the call to m1(). Importantly, the underlying runtime Derived object is fully capable of receiving either message. Type-imposed restrictions, applied at compile time, invalidate both attempts. From the type checker's point of view, the underlying object's actual capabilities are not relevant. The type checker rigidly enforces the restrictions imposed by the compile-time reference type and remains unconcerned with the actual object. In fact, at compile time, the type checker can't even be sure what that underlying object's capabilities may be. It can only provide a compile-time guarantee that runtime implementation code for each operation of the attached reference type will be accessible.

Primordial interface

The previous discussion and figures omitted the presence of the primordial Object class. That class, however, plays an integral part in every Java class hierarchy. By leaving out the optional extends clause, class Base implicitly extends Object and, therefore, subclasses and subtypes Object. Using an attached IType reference, Figure 5 includes the Object class and the requisite changes to the Derived object.

Figure 5. Derived references attached to Derived object, version 2

Of course, the methods of class Object aren't aa(), bb(), etc. As of Java 2 Platform, Standard Edition, Version 1.3, type Object declares nine operations: equals(Object), getClass(), hashCode(), notify(), notifyAll(), toString(), wait(), wait(long), and wait(long,int). Figure 5 does not show all the methods or the actual names. It does show an IType reference unaffected by the additional implementation code and object mappings. Just as an IType reference can't view the m1() method, neither should it see any of the public methods of Object. The following code, however, reveals a slight surprise:

System.out.println( derived.equals( base ) );
System.out.println( iType.equals( base ) );   // This is legal!

The first statement is type conformant. Since Derived subtypes Base, which subtypes Object, a reference of type Derived may access operations declared by type Object. So a Derived reference may access the equals(Object) method. (That is not shown in Figure 5, which applies to the second code statement.)

The second statement, however, should arguably cause a compile-time error. Type IType does not include operation equals(Object). It is not sufficient that the Derived object happens to possess a mapping for that method. It maps m1() too, but the IType reference can't access method m1().

How can we reconcile the apparent discrepancy? We could certainly argue that no harm has resulted. Since every object must be instantiated from a concrete class, and every class must descend from the primordial class Object, we know that at runtime the equals(Object) method call will not fail since implementation for that method exists in class Object. Nevertheless, that argument appeals to runtime issues and type conformance occurs at compile time. How does the compiler, acting as type checker, allow type IType to include operations neither declared by it or any of its supertypes? In our example, IType has no supertypes.

Figure 6 illustrates the conceptual view you actually receive for an IType reference attached to a Derived object. The Java type system grants the IType reference access to all operations defined by type Object.

Figure 6. Derived references attached to Derived object, version 3

To resolve that access in a type-consistent manner, you could define an interface named java.lang.Interface that declares each java.lang.Object type operation with the appropriate return type. Then you could have class java.lang.Object implement java.lang.Interface. That would not change the behavior or implementation of class java.lang.Object in any way. Finally, you could specify that any interface declaration that does not explicitly declare an extends clause implicitly extends java.lang.Interface, making java.lang.Interface the primordial interface. You would then have the necessary type hierarchy to explain the structure of Figure 6.

As individual developers, of course, we can't simply perform the above steps. The required changes to package java.lang and the Java Language Specification are beyond our jurisdiction. We can, however, strive to understand the slight aberration in the Java type system.

Advantages

The current Java type system's tacit acceptance of a primordial interface results in several advantages. Java developers often rely on class Object being the root of all class hierarchies. For example, the interface java.util.Collection defines operation add(Object). By specifying Object as the parameter, every concrete class implementing the Collection interface must allow any runtime object to be added to the collection. That design feature is particularly important in light of Java's lack of support for parametric polymorphism.

From a type-oriented perspective, operation add(Object) passes compile-time type checking even for interface declared types. The code below illustrates that point:

List myList = new ArrayList();
IType iType = new Derived();
myList.add( iType );

The add(Object) method call uses an IType reference for the Object type parameter. Recall that IType does not explicitly subtype Object. Curiously, the JLS covers that situation in section 5.1.4 by declaring the legality of widening reference conversions "from any interface type to type Object." Method invocation triggers the conversion of each method argument, so the type system converts the IType reference to an Object type as part of the method call.

A further advantage concerns a potential JVM performance hit when using an invokeinterface instruction as compared to invokevirtual. Interpreting the method call iType.equals( base ) as a virtual call to an object method exhibits performance advantages over invoking an interface method. I will cheerily declare further discussion to be beyond this article's scope since, in reality, it is actually beyond my comfort level.

Conclusion

Though the Java Language Specification declares that no primordial interface exists, the language actually behaves otherwise. Granted, that innocuous discrepancy causes no harm in Java implementation code; nonetheless, Java developers should strive to understand the language, even its minor idiosyncrasies. This article investigates a situation in which strict adherence to the Java type system should prevent an interface type reference from accessing operations defined by type Object. Rather than take issue with the JLS, a type-oriented, conceptual view sufficiently explains the observed compile-time behavior of an implied primordial Java interface.

1 2 Page 1
Page 1 of 2