Skip to main content.

Web Based Programming Tutorials

Homepage | Forum - Join the forum to discuss anything related to programming! | Programming Resources

Tricks of the Java Programming Gurus

Chapter 16 -- Building Stand-Alone Applications

Chapter 16

Building Stand-Alone Applications


CONTENTS




Although much of the initial excitement about Java has centered on applets, Java is also a good language for writing stand-alone applications. Java and its library make it relatively simple for applications to perform tasks that require much more effort in other languages. Java also has the same kinds of "programming-in-the-large" features that have helped to make C++ popular, but in a simpler framework. Currently, Java is implemented as an interpreter, which makes it too slow for some purposes. But as native-code compilers become available, easing performance concerns, Java will probably become the language of choice for writing many kinds of stand-alone programs.

This chapter is an introduction to using Java for your applications. It explains how to write a stand-alone program in Java and introduces Java's special capabilities, which you can use to make better applications.

Writing and Running a Java Program

To write a Java program that can be run on its own, you create a class with a method called main. This procedure may feel familiar but also a little strange to C and C++ programmers: because all methods in Java are a part of classes, you can't simply write a function called main. You must put it in the context of a class.

Furthermore, to be recognized by the Java virtual machine as the starting point of a program, the main method must have certain other characteristics. Books on programming languages often contain an example of the simplest possible program, and such an example is useful because it makes clear what is essential and what is not. Here's the simplest possible Java program, which does nothing at all. It's the only program I have ever written that I'm absolutely certain has no bugs:

class Minimal {

    public static void
    main (String argv[]) {
    }
}

Unlike applets, an application class doesn't have to extend to any particular other class. But the main method has to be just right:

  1. It must be public static.
  2. It must be named main.
  3. Its type must be void.
  4. It must take one parameter: an array of String objects.

This list contains some interesting details.

Declaring the method static indicates that main is a class method, and that it doesn't execute in the context of an instance. When the application starts, the class has been initialized (so static variables already have been initialized, and static initializer blocks have been executed), but normal instance variables don't exist; no instance of the class is created. The main method doesn't have access to any nonstatic fields of the class.

C and C++ programmers might be wondering why the type of the main method can't be int. In those languages, the exit status of a program can be set in two different ways: either by a call to exit or by the main function returning an integer. But in Java, you have only one way to set the exit status: you must call System.exit.

The single parameter contains the command-line arguments, one per element of the array. To find out how many arguments you have, check array.length. Unlike C or C++, however, only the actual arguments to the command are included-argv[0] contains the first argument, not the name by which the command was invoked. Currently, the Java library doesn't provide a way to learn that information.

To run a Java application, invoke the Java virtual machine with the name of the application class as the first argument and application arguments after. The class file must be in Java's class search path (defined by the CLASSPATH environment variable), and you specify the name of the class, not the name of the file. To invoke our minimal application with three arguments, for example, do the following:

java Minimal one two three

(Of course, the Minimal class doesn't do anything with command line arguments it receives.) If the application class is in a package, you must specify the entire name of the class, package and all.

For command-line environments, this way of invoking programs is a bit clumsy. You can write a shell script or batch file wrapper to permit executing the application just by the command name. I anticipate that future releases of the Java development kit may include a tool to build such scripts or to generate a special version of the Java virtual machine that runs an application automatically.

Properties

The Java runtime system makes use of an idea called system properties to provide programs with a way of learning about the execution environment. Using properties, your code can learn about the Java environment and the machine and operating system on which it is running. Properties can also provide information about the user who is running the program, including the user's login name, home directory, current directory, and preferences.

You can think of properties as a restricted form of Hashtable, where the keys and values must be strings. The Java virtual machine initializes the properties table with a set of useful properties, as shown in the following table:

Property NameExplanation
java.version Java version number
java.vendor Java vendor identification string
java.vendor.url Java vendor URL
java.home Java installation directory
java.class.version Java class version number
java.class.path Java classpath
os.name Operating system name
os.arch Operating system architecture
os.version Operating system version
file.separator File separator ("\" under Windows)
path.separator Path separator (";" under Windows)
line separator Line separator ("\r\n" under Windows)
user.name User account name
user.home User home directory
user.dir User's current working directory

These properties are always guaranteed to be present in a Java virtual machine. (Properties are considered sensitive resources, and some properties are not visible to applets.)

The System class provides three static methods that can be used to access properties. The getProperty(String key) method returns a string that is the value of the property named by the key parameter. If the property is one that might not be available, you can use getProperty(String key, String def). The def parameter represents a string to use as a default value-the method returns the property value if it has been set, the def parameter if otherwise. The getProperties() method returns all the system properties as an instance of java.util.Properties (a subclass of Hashtable).

You also can set properties explicitly, but only as a group. The setProperties(Properties prop) method takes a Properties object as a parameter and completely replaces the system properties with the new list. A method for setting individual properties would have made it too tempting to use the system properties as makeshift global variables.

Properties and Environment Variables

UNIX and Microsoft Windows utilize the concept of environment variables, which are variables maintained by the operating system to which all programs have access. Each user has a separate set of environment variables, so users can set the variables as a way of supplying useful information to applications.

That capability is useful, but it's not portable: not all systems have environment variables, and the ones that do sometimes have different conventions about how they are used. UNIX programs, for example, can rely on an environment variable called PAGER, which contains the name of a command that the user likes to use to view output in screen-sized chunks, and applications that generate a lot of output can make use of that variable to present the information to the user in a useful way. Other systems don't use an environment variable for storing that piece of information or (more commonly) don't cater to such a thing at all.

The Java designers decided that system properties could be used to provide a portable, uniform way for applications to learn about the execution environment, including the kind of information that would ordinarily be found in environment variables on systems that support them. This section explains how to make environment variable information available to your Java application.

The direct way to run a Java application is by invoking the Java interpreter directly from the command line. Suppose that you write a Java program called Resolve, which searches the user's execution path for a command with a particular name. To run the Resolve program, you might type the following:

java COM.MCP.Samsnet.tjg.Resolve lostapp

This command is not very friendly. You may want to provide a wrapper script or command file to make the process easier. On UNIX, for example, your script might look like this:

#!/bin/sh
java COM.MCP.Samsnet.tjg.Resolve $*

If you call that file resolve and place it in a directory that is in the execution path, running the program is much easier:

resolve lostapp

Under Windows, you may simply provide a shortcut that contains the appropriate Java command line. In either case, the point is that you arrange for the messy invocation of the Java interpreter to be hidden from users.

The problem is that the resolve program needs access to the PATH environment variable to do its job, and the way you've done things so far, the program doesn't have that access. You do have a way to solve the problem, though. The Java interpreter enables you to set system properties on the command line. Using the -D option, you can supply the name and value for a property, and you can supply multiple definitions on the command line. So you can change the resolve wrapper script to look like the following:

#!/bin/sh
java -Denv.PATH="$PATH" COM.MCP.Samsnet.tjg.Resolve $*

Now when you invoke the program, the virtual machine defines a property called env.PATH based on the value of the PATH environment variable before the main method ever begins execution, and the program can access that property to get the information it needs.

Application Instances

Although Java applications are invoked as class methods, independent of any instances, it is often a good idea to build your applications so that they do run as objects. Such a strategy can yield useful flexibility. If you are writing a document editor of some sort, opening a new document could simply involve creating a new instance of your application. An application built that way might also be easily adapted to run as a component of some other, more inclusive application or framework, or even as an applet.

If you decide to build your application that way, then the main method becomes a sort of gateway or entry point. It parses and validates the arguments, converting them in some cases to more useful Java objects rather than simple strings. It might also verify that certain necessary resources are available, or load libraries that are used by the application proper. If the application supports network extensibility, the main method should probably also initialize the security manager, ensuring that security restrictions are in place at the earliest possible moment. Ultimately, however, the goal of the main method is to create a new instance of the application class, which does the real work.

The BloatFinder Application

As an example of how to write a stand-alone application, I've written the BloatFinder class, a simple disk space analysis program. The example illustrates the basics of writing stand-alone Java applications, and it also follows the design suggestions I've made. After it validates arguments, the main method creates an instance of BloatFinder to do the actual work; this means that other applications can use BloatFinder as a utility class, not just a self-contained program. Also, although I don't make use of system properties directly, I do make careful use of the File.separator static variable to learn the system-dependent directory separator character. I could have learned the same information from the system properties; in fact, the File class initializes the variable that way:

public static final String separator =
    System.getProperty("file.separator");

Most disk space analysis programs are not too helpful when you're trying to find large batches of wasted space that can be reclaimed. Most tell you only the size of files directly in a directory instead of recursively totaling files in subdirectories. Others, such as the "du" program on UNIX systems, give you the full total for a directory and all subdirectories, but they provide little help in narrowing down the real source of the bloat. You may not be surprised, for example, to find that most of the space on your disk is taken up by files in the "software" directory; what you really want to know is whether one or two subdirectories, possibly hidden several levels deep under "software," contain a significant percentage of the total. In short, typical disk space analysis programs either provide too much information or too little.

BloatFinder tries to do a little better. It recursively calculates directory sizes like du does, but it reports only directories that are larger than a certain threshold size.

The BloatFinder Class

The program is too long to include in one listing. The BloatFinder.java file itself contains the BloatFinder class and a utility class called DirEnum. Another class required by the application, DirNode, is in a separate file. Listing 16.1 shows an overview of BloatFinder.java, with comments taking the place of the methods and the DirEnum class.


Listing 16.1. BloatFinder overview (BloatFinder.java, part 1).
/*
 * BloatFinder.java       1.0 96/04/27 Glenn Vanderburg
 */

package COM.MCP.Samsnet.tjg;

import java.io.*;
import java.util.*;

/**
 * An application which finds the largest directories in a directory tree.
 *
 * Usage:
 * <pre>
 * BloatFinder -t threshold -d search-depth directory-name
 * </pre>
 * where threshold is the smallest size of directory to report (default
 * 5 megabytes), and search-depth is the maximum directory depth to report
 * (default 5).  The directory named on the command line is level 0.
 *
 * BloatFinder can also be used as a utility class by other applications.
 * It can supply an enumeration of all of the identified directories and
 * their sizes.
 *
 * This program traverses a directory hierarchy, calculates total space
 * used by each directory, and reports directories which seem to be using
 * "more than their fair share" according to a heuristic criterion which
 * I just made up. :-)
 *
 * Essentially, a directory is reported if its size (calculated as the size
 * of the directory itself, plus the size of files it contains, plus the
 * recursively calculated size of all of its subdirectories) is greater
 * than the <em>working</em> threshold at the current depth.  The working
 * threshold is calculated by the following formula:
 *
 * <pre>
 * wt = threshold - (((threshold/2) / (searchDepth-1)) * (depth-1));
 * </pre>
 *
 * that is to say, it begins as the specified threshold at the top of the
 * hierarchy, and is reduced by increments at each level until, at the
 * deepest level, it is half the level at the top of the tree.
 *
 * @version     1.0, 27 Apr 1996
 * @author Glenn Vanderburg
 */

public
class BloatFinder
{
    int threshold;
    int searchDepth;
    String dirname;

    DirNode top;

    // Method: public static
    //                main(String args[])              Listing 16.2
    // Method: public BloatFinder(String dirname)      Listing 16.3
    // Method: public BloatFinder(String dirname,
    //                            int threshold,
    //                            int searchDepth)     Listing 16.3
    // Method: public execute()                        Listing 16.4
    // Method: public elements()                       Listing 16.4
    // Method: public report(PrintStream out)          Listing 16.4
    // Method: static usage()                          Listing 16.2
    // Method: static usage(String errmessage)         Listing 16.2
}

// Class: DirEnum extends Vector
//                implements Enumeration               Listing 16.5

The program was designed from the start as a stand-alone application, but the needs of other programs that might want to use BloatFinder as a utility class were considered at every point. The main method parses and validates the command-line arguments, and when that work is done, it creates an instance and calls methods that direct the instance to do the work. The main method doesn't even supply the default values for command-line options; the constructors do that job so that the defaults can be supplied even when BloatFinder is used by another application.

Listing 16.2 shows the three static methods: main and the two usage methods (some of the repetitious argument parsing has been replaced by a comment).


Listing 16.2. BloatFinder static methods (BloatFinder.java, part 2).
/**
 * The main method for standalone application use.
 *
 * @param args the command line parameters
 */
public static void
main (String args[])
{
    // The constructor supplies a default value if -1 is passed
    // for these two.
    int threshold = -1;     // No value specified yet.
    int searchDepth = -1;
    
    // explicit initialization to avoid compiler warnings:
    String dirname = null;
    DirNode top;

    // I guess you can tell by the argument syntax that I'm a Unix guy ...
    for (int i=0; i<args.length; i++) {
        if ("-?".equals(args[i])) {
            usage();
            return;
        }
        else if ("-t".equals(args[i])) {
            if (++i < args.length) {
                try {
                    threshold = Integer.parseInt(args[i]);
                    if (threshold <= 0) {
                        usage("Threshold can't be negative: " + threshold);
                        System.exit(-1);
                    }
                }
                catch (NumberFormatException e) {
                    usage("Threshold must be an integer: " + args[i]);
                    System.exit(-1);
                }
            }
            else {
                usage();
                System.exit(-1);
            }
        }
        else if ("-d".equals(args[i])) {
            // Essentially the same as for "-t" ...
        }
        else {
            if (args[i].startsWith("-")) {
                usage("Unrecognized option: " + args[i]);
                System.exit(-1);
            }
            else {
                dirname = args[i];
                if (++i < args.length) {
                    usage("Too many arguments.");
                    System.exit(-1);
                }
                break;
            }
        }
    }

    if (dirname == null) {
        usage("Directory name not specified.");
        System.exit(-1);
    }

    // Now that the command line processing is done, we actually
    // create an instance to do the work.  If someone wants to write
    // a larger application which includes the BloatFinder functionality,
    // they can just create an instance with the appropriate parameters.

    BloatFinder app = new BloatFinder(dirname, threshold, searchDepth);
    app.execute();
    app.report(System.out);
}

/**
 * Prints a generic usage message to System.err.
 */
static void
usage ()
{
    System.err.print(
        "Usage: BloatFinder [-t threshold] [-d search-depth] "
        + "directory-name\n\n"
        + "where threshold is the smallest size of directory to report,\n"
        + "  and search-depth is the maximum directory depth to report\n"
        + "      (default 5).\n"
        + "The directory named on the command line is level 0.\n"
    );
}

/**
 * Prints an error message, followed by a usage message, to System.err.
 * @param errmessage the error message to print.
 */
static void
usage (String errmessage)
{
    System.err.println(errmessage);
    System.err.println("");
    usage();
}

As mentioned previously, the constructors supply the default values for the options. Having one simple constructor is handy when you want all the defaults, but to avoid having four separate constructors, I use values of -1 to indicate unspecified values, instead of overloading the constructor for each possible combination of supplied options. Listing 16.3 contains the code for the constructors.


Listing 16.3. BloatFinder constructors (BloatFinder.java, part 3).
/**
 * Constructs a new BloatFinder with default parameters.
 * @param dirname the topmost directory in the hierarchy
 */
public
BloatFinder (String dirname)
{
    // Use the defaults
    this(dirname, -1, -1);
}

/**
 * Constructs a new BloatFinder.
 * @param dirname the topmost directory in the hierarchy
 * @param threshold the initial reporting threshold (-1 means
 * use the default value)
 * @param searchDepth the depth in the hierarchy to search
 * (-1 means use the default value)
 */
public
BloatFinder (String dirname, int threshold, int searchDepth)
{
    this.dirname = dirname;

    // If the values are -1, supply a default.  Doing it this way
    // saves us from having to have four separate constructors for
    // the different permutations of initialization parameters, and
    // also saves the main method from having to call all four of them.
    if (threshold == -1) {
        this.threshold = 1024*1024*5; // 5 megabytes
    }
    else {
        this.threshold = threshold;
    }
    if (searchDepth == -1) {
        this.searchDepth = 5;
    }
    else {
        this.searchDepth = searchDepth;
    }
}

Listing 16.4 contains the meat of the BloatFinder class (although most of the real work is done in the DirNode class, which is shown next). The execute method scans the directory hierarchy looking for large directories. After that task is complete, the elements method can be used to get a list of those directories, or the report method can be used to print a formatted report of the list. The main method uses report; other applications might do the same, or they may prefer to get the list and present it in their own manner (such as displaying it in a listbox so that a user could select directories for deletion or closer inspection).


Listing 16.4. BloatFinder public methods (BloatFinder.java, part 4).
/**
 * Searches the directory hierarchy, calculating sizes and
 * collecting directories which exceed the threshold (or which
 * have children which do).
 */
public void
execute ()
{
    top = new DirNode(dirname);
    top.getSize(0, threshold, searchDepth);
}

/**
 * Builds an enumeration of the directories collected by execute.
 * The enumeration will consist solely of DirNode objects.
 * @return an Enumeration of DirNode objects.  Only directories which
 * exceed the threshold (or that have children which do) are included.
 * The enumeration is ordered as for a preorder tree traversal.
 * @see #execute
 */
public Enumeration
elements ()
{
    if (top == null) {
        execute();
    }
    return top.elements();
}

/**
 * Prints a report about large directories.
 * @param out a PrintStream to accept the output (for example, System.out)
 */
public void
report (PrintStream out)
{
    if (top == null) {
        execute();
    }

    try {
        for (    Enumeration e = elements();
                 e.hasMoreElements();
                 ) {
            DirNode d = (DirNode) e.nextElement();
            out.println(d.size + "\t" + d.pathname);
        }
    }
    catch (ClassCastException e) {
        // This won't happen unless someone introduces a bug
        // into DirNode, so we'll ignore it.
    }
}

The DirEnum Class

The BloatFinder.java file also contains a simple utility class, DirEnum, which is used by DirNode to provide an enumeration of the results of its directory hierarchy scan. DirEnum implements the Enumeration interface so that it can be used successfully by other classes, but it also extends Vector to make it easy for DirNode to build the enumeration piece by piece. Listing 16.5 contains the code for DirEnum.


Listing 16.5. DirEnum class (BloatFinder.java, part 5).
class DirEnum extends Vector implements Enumeration
{
    private int curIndex = 0;
    
    public boolean
    hasMoreElements ()
    {
        return curIndex < elementCount;
    }

    public Object
    nextElement ()
    {
        if (curIndex >= elementCount) {
            throw new NoSuchElementException();
        }
        return elementAt(curIndex++);
    }
}

The DirNode Class

The real logic of scanning the directory hierarchy, calculating sizes, and deciding whether a directory exceeds the specified threshold is performed by the DirNode class. It is a subclass of File, so it has ready access to methods for finding the size of a file and learning about a directory's files and subdirectories. An overview of the DirNode.java file is shown in Listing 16.6.


Listing 16.6. DirNode overview (DirNode.java, part 1).
/*
 * DirNode.java       1.0 96/04/27 Glenn Vanderburg
 */

package COM.MCP.Samsnet.tjg;

import java.io.*;
import java.util.*;

/**
 * An extension of File which scans subdirectories looking for
 * directories which exceed a certain size threshold.  It is
 * primarily a utility class for BloatFinder, but code which uses
 * that class sometimes needs direct access to DirNode objects.
 *
 * @see BloatFinder
 *
 * @version     1.0, 27 Apr 1996
 * @author Glenn Vanderburg
 */

public
class DirNode extends File
{
    /**
     * The total size of this directory.
     */
    public int size = 0;

    /**
     * The name of this directory relative to the top of the
     * specified hierarchy.
     */
    public String pathname;

    boolean sizeCalculated = false;
    boolean qualifies = false;

    Vector subdirs;

    /**
     * Constructs a new DirNode.
     * @param name the name of the file to represent.
     */
    DirNode (String name)
    {
        super(name);
        pathname = name;
    }
    
    /**
     * Constructs a new DirNode for a file within a directory.
     * @param dir the directory
     * @param name the name of the file within dir
     */
    DirNode (DirNode dir, String name)
    {
        super(dir, name);
        pathname = dir.pathname + File.separator + name;
    }

    // Method:         getSize(int depth,
    //                         int threshold,
    //                         int searchDepth)        Listing 16.7
    // Method:         elements()                      Listing 16.8
    // Method: private elements(String prefix,
    //                          DirEnum vec)           Listing 16.8
}

The complicated part of DirNode involves calculating the size and deciding whether the directory exceeds the threshold. The threshold decreases slightly at each level. Directories must remember subdirectories that exceed the threshold. The getSize method, which does all that work, is shown in Listing 16.7.


Listing 16.7. DirNode getSize method (DirNode.java, part 2).
/**
 * Calculates the size of this DirNode.  If it is a file,
 * returns length(); otherwise, the total size of all contained
 * files and subdirectories is calculated.  Along the way, the
 * method determines whether the directory is worth reporting
 * based on threshold and searchDepth.
 * @param depth the depth of this directory
 * @param threshold the size threshold of interest
 * @param searchDepth the maximum depth of interest
 * @return the size of this file or directory
 */
int
getSize (int depth, int threshold, int searchDepth)
{
    if (!sizeCalculated) {
        size += length();

        if (!isDirectory()) {
            sizeCalculated = true;
            return size;
        }

        // Collect data about all files and subdirectories
        String files[] = list();
        subdirs = new Vector();
        for (int i=0; i<files.length; i++) {
            DirNode cur = new DirNode(this, files[i]);
            if (cur.isDirectory()) {
                int size = cur.getSize(depth+1, threshold, searchDepth);
                size += size;
                if (cur.qualifies) {
                    // If any of our subdirectories show up in
                    // the results, then we should, too.
                    subdirs.addElement(cur);
                    qualifies = true;
                }
            }
            else {
                size += cur.length();
            }
        }
                
        if (!qualifies || (depth >= searchDepth)) {
            // If none of our children have to report, then
            // we can discard our vector of subdirectories.
            subdirs = null;
        }
        else {
            subdirs.trimToSize();
        }
            
        sizeCalculated = true;

        // Calculate working threshold
        int wt;
        if (threshold == 0) {
            // Always report the topmost level ...
            wt = 0;
        }
        else {
            wt = threshold - (((threshold/2) / (searchDepth-1))
                              * (depth-1));
        }
            
        if (size >= wt) {
            qualifies = true;
        }
    }
    return size;
}

After the size is calculated, the only other responsibility of DirNode is providing an Enumeration of the results upon request. Listing 16.8 shows the two methods that perform this task; they also make use of the DirEnum class.


Listing 16.8. DirNode elements methods (DirNode.java, part 3).
/**
 * Builds an Enumeration of large directories beneath this directory.
 * @return an Enumeration of DirNode objects.
 * @see BloatFinder#elements
 */
Enumeration
elements ()
{
    return elements(new DirEnum());
}

/**
 * Adds to a DirEnum all large directories beneath this directory.
 * @param vec the Vector being built
 */
private Enumeration
elements (DirEnum vec)
{
    vec.addElement(this);
        
    if (qualifies && subdirs != null) {
        for (int i=0; i<subdirs.size(); i++) {
            try {
                DirNode cur = (DirNode) subdirs.elementAt(i);
                cur.elements(vec);
            }
            catch (ClassCastException e) {
            }
        }
    }

    return vec;
}

Using Java's Special Features

This chapter provides an introduction to the basics of building Java stand-alone applications, but it certainly doesn't cover everything you need to know. Most stand-alone programs make heavy use of many parts of the Java library. If you are interested in building Java applications, you might have turned to this chapter first, but you will also find many other chapters in this book to be useful. This section describes some of the Java library features that are important to application developers, with references to other chapters that can help you make use of them. Reading it, you might also learn some reasons why you would want to write your next application in Java.

Using the Network

One of Java's most compelling and important features is its easy, uniform, built-in network capabilities. The network will be important for many Java applications, whether for applets and dynamic extensions, communicating and collaborating with applications on other computers, or simply storage and retrieval of data.

Several other chapters deal with Java and networking. Chapter 3, "Exploiting the Network," deals primarily with applets rather than applications, but it provides an introduction to Java's network classes. Chapter 35, "Taking Advantage of the Internet in Development," talks specifically about the ways that the network can be used to make applications more useful and versatile. Finally, you can read Chapter 34, "Client/Server programming," to learn about building distributed applications that make serious use of the network to accomplish their core tasks.

Cross-Platform GUIs

Most modern applications go beyond a textual, command-oriented interface. Graphical interfaces with menus, dialog boxes, and other graphical features have been important for a while, and there's no going back.

Java comes with a graphical user interface toolkit called the Abstract Window Toolkit (AWT). The AWT is notable because it's a cross-platform toolkit: Java programs that use it can provide graphical user interfaces on Windows, the Macintosh, UNIX systems, OS/2-any platform that Java has been ported to.

With the rise of the Internet and the current interest in inexpensive, portable computing, this sort of flexibility will be important. What the common computer platform of the future will be is not certain, and whether one dominant standard will exist is not even clear. AWT interfaces will be able to adapt to multiple platforms. In its current state, the AWT has some rough edges and is one of Java's weak points. But it has a good foundation, and it will certainly improve.

To learn more about using the AWT classes, see Part 3, "The Core Classes: AWT Tricks," and Part 4, "The Core Classes: Graphics Tricks."

Summary

Dynamically loaded extensions are probably the most exciting prospect for Java applications. Applets are only a part of the story. The original Java-based Web browser, HotJava, can locate and download Java classes to handle new URL protocols and new document, image, and audio formats.

Such features are useful for other programs besides just Web browsers. Spreadsheet or database programs could support dynamic addition of new datatypes. Editors or word processors could download handlers for specialized notations (such as mathematical equations or chemical diagrams) that might not be needed by casual users. Image processing programs could download modules for generating special effects. Many possibilities exist.

With such dynamic extensions, other people can add new functionality to your application for you. As with operating systems that become successful partly due to the efforts of application vendors whose offerings add value to the operating systems, you can use the enthusiasm and contributions of your user base to make your application more attractive to new users.

If you want to learn more about writing network-extensible applications, a good place to start is Chapter 17, "Network-Extensible Applications with Factory Objects." Because code loaded dynamically from the network can't be trusted completely, you should also read the chapters in Part 6, "Security," to learn how to design and build an effective security policy for your application.