This article appeared in Java Pro magazine in June/July 1998

Java Gotchas

The Java language has several gotchas to trap
unwary developers, but there are workarounds

by Lee Fesperman

Java and C++ are similar in several ways, including syntax, primitive types, and operators. But unlike C++, which was layered on top of C, Java has no legacy capabilities to support. Thus Java is a much cleaner and simpler language than C++. Java supports primitive types, so it is not a pure object language, but otherwise Java is thoroughly object-oriented. Throw in Java's network orientation, and it becomes clear why Java is a perfect match for the multi-tiered, multi-server, component-oriented environment of the future.

Yet the simpler design of Java sometimes gets in the way. Certain aspects of the language are stumbling blocks to developers. These are traps in the language itself, regardless of the class libraries used.

This article will concentrate on aspects of the Java language that may be surprising and perplexing to developers coming from C++ or from any language.

These Java "Gotchas" are not suggestions for changes to the language. While these are certainly possible enhancements to Java, many of the aspects are inherent to the basic design of the language. Changing them would do severe damage to Java.

Interfaces Interfaces are essential to the Java language. They provide for multiple inheritance as well as information hiding and code re-use. Interfaces are similar to abstract classes but have important restrictions:

  • All method definitions in an interface are implicitly abstract and have no implementation body. An abstract class can declare both abstract and non-abstract methods. The non-abstract methods are for common code shared by derived classes. Because all methods declared in an interface are abstract, each class that implements an interface must declare bodies for the methods of the interface. There can be no sharing of code between derived classes. Classes implementing an interface must duplicate any common code.

  • Interfaces cannot directly model classes because they do not allow field variables. An interface may declare static final variables, but those are effectively constants, not variables. Abstract classes can declare field variables. Interfaces, on the other hand, can only support variables indirectly. Interfaces emulate field variables using wrapper methods for getting and setting the variables. Classes implementing an interface must include duplicate versions of the variable wrappers.

    The following conversion illustrates the two restrictions on interfaces:

    abstract class Linked
    {
       Linked next = null;
       int count()
       {
          int count = 1;
          if (next != null)
             count += next.count();
          return count;
       }
    }

    The Linked class provides a base class for a general linked list. The field variable next links to the next element in the list, and the common method count() computes the size of a list or sub-list. Converting the abstract class to an interface produces:

    interface Linked
    {
       Linked getNext();
       void setNext(Linked next);
       int count();
    }

    The changes include the following:

  • Removing the next field variable and replacing it with methods for accessing: getNext(), setNext().
  • Changing the count() common method into an abstract method.
  • Declaring bodies for the interface methods getNext(), setNext(), and count() in each class implementing Linked.

    An example of a class implementing the Linked interface is shown later in the section entitled, "Multiple Inheritance."

    Summary: To replace abstract classes with interfaces, use variable wrappers and duplicated code in derived classes.

    Destructors
    In most circumstances, finalize() is useless as an object destructor. If a class has a finalize() method, Java will only call it during garbage collection. However, Java does not guarantee when or in what order it will call finalize() methods. An object may never be garbage collected, so finalize() is not called even when the application terminates. Additionally, there is no finalize() for classes—finalize() can't be static.

    The unpredictable nature of finalize() is not acceptable, except in rare cases. Classes that require cleanup after all access is complete should use explicit destructor methods. Possible actions by destructor methods include the following:

  • Flushing file buffers.
  • Closing external file access.
  • Disconnecting from referencing objects and classes-vectors, hashtables and other links.
  • Removing the object from screen display.
  • Sending destructors to child or linked objects.

    You define object destructors as instance methods and class destructors as static. Like constructors, user-defined destructors for a derived class normally call the destructor for the base class:

    public void destruct()
    {
       ...
       super.destruct();
    }

    The Java compilers don't recognize user-defined destructors as anything special, so applications code must use them properly. You need to perform the following steps:

    1. Ensure the destructor is always called.
    2. Deal with multiple references to the object or class, perhaps using a reference count.
    3. Avoid further use of the object or class after calling the destructor method.

    This enum Goes to Eleven
    Although Java syntax borrowed heavily from C++, there are many C++ features that Java chose to omit. At times, the lack of a particular feature makes Java programs cumbersome to implement. One such feature is the ability to define enumerated types, such as those declared with the C++ enum keyword.

    See entire sidebar

    Summary: Instead of finalize(), create user-defined destructors for objects and classes, as needed, and ensure proper use by application code.

    Function Pointers
    In C/C++, you can pass a pointer to a function to other functions. Java does not have this capability. You need to use methods in derived classes, often a more clumsy way to deal with the problem.

    In Java, you pass a base class or interface as a parameter to methods instead of a function pointer. The base class or interface defines an abstract method as a prototype for the function to be used. Each derived class supplies the appropriate method body.

    A Java method might use an interface in place of a function pointer:

    void processLines(
       BufferedReader in, ProcessLine out)
    throws IOException
    {
       String line;
       while (
          (line = in.readLine()) != null)
          out.process(line);
    }

    processLines() reads lines from a stream and passes each line to a method for processing. It uses the ProcessLine interface to declare the processing method:

    interface ProcessLine
    {
       void process(String line);
    }

    A derived class implementing the processing routine:

    class Printline implements ProcessLine
    {
       void process(String line)
       {
          System.out.println(line);
       }
    }

    The PrintLine class prints each line. This code uses PrintLine() to print a stream as lines:

    processLines(new BufferReader(in),
       new PrintLine());

    The introduction of inner classes and anonymous classes in Java 1.1 makes emulation of function pointers more convenient:

    processLines(new BufferReader(in),
       new ProcessLine()
    {
       void process(String line)
       {
          System.out.println(line);
       }
    });

    This eliminates the need for a derived class like PrintLine. The anonymous class can reference variable and methods in the containing class.

    Summary: In places where you might normally need function pointers, use an abstract method in a base class or interface as a prototype and implement appropriate methods in derived classes.

    Multiple Inheritance
    Without true multiple inheritance, Java forces some difficult design choices. A Java class may inherit from only one base class. Any additional inheritance is through implementation of interfaces. When a class is has more than one distinct parent, you must select one parent to be the single base class. The other parents must be interfaces. This is not an easy choice.

    In larger projects, there are usually several situations where classes require multiple inheritance. Often, this involves the major classes in the application, producing difficult design decisions that you need to make early in the process. Potential future code re-use is a consideration, too. Improper choices for base classes versus interfaces can complicate later enhancements, at worst forcing a rewrite of the base system.

    Interfaces have advantages over classes because of improved information hiding and code re-use. A very clean design might use interfaces instead of base classes. This would provide the maximum flexibility in code re-use. However, as we've discussed, interfaces do have limitations versus base classes.

    Interfaces also solve the nasty virtual class problem in C++. Virtual classes are a counterintuitive solution to the problem of a class inheriting from the same class more than once. This is equivalent in Java to the situation where a base class and derived class implement the same interface. Interfaces that extend other interfaces may also produce duplicate references. Java merges the duplicate references into one without any fuss.

    In choosing which parent to model as a class (if any), you must consider a number of aspects. See the sidebars, "Guidelines for Multiple Inheritance" and "Designing a class with Multiple Inheritance."

    You can use multiple inheritance to extend standard Java classes:

    class LinkedVectors extends 
       Vector implements Linked
    {
       Linked next = null;
       Linked getNext()
       {
          return next;
       }
       void setNext(Linked next)
       {
          this.next = next;
       }
       int count()
       {
          int count = 1;
          if (next != null)
             count += next.count();
          return count;
       }
    }

    The LinkedVectors class provides a linked list of vectors. It extends the Vector class and implements the Linked interface, defined in the "Interfaces" section.

    Summary: To properly design multiple inheritance, follow guidelines in the Sidebar.

    Class Identity
    Java has limited facilities for determining the identity of an object. You use the instanceof operator to test if an object is a subclass of a specific class or implements a specific interface. However, when you need to test an object for membership in a set of distinct classes and interfaces, you get a cumbersome chain of if statements:

    if (obj instanceof class1)
       ...
    else
       if (obj instanceof class2)
          ...
       else
          if (obj instanceof class3)
             ...

    There is no direct way to extract the identity of an object and use it as a value in a switch, for instance. The only general solution for testing a set of classes and interfaces is to define a common base class or interface. The common base entity includes a variable or method that gives an identifying value for each derived entity.

    For externally defined classes, you can define derived classes that implement a common interface for identification. However, many of the basic classes, such as the wrapper classes for primitive types—Integer, Double, and so on—are final and aren't extensible. You must use instanceof to test for membership in these classes.

    There is a technique to retrieve an integer value representing the class of an object. You can obtain the hash code of the Class object for use as a unique id:

    int id = obj.getClass().hashCode();

    Unfortunately, this won't work for switch. However, you can build a HashTable for class identity with getClass(). Each HashTable entry could contain a value identifying the class for use with switch.

    Summary: To test membership in a set of classes and interfaces, use a common method or variable to retrieve object identity as an integer value where possible, otherwise use multiple instanceof tests.

    Class/Interface Source Files
    Java requires that each public class or interface declaration be placed in a separate file. The file name must be the public class or interface name. A Java source file may contain any number of class declarations and interface declarations but only one can be public.

    This organization leads to a proliferation of source files in a Java package. Also, the work-arounds for many of the Java traps described in this article require the creation of additional classes and interfaces producing even more source files. A large number of source files causes organizational and comprehension problems.

    Larger applications can reduce the problem by breaking packages up into smaller subsystems. Names for public classes should be descriptive and preferably multi-word.

    Summary: To reduce the impact of many source files, break up larger packages into smaller packages and use descriptive names for public classes.

    By Reference Parameters
    Normally, the only way a method can pass back a result is as a return value. Java passes parameters by value to methods. It does not provide by reference or by address parameters, as do other languages. You can't use method parameters directly for retrieving results.

    By reference parameters can be emulated using container objects:

    class StringContainer
    {
       String string;
       void setString(String string)
       {
          this.string = string;
       }
       String getString()
       {
          return string;
       }
    }

    Container objects have a field variable for receiving results.

    int myMethod(StringContainer con)
    {
       int x;
       ...
       con.setString("some text");
       return x;
    }

    A method using containers stores result values in objects passed as parameters.

    StringContainer con =
           new StringContainer();
    int x = myMethod(con);
    System.out.println(
       "myMethod returns " + x + 
       " and " + con.getString());

    Code calling the method uses the container to receive the string.

    Defining additional classes as containers increases the complexity of applications by adding more source files; see section entitled, "Class/Interface Source Files. " A better choice is to use an array as a container:

    int myMethod(String string[])
    {
       int x;
       ...
       string[0] = "some text";
       return x;
    }

    myMethod() returns an int result and passes back a String object through an array:

    String string[] = new String[1];
    int x = myMethod(string);
    System.out.println(
       "myMethod returns " + x + 
       " and " + string[0]);

    Code calling the method uses the array to receive the string.

    Summary: To emulate by reference parameters, use container objects or arrays.

    Conclusion
    Java is a comparatively clean language but has aspects that perplex even the most experienced developer. Most of the traps in the language have acceptable workarounds. However, the nature of multiple inheritance in Java will continue to be a stumbling block, especially in larger projects and in code re-use.

    Java is a powerful, general-purpose language. You can use it on most programming tasks—systems and applications, large and small. The main problem with Java is performance. Certain truly time-critical applications may never be feasible in Java.

    Much of the concern today about Java performance centers on the speed of byte code interpreters and JIT compilers. This is likely to change in the near future as full-fledged native code compilers appear. Performance of compiled Java will not be better than closer-to-the-metal languages like C++ but will be quite acceptable.

    For time-critical applications, the real performance issue is with Java garbage collection. Garbage collection overhead will bring time-critical applications to their knees. While there will be significant improvements in garbage collection-technology to deal with some of the problems, it won't cover all situations. Certain applications will not be appropriate for Java or, at best, will require significant reliance on native methods, written in a more efficient language.


    Lee is the developer of the FirstSQL RDBMS and is currently rewriting the entire RDBMS in pure Java. He can be reached at firstsql@ix.netcom.com. His company's Web site is at www.firstsql.com.


    What did you think of this article? Send your e-mail to the editors at java-pro@fawcette.com.

     © 1999 FAWCETTE TECHNICAL PUBLICATIONS, all rights reserved. Java Pro is an independent publication not affiliated with Sun Microsystems. Sun Microsystems is not responsible in any way for the editorial policy or other contents of the publication. All contents of Java Pro are copyright © 1999 Fawcette Technical Publications, unless otherwise noted. "VBITS" and "Interactive Developer" are trademarks of Fawcette Technical Publications, a California Corporation, James E. Fawcette, President. Java, all Java-based marks and logos, and the Duke Logo are trademarks of Sun Microsystems Inc. The Duke Logo is used by permission. Rather than put a trademark symbol in every occurrence of other trademarked names, we state that we are using the names only in an editorial fashion with no intention of infringement of the trademark. Although all reasonable attempts are made to ensure accuracy, the publisher does not assume any liability for errors or omissions anywhere in the publication.