This article may contain URLs that were valid when originally published, but now link to sites or pages that no longer exist. To maintain the flow of the article, we've left these URLs in the text, but disabled the links.
|
java911@microsoft.com |
The basic syntax of Java code has a lot in common with C++. In fact, the code looks pretty similar from a distance (especially if you squint). Keywords, variable declarations, statements, loop constructs, and conditionals all look about the same in Java as they do in C++. But don't let that fool you! Java is a completely different beast in many ways. Let's take a closer look at some of those differences.
Classes and Objects
|
HelloWorld.cpp
#include <stdio.h>
void main (int argc, char** argv)
{
printf("Hello, World!");
}
Notice that the C++ function main is declared at the global lexical scope. But in Java, hello world it, looks like this:
// HelloWorld.java
/**
* Hello world class
* @author Jonathan Locke
*/
public class HelloWorld
{
/**
* Main application entrypoint.
* @param arg Command line arguments
*/
static public void main(String[] arg)
{
System.out.println("Hello, World!");
}
}
The main program entry point is implemented at the class scope (inside class HelloWorld, in this case) as a static method. While subtle, this distinction is important, and stems from the fact that Java is an almost purely object-oriented language. In fact, the only entities in Java that are not objects (or arrays of objects) are the primitive types (Boolean, byte, char, short, int, long, float, and double). Everything else in Java is a "first-class" Object of some kindan instance of some class that inherits directly or indirectly from java.
lang.Object.
String s = (String)System.out;
will be flagged by the compiler with an error like this:
Microsoft® Visual J++ Compiler Version 1.02.7315
Copyright (C) Microsoft Corp 1996-1997. All rights reserved.
CastFail.java(18,38) : error J0067: Cannot convert 'PrintStream' to 'String'
Interfaces
Exceptions
Another big win for Java over C++ is the design of Java's exception handling. In Java, there are two types of exceptions: checked (those exception classes derived from java.lang.
Exception) and unchecked (those derived from java.lang.
Error). Checked exceptions impose a special restriction on programmersif a method is declared as throwing a checked exception, the caller must either handle the exception with a try/catch block or must itself declare that the exception may be thrown. This allows the compiler to ensure that all checked exceptions are handled in some fashion at compile time. Such compiler assistance is a big advantage over C++, which provides no such checking. In C++, it is necessary for you to know what exceptions are thrown from what pieces of code in order to ensure that they get caught.
With some C++ compilers it is possible to work with the operating system to resume or retry certain exceptions. Microsoft's Win32 Structured Exception Handling (SEH) mechanism is exposed through their compiler in just this way. If such a feature were available in Java (and I'm not necessarily saying that it should be), you could create dynamically growable arrays by catching and resuming an ArrayIndexOutOfBounds exception after resizing the array.
Threading
Built-in support for threading and synchronization primitives in Java is another major difference between the two languages. In C++, threading is considered the operating system's territory and no attempt is made to make threading portable. The downside of this is that your threading code won't port. The upside is that your operating system will very likely provide a wider array of more efficient synchronization APIs.
Although guarantees about the precise behavior of threads from platform to platform are very weak in Java, the basic notion of a thread is part of the language. Threads are created by instantiating a java.lang.Thread object, then passing in a target interface (java.lang.Runnable) that specifies the code that the new thread should run. When the thread's start method is later called, a new thread is created and enters the target object's run method via the Runnable interface.
As various threads run through the system, the Java synchronized keyword can be used to ensure that only one thread can have access to an Object at a given time. It turns out that this simple serialization primitive is also sufficient to construct higher-level synchronization constructs like semaphores and reader/writer guards. Unfortunately, because Java synchronization is essentially an efficient form of polling, higher-level constructs aren't always the most efficient.
Besides efficiency, one other big downside to threading in Java (and I'm serious about this) is that it is so easy to do. Programmers with little or no background in multithreading can create threads in no time flat. In fact, you can create and start a thread with one line:
class ThreadMe implements Runnable
{
public ThreadMe()
{
new Thread(this).start();
}
public void run()
{
}
}
Such simplicity can lead to situations where inexperienced people are creating (and probably mismanaging) too many threads. And even those of us with lots of multithreading experience have trouble deciding how much synchronization to apply and where to put it. If you use too little synchronization, you can end up with race conditions; use too much synchronization and you can end up with deadlocks. While it's certainly not a bad idea to make threading easy, it does have a lot of potential to make trouble if it's misused.
Memory Management
Because of the tight restrictions on pointers in Java (which don't exist in C++), it is possible for the Java Virtual Machine to reclaim unreferenced storage (memory) automatically. This is wonderful because programmers (especially beginners) no longer have to spend so much time trying to find memory leaks in applications. Unfortunately, there are ways of defeating the garbage collector. One problem that can crop up is that the garbage collector falls behind on its duties if a program is creating a lot of garbage. In practice, this often turns out to be fatal for the application. Another, more obvious problem is that objects cannot be garbage collected until there are no references to them. This sometimes makes it necessary to explicitly null out object references to allow the objects to be collected. While this isn't as difficult as making proper use of stack objects and the C++ delete keyword, it does require that the programmer think a little about object usage at times.
When an object has no references and is about to be collected, the garbage collector makes a call to the object's finalizer method. This method can be used to reclaim resources associated with the soon-to-be-reclaimed Java object. Unfortunately, there aren't any guarantees about what thread will call this method. Therefore, be very careful about the methods you call in a finalizer, as you can easily wind up in a deadlock situation.
C++ programmers may be tempted to see finalizer methods as equivalent to C++ destructors, but nothing could be further from the truth. C++ destructors are used to reclaim resources (particularly memory) used by the object being destructed. But in Java, those same resources will eventually be reclaimed by the garbage collector. The only exceptions to this (and the reason that finalizers are part of Java to begin with) are native resources that are entirely outside the realm of the garbage collector. So, generally speaking, only classes with native methods are ever going to need finalizers. And if you are writing in pure Java, chances are that you'll never need to use them.
Templates
C++ has the notion of generic templates, which are, roughly speaking, type-parameterized classes (or functions). Also, C++ has a useful standard library of these templates called the Standard Template Library (STL). Although there has been talk of adding a similar feature to Java, it is not available at the time of this writing. One downside to the nonexistence of parameterized types in Java is that it isn't possible (or at least, isn't convenient) to create type-safe container objects. The Vector class in java.util is a perfect example of this problem. The Vector.addElement method takes a java.lang.Object, and Vector.elementAt returns a java.
lang.Object. This means that it is possible to put the wrong type of object in an array and also to miscast the Object when you pull it out again. Both are trouble waiting to happen.
Other Issues
Java is certainly less efficient than C++ for a variety of
reasons. First and foremost, C++ compiles straight to native code while Java compiles to bytecode, which is later interpreted on the target machine. Interpreting bytecode takes time. While it is theoretically possible to take advantage of bytecode to perform aggressive runtime optimizations in the interpreter, there are many applications for which Java will probably always be slower than C++.
Besides the native code versus bytecode issue, C++ has certain speed advantages over Java because of its static nature. Because it is much less dynamic, C++ can make more assumptions about code. It can do a better job inlining and optimizing. Except for explicitly final
methods, Java can rarely do any inlining at all.
Also, other features like C++ macros, templates, and stack variables can speed things up. And at runtime, C++ doesn't have to check type casts or array accesses. There is a price for all these little speed advantagesthey all have dangers. Whether it's a binary compatibility issue or a stack/pointer crash, C++ is faster but less safe than Java.
JavaDoc is a wonderful thing. Classes and members in Java code that are preceded by JavaDoc-style comments (enclosed in /** */) are processed by a tool included with the JDK that extracts and formats API documentation. This helps to keep the state of external documentation in sync with changes to the source code. It is great that this is part of the language, but similar tools are available for C and C++. Autoduck, a too that originated within Microsoft and currently available at ftp://ftp.accessone.com/pub/ericartzt/autoduck.zip, is just such a tool and produces results every bit as nice as JavaDoc.
The lack of a preprocessor in Java is great most of the time. The only part that I really miss is the ability to ifdef code for various build types (very important in real-world coding environments). Microsoft has remedied this situation in
their compiler.
Conclusion
While there are many books and references comparing Java to C++ with all the gory details, I think it's especially important to understand the big picture. I hope this article has helped in that respect, and I wish you the best of luck, whether you choose to program in Java or C++. Cheers!
From the September 1998 issue of Microsoft Interactive Developer.