Sun Java Solaris Communities My SDN Account Join SDN
 
Article

Java Objects are Conscious

 
 

Articles Index


Consider that the reusability and, indeed, the lifetime of a piece of code depends heavily on the number of type restrictions placed upon the data elements by that code. The fewer restrictions on the type of data elements operated on, the more widely applicable the code will be. For example, a list manager that was restricted to operate only on integers could not be reused to manage a list of real numbers despite the fact that the majority of the code would be identical. This article describes how Java promotes considerable type flexibility and reusability when referring to data elements as well as when creating data elements. Java goes as far as to allow type information to be a variable. This mechanism can be used to create and examine the functionality of objects whose type is computed at runtime.

Introduction

In nonobject-oriented programming languages, the type of each element must be known at compile time, which results in very inflexible code. Object-oriented languages relax this constraint--only the kind of element needs to be known. Java takes this a step further. Interfaces allow Java code to refer to behavior of an object regardless of the kind of object.

Certainly flexibility in how a program refers to data elements is crucial, but what about the creation of data elements? Consider a method, tmake, that constructs a generic tree data structure. To create the various nodes, tmake would do a lot of new TreeNode() operations. Now imagine that you must subclass the generic tree nodes, defining MyTreeNode, to add specific behavior. The method tmake could not be reused to create trees consisting of MyTreeNode elements--it is restricted to creating elements of a specific type. Java provides a solution to this inflexibility by allowing programs to specify variable type information, thus, deferring type information to runtime.

Class definitions in many object-oriented languages, such as C++, are compile-time entities that mainly specify how the compiler is to lay out memory. Because Java binds method names to code on-the-fly at runtime, Java class definitions must exist at runtime. Consequently, each Java class in your program results in a corresponding class definition object of type Class.

Every regular object that you create has a reference to its class definition object; that is, Java objects are conscious--they know their identity. Method getClass defined in class Object can be used to obtain an object's class definition:


Button b = new Button("OK");

Class bClassDef = b.getClass();


Here, bClassDef refers to the class definition for Button.

To obtain the class definition object for a particular class, call the static method forName in Class:


try {

  Class bdef=Class.forName("Button");

} catch (ClassNotFoundException e) {

  System.err.println("class Button not found");

}


This runtime type information makes Java more robust than C++, for example, because you cannot operate on an object of incorrect type. In C++, you can cast an object to whatever type you want and then operate on it in an unsafe manner. In Java, the operation is safe.

Object Factories

So, what can you do with a class definition object (of type Class)? One of the things you can do is make new instances of that class. While Java provides a new operator for this purpose, new is a compile-time operator in that it cannot take a variable argument:


String cname = "Button";

Button b = new(cname); // INVALID!


An object factory, the runtime equivalent of the new operator, would be a useful thing to have. Consider how you would write a program that allowed users to specify a set of objects that had to be created while the program was running? More specifically, how could you create instances of classes that were unknown when you wrote your program? This is precisely the requirement for GUI builders. Also, one can imagine a presentation designer that allowed users to drag-n-drop a variety of Java Component objects onto the pages.

The following method represents a factory that takes a class name in a String and returns an instance of that class.


Object factory(String p) {

 Class c;

Object o=null;

try {

c = Class.forName(p);// get class def

o = c.newInstance(); // make a new one

  } catch (Exception e) {

// either class not found,

// class is interface/abstract, or

// class or initializer is not accessible.

System.err.println("Can't make a " + p);

  }

  return o;

}


The constructor (with no arguments) for the specified class is called, but there is no way to call a constructor with arguments.

You can place factory in a class called ObjectFoundry as a public static method so that it can be used by all parts of your program. You can allow exceptions to flow through to the caller of the factory so that an appropriate error message can be generated:


public class ObjectFoundry {

  public static Object factory(String p)

      throws ClassNotFoundException,

           InstantiationException,

           IllegalAccessException {

    Class c = Class.forName(p);

    Object o = c.newInstance();

    return o;

  }

}


Your factory can then be used as follows:


public class T {

  public static void main(String[] args) {

    Button b;

    try {

      b = (Button)

          ObjectFoundry.factory("Button");

    } catch (Exception e) {

      System.err.println("Can't make Button");

    }

  }

}


Returning to the tmake method from the introduction, you can see that tmake could easily be extended to create trees consisting of arbitrary tree nodes.

Checking an Object's Identity

In the last section, we introduced the notion that you sometimes have to create objects of types that can only be determined at runtime. We defined a runtime equivalent of the new operator. It turns out that a runtime equivalent of the instanceof operator is also useful; that is, you may want to compare the identity of an object to a type known only at runtime. For example, you may want to create a list manager that only contains objects of a specific type that is specified during list manager construction.

The operation obj instanceof type checks more than just whether or not obj is an instance of type. Operator instanceof checks:

  1. Whether or not obj is an instance of class type or any class on the path to the root of the class hierarchy (that is, the superclasses).
  2. Whether or not obj is an instance of a class that implements type if type is an interface instead of a class.

So, in effect, instanceof checks whether obj is a particular kind of object or whether obj can behave according to a particular interface. You distinguish between these operations because you may wish to examine identity rather than behavior. For example, you may want to know that some object is a simple Timer rather than a TimeBomb that also implements some interface Ticking. The following method embodies operation 1:

public static boolean

isKindOf(Object obj, String type)

    throws ClassNotFoundException {

  // get the class def for obj and type

  Class c = obj.getClass();

  Class tClass = Class.forName(type);

  // check against type and superclasses

  while ( c!=null ) {

    // have we found the given classname?

    if ( c==tClass ) return true;

    c = c.getSuperclass();

  }

  return false;

}


To get a runtime equivalent of operator instanceof, you check both isKindOf and, if the type is an interface, check to see if obj implements that interface using a method, behavesLike, which is defined in the next section:


public static boolean

instanceOf(Object obj, String type)

    throws ClassNotFoundException {

  Class tClass = Class.forName(type);

  if ( tClass.isInterface() ) {

    return behavesLike(obj, type);

  }

  else {

    return isKindOf(obj, type);

  }

}


Checking an Object's Behavior

While you might want to check the identity or ancestry of an object, most of the time you care only how an object behaves (because you want to send it a message). For example, consider a simple remote-method invocation mechanism. Objects could be created on one system, R, and receive messages (method calls) from objects on another system, S. In principle, the object on S would only care that the object on R answered a set of messages--the actual class name of the R object may be unknown to S. If the R object were a database manager, then the S object would only care that it answered add and delete, for example. In order to "hook up" the objects on R and S, R could send S the name of an interface it implemented; in other words, R could send S the name of a role it was going to play.

Testing interface compliance is a matter of asking an object for the list of interfaces it implements and then checking the desired interface name for membership in this list. We have implemented a method called behavesLike as an analog to isKindOf defined in the previous section:


public static boolean

behavesLike(Object obj, String behavior)

  throws ClassNotFoundException {

// get interface def for behavior

Class bClass = Class.forName(behavior);

Class c = obj.getClass();

// walk back up hierarchy to check interfaces

 while ( c!=null ) {

// get interfaces for this class

 Class[] ia = c.getInterfaces();

// is behavior among them?

  for (int i=0; i&ltia.length; i++) {

    if ( ia[i] == bClass ) return true;

   }

   c = c.getSuperclass();

 }

  return false;

}


The method behavesLike reports true when obj is a subclass of a class that implements behavior, because a class that implements an interface forces all subclasses to satisfy that interface as well. You have to walk back up the class hierarchy, because the method getInterfaces returns only the list of interfaces for the associated object.

Examples Using ObjectFoundry

To test the ObjectFoundry class, this example defines an interface and a few classes:


interface Mortal {

  public void kill();

}

class Animal implements Mortal {
  String species;
  public Animal() {
    species="unknown";
  }

  public Animal(String sp) {
    species=sp;
  }

  public void kill() {
    //play("BestOfNewAgeCD.au");
  }
}

class Dog extends Animal {
  public Dog() {
    species = "Dogus Chewus Maximus";
  }
}

Given those classes and these definitions:


Animal animal = 
(Animal)ObjectFoundry.factory("Animal");

Dog dog = 
(Dog)ObjectFoundry.factory("Dog");


you can ask the following questions:

  • Is a Dog an instanceOf Animal?
    ObjectFoundry.instanceOf(dog,"Animal")==true.
  • Does an Animal behaveLike an Animal?
    ObjectFoundry.behavesLike(animal, "Animal")==false.
  • Does a Dog behaveLike an Animal?
    ObjectFoundry.behavesLike(dog, "Animal")==false.
  • Does a Dog behaveLike a Mortal?
    ObjectFoundry.behavesLike(dog,"Mortal")==true.
  • Is a Animal an instanceOf Mortal?
    ObjectFoundry.instanceOf(animal,"Mortal")==true.

The uses of ObjectFoundry methods would, of course, have to be enclosed in try-catch blocks, because ObjectFoundry methods throw exceptions.

Conclusion

Code reuse is of primary importance and object-oriented languages in general represent a tremendous step toward the goal of widely applicable code. By allowing code to refer to data elements by the kind of element rather than a specific type, object-oriented programs are very flexible. Java provides two significant advantages over many other object-oriented languages in that (i) Java interfaces allow programs to refer to elements by their behavior rather than their identity (class name) and (ii) Java allows type information to be a variable, thus, allowing the behavior or kind of element to be computed at runtime. The latter capability increases flexibility in one of the last situations in which object-oriented programs require specific, compile-time, type information--object creation.

Source Code

The following Java source files provide the series of tests and class definitions described in this article.