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
 

Introduction
In the first article of this series, we compared how Java and C++ support various object-oriented concepts. In this second and final part, we cover other programming language concepts and examine differences in how the two languages support them. Each programming language concept has its own section, first presenting the concept in commonly-used terms and then comparing how ANSI C++ and Java 1.0 implement, or can be used to implement, it.

Object and Class Initialization and Finalization
Objects and classes both need to be given the opportunity to initialize their contents before they are used, and to clean up before they are destroyed. For objects, this is typically done using constructors and destructors.

Because Java has garbage collection, destructors are not needed. However, there is still the need to allow an object to be cleaned up before being deallocated; for example, to release resources other than memory, such as file handles. To allow for this, Java classes may have a finalize operation. This operation is invoked automatically when the object is about to be garbage collected.

C++ allows very limited class initialization capability - class attributes may be initialized only to constant values and the initialization must occur outside the class definition.

Java allows more general class initialization. Just as Java has constructors for initializing instance variables when an object is created, it also has static initializers for initializing class variables. Static initializers are class operations with no name, parameters, or return value that are invoked when a class is loaded. Static initializers can be very useful when any form of computation is needed for initialization, such as to initialize an array in an algorithmic fashion. This is shown in the example in Listing 1, which initializes a large matrix to the identity matrix.

Static components of classes must be initialized before their first use. Typically, this is done when the class is loaded. In C++, the order in which classes with static components are initialized is indeterminate because all the classes are loaded in before main is called and the dependencies between the classes is not known at this time. Java loads classes on demand and thus the order in which classes are loaded is determinate; the order is based on the dependencies between the classes.

Memory Management
Every substantial program needs to manage dynamic memory. Memory management in C++ is done via pointers to raw memory. Java does not provide access to raw memory; it only provides symbolic handles to memory in the form of object references.

Object Allocation and Deallocation
C++ objects can be allocated on the stack or the heap. In C++, the programmer is aware of this distinction and must handle each case differently. At runtime, when an identifier of a particular class type comes into scope, the memory is allocated on the stack to store an object of that type. Objects allocated on the stack are automatically deallocated when they go out of scope. Objects are allocated on the heap by using the new operator, which returns a pointer to raw memory. Whenever an object is allocated, either automatically on the stack or on the heap by invoking new, the object's constructor is invoked. If an object is allocated on the heap, the programmer is responsible for deleting the object using delete. Whenever an object is deallocated, either automatically or with delete, the object's destructor is first invoked to allow internal cleanup and deletion of aggregate variables.

Java provides only one way of allocating objects. No storage is allocated for an object when an object reference is declared. The programmer is responsible for allocating storage using the new operator. Java's new operator returns an object reference as opposed to a raw memory pointer. However, since Java uses garbage collection, there is no need to explicitly deallocate objects; hence, there is a need for neither the destructors nor the delete operator.

Garbage Collection
Java provides automatic garbage collection, which eliminates the need to deallocate storage explicitly as in C++. The Java garbage collector runs as a separate low-priority thread, collecting objects that are no longer needed.

Custom Memory Management
C++ allows the programmer to overload the new and delete operators to provide a custom memory management scheme. Java does not allow overloading of operators, including new. Although the ability to overload new might occasionally be useful, it is most useful when memory allocation primitives are provided, in which case it can be used to implement a custom memory management scheme. Java does not provide such primitives for security reasons, so allowing overloading of new would complicate the language needlessly.

Memory References
Memory references can be put into two categories: data references and operation references. At runtime, a data reference points to a block of data. An operation reference points to the executable code in memory that corresponds to an operation.

C++ provides access to raw memory in the form of pointers. Java does not provide pointers to raw memory. Its model for allocating and referencing memory hides raw memory from the programmer. This model presents the raw memory to the programmer as objects and symbolic handles to them in the form of object references.

Data References
In C++, data references (pointers) are needed to allocate and deallocate memory off the heap, to access the data in this allocated memory, to manipulate arrays and to implement data structures such as trees and linked lists.

The model for allocating and referencing memory provided by Java does not permit access to raw data memory - data may be accessed only through symbolic handles. Arrays are provided as objects and array elements are accessed using indices. Consequently, there is no need for pointers for arrays.

In Java, the resolution of a symbolic handle to a memory address takes place at runtime. This indirect memory access model allows the language to support compile-time type safety and the runtime verification of object access code. This model also eliminates the possibility of having corrupted memory, which is a major problem in C++.

Operation References
C++ allows the programmer to take the address of a function and pass it around as a function pointer. The function can then be invoked by dereferencing the pointer. Function pointers are often used to implement callbacks. For example, consider a C++ function prototype for generating random numbers between a minimum and maximum value:

double (*getRandomNum)(double min, double max);

Now consider a method for estimating the mean value for the random numbers generated by a random number generator passed as an argument:

double mean(double (*getRandomNum)(double, double),
     double min, double max);

Any random number generator having the prototype of getRandomNum can be passed to the function mean. The function mean can use this random number generator to generate a sequence of random numbers which can be used to estimate the value of the mean.

In Java, the same effect can be achieved by defining an interface as follows:

interface RandomNumberGenerator {
    double getRandomNum(double min, double max);
}

A class designed for generating random numbers can be implemented using this interface. For estimating the value of the mean, a class, MathUtil, containing a class operation, mean, can be defined:

class MathUtil {
public static double mean(RandomNumberGenerator randGen,
double min, double max);
}

The class operation mean accepts an instance of any class that implements interface RandomNumberGenerator. For example, if a class, UniformRand for generating random numbers with uniform distribution implements the interface RandomNumberGenerator, the operation mean can be invoked as follows:

double estimatedMean =
MathUtil.mean(new UniformRand(), 5.0, 10.0);

It is possible to take a similar approach for implementing callbacks in C++. However, a large number of C++ class libraries rely on function pointers because it results in fewer classes and can be more efficient.

Errors and Exceptions
Exceptions allow change in the normal flow of control in a program when some abnormal event, usually an error, occurs. They are useful for writing safe programs by providing a well-defined alternate flow of control to deal with errors. The exception handling mechanism provided by Java is similar to that in C++. When a program does something illegal or an abnormal runtime condition occurs, the runtime can raise an exception. The program can also raise an exception explicitly using the throw statement. Both languages provide a mechanism for detecting and handling exceptions.

Resource Deallocation
In C++, when an exception is thrown, the runtime searches for a handler up through the calling chain. The call stack is unwound to the stack of the function or block that contains the handler. In the process, destructors for objects that are allocated on the stack are called automatically as they are removed from the stack. However, this is not the case with objects dynamically allocated on the heap. The burden of keeping track of dynamic memory and deallocating it is on the programmer. Generally, this task is accomplished by using smart pointers and proxies such as those provided by the ANSI C++ Standard Library. Since Java does automatic garbage collection, this problem does not exist.

Threaded Exceptions
Multithreading is provided as part of the Java language, and the Java exception handling mechanism is thread-aware. When an exception is thrown in a thread, the runtime searches for a handler up through the calling chain for that thread. If no handler is found, the uncaughtException method of class ThreadGroup for that thread group is called and the thread terminates. When an exception is thrown, locks held by the abruptly terminating synchronized blocks and operations are released. Threads are not part of the C++ language, and thus multithreaded exceptions are not part of its specification.

Exception Hierarchy
C++ provides an exception class hierarchy of default exception types, but in fact any variable of a class or primitive type may be thrown as an exception. Java also provides an exception class hierarchy but requires that every exception be an instance of Throwable, the root class in this hierarchy, or one of its subclasses.

In C++, if an exception is not caught at a given level in the calling chain, it is passed on to the caller. If the exception is not caught at any level, the program will terminate. Operations may optionally declare exceptions they can be expected to throw. The same is true of Java, except that Java has two types of exceptions: checked and unchecked. Unchecked exceptions behave like C++ exceptions. However, when an operation raises a checked exception, it must declare it as part of its signature or a compile-time error will occur. Such a declaration makes it known to the caller that it must either handle the exception or declare it in its own signature, thereby propagating it to its caller. Using checked exceptions is recommended because the compiler ensures that the programmer must consider each exception raised by every operation that is called and make a conscious decision as to how to handle it. C++ has no such compile-time checking.

Java provides another orthogonal classification for exceptions, recoverable and irrecoverable. Irrecoverable exceptions are used to signal an abnormal situation from which the application cannot recover, such as a virtual machine error. Even though an application can catch these exceptions, it is not recommended. Recoverable exceptions are used in all other situations.

The Java exception hierarchy is shown in Figure 1. Exception classes predefined in Java are shown as solid boxes, while the dotted boxes show the checked/unchecked and recoverable/irrecoverable status of exceptions derived from various points in the hierarchy. The names of the provided classes are somewhat misleading. Although all are really exceptions, the Exception class is actually the root of the recoverable exception hierarchy, while the Error class is the root of the irrecoverable exception hierarchy. Throwable is the root class of the entire hierarchy. As shown, subclasses of Exception are checked, while subclasses of RuntimeException and Error are unchecked.

Figure 1
Figure 1:

Parameterized Types
C++ provides templates to allow definition of parameterized types. Templates provide a way of specifying how a family of related classes can be made. They are most useful in creating type-safe collection classes. A class template for a family of collection classes specifies how a collection class can be made for a particular element type. A template is written in terms of one or more type parameters. For example, for collection classes, a type parameter may refer to the type of elements. A specific collection class is created from the template when the element type is specified at the time of use. For different element types, different collection classes belonging to the same family are generated at the time of use which can result in bloated compiled code. This problem can be eliminated by designing intrusive template collection classes, so-called because the classes of elements in the collection must inherit from a common superclass. This restriction makes it difficult to use a collection class to store instances of a preexisting class.

Although Java does not support parameterized types, it is possible to implement type-safe collection classes because Java objects have more runtime type information than C++ objects. For example, the constructor of a type-safe collection class can take a prototype element, an object of the same type as the elements that will be inserted. Now, before allowing insertion, the collection can check if the element to be inserted and the prototype are of the same type, thus enabling it to enforce type safety. If the element is not of the desired type, it can reject the element by throwing an exception. This approach is nonintrusive and this collection class can be used to store objects of any specified type by providing an appropriate prototype at the time of instantiation. The class can be implemented so that it does not do type checking if no prototype is provided when the constructor is invoked, thus allowing the use of the collection class for implementing heterogeneous as well as homogeneous collections.

The approach described here for a type-safe Java collection provides significant run-time flexibility as compared to most C++ template classes. However, C++ template collection classes enable more compile-time checking. Additionally, C++ template classes can be parameterized by primitive types as well as classes, while only objects may be stored in a collection class using the approach described for Java.

Naming
C++ allows classes to be nested, and the name of a nested class is scoped by the class containing it. C++ also provides the namespace construct for providing another level of scoping for classes. If a namespace, foo, contains a class, container, which in turn contains a class, contained, contained would be fully qualified as foo::container::contained. In this manner, it is possible to have more than one class with the same name, as long as they reside in different namespaces. To avoid ambiguity, the classes must be referred to by their fully qualified names. Namespaces help keep names simple and natural, while preventing naming conflicts.

Java does not support nested classes. However, Java does have a package construct which provides functionality similar to C++'s namespace. In Java, a fully qualified class name consists of the package name followed by a dot followed by the class name: package.class. A class is made part of a package by declaring the package name in a package statement at the beginning of the source file. If no package statement appears, the class becomes part of the anonymous package. Classes in the anonymous package may not be imported into other files.

While neither Java nor C++ allows nested namespaces or packages, Java package names may have dots in them, which provides the illusion of nested packages. An example is the best way to explain this. Part of the standard library included with Java is the java.awt package, which contains the abstract windowing toolkit classes such as java.awt.Component. There is another logically related package called java.awt.image. This is a completely independent package in the sense that the statement import java.awt.* will not import the java.awt.image package. However, it is named to imply that it is a "subpackage" of java.awt. This is a very nice bit of syntactic sugar that C++ does not support.

Note that while both C++ namespaces and Java packages are used for name scoping, Java packages are also used for access control. The next section offers more information on this.

Access Control
An important feature that supports encapsulation in object-oriented languages is access control, whereby a programmer may control who is allowed to access the constructs defined. For example, attributes may be marked private to prevent them from being accessed anywhere except by operations in the defining class.

Access Control for Classes
All classes in C++ are accessible anywhere, including nested classes. In Java, classes belong to a package and may be marked public to indicate they are accessible to anyone, or not marked to indicate they are accessible only within the package.

Access Control for Operations and Attributes
A C++ class can declare its operations or attributes public, protected, or private, making them accessible from any other class, only to subclasses, and only within the class, respectively. A class may declare another class its friend, allowing the latter to access all operations and attributes of the former.

Java provides a richer set of access control specifiers but does not support the friend mechanism. Access to operations or attributes of a class depends on the level of access granted by the class and on whether the class wishing access is in the same package as the class that declares them. Thus the package is used not only as a name scoping mechanism but also to provide access control.

Java provides five levels of access on operations and attributes: public, protected, private protected, default, and private. Default applies if none of the keywords are used. The semantics of the various access levels are summarized in Table 1, for both public and nonpublic classes.

Table 1
Table 1:

Although Java does not support friends, if both classes can be put in the same package and use the default level, then a similar effect can be achieved. The difference is that the C++ friend construct provides unidirectional access control, while this solution in Java would allow bidirectional access. There is no way in Java to have two classes in different packages have access to one another without using public variables. While such access may occasionally be useful, this is not a serious limitation. In fact, it may be seen as encouraging good design: using friends violates encapsulation and it makes sense to put all such classes as close together as possible (such as in the same package).

Arrays
Java provides arrays as objects. It eliminates the need for pointer arithmetic.

For each primitive type, built-in class and interface, and user-defined class and interface, the language implicitly provides an array class. For example, int[] is provided as an array class to store elements of type int. If Frog is a subclass of Amphibian, then array Frog[]is a subclass of array Amphibian[]. Figure 2 illustrates these concepts.

Figure 2
Figure 2:

In both C++ and Java, the first element of an array has index 0. Unlike C++, when a Java array element is accessed, the array index is checked and if it is out of bounds an ArrayIndexOutOfBoundsException is thrown.

Neither Java nor C++ provide a separate type for multidimensional arrays; instead, they may be implemented as arrays of arrays.

Dynamic Behavior
When a program is executed, several things happen behind the scenes in the runtime to make a program execute correctly. A language compiler ensures that the machine code generated results in the right behavior to support the model that a language supports.

In C++, dynamic binding on invocation of virtual functions to support polymorphism is an example of dynamic behavior. Java supports a more dynamic model than C++, which opens up some novel possibilities. Understanding the dynamic nature of Java is crucial to truly appreciate some of its behavioral nuances. Armed with this understanding, useful tradeoffs can be made that enable one to trade some performance for increased flexibility where it is appropriate in an application.

Interpreted versus Compiled
Java source code is compiled into byte code that contains instructions for a virtual machine called the Java Virtual Machine. Byte-compiled code is then interpreted at program execution time. The fact that Java is interpreted makes it somewhat slower than comparable compiled C++ code. This performance problem may be mitigated by just-in-time Java compilers that promise to soon become ubiquitous.

One implication of the fact that Java is interpreted and the fact that linking is only done at runtime is that recompilation of subclasses, because of changes made to superclasses, is not necessary in most cases. This common C++ problem, where subclasses need to be recompiled to take into account the change in size and the changed offsets to access instance variables, does not exist in Java. Elimination of this "fragile superclass" problem results in faster development cycles when coding in Java, since the impact on recompilation when a change is made is typically less than in C++. The combination of architecture-neutral byte code and interpretation of the same contributes to the portable nature of Java.

Dynamic Loading
Many of the dynamic behavioral features of Java can be traced to how the language does symbol resolution, so this process is examined in some detail.

In Java, before a class or interface can be actively used it has to be loaded, linked, verified, prepared and initialized by the Java Virtual Machine. Verification ensures that the binary code for a class is correct. During preparation, static fields are created and initialized to default values. Static initializers are executed during the initialization phase that follows the preparation.

The first step in loading a class or interface happens somewhat differently in Java than in C++, where all symbols need to be resolved at link time. The C++ compiler ensures that for each symbol that is referenced in the program, there exists a definition. If the definition, such as a function implementation, exists in a shared library, it does not load the definition into the executable at link time. However, it ensures that there exists a shared library that will provide this definition at run time. In Java, it is more appropriate to think of a program as incrementally growing as needed, instead of the conventional notion of a monolithic executable. A range of possibilities exists with dynamic loading. On one end is shared library-like behavior: when a class reference is encountered, the Java runtime looks for the class in a set of directories specified in the CLASSPATH environment variable. This is programmer transparent dynamic loading, just like shared library symbol loading in C++. Furthermore, classes in Java can be loaded by programmer request, both locally and remotely. C++ does not provide this facility as part of the language.

The fact that a symbol can be loaded on demand can be used to advantage in Java, for example, when a stream of bytes is being assembled into an object. The stream of bytes may be a persistent representation of an object in a database. When the stream is parsed to reconstruct the objects in memory, a symbol encountered in the stream may require a class that is not loaded. Such a class can be loaded at the programmer's request, and the object corresponding to the stream can be constructed based on the loaded definition. Java also provides access to compilation via java.lang.Compiler. One can write a Java program that generates Java code, compiles it, loads it and uses it, all in a single session!

Dynamic Specification of Shared Libraries
Like C++, Java enables loading of shared libraries dynamically. However, the name and location of a shared library need not be specified at compile time. In C++, the shared library name is bound at compile time.

As an example, consider loading a shared library in Java to access a native method. Java provides a mechanism to access native code on a platform. A method that is implemented in C is called a native method. A Java method that accesses such native code is declared native. The compiled native code is placed in a shared library which must be loaded before it is accessed. This can be done by loading the library in the static initializer of the class that declares the native methods. For example:

class SignalProcessor {
static {
loadLibrary(java.lang.System.getProperty
(MyTransformLib));
}
public native FourierTransform();
}

The name of the library passed to the loadLibrary method of java.lang.Runtime is obtained from an environment variable or system property, as it is called in Java.

Object Memory Layout
Since objects in Java are accessed via handles, discussed earlier in the Memory References section, code that accesses data in an object is insulated from changes made to the class that defines the object. In C++, changing any data in a class would require recompiling the code that accesses the data, since memory offsets would have changed. In Java, this recompile is not required.

Meta Information
One feature of Java is that every class has a common parent at the root of the class inheritance hierarchy, the Object class. You may be confused by the name of this class; at first blush it may seem that, being a class, it would be more appropriately named Class instead of Object. However, upon deeper inspection the reasoning behind the name becomes clear.

In general, a class is named for the type of object that it defines. For example, a class which defines a stack might be named Stack. The Stack class itself is not a stack; instances of the Stack class are stacks. Note that instances of subtypes of Stack are also stacks. By analogy, the Object class itself is not an object (it is a class); instances of the Object class are objects. Since Object is an abstract class and cannot be instantiated directly, only instances of subclasses of the Object class are objects. Since all classes are subclasses of Object, all instances of all classes are objects.

Java actually does provide a class named Class. This class, however, truly represents classes rather than an object. Every class and interface is represented at runtime by an instance of Class. A class like Class, whose instances are themselves classes or that maintain information about classes, is known as a metaclass.

Security
C++ does not support security as part of the language, while security is one of the defining properties of Java. Java has no language features which allow undefined behavior, such as pointers and incorrect memory accesses, making Java programs very robust. Malicious or unwanted behavior is prevented, such as accessing memory not allocated to you. In addition, the Java runtime does byte code verification to ensure that the program being executed was not modified after the byte code was generated.

Concurrency
Java provides language-level support for concurrent programming in the form of threads by the synchronized keyword. C++ does not provide direct support for concurrency.

Portability
Although C and C++ make claims to portability, they support portability only at the source code level and then only if the programmer takes special pains to write portable code. In contrast, a defining feature of Java is that it guarantees portability,including at the byte code level. Specifically, Java has four features that guarantee portability:

  • All primitive data types are defined in an architecture-neutral manner, with their representations and operations defined consistently regardless of the platform.
  • Compilation of Java code generates Java virtual machine instructions, which are mapped to instructions of a real microprocessor by an interpreter (or just-in-time compiler) at execution time. This allows the byte code to be executed on any machine that has a Java interpreter.
  • Java provides a set of GUI components which are mapped at runtime to the native windowing environment to provide for portability of the user interface.
  • The java.lang.System class provides platform-independent access to system functions.
Conclusion
Java builds upon the best concepts of C++ and many other programming languages and environments to provide an elegant computing environment that makes what the authors believe to be intelligent, pragmatic choices. Simultaneously supporting the object-oriented model in a clean manner, guaranteeing secure and robust software, and providing many distribution transparency features, we believe Java provides a very significant step forward in computing technology.

We hope that the comparison between Java and C++ provided here will pique the interest of those not already immersed in Java and will provide an easier transition for programmers in moving from C++ to the exciting new phenomenon that is Java.

References

  1. Flanagan, D. "Java in a Nutshell," O'Reilly & Associates, Inc., 1996.
  2. Gamma, E., R. Helm, R. Johnson, and J. Vlissides. "Design Patterns: Elements of Reusable Object-Oriented Software," Addison-Wesley, 1995.
  3. Gosling, J. and H. McGilton. "The Java Language Environment: A White Paper", Sun Microsystems, 1995.
  4. Gosling, J., B. Joy, and G. Steele. "The Java Language Specification," The Java Series, Addison-Wesley, 1996.
  5. ISO/ANSI C++ Draft X3J16/95-0087, April 1995.
  6. Stroustrup, B. "The C++ Programming Language," Second Edition, Addison-Wesley, 1991.
About the Authors
Suchitra Gupta received his Ph.D. from the State University of New York at Stony Brook. Currently, he works for U S WEST Communications.focusing on design and development of distributed applications using object-oriented techniques. He can be reached at [email protected]

Jeffrey M. Hartkopf received his B.S. and M.S. in Computer Science from the University of Colorado at Boulder. He works at U S WEST Communications on a groundbreaking project in computing systems management. He is also participating in the development of an enterprise platform for large scale client/server applications. He can be reached at [email protected]

Suresh Ramaswamy received his B.S. in Electrical Engineering from the University of Bombay and his M.S. in Electrical Engineering from the USC. Currently, he works at US WEST Communications, where he has been working on a development platform for enterprise class client-server applications. He can be reached at [email protected]

	

Listing 1: Static Initializers in Java.

public class Matrix {
	private static int identityMatrix[8][8] = new int[8][8];

	static {
		for (int i = 0; i < 8; i++)
			for (int j = 0; j < 8; j++)
				// initialize diagonal elements to 1
				identityMatrix[i][j] = (i == j ? 1 : 0);
	}

	// ...
}
 

 

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.