|
Comments
Did you read today's front page stories & breaking news?
SYS-CON.TV
|
General Java The Java Programming Language Trade-offs
The Java Programming Language Trade-offs
By: Ajit Sagar
Nov. 1, 1998 12:00 AM
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.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. 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: 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 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. 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. 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: 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 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. 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 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 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 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 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: 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. Reader Feedback: Page 1 of 1
Latest Cloud Developer Stories
Subscribe to the World's Most Powerful Newsletters
Subscribe to Our Rss Feeds & Get Your SYS-CON News Live!
|
SYS-CON Featured Whitepapers
Most Read This Week
Breaking Cloud Computing News
|
|||||||||||||||||||||||||||||||||||||||||||||||||