Chapter 1. Advanced Java

  • Basic Java

  • Java I/O Routines

  • Introduction to Threading in Java

  • Object Serialization

  • Performance

  • A First Look at Java Networking in Action

Our tour of Java networking begins with a simple and quick tutorial on several of the advanced features of the Java programming language. From there, we dive straight into the application programming interfaces (APIs) associated with connecting Java objects across disparate machines and networks. Each of these APIs has both strengths and weaknesses, and we certainly highlight the strengths while exposing the weaknesses. Finally, we describe the tools necessary to provide a safe environment for your Java applications, without sacrificing the power of the language itself. Our discussion begins here, with the fastest object-oriented tutorial this side of the Mississippi.

Basic Java

When beginners first take to C++, their primal screams can be heard for miles. Often, emergency crews are dispatched immediately to prevent the serious injuries that are typically endured when beginners are first confronted with the dreaded *pointer->. Enough to make a grown man cry, C++ is a powerful yet incredibly difficult language.

Enter Java. Java is object-oriented, modular, elegant, and—in the hands of a master—quite poetic! Java code can be beautiful and powerful, fun and exciting, and, most importantly, incredibly useful!

This chapter focuses on some of the advanced concepts you need to grasp in order to support your further endeavors using Java. Throughout the discussion, you will see sample code that highlights some of Java's inherently object-oriented features: encapsulation and information hiding, modularity, inheritance, and elegance. We intend this chapter to provide you with a base of terminology, not a comprehensive Java language tutorial. Beginners should be forewarned: This book assumes you know the language.

Much of what is discussed in this chapter is the fundamental design aspects of an object-oriented language. For seasoned programmers, the urge to skip this chapter will be strong. However, many of the advanced features of Java, as well as the architectural decisions that must be made for a Java networked application, are based on the fundamental concepts we describe in this chapter and are of great importance to both veteran and rookie networking programmers alike.

Object-Oriented Design Using Java

In Java, you declare classes as a collection of operations performed on a set of data. Because data cannot be passed by reference (Java is a pointer-free language—let the cheering begin!), Java classes are needed to contain data so that it can be modified within other classes.

Classes vs. Interfaces

The prevailing assumption about Java is that you are unable to separate implementations from interfaces. However, this assumption is false. Java provides an interface component that is similar to its class counterpart except that it is not permitted to have member functions. Indeed, other objects that will implement its method and variable definitions, as illustrated in the following snippet, must reuse this interface.

public interface MyAdvancedJavaInterface
{
    public abstract void methodOne();
    void.methodTwo();
}

public class MyAdvancedJavaClass implements MyAdvancedJavaInterface
{
    MyAdvancedJavaClass()
    {
    }

    public void methodOne()
    {
        . . .
    }

    public void methodTwo()
    {
        . . .
    }
}

All member functions declared within interfaces are, by default, public and abstract. This means that they are available for public consumption and must be implemented in a class before they can be used. Furthermore, interfaces do not have constructors and must be extended before they can be used.

Data Members

Good object-oriented style dictates that all data members of a class should be declared private, hidden from any operations other than those included in the class itself. But, any experienced object-oriented (OO) programmer will tell you in no uncertain terms that this is often stupid and inane for small classes. Because structs are not available in Java, you can group data into one container by using a class. Whether you subscribe to the artificially enforced private-data-member scheme of C++ or the language-enforced scheme of Smalltalk is entirely up to you. Java, however, assumes that data members are public unless otherwise instructed, as the following snippet suggests.

public class MyAdvancedJavaClass
{
    public int numItems;
    private int itemArray[];
};

Methods

Another important component of the Java class is the operation, or method. Methods allow outside classes to perform operations on the data contained in your class. By forcing other classes to utilize your data through the classes, you enforce implementation hiding. It doesn't matter to other classes that your collection of data is an array, for as far as those classes are concerned, it could be a Vector. Somewhere down the line, you could change the implementation to a HashTable if efficiency becomes a concern. The bottom line is that the classes that use your methods don't care, and don't need to know, so long as the method signature (the method name and its accompanying parameters) remains the same. The following code shows how a method can be introduced within a class.

public class MyAdvancedJavaClass
{
    public int numItems;
    private int itemArray[];

    public void addItem(int item )
    {
        itemArray[numItems] = item;

        numItems++;
    };
};

Constructors

But, there is one small problem with this example. The data is never initialized! This is where the notion of constructors comes in. Constructors set up a class for use. Classes don't need to specify a constructor; indeed a constructor is, by default, simply a function call to nothing. In this case, however, our class must call a constructor because our data needs to be initialized before it can be used.

In Java, everything is inherited from the superclass Object. All Objects must be initialized, or allocated, before they are used. For example, the declaration

public int numItems;

specifies an integer value. The int is a primitive type, but just like an Object, and therefore int needs to be initialized. We can do so in the declaration itself

public int numItems = 0;

or we can use the constructor and initialize the array as well

public class MyAdvancedJavaClass
{
    public int numItems;
    private int itemArray[];

    MyAdvancedJavaClass()
    {
        numItems = 0;
        itemArray = new int[10];
    }

    public void addItem(int item)
    {
        itemArray[numItems] = item;
        numItems++;
    };
};

Keep in mind that initializing a variable at its declaration affords little flexibility for any classes or methods that subsequently will use your object. A constructor can be modified easily to accept incoming data as well, enabling you to modify your object depending on the context of its use:

public class MyAdvancedJavaClass
{
    public int numItems;
    private int itemArray[];

    MyAdvancedJavaClass(int initialValue,int arrayLength)
    {
        numItems = initialValue;
        itemArray = new int[arrayLength];
    }
    public void addItem(int item)
    {
        itemArray[numItems] = item;
        numItems++;
    };
};

An object is allowed to have several constructors, so long as no two constructors have the same method signature (parameter list):

public class MyAdvancedJavaClass
{
    public int numItems;
    private int itemArray[];

    MyAdvancedJavaClass()
    {
        numItems = 0;
        itemArray = new int[10];
    }

    MyAdvancedJavaClass(int initialValue,int arrayLength)
    {
        numItems = initialValue;
        itemArray = new int[arrayLength];
    }

    public void addItem(int item)
    {
        itemArray[numItems] = item;
        numItems++;
    };
};

Sometimes, confusion may arise when there are several constructors that all do the same thing, but with different sets of data. In Java, constructors are allowed to call themselves, eliminate duplicate code, and enable you to consolidate all your constructor code in one place:

MyAdvancedJavaClass()
{
/*  Insteadof…
    numItems = 0;
    itemArray = new int[10];
*/
    // call the more specific constructor
    this(0, 10);
}

MyAdvancedJavaClass(int initialValue,int arrayLength)
{
    numItems = initialValue;
    itemArray = new int[arrayLength];
}

Constructors are powerful tools. They enable you to create classes and use them dynamically without any significant hard-coding. As we will see, good constructor design is essential to an object-oriented architecture that works.

Creating and Initializing an Object

We mentioned earlier that all Java classes inherit from the Object superclass. The constructor for an Object is invoked using the new operation. This initialization operation is used at object creation and is not used again during the object's lifecycle. One example of an object being initialized is the array initialization in our sample class. The new operation first allocates memory for the object and then invokes the object's constructor.

Because we created two kinds of constructors, our sample class can be invoked in one of two ways:

myAdvancedJavaInstance1 = new MyAdvancedJavaClass();
myAdvancedJavaInstance2 = new MyAdvancedJavaClass(10, 100);

The first instance of our class is initialized to the default values 0 and 10. When we invoked the new operation on this instance, the new operation set the values appropriately, and created a new instance of Array within the class instance. The second instance of our class set numItems to 10 and created a 100-item Array.

As you can see, this kind of dynamic class creation is very flexible. We could just as easily create another instance of our class with entirely different (or the same) initial values. This is one of the basic principles of object-oriented design espoused by languages such as Java.

Each instance of the object maintains a similar-looking but entirely different set of variables. Changing the values in one instance does not result in a change in the values of the variables of the other instances. Remember, an instance of a class is like your BMW 328i convertible. As the analogy in Figure 1-1 illustrates, it looks as cool as every other BMW 328i, but just because you modify yours to remove the annoying electronic inhibition of speed, that doesn't mean every other Beemer also will be changed!

Just as customizing your BMW makes it different from other BMWs, modifying variables in one instance doesn't change them in all instances.

Figure 1-1. Just as customizing your BMW makes it different from other BMWs, modifying variables in one instance doesn't change them in all instances.

Applying Good Object-Oriented Design Skills

Maybe you're tired of driving your minivan because your husband (or wife) makes you! What you really want is a BMW Z3 roadster. So, you drive your behemoth Toyota van down to the nearest BMW dealer and trade it in for the Z3. Now, because you have a different car, does that mean you have to learn how to drive all over again? This is obviously not the case (unless you just traded in a Volvo, in which case you have to learn to drive to begin with). That's because the world, yes the same world that brought you Elvis and Hillary Clinton, is inherently object-oriented.

Inheritance

Your Z3, and every other car on the road, is a car, pure and simple. All cars have accelerators, brakes, steering wheels, and, even though you don't use them in a Beemer, turn signals. If we take this analogy further, we can say that every car inherits from the same "base class," as illustrated in Figure 1-2

In any object-oriented environment, classes inherit the characteristics of their base classes.

Figure 1-2. In any object-oriented environment, classes inherit the characteristics of their base classes.

A base class is a special kind of object that forms the foundation for other classes. In Java, a base class is usually inherited later on. Think of derived classes as "kinds of" base classes. In other words, "a BMW Z3 is a kind of car." With that in mind, we create the following class structure:

public class Car
{
}
public class BMWZ3 extends Car
{
}

The extends keyword tells the BMWZ3 class to utilize the properties, values, and behavior of the Car base class. But there is one small problem. Can you ever drive a generic "car"? No, because there is no such thing. There are always kinds of cars, but never a specific thing that is known simply as a car. Java gives us the notion of an "abstract base class."

An abstract base class is, quite simply, a class that must be inherited from. It can never be used as a stand-alone class. In Java, the abstract keyword gives a class this unique property.

public abstract class Car
{
    int topSpeed;
}

public class BMWZ3 extends Car
{
}

In this situation, the Car class can never be instantiated or used as is. It must be inherited. When the BMWZ3 class inherits from Car, it also obtains all the variables and methods within the Car class. So, our BMWZ3 class gets to use topSpeed as if it were its own member variable.

Somewhere in your code you might want to check what type of variable you are using. Java provides the instanceof keyword to enable you to inquire as to what the abstract base class of an object is. For example, the following two code snippets would return the value true:

BMWZ3 bmwVariable;
FordTaurus fordVariable;

if(bmwVariable instanceof Car) . . .

if (fordVariable instanceof Object) . . .

whereas the following code snippet would return the value false.

if (bmwVariable instanceof PandaBear)

Notice that Java's inheritance model is quite simple. In C++, objects are allowed to inherit from one or more abstract base classes and can be made to inherit the implementation of those interfaces as well. Java, as a matter of simplicity, does not allow this, nor does it plan to at any time in the future. There are ways to get around multiple implementation inheritance, but they do not really involve inheritance at all. The bottom line is that if you need to use multiple implementation inheritance, you probably won't want to use Java.

Code Reuse

Let's say that you are putting together your son's bicycle on Christmas morning. The instructions call for you to use a Phillips-head screwdriver. You take the screwdriver out of the toolbox, use it, and put it back. A few minutes later, you need the screwdriver again. Surely you would use the same screwdriver, not go to the hardware store and buy a new one!

Likewise, code reuse is of vital importance to the programmer on a tight schedule. You will need to streamline your code so that you can distribute commonly used tasks to specific modules. For example, many of the online demonstrations we provide with this book include animation examples. Rather than recreate the animation routines, we reused the same set of animation tools we developed beforehand. Because we coded the animators with reuse in mind, we were able to take advantage of a strong interface design and an effective inheritance scheme.

OOP—Strong, Efficient, and Effective

Whew! Whether this is your first foray using the Java language or your 101st, all of your design begins in this one place. There are three steps to creating an object that you can use time and again:

  1. Strong interface design

  2. Efficient class implementation

  3. Effective inheritance

With the fundamentals of object-oriented programming under your belt, you are ready to explore the simplicity with which you can create programs in Java that handle input and output. The Java I/O routines are not only easy, but extremely powerful. Bringing your C++ I/O to Java will result in as little functional loss as migrating object-oriented design techniques to Java from C++.

Java I/O Routines

Java provides several tools for the input and output of data, ranging from the Abstract Window Toolkit (AWT) or the Swing Components to the core System functions of Java classes. The AWT is exactly what it says it is: a set of components for designing windows and graphical user interfaces that uses the peer components of the underlying operating system for their implementation. The Swing Components do the same thing, but rather than using the peer components of the host operation system, all the components are 100% pure Java components and can take on the look and feel of the components of the host operating system or have their own "custom" look and feel. The core System classes are built-in routines for gathering and disseminating information from Java objects.

This section highlights some of the input and output routines provided by the core Java capabilities as well as the Swing Components and Abstract Window Toolkit. As we delve further into the realm of networked programming, we will discover that much of what drives our decisions on a networked architecture will be that which is detailed in this section. Because input and output are the most important actions a computer program performs, we must develop a strong understanding of the I/O capabilities and limitations of Java.

Streams

Imagine your grandfather fishing in a stream. He knows that as long as he stays there, he's going to get a bite. Somewhere, somehow, sometime a fish is going to come down that stream, and your grandfather is going to get it.

Just as your grandfather is the consumer of fish, your applications are either consumers or providers of data. In Java, all input and output routines are handled through streams. An input stream is simply a flow of data, just as your grandfather's stream is a flow of fish. You can write your application to fish for data out of your input stream and eventually to produce data as well. When your application spits out information, it does so through a stream. This time, your application is the producer, and the consumer is another application or device down the line.

Java provides several different kinds of streams, each designed to handle a different kind of data. The standard input and output streams form the basis for all the others. InputStream and OutputStream are both available for you to use as is, or you can derive more complicated stream schemes from them. In order to create the other kinds of Java streams, first you must create and define the basic streams.

Perhaps the most-used stream formats are the DataInputStream and the DataOutputStream. Both of these streams enable you to read or write primitive data types, giving you the flexibility within your application to control the results of your application's execution. Without this kind of functionality, you would have to write specific bytes rather than reading specific data.

File buffers are a method commonly used to increase performance in an input/output scheme. BufferedInputStreams and BufferedOutputStreams read in chunks of data (the size of which you can define) at a time. When you read from or write to the buffered streams, you are actually playing with the buffer, not the actual data in the stream. Occasionally, you must flush the buffers to make sure that all the data in the buffer is completely read from or written to the file system.

Sometimes you will want to exchange information with another application using a stream. In this case, you can set up a pipe. A pipe is a two-way stream, sort of. The input end of a pipe in one application is directly connected to the output end of the same pipe on another application. If you write to the input of the pipe, you will read the same exact data at the pipe's output end. As you can see in Figure 1-3 this is a pretty nifty way to promote interapplication communication.

Pipes enable interaction between two or more applications.

Figure 1-3. Pipes enable interaction between two or more applications.

Last, you will eventually want to fiddle with files on your local file system. The FileInputStream and FileOutputStream enable you to open, read, and write files as we will show you in a moment. Remember that Java has strict restrictions on applet security, so most file streams can be manipulated only by applications. For more information, consult Chapter 13, "Java and Security."

The Java Core System

In Java, applications are allowed to write to the standard output devices on a machine. If you use a Web browser such as Netscape, the standard output to which Java writes is the "Java Console" mentioned in one of Navigator's windows. If you write a Java application (i.e., a stand-alone applet), the standard output device is the command line from which you execute the program.

The System Class

One of the classes Java includes in every applet or application, whether you specify that it do so or not, is the System class. The System class provides support for input/output (I/O) using the Java console; you are to provide the ability to write to the console, read from the console, and write errors to the user. The Java console is provided in two ways, one for browsers and one for applications. In the browser environment the console is a separate browser window that has controls for scrolling and clearing. For applications run from the operating system (OS) command line, the console is the text interface you see and suffers the same problems as the text base OS environment (lack of scrolling backwards). The Java console is really intended to provide the same level of user interactivity as the C++ cin, cout, and cerr objects. The names of the standard Java streams are in, out, and err; these names can be changed using the System classes setIn, setOut, and setErr methods. Changing the names of these streams can only be done by the SecurityManager.

Input Using the System Class

Input in the System class is actually handled by the InputStream class contained in the Java I/O routines. System.in is an object of type InputStream that is created, maintained, and initialized by the System class. In other words, it's yours for the taking; you don't have to do a thing to use it.

The InputStream class assumes that you will be reading from the standard input stream (the keyboard you are sitting at). A stream is a sequence of characters retrieved from somewhere. The standard input stream is the location that your operating system uses to get data from you. Because streams are defined as characters from a source, it is entirely conceivable that a stream could be a file, a modem, a microphone, or even a connection to another process running on your computer or another computer. As a matter of fact, Java treats files and other peripherals as streams. This abstraction of a stream simplifies I/O programming by reducing all I/O to a stream.

So, how do you get input from the user? Simply use the System class's input stream to get the information you require. The input stream is an object with several methods to facilitate data input. For example, there are primitive, yet useful, routines to get characters and strings, to read integers and other numbers, and even to get a stream of unfiltered and untranslated bytes. Deciding which routine to use is simply a matter of which kind of data you wish to read. In our example, we will read and write strings:

public class InputOutputTest()
{
    String str;    //private data

    public void getInput(){
        // read a string from the Java console keyboard (sysin)
        str = System.in.getln();
    }
}

Output Using the System Class

As with input, output is handled through streams. How can output be a stream if a stream is a sequence of characters from a source? Well, the source is your application, and the stream is routed to a device known as the standard output. The standard output is usually your monitor, but it could be other things as well. Most notably, the standard output is set to be the Java console when an applet runs within Netscape Navigator. When you run the following example from within an applet, watch your Java console for the output. If you run it from within an application, the output should show up on the command line.

public class InputOutputTest(){
    String str;     // classdata

    public void getInput(){
        // read a string from the keyboard
        str = System.in.getln();
    }

    public void drawOutput(){
        // write a string to the console screen
        System.out.println(str);
    }
}

Files

The stream classes would be pretty useless if you couldn't manipulate files as well. There are several security mechanisms defined in the security model used by Java-capable browsers for running applets. These mechanisms prevent unguarded file access and will be discussed in more depth in Chapter 13, "Java and Security." But for now, simply assume that as long as you are not writing an applet, you will be able to manipulate files. In the purest sense, standard input and output are files. As such, they are sometimes subject to the same applet security restrictions, so be forewarned.

The Basics

When reading and writing to and from files, there are three steps that must be followed:

  1. Open the file for reading or writing.

  2. Read or write from the file.

  3. Close the file.

It is important to do each step. Failing to open a file will, obviously, prevent you from reading. But perhaps not as intuitively, you must still close the file or you may wreck your file system. Every application is allowed a certain number of file descriptors (handles) that maintain the status of a file. If you run out of available file descriptors, you will no longer be able to open any other files. The following snippet uses the FileReader class to read the contents of a file specified on the command line and the PrintWriter class to write it to the Java console:

import java. io.*;
public class ShowFile{
  public static void main (Stringargs[]){
  try{
    FileReader fin = new FileReader(args[0]);
    PrintWriter consoleOut = new PrintWriter(System.out, true);
    char c[] = new char[512];
    int count = 0;
    while ((count=fin.read(c))!=-1)
       consoleOut.write(c,0,count);
    consoleOut.flush();
    consoleOut.close();
    fin.close();
  }
  catch(FileNotFoundException e){
     System.out.println(e.toString());
  }
  catch(IOException e) {
     System.out.println(e.toString());
  }
}

When opening a file, you have three options. You can open the file for reading so you can extract data from it, but you will be prevented from writing to the file unless you close it and open it for writing. You can open it for writing, but you will be prevented from reading from it. Finally, you can append to a file, which is similar to writing except that it preserves any data already in the file.

Taking Files One Step Further

So what do files have to do with networked computing? Well, the diagram in Figure 1-4 offers a graphical representation of input and output streams. Remember that streams are merely interfaces to collections of data. What if that data is located on a network connection rather than in a flat file or a keyboard?

With Java, your input or output need not reside on the same physical machine on which your application is running.

Figure 1-4. With Java, your input or output need not reside on the same physical machine on which your application is running.

The standard interface to a network in the computer world is a socket. A socket is a connection between processes across a network. The processes can be located on the same physical machine, the same Local Area Network, or even across the world on different LANs. The three basic steps still apply:

  1. Open a connection to the remote process.

  2. Read or write data.

  3. Close the connection.

Again, as with file manipulation, you can use the InputStream and OutputStream objects to interface to the socket. In fact, sockets are nothing but files in the purest sense. The advantage to this file-centric hierarchy is perhaps not as obvious as it should be. In the end, all three forms of input sources are completely interchangeable. You should not write your applications to be specific to a specific kind of file. In an object-oriented design, the objects you create should simply know that they will have to read or write data down the line.

The Abstract Window Toolkit and Swing Classes

The AWT is a half-baked attempt to create a user interface toolbox for programmers. Because all the various classes, containers, and widgets in the toolkit are capable of being used both in the applets embedded in Web pages and in the stand-alone applications on your desktop, it is a powerfully extensible tool. At the heart of this kind of flexibility is the idea that the toolkit is an abstraction—in other words, a layer on top of your current windowing system. This abstraction is more understandable if you know the background behind it. When Sun was courting its early customers, Netscape insisted that the Java Virtual Machine (JVM) included in its browser must create widgets that had the exact look and feel of the host operating system's widgets. Since "Swing" wasn't yet a gleam in its father's eye, the only way to accomplish this was to use the peer components of the host operating system. Thus we can truly say that the AWT is an abstraction of the windowing system of the operating system.

Your current windowing system may be anything from X11/Motif to Windows 95's own window system. In any event, the AWT ensures that native calls are made to these windowing systems in order to allow applications to run on top of the desktop. For applets within a Web page, the browser manufacturer essentially creates a windowing system that renders the AWT's widgets within itself.

The end result of all this is that eventually a native call is made for each action taken by the AWT. Your applications need not be aware of this, for Java's platform independence ensures that, no matter the platform on which you execute bytecodes, the results will be identical.

One of the problems with this approach to user interface (UI) implementation is that when making a UI that must be rendered the same way on all the platforms it is to be targeted to, small differences in the way that components are rendered on each of the targeted systems may cause the overall effect to have problems. For instance, a UI having several closely aligned text fields may look good on Windows platforms but appear overlayed on UNIX machines.

One of the major complaints about the AWT by people used to building user interfaces for enterprise applications was that it had a relatively small set of widgets and low functionality. AWT provided only slightly more functionality than the widgets provided in HTML's forms controls. In early 1997 the work on JDK 1.1 incorporated a number of new pieces including Netscape Corporation's Internet Foundation Classes (IFC), components from IBM's Taligent Division, and Lighthouse Design. The first release of Swing 1.0 in early 1998 contained almost 250 classes and 80 interfaces. The art of user interface creation had been raised to a new level and was now able to go head to head with platform-specific development tools.

The Java 1.2 platform provides a set of components (Swing) that eliminate this problem by eliminating the use of peer components. The Swing components are pure Java and will render reliably on all host platforms. With Swing the native look and feel of Windows, Motif, or Mac widgets are options from a predefined list of look and feels that are extensible by the user.

Input Alternatives

The AWT and Swing contain widgets designed to elicit response from the user. From simple text areas to more complex dialog boxes, each one is designed to funnel information from the user's keyboard to your application. Most of them are very easy to use and program, so we'll leave it to the several Java books on the market to provide you with a reference and a basic list and explanation of the elements that are included.

Remember that input in a windowing system is not limited to typing words on the screen. Every push button, checkbox, or scroll bar event is a form of input that you may or may not choose to deal with. Every AWT class has some way or another of checking the status of its input mechanism. For example, your scroll bar will be able to tell you if it has been moved. You may choose then to take some action, or let the AWT do it for you. There is no need to implement scrolling text for a scroll bar when the AWT is fully capable of doing it.

Output Alternatives

Obviously, the easiest way to display output with the AWT is to display something graphically. The AWT supports simple graphics routines for drawing, as well as for the usual suite of labels, multimedia, and widget manipulation. Output is significantly easier using the AWT. Without the toolkit, you would have to manage not only what to do with the input you receive, but also how to display your response.

I/O in Short

Input and output are at the heart of every program you create. No matter what the objective of your application, somehow you will need either to get a response from the user, to display a response, or maybe even both. To take things one step farther, your input or output need not reside on the same physical machine as that on which your application is running. Indeed, that is the very subject of this book. By stretching your applications to fit a networked model, you will be able to take full advantage of the input and output schemes offered to you by Java.

When your applications receive several inputs, they will often get inundated with processing. To alleviate this, Java provides a full suite of threading utilities, which we discuss in the next section. Threads allow your applications to execute steps in parallel. So, when your application receives two different inputs simultaneously, you can use threads to simultaneously resolve them and produce output.

Introduction to Threading in Java

Multithreaded (MT) programs are the current rage in computer science. Books upon books upon books have been written that describe the benefits of threading, the threading features inherent in various operating systems, and the various forms of threaded architectures.

So, what on earth are threads? How can you use them in your programs? Will threading continue to work in those applications that run native on operating systems that do not support threading? What does it mean to be MT-safe, and how do you design an MT-safe program?

The entire realm of multithreaded and multitasked programming transcends the scope of this book. We will confer that knowledge of the topic that is directly related to the ideas of networked programming and, in cases where more research may be warranted, direct you to the appropriate resources.

What Are Threads?

Let's say you're sitting in your living room watching another Washington Redskins victory. You get bored watching the massacre of the Dallas Cowboys, and you decide that you would like to see the 49ers game in progress. In the good old days, you would have to actually switch channels and choose between one or the other. But, these days, televisions have Picture-in-Picture (PIP) capability. By pressing the PIP button on your trusty remote control, you can watch the Redskins demolish the Cowboys on a little box in the corner of the TV while watching the 49ers on the rest of the screen.

This is a prime example of multithreaded programming. The little screen and the big screen share resources (in this case, the area of the full television screen), but they are not able to affect one another. In the areas in which the two games collide, one screen gives way to another.

Threads in Your Computer

In the computer world, multithreaded applications exist similarly to those in the television world. They share the same area, in our case the television screen, in reality the physical process in which the application resides and is permitted to execute. Multithreaded applications are able to execute independent pieces of code simultaneously. Each of these independently executing pieces of code is known as a thread.

Threads are implemented differently by different operating systems. In Solaris, for example, threads are defined and maintained in the user environment. The operating system maintains responsibility over the process, regardless of what the process decides to do with itself. In a sense, the operating system treats the process as an object. The OS only cares about the interface to the process, or how it starts up, shuts down, begins execution, and performs similar operations. It has no feelings whatsoever about how the process handles information.

In fact, this is the fundamental concept of threads. Threads exist as a user-created and user-managed aspect of a program. The operating system could care less if there are multiple threads in the executable or if it is single threaded. Furthermore, the operating system will not help you resolve conflicts. All it cares about is the integrity of the process, not about what goes on inside it.

Handling Conflicts

Let's say you have a couple of threads prancing along merrily within your application. Suddenly, they both access the same piece of data at the same time. This results in what is known as concurrent access. Concurrent access errors occur as a result of poor thread management on the part of the main application.

Access errors occur in everyday life, too. Let's say you've scheduled an appointment from eleven in the morning to one in the afternoon. Carelessly, you forgot your all-important staff meeting at twelve-thirty. Obviously, you can't be in two places at once! The end result is that you've placed yourself in two meetings. The threads within our applications similarly have accessed identical data at the same time.

When creating a thread, the first thing you must determine is what data that thread will touch. You then have to fence off that data so that only one possible thread can ever touch it at any given moment. In Solaris, this is done with a concept called mutual exclusion. A mutual exclusion lock placed around your data ensures that it will never be permitted to enter a concurrent access situation.

Imagine a relay team of four people competing at the upcoming Olympics. The first runner on the relay team is given a baton that must be passed to a teammate before that teammate is allowed to run. If the teammate runs without the baton, she is disqualified. However, if the baton is passed properly, the runner can continue until she arrives at the finish line or must pass the baton to another teammate.

Likewise, different threads can obtain the lock around the data so long as the lock is available. If the lock is unavailable, the thread must wait, effectively suspending itself, until the lock is available. There are specific settings to allow threads to continue without waiting, but these settings are beyond the scope of this book. If one thread grabs a lock but never lets go, then it will have deadlocked the entire application. When your methods obtain a thread, make sure that they give it up somehow. Otherwise, the rest of your application will wait for a lock that will never come free.

For more information on threads, consult the excellent Sun Microsystems title, Threads Primer by Bill Lewis and Daniel J. Berg.

Threading in Java

Creating and debugging threads in Java is considerably simpler than doing so in C++. Deadlocks in Java are much easier to prevent, and a ton more intuitive. But multithreaded applications in Java are not as robust or as powerful as their C++ counterparts. In short, there are tradeoffs to threading in Java, for it is not an all-encompassing answer to the multithreading question.

What threads in Java do is provide you, the application programmer, with a consistent interface to the threads of the underlying host environment. Anything that may be "quirky" in the threads of the hosting operating system will still be there. This consistency of API is important as our target environment is any platform that there is a JVM written for, and the consistency helps make our code more portable and reusable.

Java treats threads as user-level entities as well. A Java applet or application runs within a process space defined in the Java Virtual Machine. The JVM allocates processes and the resources for each process and allows the applet or application to define how that process space is used. Java programs that implement threads must do so using the Thread class or a derivative thereof.

The Thread Class

Java's language hierarchy, which includes the likes of Strings, Integers, and so on, also contains a powerful, yet incredibly simple Thread object that you can implement within your programs. The Thread class provides all the functionality necessary for you to create fully multithreaded and MT-safe applications using the Java language.

Note

Two approaches to spawning threads in Java are worth noting, as outlined in the following sections. Many of our networking examples later on will make heavy use of one or the other method. As always, there are tradeoffs and benefits for each architectural decision you make.

Using the Entire Class As a Thread

The first method we could employ involves spawning threads in which an entire class can reside. For example, we spawn a thread and then create a runnable class and attach it to the thread. Now the entire class exists within the thread and the stream of execution for that class is maintained by the thread. If the thread is destroyed, the stream of execution is likewise destroyed.

The biggest advantage to this method is that the class need not know anything about how it is to be implemented. Take a look at the following example:

public class Animator extends Panel implements Runnable
{
    Animator() { … }

    public void run() { … }
}

public class AnimatorManager
{
    Animator animations[];
    Thread animationThreads[];

    AnimatorManager() { … }

    public void createAnimation(
        Animatoranim
    )
    {
        // first spawn a thread for the class
        // now let the thread continue…
    }
}

The AnimatorManager class is responsible for creating a series of Animator objects, spawning a thread for the object to execute in and shutting down, suspending, resuming, or inquiring about the status of the thread. Note how the Animator does not know or care whether it will be in a thread of execution or in an entire process. It is a runnable class, meaning that whatever is contained within the run function will be executed if the parent process or thread allows.

The object is created normally, and our AnimatorManager assumes that the object is already created. The Thread is created, but the object is passed to it as a parameter. The corresponding constructor in the Thread class knows that the runnable object will reside solely within its thread of control.

public class AnimatorManager
{
    Animatoranimations[];
    ThreadanimationThreads[];

    AnimatorManager() { … }

    public void createAnimation(
        Animatoranim
    )
    {
        // first spawn a thread for the class
        animationThreads[currentThreadCount] = new Thread(anim);

        // now let the thread continue…
        animationThreads[currentThreadCount].start();
    }
}

Note

Remember that Java is inherently object-oriented, so this kind of thread creation is quite within the reach of the language. There is no funny business going on here. A thread is created and an object is told to live within it. It is actually quite intuitive in an object-oriented sense. The next method hearkens back to the days of structured programming.

Inheriting from the Thread Class

The second way to implement threads is to create a class that inherits from the Thread class. In the first method, we created an object that was a free-standing object in its own right. In this case, we will create an object that is a Thread object from the beginning. In essence, the JVM treats both methods as similar and reasonable means to spawning threaded objects, and both are acceptable from a style perspective.

Inheriting from the Thread class is actually quite simple. Instead of extending from Panel or Applet, your class simply extends from Thread. In your init method or constructor, you must initialize the thread as well. Obviously, your class must be aware that it is running in a thread.

The thread code for a class that inherits from Thread is in the Run method. As in a class that implements Runnable, inheriting from Thread automatically enables you to implement the run method. Any code you want to manage the thread should be placed there. If you need to make the thread sleep or suspend, that's where you should place it.

The difference, however, between extending Thread and implementing Runnable is that when you inherit from Thread, your entire class is a thread. The thread must be started and stopped from within the class, unlike the other method in which the thread controls are outside the class itself (see Figure 1-5).

Thread controls are accessed from different locations depending on the method chosen.

Figure 1-5. Thread controls are accessed from different locations depending on the method chosen.

Take a look at the following example, and notice how the constructor calls the start method or the thread:

public class Animator extends Thread
{
    Animator()
    {
         start();
    }

    public void run() { … }
}

As you can see, the class is clearly a threaded class. What happens if you want to use the class's methodology without using threads? You'll have to create a new class that doesn't use threads, or you'll have to revert to the first method. Implementing Runnable and placing your thread controls outside the target class is the preferred way of using threads, but inheriting from threads can be particularly useful for highly modular code in which you want to package an entire object that does not rely on anything else.

Thread Controls

A thread has several control methods that affect its behavior. Simply starting and stopping a thread's execution are but two of the many tools available to you to manipulate how programs execute. For example, on several occasions, you will want to pause a thread's execution, and eventually resume it.

The Thread class offers us a rich set of methods for controlling threads:

  1. start

  2. stop

  3. suspend (deprecated in JDK 1.2)

  4. resume (deprecated in JDK 1.2)

  5. sleep

  6. destroy

  7. yield

  8. join

  9. run

  10. isAlive

The start method does exactly what it says. It tells the thread that it may begin execution of all the steps contained in the run method. The run method itself may call any of the preceding thread controls, but obviously you will want to restart the thread somewhere if the run method decides to suspend it!

The stop routine terminates the thread and prevents the run method from executing any further steps. It does not, however, shut down any subthreads that it may have created. You must be careful and make sure that every thread you create eventually either terminates on its own or is terminated by its parent. Otherwise, you could very well have several threads executing and consuming resources long after the applet or application has terminated.

The suspend and resume routines are pretty self-explanatory. When suspend is called, the thread ceases execution of its run method until resume is called somewhere down the line. If your parent thread needs to inquire about the current running status of a thread, it may call the isAlive method and find out if the thread is stopped. Obviously, if the thread isn't stopped, and it isn't running, it must be suspended. Note, in JDK 1.2 suspend and resume are deprecated due to problems with deadlock situation occurring. When a thread has locked a system resource and is then suspended, other threads cannot access the resource until the suspend is resumed. If the thread that is supposed to do the resume first tries to lock the resource, a deadlock occurs.

The join method causes the currently executing thread to wait until it has stopped; the current thread then blocks until:

  1. The currently executing thread is interrupted.

  2. The currently executing thread is terminated.

  3. The specified timeout has expired; if a time is not specified, the thread will wait indefinitely.

Last, the sleep method tells the thread to pause for a given number of milliseconds. It is particularly useful for the clock because we want it to "tick" every second.

The state diagram in Figure 1-6 should make clear the thread timing you need to be aware of. Remember that, before anything can be done to a thread, you must call start on it. Once you are finished with the thread of execution, you must call stop.

The control methods that affect a thread's behaviour.

Figure 1-6. The control methods that affect a thread's behaviour.

Synchronized Methods

Conflict handling within Java is implemented using method synchronization. If you have data that could potentially deadlock between two threads, then you must declare the functions in which the data is modified as synchronized. Java prevents multiple threads from entering the synchronized methods and thereby eliminates the possibility of deadlock.

Creating a synchronized method is actually quite easy. It is simply a matter of declaring that the function will be synchronized in the method signature, as can be seen in the following snippet.

public class ThreadClass
{
    int data;

    …
    public void synchronized addToData(
        int addend
    )
    {
        data += addend;
    }
    …
}

There are a couple of important caveats to synchronized functions. Because multiple threads may require entry to a synchronized function, it is better to keep any function that is declared as synchronized short and sweet. When one thread enters a synchronized function, keeping its time spent in the function to a minimum will keep your programs running smoothly. After all, the idea behind threading is to get your programs to execute steps in parallel, not to spawn threads that end up waiting forever for each other to finish with the data.

yield( ), wait( ), and notify( )

In an application with multiple threads, often you will have many threads competing with one another for resources. One way to allocate those resources effectively is to set the relative priorities of each thread. We will discuss that in a moment, but right now let's discuss some of the specific steps you can take within the thread itself. Remember that when threads execute, they all share the same process space in which the application resides. Like a bunch of kids forced to share a toy, the threads compete and vie for control of the process. Like any good parent, however, you have several tools at your disposal to make sure the threads cooperate.

Sometimes you will want to control entry into a function and label the function as synchronized. Even though the function is long, you want to yield control of the function pretty early on. You can call the notify method to tell the parent thread that you are finished with the synchronized lock.

In order to make a thread stand by for a notify message, you must add the wait method to the thread's execution routines. The notify method is called somewhere in an executing thread. Once notify is called, any thread awaiting execution on a wait call automatically proceeds.

Another way to give up the process space in which a thread runs is to call the yield routine specifically. When yield is called within a thread, the thread gives up any scheduling priority, process space, or claim to its current turn in the sharing cycle.

Thread Priorities

Tip

A more elegant, yet more confusing, way to control threads is by setting their priority. Obviously, when you set a thread to have a high priority, it gets first crack at any processing time and resources. You should be careful and judicious in setting thread priorities. Even with the best of intentions, you could very well defeat the purpose of using threads to begin with should you set every thread at a high priority.

In Java, threads may have one of three priorities: minimum, normal, and maximum. You may set the priority using the setPriority method of the Thread class and retrieve the priority of any thread by using the getPriority method, like so:

Thread threadOne;
Thread threadTwo;
Thread threadThree;

threadOne.setPriority(Thread.MIN_PRIORITY);
threadTwo.setPriority(Thread.NORM_PRIORITY);
threadThree.setPriority(Thread.MAX_PRIORITY);

Because threads are a powerful and underused aspect of most Java programs, thread scheduling and prioritizing is a flexible and equally powerful way to control how your applications behave and execute.

Daemon Threads

There are two kinds of threads. So far we have discussed application threads, which are tied to the process and directly contribute to the running of the application. Daemon threads, on the other hand, are used for background tasks that happen every so often within a thread's execution. Normally, an application will run until all the threads have finished their execution. However, if the only remaining threads are daemon threads, the application will exit anyway.

Java itself has several daemon threads running in the background of every application. Java's garbage collection is controlled by daemon threads known in computer science parlance as reaper threads, or threads that run through an application looking for dead weight. In the garbage collection thread's case, the dead weight happens to be unused but allocated memory.

If your application needs to set up a daemon thread, simply call the setDaemon method of the thread, as shown in the following snippet. The application in which the thread resides will know to ignore that thread if it needs to execute, and program execution will continue normally.

Thread t = new Thread(myClass);
t.setDaemon(true);

Thread Summary

Threads are one way in which you can affect the behavior of an object. Serialization is another. Serialization allows you to store your objects as strings. When we use threads, we do so to change how it behaves when it is running. Serialization does not allow us to preserve that runtime behavior, only the class's static behavior and characteristics. Whenever you reconstruct a serialized class, only your class will be reconstituted correctly, not any of the threads. Therefore, it is important that your threads be as object-oriented as possible so that they can store their state when necessary.

Object Serialization

Serialization is a concept that enables you to store and retrieve objects, as well as to send full-fledged objects "over the wire" to other applications. The reason serialization is of such vital importance to Java should be clear: without it, distributed applications would not be able to send objects to each other. That means that only simple types such as int and char would be allowed in parameter signatures, and complex objects would be limited in what they could do. It's sort of like saying you would have to talk like a 3-year-old whenever you spoke with your boss. You want to have a complex conversation, but you are limited in what you can say.

What Is Serialization?

Without some form of object storage, Java objects can only be transient. There would be no way to maintain a persistent state in an object from one invocation to another. However, serialization can be used for more purposes than maintaining persistence. The RMI system uses object serialization to convert objects to a form that can be sent over a communication mechanism.

When an object is serialized, it is converted to a stream of characters. Those characters can be sent over the wire to another location. Parameters passed in remote objects are automatically translated into serialized representation. Once an object is serialized, it can be safely sent via a communication method to a remote location.

The serialization routines have been incorporated into the standard Java Object class with several routines to facilitate the writing and reading of a secured representation. There are several security concerns that you must be aware of, and we will discuss those in a moment. Without object serialization, Java could never truly be an effective Internet language.

Handling Object Relationships

An important consideration of the object serialization facilities is that the entire process is executed in a manner transparent to any APIs or user intervention. In other words, you need not write any code to utilize serialization routines. When writing an object, the serialization routines must be sure to do so in a manner that allows full reconstruction of the object at a later time. Not only must the class structure be saved, but the values of each member of the structure must be saved as well. If you had a class with the following representation:

public class CuteBrownBear
{
    Color eyeColor;
    float heightInches;
    float weightPounds;
}

It must be saved so that the values of eyeColor, heightInches, and weightPounds are preserved and can be restored once the reading functions are invoked. Sometimes, however, things can become complicated when objects begin to refer to one another. For example, the following class contains CuteBrownBear as well as several other toy objects that we must save as well:

public class ToyBox
{
    CuteBrownBear bearArray[5];
    ActionFigure actionFigureArray[5];
}

The serialization routines must not only serialize the ToyBox object, but the CuteBrownBear objects and ActionFigure objects as well. To handle this kind of situation, the serialization routines traverse the objects it is asked to write or read. As it traverses an object representation, it serializes any new objects automatically. If, down the line, it finds another object of a type already serialized, it merely modifies the earlier serialized representation to refer to the new instance. In this manner, serialized objects are compact and efficient without much duplicated code.

For example, when we need to serialize the ToyBox object, the serialization routines first serialize CuteBrownBear in array position one. Array positions two through five are not serialized on their own; rather the original serialized representation is modified to point to their locations and values. So, the final serialized object has one reference to the CuteBrownBear object, plus five sets of data values.

The Output Streams

Serialization output is handled through the ObjectOutputStream. Serialization calls refer to the writeObject method contained within the stream, passing it the instance of the object to be serialized. The stream first checks to see whether another instance of the same object type has been previously serialized. If it has, the routines handle it as we discussed in the previous section, merely placing the new values alongside the representation. If, however, the object has yet to be serialized, the routines create a new serialized representation and place the values next to it.

Most serialization is handled transparently. But an object may at any time begin to handle its own serialization by reimplementing the writeObject method. The writeObject method is part of every Object class and can be overridden on command. If you need a finer grained serialized representation, or would like to include some kind of encryption or other technique between serialization endpoints, this is where and how you do it.

As an example, let us instantiate a CuteBrownBear object and serialize it:

// create the streams here . . .
FileOutputStream fileOut = new FileOutputStream("filename");
ObjectOutputStream objectOut = new ObjectOutputStream(fileOut);

// instantiate the new bear object
CuteBrownBear bear = new CuteBrownBear();

// serialize the bear
objectOut.writeObject(bear);

Handling Object Webs

An object web is a complex relationship between two or more objects in which objects refer to other objects that may eventually refer back to it. If you were to serialize such an object representation, you could potentially be caught in an infinite loop. Let's say we had a system of roads between three cities, Seattle, Washington, DC, and San Francisco. We want to take an end-of-summer road trip and visit each city. The only instruction the auto association gave us was "if you hit one of these three roads, follow it until it ends."

Following that logic, we would start at San Francisco, go to Seattle, visit the Redskins, come back to the Golden Gate, and go to Seattle, and so on (see Figure 1-7).

An example of serialization in which you need to store objects that are linked by a circuitous route.

Figure 1-7. An example of serialization in which you need to store objects that are linked by a circuitous route.

Likewise, if we were to serialize San Francisco, then Seattle, followed by Washington, DC, and keep following the path back to San Francisco, we would end up following the same loop an infinite number of times. This lattice arrangement ensures that a simple tree-based algorithm will not suffice. Java's object serialization routine accounts for this kind of structure in the same manner that it handles multiple objects of the same type in the same stream.

Because of these object webs, any serialization must take into account those objects that have already been serialized. So, in addition to the serialization methods, Java's object serialization routines also keep track of the object's serialized state. Moreover, Java also keeps track of whether object types have been serialized as well. In so doing, it can keep track of the data contained in the object, not the entire object itself.

Reading Objects

Reading objects is a matter of taking the serialized representation and reversing the process that created them in the first place. Remember to handle your deserialization in the same order as your serialization, traversing any trees in a similar fashion. The objective is to reconstruct the original object.

The deserialization routines are handled with a corresponding ObjectInputStream and the readObject method contained therein.

Once again, to obtain control over serialization routines for your object, you need to override and reimplement the writeObject and readObject routines.

Security and Fingerprinting

Sometimes objects can be serialized surreptitiously by other objects linked in by your application. If your object does things that you would prefer to keep private and unknown to the world, then you need to disable your objects. Serialization can be disabled for an object by adding the private transient tag to the class definition:

private transient class CuteBrownBear
{
    . . .
}

Or the object itself can override the serialization routines and return a NoAccess Exception. The NoAccessException tells any object that attempts to serialize your implementation that it may not do so. Furthermore, it gives a sufficient debugging warning to any applications that may reuse your object.

public class CuteBrownBear
{
    . . . the rest of the CuteBrownBear class goes here. . .
    public void writeObject(. . .) throws NoAccessException
    {
    }

    public void readObject(. . .)throwsNoAccessException
    {
    }
}

Serialization Overview

Java automatically handles its own object serialization for you. However, if you are so inclined, you may reimplement the serialization routines within your own objects. We have presented you with several serialization concerns in this chapter. If you are going to handle the serialization for a given object, make sure you conform to the various restrictions we have given you. If your objects do not handle their serialization properly, your entire object system may not be serializable.

Yet another issue of importance to Java programmers is performance. While serialization ensures that our objects can be saved and restored, performance issues strike at the very limitations of the language. The greatest programmers in the world can build the applications seen only in science fiction, but they are prevented from doing so by limitations in their hardware and the speed with which their software can be run.

Performance

Performance issues are the primary reason why most major corporations have not yet begun wholesale revisions of their existing computer systems to use the Java language. Although many of these issues are real and Java has yet to become the perfect language in all respects, it is not necessarily true that performance is a major show-stopper. Often, the perception is not reality.

Performance Issues

When we speak of performance in Java, we are actually speaking of two very different problems. The first is the download performance of an applet. Today, your hard-core applets will often contain upwards of 20 to 30 classes. Incorporate a mechanism such as Java IDL or Java RMI, and the communication infrastructure may add up to 100 different classes of its own. In order for the applet to run, each of those classes must be downloaded in order to be used.

The second major issue behind performance is runtime performance. For both applets and applications, the speed with which Java computes is pretty slow. Compared to comparable statistics for similar applications written in C++, Java does not measure up. There are several initiatives and technologies becoming available that may render that issue moot.

Download Performance

For applet writers, download performance is the single most important hurdle to overcome. While most programmers can create truly artistic programs that can accomplish a wide variety of things, they often meet a brick wall when their customer tries to download them within a browser. In order to study the download performance of an applet, we must first discuss how an applet is downloaded to begin with.

Java incorporates an object called the class loader. The class loader locates the class to be downloaded, goes about fetching it, and recognizes any other dependent objects and downloads those as well. The browser does the actual downloading and the class loader merely tells it what to do. When the browser downloads an object, it first establishes the connection to be used (see Figure 1-8). Once the connection is made, the object is checked to make sure that it has not been downloaded previously. If it has been downloaded before, it is not downloaded, and the connection is closed. If the class has not been downloaded before, it is downloaded, and then the connection is closed.

Download performance is measured by the time it takes to perform the steps involved.

Figure 1-8. Download performance is measured by the time it takes to perform the steps involved.

So, the time it takes to download an object is determined by four factors as illustrated in Figure 1-8:

  1. Time to open a connection.

  2. Time to verify a file.

  3. Time to download the file.

  4. Time to close the connection.

And most importantly, the same four steps are applied to every single class in your entire object system. No matter what you do, you will have to spend the time to download the files. There's no getting around that part because you need those files to run your applet. However, the time spent establishing and closing connections is a waste because you are essentially doing the same thing to the same location each time.

The brilliant engineers behind Java recognized this problem and created the Java Archive. It enables you to gather all of your files, stick them in one large archive file, and let everything get downloaded in one fell swoop. This means that there need only be one open connection, one download, and one closure for the entire system of object files.

Using Java Archives is a rather simple process. You must first use the jar utility, which UNIX users will find quite similar to their tar program, to archive your files. This is not unlike "zipping" a bunch of files into one. Once completed, you simply specify the archive in the applet tag in your HTML code:

<applet archive="archivename.jar"
       codebase="../classes/"
       code="PrashantIsCool.class">

   . . . HTML text here . . .

</applet>

Java Archives greatly improve the download performance of your applets. Without something like them, applets would be restricted to small, compact programs that accomplish little more than animating a dancing duke. The trick is that the browser has to support archives. Currently, Netscape Navigator and Internet Explorer support ZIP files, and both plan to support the jar standard once it is completed.

Runtime Performance

Runtime performance is a different beast altogether. Where download performance was a relatively simple issue to resolve, runtime performance requires a significant investment in compiler technology. Thankfully, the Java engineers are ahead of the curve on this as well. They have put together a specification for a Just In Time (JIT) compiler.

Remember that Java is an interpreted language. This means that the code you develop is only halfway compiled into bytecodes. The bytecodes are then translated by your local virtual machine into native code. Finally, that native code is run on your machine. When an application executes, the bytecodes are washed through the virtual machine, and the result is then executed on your platform. This ensures platform independence because the bytecodes are translated by the virtual machine into native code as indicated by the flow diagram in Figure 1-9.

Performance of Java using a virtual machine.

Figure 1-9. Performance of Java using a virtual machine.

Today, non-Java applications are always compiled for the native machine, meaning that you are locked into the platform for which you bought the software but can bypass the virtual machine altogether (see Figure 1-10).

Performance of native, non-Java code.

Figure 1-10. Performance of native, non-Java code.

When Java came out with its promise of platform independence, people rejoiced because they no longer had to develop for every computer under the sun. However, the enthusiasm was tempered by the fact that Java was an interpreted language, meaning that the extra steps involved in translating Java code into native code made applications significantly slower. Furthermore, the bytecodes generated by the Java compiler were created with platform independence in mind. This meant that in order to preserve an adequate middle ground, Java bytecodes were arranged so that no platform necessarily got an advantage when it came time to translate into native code. The end result was that not only did it take a bit more time to interpret the code, but also that the code was interpreted from a platform-independent state caused the resulting native code to execute more slowly.

The JIT compiler solves most of these issues by enabling you to generate native code from your interpreted bytecode. The native code then performs exactly as it would have performed had the program been originally programmed in a native language.

As you can see from Figure 1-11, the JIT exists as part of the virtual machine, and JIT compilation happens automatically if the compiler is installed. Some virtual machines will allow you to turn off JIT compilation, but that should be necessary in only rare cases. Currently, several vendors including Sun, Microsoft, and Symantec are offering JIT compilers that either can be purchased as add-ons to a native virtual machine or are bundled as part of their own virtual machine.

Performance of Java using a JIT compiler.

Figure 1-11. Performance of Java using a JIT compiler.

Summary of Performance Issues

Performance is an issue of vital importance to Java programmers. Because of Java's promise as a platform-independent language, several architectural decisions were made to create the language. However, some of these decisions have contributed to Java's faults. Many of these issues have been addressed, namely download and runtime performance. Further deficiencies in the Java language will be corrected as time goes on if Java is to achieve its potential. Ultimately, the growth in applications using the language will uncover these faults as well as the corrections to them.

With several of the major benefits of the Java language under our belt, we can turn to finally developing a networked application. Our networked applications will use many of the techniques we have discussed thus far, as well as several more we will introduce along the way. Congratulations! Your first foray into Java networking is about to begin.

A First Look at Java Networking in Action

So far you have learned the three basic things you need to know in order to write networked applications in Java:

  • Object-oriented design

  • Input and output fundamentals

  • Threading

A good object-oriented design will allow you great flexibility in creating clients and servers. You can extend the functionality of a well-designed class very easily. You can either alter the nuances of the class's architecture in order to facilitate the kind of communication you desire or publish your class to the "world" so that it can be used as it was intended to be used.

Solid input and output fundamentals enable your classes to process data quickly and efficiently. With a strong I/O functionality, your classes can accept, manipulate, and return data without much hassle. And once again, you can publish your class to the "world," specifying exactly which data you will accept and streamlining the processing power of your objects.

Effective threading principles will enable your class to produce fast turnaround times on object requests (those methods invoked upon your object), make good use of system resources, and begin to create an entire collection of objects that work together without affecting system performance. Figure 1-12 illustrates how a server can effectively handle information by spawning threads to process that information.

Threading can prevent servers from being bogged down.

Figure 1-12. Threading can prevent servers from being bogged down.

Good networked applications have three things in common:

  • Useful interface definitions

  • Pragmatic data definitions

  • Efficient processing of data

Hopefully, the treatment of these three topics in this chapter so far has provided you with a means to satisfy the criteria set forth earlier and publish networked Java objects that take full advantage of the language.

Pulling It All Together

Throughout this book, we will reimplement the following featured application. Our Internet Calendar Manager is a simple tool designed to enable you to schedule appointments over a network. Because of Java's platform independence, you will be able to run this application on both your Windows laptop as well as your SPARC station. Because the data is held in a central repository with the Internet used as the communication mechanism between the two, it will not matter where you run the application because—no matter what—you will be manipulating the exact same data.

Road Map for Success

Your first task is to outline a clear object-oriented strategy to complete your project. For example, the Internet Calendar Manager was designed with modularity as its most crucial element. We wanted to be able to remove and replace certain parts of the program as often as we needed to without affecting the rest of the application. With that in mind, we created the class structure as shown in Figure 1-13.

The class structure of our Internet Calendar Manager was created with modularity in mind.

Figure 1-13. The class structure of our Internet Calendar Manager was created with modularity in mind.

As you can see, changing a component in the Scheduler does not at all affect the Calendar portion of the application. Each module is entirely separate from the other. This is an example of code reuse and modularity. Furthermore, the Network module keeps our network interaction limited to one module. All initialization, data exchange, and remote invocations take place only from within the module itself.

Furthermore, we recognized a series of objects that we would require throughout the application. Most of these are not specific to the implementation of any module; rather, they are helper objects that deal with wide ranging things from multimedia (sounds and pictures) to animation. These objects were placed in the Utilities module so that they could be used as needed.

Project Planning

Once the project is divided as we did in the previous section, we must define the interfaces with which objects would talk to one another. In particular, the modularity of the Network component enabled us to redo it for each section without in any way affecting the rest of the application. In fact, the entire Network module wasn't even completed until two weeks before press time. The rest of the application was finished and working, talking to the Network module, but was never communicating with any remote objects.

User Interface

The Internet Calendar Manager we created is a stand-alone Java application. We made it so for ease of use. An applet version of the same application will reside on the Web site for this book. In any event, the UI components are the same. A series of buttons along the top of the application control which of the two tasks you can do: add an appointment or delete an appointment.

Pressing the Scheduler button takes you to the Add an Appointment section. There, you can specify the reason for the appointment and the time for which you would like to schedule it. Pressing the Schedule button sends the appointment to the Network module, which, in turn, talks to the server and places the appointment in the data repository.

The Calendar button takes you to the Calendar application. The Calendar application allows you to view a list of all the appointments scheduled, and the reasons and times for them. You may also delete appointments from within this application.

The content of the card layout is dependent on which button you press, as shown in Figure 1-14.

The main GUI for our featured application.

Figure 1-14. The main GUI for our featured application.

Finally, the exit button gracefully terminates the connection, telling the Network module that it wants to exit.

Network Modules

The Network module will be changed from chapter to chapter to reflect the new form of network communication. However, the APIs will remain the same. The Network module provides an abstracted layer above the network communication mechanism of choice. In so doing, we can provide a series of four methods that are of importance to the user, while keeping the network hidden from the rest of the application:

public class NetworkModule
{
    public void scheduleAppointment(
        String reason,
        int time);
    public Vector getAppointments();

    public void initNetwork();

    public void shutdownNetwork();
}

As far as the rest of this application is concerned, the Network module will accept information and do something with it over the network. Precisely what that something is is of no concern to the application itself.

Servers

The Network module would be useless without a server for it to talk to. Every server implements the same exact routines, regardless of whether it is a Java Database Connectivity (JDBC) server, a Remote Method Invocation (RMI) server, or an Interface Definition Language (IDL) server. In fact, the server itself is interchangeable, enabling us to choose on the fly to which one we want to talk. Simply run the proper application to take advantage of the communication mechanism of your choice.

public interface InternetCalendarServer
{
    void scheduleAppointment();

    void getAppointments();
}

The interface definition in this snippet does not take into account any kind of data structure in which to store an appointment. The server code implements both of the foregoing methods, as well as establishes and defines the following data structure:

public interface InternetCalendar Server
{
    Appointment Type
    {
       String reason;
       int time;
    }

    void scheduleAppointment(
       AppointmentType appointment
    );

    AppointmentType[] getAppointments();
}

Keep in mind that the interface definitions shown are pseudo-code only. As we will see later, server definition varies widely between each communication alternative. In Java IDL we will see how an entire language is available with which to define servers. In Java RMI we can create servers using Java itself.

Note

In an effort to show you how easy and fun network programming can be with Java, we have devised a simple application that we will redo every chapter. In one chapter we will use sockets, in another CORBA. Eventually, you will have six different applications that do the same thing. With the six applications, you can compare ease-of-use and performance, as well as figure out what all the hubbub is about network programming. The next four chapters will explore the basic alternatives available to network programmers intent on using the Java language.

Summary

Wow! Not only have we learned the nuances of the Java programming language, but we've also deleved into the wide world of threads, explored some of the important performance issues we need to deal with, and seen how easy it is to save and restore our creations. These are great tools to have as we begin our journey through the realm of interprocess communication, networked programming, and distributed design.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset