Thanks type and gentle class

Type and class are not interchangeable terms, but two carefully distinguishable object-oriented concepts

In Shakespeare's Hamlet, the king and queen of Denmark address two of Hamlet's buddies with "Thanks Rosencratz and gentle Guildenstern" and "Thanks Guildenstern and gentle Rosencratz." Critics reference these lines to declare the two fellows as indistinguishable, perhaps with an eye toward their function in the play.

And so it is with type and class. In the seminal work Object Oriented Analysis and Design with Applications, (Addison Wesley, 1994), Grady Booch declares, "For our purposes, we will use the terms type and class interchangeably." A footnote goes on to explain that, "A type and a class are not exactly the same ... For most mortals, however, separating the concepts of type and class is utterly confusing."

Certainly Booch deserves the eminent status he has attained in the object-oriented community. Nonetheless, I disagree with his assessment, though in fairness I should acknowledge that he speaks from a language-independent view. Fortunately for Java developers, the Java language has taken a positive step in facilitating a clearer distinction between type and class. To explore that distinction, we will examine type and class from a Java perspective.

The advantage of type

Programming language types characterize the values used in the course of program execution. Though limited compared with modern languages, FORTRAN first introduced types in 1954. The language syntax allowed programmers to distinguish between numeric types for integer and floating-point arithmetic. Variables beginning with those letters between i and n were implicitly typed as integers. Relics of that convention survive today, as many programmers still use the letters i and j for array subscripting.

The FORTRAN typing scheme's primary benefit was code optimization for the underlying hardware system. Computer language evolution quickly raised type from its restricted realm of urging faster code production from compilers to allowing users to define their own data types. User-defined types are the pragmatic extension of abstract data types from type theory. An abstract data type is an ordinary type along with a set of operations. Abstract data types effectively shield module internal mechanics by permitting interaction only through published type operations. You will recognize the class concept as the manifestation of abstract data types in object-oriented languages. An object-oriented class is an abstract data type with full or partial implementation of each declared type operation.

User-defined data types realize significant benefit in extending a programming language's primitive type system. Expressive combinations of primitive and user-defined types create new, more complex types. Importantly, these user-defined types are first-class citizens, meaning that objects characterized by these types enjoy privileges similar to those of primitive types, thereby facilitating efficient data structure management.

The introduction of programming language types also paved the way for program text type-checking. Type operations restrict the permissible interaction with a user-defined type, thereby declaring the explicit boundaries of a system module. A type-checker, based on well-defined typing rules, enforces the proper use of program types by ensuring the integrity of these boundaries. Type-checking's primary purpose is to prevent program execution errors.

Java's specification as a strongly typed language enables the static determination of all variable and program expression types. That allows most type enforcement to occur at compile time. A static type-checker, usually the language compiler, ingests the program text and verifies that variables and program expressions conform to the typing rules.

User-defined data types offer further benefit by providing valuable insight into program meaning. The declaration of types and their use in a program give a partial program specification. Like descriptive variable names, type declarations reveal clues to system structure.

Defining types

The Java language provides two separate mechanisms for declaring user-defined types. A class defines a type with a partial or full implementation, whereas an interface defines a type free of any implementation concerns.

Defining types with classes

Most programmers think of Java classes as primarily code implementation repositories. A class text defines an implementation template that effectively becomes an object-creation blueprint. This important aspect of the class concept reflects the manner in which Java facilitates code modularization. Classes provide the primary unit for decomposing a software system into understandable and manageable pieces. In turn, that modularization significantly aids the comprehensibility of larger systems.

Class inheritance through the Java extends clause further facilitates modularization. Module, or implementation, inheritance offers a way to create subclasses that automatically utilize implementation code from the superclass module. Subclasses build on superclass implementation and thus provide a means for module extension. This implementation-centric view of inheritance emphasizes code reuse.

Java classes also play an additional significant role. As well as providing implementation code, every class also defines a type. We often speak of this type as the public interface to the class. That terminology is somewhat confusing in light of the Java language interface keyword. Regardless, the set of public methods declared by the class defines a new type. This new type enjoys all the privileges and benefits of the Java type system in a manner that is completely independent of implementation code. Whether concrete or abstract, a class always defines a single new type.

Inheritance via the extends clause also plays another important role that mirrors a class's ability to define a type. Type, or interface, inheritance provides a mechanism for creating subtypes that build on a supertype's public interface. Since a type B declared as a subtype of A also has type A, the type-checker permits any variable or program expression of type B when expecting a variable or program expression of type A.

As an example, consider the following definition of 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()";
  }
}

The class provides implementation for three methods: m1(), m2(String), and p(). Only public method declarations become part of the defined type, so type Base consists of operations m1() and m2(String). (For this discussion, I'll ignore the implementation and type operations inherited from the primordial Object class.) Type Base does not include the private method p(), thus the type-checker rejects the following code:

Base ref = new Base();
System.out.println( ref.p() );      // REJECT

The second statement calls method p() through the type Base variable ref. The type-checker rejects that expression since type Base does not have an operation p(), even though the attached object possesses implementation code for method p(). The type-checker rigidly enforces that the declared type of ref cannot call method p(). Now consider the following definition of class Derived:

public class Derived
  extends Base
{
  public String m1()
  {
    return "Derived.m1";
  }
  public String m3()
  {
    return "Derived.m3()";
  }
}

Through the extends clause, Derived both subclasses and subtypes Base. As a subclass, Derived performs module extension by overriding the implementation of method m1() and by declaring and implementing a new method, m3(). Derived automatically reuses the Base implementation of method m2(String).

The class text also declares a new type, Derived, as a subtype of Base. By building on Base, Derived contains the two operations m1() and m2(String) as well as the new operation m3(). As a subtype, Derived performs type specialization; that is, type Derived is a special kind of Base. Importantly, the type-checker allows the legal substitution of any type Derived variable or program expression where expecting type Base. For example, the type-checker accepts the program statement

Base ref = new Derived();

The variable ref has type Base and the expression new Derived() has type Derived. The type-checker examines the type hierarchy to verify type Derived conforms to type Base. Note that type conformance relies only on the type hierarchy defined through class inheritance. I'll touch on that important fact again when investigating types defined via the interface concept.

Defining types with interfaces

Curiously, Java interfaces offer perhaps the clearest distinction between type and class. An interface, quite simply, declares a type, deferring partial or full implementation to abstract or concrete classes. At first glance, that may seem to be the same as a class with no implementation. In fact, Java interfaces are often compared to C++ pure abstract classes. That misguided comparison, however, clouds an important Java language contribution. An interface not only separates type declaration from module implementation, it also facilitates the separation of type inheritance and implementation inheritance.

Java clearly distinguishes between these two kinds of inheritance. The Java language specification restricts module extension to single implementation inheritance, whereas type specialization supports multiple interface inheritance. While code reuse through module extension requires class inheritance via the extends clause, type specialization results either from class inheritance or from interface inheritance via the implements clause.

When using the extends clause, type specialization may be viewed as a secondary result of module extension. With the implements clause, however, type specialization takes front and center. The Java interface keyword declares a type and the implements keyword guarantees that a class either implements or defers the implementation of all operations for each type specified in the implements clause. That guarantees that objects instantiated from concrete classes possess implementation code for all the operations of each type specified in the implements clause.

Consider the following interface and class definitions:

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

Interface Foo declares a new type, Foo, with a single operation, m3(). The definition of class Derived differs from before only by the addition of the implements Foo clause. As previously demonstrated, by extending Base, class Derived inherits implementation code from class Base and type Derived inherits the operations of type Base. The additional implements Foo clause means type Derived also subtypes Foo. Thus you achieve multiple type inheritance: type Derived subtypes type Base and type Foo. Let's examine the ramifications of these type definitions.

Derived derived = new Derived();
Foo  foo  = derived;
Base base = derived;
foo = base;                        // REJECT

The first statement attaches a type Derived variable to a type Derived object-creation expression. The second statement attaches a variable of type Foo to the created object. The type-checker enforces conformance between the type Derived variable and the type Foo variable by verifying that Derived actually subtypes Foo. The third statement similarly passes type-checking. Note that these conformance checks consider the variable type, not the attached object type. In fact, later I'll explore the notion that objects do not have type. The type-checker rejects the fourth statement since Foo does not subtype Base, even though foo and base point to the same object. Incompatible variable types prevent this attempt to attach a variable to the very object to which it is already attached.

Now let's take a look at method invocation via the different variable types.

Derived derived = new Derived();
Foo  foo  = derived;
Base base = derived;
// Section 1: call m3()
System.out.println( derived.m3() );
System.out.println( foo.m3() );
System.out.println( base.m3() );      // REJECT
// Section 2: call m1()
System.out.println( derived.m1() );
System.out.println( foo.m1() );       // REJECT
System.out.println( base.m1() );

Section 1 calls method m3() through three different reference variable types. Since type Base does not possess operation m3(), the type-checker rejects the section's third statement. Similarly, the type-checker rejects the second statement in Section 2 since type Foo does not possess operation m1(). Remarkably, the three variables remain attached to the same object for every method call, and that object has implementation for both the m1() and m3() methods. In the rejected statements, the type-checker overrules the underlying Derived object's ability to perform the specified method call by enforcing variable type conformance.

An acceptable exception

In Java Language Specification, James Gosling et al. proclaim that, "Variables have type, objects have class." That surprising statement contradicts the common practice of referring to an object's type. Gosling further notes, as discussed in the article, that types exist at compile time and objects exist at runtime.

1 2 Page
Join the discussion
Be the first to comment on this article. Our Commenting Policies
See more