HomeDigital EditionSys-Con RadioSearch Java Cd
Advanced Java AWT Book Reviews/Excerpts Client Server Corba Editorials Embedded Java Enterprise Java IDE's Industry Watch Integration Interviews Java Applet Java & Databases Java & Web Services Java Fundamentals Java Native Interface Java Servlets Java Beans J2ME Libraries .NET Object Orientation Observations/IMHO Product Reviews Scalability & Performance Security Server Side Source Code Straight Talking Swing Threads Using Java with others Wireless XML
 

So I get to the office in the morning and see Mr. Job Prospect's résumé lying on my desk. That gives me about 20 minutes to think of interview questions I'd like to ask him. A quick scan of the résumé reveals that he's done some serious work in Java that includes programming with JFC, JavaBeans, Java threads, Java Applets - the works. As usual, I decide to play devil's advocate. That always throws them for a loop.

After the regular stuff, which Job passes with flying colors, I ask, "What's so great about Java?" He gives me the look I've received from several candidates - a look that says, "What a silly question! The answer is obvious. Java is the coolest, OO-pumped, distributed, reusable, superduper, kick-ass language there is." The next question really has Job questioning my sanity: "Why not use C++?"

Java is a very productive language that's revolutionized business computing. It's a clean language that fosters good design practices and provides inherent support for object-oriented design. However, it's neither a panacea for all computing ailments nor the utopia of the programming world. Choosing Java over other programming languages comes with its own share of compromises and trade-offs. A common misconception is that Java is here to replace C++ (and every other language) and is going to be the only language in the marketplace. Java is certainly making an unprecedented impact in the computing arena. But a large part of the computing problems that the Java programming language addresses are ones that have appeared with new computing paradigms. Most programming languages that manage to survive more than a couple of years in the computing marketplace have merits and unique strengths that make them ideal for their turf. Java is in the process of defining its turf and is doing a pretty good job. However, it doesn't provide a solution for every problem in every domain.

In this article I'd like to discuss a couple of features and idiosyncrasies of Java and the costs of their usage. We'll look at the lack of multiple inheritance in Java, garbage collection, references and an interesting peculiarity of Java's method scoping.

To Multiply Inheritance or Not?
One of the main language features that the designers of the Java language decided to drop off the plate was multiple inheritance. Inheritance in the context of object orientation is an "is-a" relationship. Multiple inheritance implies an "is-also-a" relationship. When a class inherits behavior and state from more than one class, we run into multiple inheritance. The real problem in multiple inheritance arises when the derived class inherits the same state (or member variables) from two different superclasses. This is known as the infamous DDD (Deadly Diamond of Death) and is illustrated in Figure 1. The problem surfaces when the derived class refers to the variable foo. Does it refer to Super1.foo or Super2.foo? Multiple inheritance increases the complexity of the code as well as the compiler.

Figure 1
Figure 1:

Java avoids this problem by enforcing a policy so that a class can inherit state and implementation from only one superclass. This doesn't preclude the class from inheriting pure behavior from multiple interfaces. The mechanism of inheriting state and/or behavior from a superclass is called class inheritance. The mechanism of inheriting pure behavior from a superclass is called interface inheritance. Java provides constructs to distinctly specify one or the other - the corresponding keywords in the language are interface (pure behavior) and class (state and/or behavior). Since the superclass methods have no implementation in the declaring class, the implementations for the methods defined in the superclasses have to be provided in the derived class. Hence a class in Java can simultaneously inherit from several interfaces and one single class. This is shown in the following class declaration:

public class Derived extends Super implements Interface1, Interface2

The class Derived inherits state and behavior from Super and has to provide implementations for the methods declared in Interface1 and Interface2. In most programming scenarios this stipulation leads to well-designed programs. However, there are cases in which it would be convenient to inherit state and implementation from more than one class. Consider a situation in which you have to write a small class (with maybe five additional methods) that extends the functionality of two classes - Super1 and Super2. This situation is common if you're using third-party class libraries or legacy code in which the implementation is already provided. Since you can only inherit the state and implementation from one superclass, you'll have to do the following to reuse the functionality of the other two classes:

  • Write interfaces that define the method signatures of the superclasses. Let's call them Interface1 and Interface2. Chances are, these interfaces will be available to you with the third-party or legacy code. However, you might just get the compiled class itself.
  • Extend Derived from Super1 and implement the interface of Super2.
  • Define a reference to an object of type Super2 in Derived.
  • Provide "wrapper" methods to delegate every method in Interface2 via the instance variable.

This mechanism is illustrated in Listing 1. The first two class implementations show Super1 and Super2. It's assumed that these implementations are already available and need to be extended by Derived. The next section of the listing shows the interface for Super2. Finally, the implementation of Derived is shown. Derived extends Super1 and implements Interface2 for Super2. Note that Derived doesn't have to provide wrapper methods for Super1 as it extends the implementation. However, for every method in Super2, Derived has to provide a wrapper method that delegates the call to the local reference to an object of type Super2.

Now consider that Super2 has 30 methods. To make matters worse, suppose that Derived needs to extend the functionality of another superclass with another 30 methods. Derived will have to provide 60 methods that do nothing but provide wrappers for existing implementations. This produces a "class bloat." You'll also notice a "class creep." Note that you needed to declare an extra interface for Super2. This may seem a small price to pay, but consider that in this scenario, to inherit a class, you have to add an interface. If you scale this to a large project, you end up with a large number of class/interface declarations.

If we had multiple inheritance in this case, all we'd need to do for Derived would be something like this:

public class derived extends Super1, Super2
{
// Only code specific to Derived class goes here
}

Using single versus multiple inheritance is illustrated in Figure 2. In the figure the black arrows represent inheritance from a superclass to the derived class; the red arrows indicate interface implementations; the green lines, delegation.

Figure 2
Figure 2:

It would be nice if the language had a mechanism for directing the compiler that indicated to the compiler that a class is a delegate for another class. This would at least prevent the need for derived classes to provide all the wrapper methods.

Garbage Collection
One of the major productivity gains in using Java for development came about because Java doesn't require the programmer to manage memory deallocations. In a pure Java program there's no such thing as a memory leak. Memory is allocated at instantiation; however, it's automatically released by Java's garbage collector. Freeing an object in Java is a simple task of setting its reference to null. The garbage collector frees the memory when it next collects garbage.

This is a nice scheme that makes programming easier and less prone to errors. The caveat is that the programmer has no control over when the garbage collector runs. It runs at the discretion of the Java Virtual Machine. Typically, it runs periodically and "cleans up garbage." Different VMs support different strategies for garbage collection. The bottom line is that memory is reclaimed when there's no longer any references to it.

Garbage collection works well in most programming situations. However, lack of control over memory deallocation sometimes presents interesting problems. The two main ones are in memory-constrained and real-time applications. Since the programmer has no control over exactly when memory is released, applications that are severely memory-constrained will have to rely on the garbage collector to free up memory fast enough so that no significant performance penalties have to be paid. This is typically not a problem as garbage collection algorithms have been around for a while now and are usually very efficient.

In real-time applications the problem has to do with the system resources that the garbage collector is going to consume when it runs. In these applications timing is critical, and the slowdown caused by the garbage collector may not be acceptable. Since there's no way to predict precisely when the memory is going to be freed, i.e., when the garbage collector is going to be activated, finding a workaround is a daunting task. The Java Runtime class provides a method gc() to facilitate garbage collection. A call to gc() may be made as follows:

// Tell the garbage collector to free up memory System.gc();

The purpose of this method is often misunderstood. Calling gc() doesn't deallocate memory. It's merely a hint to the VM to run the garbage collector as soon as it can. When the garbage collector actually runs depends on the runtime environment and the implementation of the garbage collector.

References
Garbage collection is based on determining when an object in the runtime process is no longer in use. In a Java application all handles to memory allocated by the application are via references to the corresponding objects. The task of the garbage collector is to identify the references to objects no longer in use and to free the memory allocated to those objects. A runtime Java application contains all the objects created during program execution, which are stored in a root set of references. As long as a reference is in the root set, the object is reachable by the program. Once the object is freed (either by setting the reference to null or by the object's finalize() method), it's ready for garbage collection (as long as no other references still point to the object). At this time the object is unreachable. Unreachable objects are ready for garbage collection.

JDK 1.2 introduces a new API that supports a finer grain of control for Java program's interaction with the garbage collector. This API, called Reference Object API, is in the package java.lang.ref. The reference object encapsulates a regular reference to a Java object (known as a referent). The Reference Object API allows developers to define degrees of "reachability." Besides defining an object as reachable or unreachable, the API also defines the following reference types (in order of reachability): Soft reference, Weak reference, Phantom reference.

A detailed discussion on the Reference Object API is beyond the scope of this article. The impact on garbage collection is that the weaker the reference, the more the incentive for the garbage collector to free its memory. Creating appropriate references using the Reference Object API gives the programmer more control over what memory is freed by the garbage collector. A good source for more information on Java references is http://java.sun.com/docs/books/tutorial/refobjs/index.html.

An Observation About Method Scoping
I'd like to wind up this discussion with an interesting aspect of Java method scoping that one of my colleagues accidentally discovered. In JDK 1.01 Java supported a keyword private protected. Methods declared with the private protected qualifier were visible only to their inherited classes. This keyword disappeared in JDK 1.1.x. Currently the following scopes exist for a method:

  • Private - A private method is visible only to methods in the declaring class (or in classes inside the declaring class).
  • Package - A package scope is the default and is in effect if none of the visibility keywords (private protected or public) qualifies the method. A method with package scope is visible to methods in the declaring class and all classes in the package. Note that it's not visible to inherited classes that are not in the package.
  • Protected - A private method is visible to methods in the declaring class, all subclasses of that class and in all classes in the package.
  • Public - A public method is visible to methods in all classes in the application.
None of these method-visibility qualifiers limit the visibility to classes' direct inheritance hierarchy. For example, consider the following inheritance hierarchy:

public abstract class Base
{
// Declarations and code

// Declaration of foo
void foo();

// more code
}

public class Derived extends Base
{
// Declarations and code

void foo()
{
// implementation
}

// more code
}

The Scope Modifier could be one of the keywords - public, protected - or neither (which implies default package scope). None of these limit the visibility of the method foo() to just these two classes. If the qualifier is protected or default (no qualifier), foo() is still visible to at least other classes in the package. What this means is that to limit the visibility to direct inheritance hierarchies, you'd have to package each inheritance hierarchy separately, i.e., Base and Derived would have to be in a separate package and the method qualifier used would be protected.

There is a way to limit the scope. If I change the code excerpt above to the following, the scope will be limited to direct hierarchy:

public abstract class Base
{
// Declarations and code

abstract private void foo();

// more code
}

public class Derived extends Base
{
// Declarations and code

private void foo()
{
// Implementation
}

// more code
}

Declaring the method private makes it invisible to all classes except Base. Declaring it abstract, however, requires that a derived class provide an implementation of the method. The method is callable only from Base and Derived. The scoping rules are contradictory. A private modifier limits the scope to the declaring class only. Subclasses are excluded from the scope of the method. However, the abstract modifier apparently negates the stipulation.

About the Author
Ajit Sagar is a member of the technical staff at i2 Technologies in Dallas, Texas. A Java certified programmer with eight years of programming experience, including two in Java, he holds a BS in electrical engineering from BITS Pilani, India, and an MS in computer science from Mississippi State University. Ajit can be reached at [email protected]

 

All Rights Reserved
Copyright ©  2004 SYS-CON Media, Inc.
  E-mail: [email protected]

Java and Java-based marks are trademarks or registered trademarks of Sun Microsystems, Inc. in the United States and other countries. SYS-CON Publications, Inc. is independent of Sun Microsystems, Inc.