|
SYS-CON.TV Webcasts
Comments
Did you read today's front page stories & breaking news?
SYS-CON.TV
|
Top Links You Must Click On
General Java Java through the Eyes of a C++ Programmer Part 2
Java through the Eyes of a C++ Programmer Part 2
Feb. 1, 1997 12:00 AM
Introduction
Object and Class Initialization and Finalization 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
Object Allocation and Deallocation 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
Custom Memory Management
Memory References 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 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 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 {
double estimatedMean =
Errors and Exceptions
Resource Deallocation
Threaded Exceptions
Exception Hierarchy 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.
Parameterized Types 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 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
Access Control for Classes
Access Control for Operations and Attributes 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: 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 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. 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 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
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 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 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 { 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
Meta Information 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
Concurrency
Portability
Conclusion 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 Reader Feedback: Page 1 of 1
Enterprise Open Source Magazine Latest 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 |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||