Survey
* Your assessment is very important for improving the workof artificial intelligence, which forms the content of this project
* Your assessment is very important for improving the workof artificial intelligence, which forms the content of this project
Exceptions in Java Christian Ratliff <[email protected]> Sr. Technology Architect Core Libraries Group DeLorme Mapping Presentation Sections 1. A lexicon and short history of exceptions. 2. Working with exceptions in Java. 3. Designing an effective exception class hierarchy. 4. Advanced exception handling. Copyright (c) 2001 DeLorme 2 Section One A lexicon and short history of exceptions. Lexicon: Actors and Actions Operation A method which can possibly raise an exception. Invoker A method which calls operations and handles resulting exceptions. Exception A concise, complete description of an abnormal event. Raise Brings an exception from the operation to the invoker, called throw in Java. Another very common word for this is emit. Handle Invoker’s response to the exception, called catch in Java. Backtrack Ability to unwind the stack frames from where the exception was raised to the first matching handler in the call stack. Copyright (c) 2001 DeLorme 4 Lexicon: Types of Exceptions Hardware Generated by the CPU in response to a fault (e.g. divide by zero, overflow, segmentation fault, alignment error, etc). Software Defined by the developer to represent any other type of failure. These exceptions often carry much semantic information. Domain Failure The inputs, or parameters, to the operation are considered invalid or inappropriate for the requested operation. Range Failure Operation cannot continue, or output is possibly incorrect. Monitor Describes the status of an operation in progress, this is a mechanism for runtime updates which is simpler than subthreads. Copyright (c) 2001 DeLorme 5 Error Handling Before Exceptions Error handling, before exceptions, took one of two forms: Parameterized forms: Address of a subroutine to be called during an error. Name of a statement labels to jump to on error. Status variables changed by the operation, and then checked by invoker at operation return. Language-based forms: Fault subroutines tied to a type or instance. Fault target defined at compile time on the subroutine. PL/I condition statements. Copyright (c) 2001 DeLorme 6 Problems with these Systems Parameterized Calls Language Forms Not required to handle exceptional results No way to detect at compile time whether the exception is handled. Limited number of conditionhandler pairings. One handler per exception per type or object. Handler for each subroutine is predefined. PL/I solution was not given to general use. How do we resolve these problems, and move from a system where error handling is both inconsistent and ignorable to something logically structured? Copyright (c) 2001 DeLorme 7 Birth of Modern Exceptions C.A.R. Hoare devised an exception processing system like: Q1 otherwise Q2 John B. Goodenough described exceptions as: Hierarchical Classified Backtracking Explicitly Declared Resumable Defaultable Copyright (c) 2001 DeLorme 8 Goodenough’s Notation /* Declare an operation G, which can raise an exception X */ DECL G ENTRY (FIXED) [X:…]; /* Call G with parameter A and handle possible exceptions */ CALL G(A); [X: handler-action] /* Handle an exception if A added to B overflows C */ C = (A + B); [OVERFLOW: handler-action] /* Handle two possible overflow exceptions separately */ C = (A * (B * B) [OVERFLOW: handler-action1]); [OVERFLOW: handler-action2] /* Handle exceptions from any element of the loop */ DO WHILE (…) CALL G(A); C = (A + B); END; [X: …, OVERFLOW: …] /* Handle an exception from any member of a statement group */ DO CALL G(A); C = (A + B); END; [X: …, OVERFLOW: …] Copyright (c) 2001 DeLorme 9 Advantages of this Notation Enables transfer of control from location of the fault, to code which knows how to handle that particular fault. Exceptions cannot be ignored, they must be caught or the application will terminate. Separates the error management from the normal code. Simplifies error processing by allowing entire blocks to be combined inside one exception handler. Exceptions are divided into three major groups: ESCAPE requires termination of the operation in progress. NOTIFY explicitly forbids termination of the operation, this is a monitor. SIGNAL allows the invoker to either terminate or resume the operation in progress. Copyright (c) 2001 DeLorme 10 Questions for Section One ? Copyright (c) 2001 DeLorme 11 Section Two Working with exceptions in Java Classifying Java Exceptions Unchecked Exceptions It is not required that these types of exceptions be caught or declared on a method. Runtime exceptions can be generated by methods or by the JVM itself. Errors are generated from deep within the JVM, and often indicate a truly fatal state. Runtime exceptions are a source of major controversy! Checked Exceptions Must either be caught by a method or declared in its signature. Placing exceptions in the method signature harkens back to a major concern for Goodenough. This requirement is viewed with derision in the hardcore C++ community. A common technique for simplifying checked exceptions is subsumption. Copyright (c) 2001 DeLorme 13 Keywords for Java Exceptions throws Describes the exceptions which can be raised by a method. throw Raises an exception to the first available handler in the call stack, unwinding the stack along the way. try Marks the start of a block associated with a set of exception handlers. catch If the block enclosed by the try generates an exception of this type, control moves here; watch out for implicit subsumption. finally Always called when the try block concludes, and after any necessary catch handler is complete. Copyright (c) 2001 DeLorme 14 General Syntax public void setProperty(String p_strValue) throws NullPointerException { if (p_strValue == null) { throw new NullPointerException(“...”); } } public void myMethod() { MyClass oClass = new MyClass(); try { oClass.setProperty(“foo”); oClass.doSomeWork(); } catch (NullPointerException npe) { System.err.println(“Unable to set property: “+npe.toString()); } finally { oClass.cleanup(); } } Copyright (c) 2001 DeLorme 15 Canonical Example public void foo() { try { /* marks the start of a try-catch block */ int a[] = new int[2]; a[4] = 1; /* causes a runtime exception due to the index */ } catch (ArrayIndexOutOfBoundsException e) { System.out.println("exception: " + e.getMessage()); e.printStackTrace(); } } /* This code also compiles, but throws an exception at runtime! It * is both less obvious and more common (an off-by-one-error). */ public int[] bar() { int a[] = new int[2]; for (int x = 0; x <= 2; x++) { a[x] = 0; } return a; } Copyright (c) 2001 DeLorme 16 throw(s) Keyword /* The IllegalArgumentException is considered unchecked, and * even making it part of the signature will not alter that. */ public void setName(String p_strName) throws IllegalArgumentException { /* valid names cannot be zero length */ if (p_strName.length() == 0) { throw new IllegalArgumentException(“…”); } m_strName = p_strName; } public void foo() { setName(“”); /* No warning about unhandled exceptions. */ } Copyright (c) 2001 DeLorme 17 throw(s) Keyword, part 2 /* Make a bad parameter exception class */ class NuttyParameterException extends Exception { … } /* To really make an invoker pay attention, use a checked * exception type rather than a Runtime Exception type, but * you must declare that you will throw the type! */ public void setName(String p_strName) /* error here! */ { /* valid names cannot be zero length */ if (p_strName == null || p_strName.length() == 0) { throw new NuttyParameterException(“…”); } m_strName = p_strName; } Copyright (c) 2001 DeLorme 18 throw(s) Keyword, part 3 /* Make a bad parameter exception class */ class NuttyParameterException extends Exception { … } /* To really make an invoker pay attention, use a checked * exception type rather than a Runtime Exception type. */ public void setName(String p_strName) throws NuttyParameterException { /* valid names cannot be zero length */ if (p_strName == null || p_strName.length() == 0) { throw new NuttyParameterException(“…”); } m_strName = p_strName; } /* Many of us will have an unquenchable desire to use a Runtime * exception in the above, but resist! */ public void foo() { setName(“”); /* This does result in an error. */ } Copyright (c) 2001 DeLorme 19 try Keyword /* The try statement marks the position of the first bytecode instruction * protected by an exception handler. */ try { UserRecord oUser = new UserRecord(); oUser.setName(“Fred Stevens”); oUser.store(); /* This catch statement then marks the final bytecode instruction * protected, and begins the list of exceptions handled. This info * is collected and is stored in the exception table for the method. */ } catch (CreateException ce) { System.err.println(“Unable to create user record in the database.”); } Copyright (c) 2001 DeLorme 20 catch Keyword /* A simple use of a catch block is to catch the exception raised by * the code from a prior slide. */ try { myObject.setName(“foo”); } catch (NuttyParameterException npe) { System.err.println(“Unable to assign name: “ + npe.toString()); } try { /* example 2 */ myObject.setName(“foo”); } catch (NuttyParameterException npe) { /* log and relay this problem. */ System.err.println(“Unable to assign name: “ + npe.toString()); throw npe; } Copyright (c) 2001 DeLorme 21 catch Keyword, part 2 /* Several catch blocks of differing types can be concatenated. */ try { URL myURL = new URL("http://www.mainejug.org"); InputStream oStream = myURL.openStream(); byte[] myBuffer = new byte[512]; int nCount = 0; while ((nCount = oStream.read(myBuffer)) != -1) { System.out.println(new String(myBuffer, 0, nCount)); } oStream.close(); } catch (MalformedURLException mue) { System.err.println("MUE: " + mue.toString()); } catch (IOException ioe) { System.err.println("IOE: " + ioe.toString()); } Copyright (c) 2001 DeLorme 22 finally Keyword URL myURL = null; InputStream oStream = null; /* The prior sample completely neglected to discard the network * resources, remember that the GC is non-determinstic!! */ try { /* Imagine you can see the code from the last slide here... */ } finally { /* What two things can cause a finally block to be missed? */ /* Since we cannot know when the exception occurred, be careful! */ try { oStream.close(); } catch (Exception e) { } } Copyright (c) 2001 DeLorme 23 finally Keyword, part 2 public bool anotherMethod(Object myParameter) { try { /* What value does this snippet return? */ myClass.myMethod(myParameter); return true; } catch (Exception e) { System.err.println(“Exception in anotherMethod() “+e.toString()); return false; } finally { /* If the close operation can raise an exception, whoops! */ if (myClass.close() == false) { break; } } return false; } Copyright (c) 2001 DeLorme 24 finally Keyword, part 3 public void callMethodSafely() { while (true) { /* How about this situation? */ try { /* Call this method until it returns false. */ if (callThisOTherMethod() == false) { return; } } finally { continue; } } /* end of while */ } Copyright (c) 2001 DeLorme 25 Steps of try…catch…finally Every try block must have at least one catch or finally block attached. If an exception is raised during a try block: The rest of the code in the try block is skipped over. If there is a catch block of the correct, or derived, type in this stack frame it is entered. If there is a finally block, it is entered. If there is no such block, the JVM moves up one stack frame. If no exception is raised during a try block, and there is no System.exit() statement: If there is a matching finally block it is entered. Copyright (c) 2001 DeLorme 26 Questions for Section Two ?? Copyright (c) 2001 DeLorme 27 Section Three Designing an effective exception class hierarchy. Java Exception Hierarchy Throwable The base class for all exceptions, it is required for a class to be the rvalue to a throw statement. Error Any exception so severe it should be allowed to pass uncaught to the Java runtime. Exception Anything which should be handled by the invoker is of this type, and all but five exceptions are. java.lang.Throwable java.lang.Error java.lang.ThreadDeath java.lang.Exception java.lang.RuntimeException java.lang.NullPointerException java.lang.IllegalArgumentException Copyright (c) 2001 DeLorme java.io.IOException java.io.FileNotFoundException 29 Creating your own exception class /* You should extend RuntimeException to create an unchecked exception, * or Exception to create a checked exception. */ class MyException extends Exception { /* This is the common constructor. It takes a text argument. */ public MyException(String p_strMessage) { super(p_strMessage); } /* A default constructor is also a good idea! */ public MyException () { super(); } /* If you create a more complex constructor, then it is critical * that you override toString(), since this is the call most often * made to output the content of an exception. */ Copyright (c) 2001 DeLorme 30 Three Critical Decisions How do you decide to raise an exception rather than return? 1. 2. 3. Is the situation truly out of the ordinary? Should it be impossible for the caller to ignore this problem? Does this situation render the class unstable or inconsistent? Should you reuse an existing exception or create a new type? 1. 2. 3. Can you map this to an existing exception class? Is the checked/unchecked status of mapped exception acceptable? Are you masking many possible exceptions for a more general one? How do you deal with subsumption in a rich exception hierarchy? 1. 2. Avoid throwing a common base class (e.g. IOException). Never throw an instance of the Exception or Throwable classes. Copyright (c) 2001 DeLorme 31 An Example of Return v. Raise try { InputStream oStream = new URL("http://www.mainejug.org").openStream(); byte[] myBuffer = new byte[512]; StringBuffer sb = new StringBuffer(); int nCount = 0; while ((nCount = oStream.read(myBuffer)) != -1) { sb.append(new String(myBuffer)); } oStream.close(); return sb.toString(); /* if sb.length() == 0 is NOT an exception. */ /* These, on the other hand, are certainly exceptional conditions. */ } catch (MalformedURLException mue) { throw mue; } catch (IOException ioe) { throw ioe; } Copyright (c) 2001 DeLorme 32 Mapping to an Exception Class When you attempt to map your situation onto an existing Exception class consider these suggestions: Avoid using an unchecked exception, if it is important enough to explicitly throw, it is important enough to be caught. Never throw a base exception class if you can avoid it: RuntimeException, IOException, RemoteException, etc. Be certain your semantics really match. For example, javax.transaction.TransactionRolledBackException, should not be raised by your JDBC code. There is no situation which should cause you to throw the Exception or Throwable base classes. Never. Copyright (c) 2001 DeLorme 33 Using Unchecked Exceptions Use unchecked exceptions to indicate a broken contract: public void setName(String p_strName) { /* This is a violated precondition. */ if (p_strName == null || p_strName.length() == 0) { throw new InvalidArgumentException(“Name parameter invalid!”); } } Be careful about creating a type derived from RuntimeException. A class derived from AccessControlException is implicitly unchecked because its parent class derives from RuntimeException. Copyright (c) 2001 DeLorme 34 Simple Type Subsumption /* Since UnknownHostException extends IOException, the catch block * associated with the former is subsumed. */ try { Socket oConn = new Socket(“www.sun.com”, 80); } catch (IOException ioe) { } catch (UnknownHostException ohe) { } /* The correct structure is to arrange the catch blocks with the most * derived class first, and base class at the bottom. */ try { Socket oConn = new Socket(“www.sun.com”, 80); } catch (UnknownHostException ohe) { } catch (IOException ioe) { } Copyright (c) 2001 DeLorme 35 A Tricky Subsumption Problem /* The catch block can perform any number of functions, a common pair is * exception encapsulation and rethrow. */ try { Connection oConnection = MagicPool.getInstance().getConnection(); Statement oStatement = oConnection.createStatement(“...”); if (oStatement.executeUpdate() != 1) { throw new CreateException(“Account could not be created.”); } /* Why is there a possible subsumption here? */ } catch (SQLException sqle) { throw new CreateException(“Unable to create requested account, due to internal error: “ + sqle.toString()); } catch (CreateException ce) { throw ce; } /* You know what we need here, eh? */ Copyright (c) 2001 DeLorme 36 Avoiding Type Subsumption try { Connection oConnection = MagicPool.getInstance().getConnection(); Statement oStatement = oConnection.createStatement(“...”); if (oStatement.executeUpdate() != 1) { throw new CreateException(“Account could not be created.”); } /* If this rethrow was missing, we would have a problem. */ } catch (CreateException ce) { throw ce; } catch (SQLException sqle) { throw new CreateException(“SQL error in creating: “+sqle.toString()); } /* This avoids a subsumption, if CreateException is derived from * SQLException. Above is a good technique for handling this problem. */ Copyright (c) 2001 DeLorme 37 Subsumption Is Good Too! public String getContent(String p_strURL) throws MalformedURLException, IOException { URL myURL = null; InputStream oStream = null; /* Imagine more code here. */ } If the code calling this is driven by a console application then subsuming the MalformedURLException makes the code easier to write. The same error text is output regardless of type. If, on the other hand, the code is in use by a GUI, then catching the two exceptions aids the development of features like URL repair. The IOException is fatal, but the user can possibly resolve the URL problem! Adrian says, “This example stinks.” Copyright (c) 2001 DeLorme 38 Type Encapsulation Because JDBC objects can raise lots of types of checked and unchecked exceptions, and because they encapsulate resources which must be released quickly, this pattern emerges: public void load(PrimaryKey p_oKey) throws CreateException { Connection oConnection = null; Statement oStatement = null; try { /* Imagine code here! */ } catch (CreateException ce) { throw ce; } catch (ClassNotFoundException cnfe) { throw new CreateException(“Caught Class!Found”+cnfe.toString()); } catch (SQLException sqle) { throw new CreateException(“Caught SQLException”+sqle.toString()); } finally { try { oStatement.close(); } catch (Exception e) { } try {oConnection.close(); } catch (Exception e) { } } } Copyright (c) 2001 DeLorme 39 Questions for Section Three ??? Copyright (c) 2001 DeLorme 40 Section Four Advanced Exception Handling The Utility of Backtracking Backtracking is the process by which the stack frame is unwound in the presence of an unhandled exception. This process has some important properties: Objects created on the stack are discarded to the GC. Methods which cannot handle the exception are cleaned up implicitly. Each stack frame has the ability to subsume the exception. The end of the line is the JVM, which logs unhandled exceptions. Under C++, each object discarded is immediately, and fully, destroyed. This is a key problem for Java, and C#: Copyright (c) 2001 DeLorme 42 finally and finalize(), siblings? Java’s garbage collector (GC) offers each method a type of destructor called ‘finalizer()’. It is called with neither timing nor order guarantees, by the JVM. This is called non-deterministic finalization and gives rise to serious problems with Java objects which encapsulate OS resources: network, graphics, and database objects. Enter the finally block: try { /* Insert code here. */ } finally { myResource.close(); } Copyright (c) 2001 DeLorme 43 Bytecode Layout Each method has a table of exception information at the start of it: start_pc - where a try block begins end_pc - where the try block ends, just before the first catch catch_pc - the location of a catch block catch_type - the class type caught by this catch block. If a finally block is in use, then each statement which can exit the try block has a special bytecode instruction attached to it (jsr). This calls the special finally subroutine. It is easy to see how a proliferation of try/catch blocks and many finally blocks can quickly grow the code and the exception table on the method. Copyright (c) 2001 DeLorme 44 Bytecode Layout, part 2 public void foo() { try { /* marks the start of a try-catch block: start_pc */ int a[] = new int[2]; a[4] = 1; /* causes a runtime exception due to the index */ /* end of the code in the try block: end_pc */ } catch (ArrayIndexOutOfBoundsException e) {/* catch_pc, catch_type */ System.out.println(“AIOOBE = " + e.getMessage()); } catch (NullPointerException npe) { /* catch_pc, catch_type */ System.out.println(“NPE = “ + npe.toString(); } } Copyright (c) 2001 DeLorme 45 Performance Issues At a macro level you can make it easier on a JIT compiler by doing the following: Do not use exceptions to manage flow of control: exit a loop with a status variable rather than raising an exception. Place try blocks outside of a while loop, rather than inside it. If you use a finally block, avoid having many exit paths in the content of the block. Remember that each exit point has a JSR attached to it. The single largest exception cost in a non-native runtime is the creation of the exception, not the catching of it. In general avoid exceptions in the tight, fast code of your implementation. While the cost is minimal in JVM, the price rises rapidly in the JIT… Copyright (c) 2001 DeLorme 46 Exceptions and JIT Optimization Optimizing a Java program at runtime, within the JIT, requires a process called “path analysis”. A path analysis graph describes the structure of the method in the form of a graph. Each of the canonical control structures (e.g. if, for, while, …) creates new nodes and edges on the graph. In the presence of exceptions, new nodes and edges can be created for each and every Potentially Excepting Instruction (PEI) in the method encountered. Considering the number of both checked an unchecked exceptions, these secondary, exceptional, paths can proliferate quickly. The larger the path analysis graph, the more difficult it is for the JIT compiler to optimize the Java application. Copyright (c) 2001 DeLorme 47 Bytecode to Machine Code When the JIT compiler is generating machine code from the bytecode it is optimized over and over again. This optimization takes advantage of the path analysis graph, and potentially reorders the work of a method. Java’s clear and precise specifications for exception handling deter reorder-based optimization. This is because program state must be correct before each PEI is invoked. (Make your eyes big and say, OOHH!) To really understand what all this means I would advise reading the presentation: The Evolution of Optimization and Parallelization technologies for Java, or why Java for High Performance Computing is not an oxymoron! by IBM’s Vivek Sarkar. Copyright (c) 2001 DeLorme 48 Why Runtime Exceptions are Not Checked 11.2.2 The runtime exception classes (RuntimeException and its subclasses) are exempted from compile-time checking because, in the judgment of the designers of Java, having to declare such exceptions would not aid significantly in establishing the correctness of Java programs. Many of the operations and constructs of the Java language can result in runtime exceptions. The information available to a Java compiler, and the level of analysis the compiler performs, are usually not sufficient to establish that such runtime exceptions cannot occur, even though this may be obvious to the Java programmer. Requiring such exception classes to be declared would simply be an irritation to Java programmers. For example, certain code might implement a circular data structure that, by construction, can never involve null references; the programmer can then be certain that a NullPointerException cannot occur, but it would be difficult for a compiler to prove it. The theorem-proving technology that is needed to establish such global properties of data structures is beyond the scope of this Java Language Specification. Copyright (c) 2001 DeLorme 49 Issues in Multithreaded Code class MyThread implements Runnable { public MyTask m_oTask = null; public void run() { if (m_oTask == null) { throw new IllegalStateException(“No work to be performed!”); } else { /* Do the work of the thread. */ } } } public void foo() { MyThread oThread = new MyThread(); /* There is no way to get the exception from the run() method! */ new Thread(oThread).start(); /* Good reason for Runtime choice!! */ } Copyright (c) 2001 DeLorme 50 Use a Callback Pattern interface ThreadCallback { public void threadException(Thread p_oThread, Exception p_oException); public void threadSuccess(Thread p_oThread); } public MySubthread implements Runnable { ThreadCallback m_oTarget = null; /* The target of our status info. */ public MySubthread(ThreadCallback p_oTarget) { m_oTarget = p_oTarget; } public void run() { try { /* work is done here. */ } catch (SomeException se) { m_oTarget.threadException(this, se); /* inform about problem. */ return; } m_oTarget.threadSuccess(this); /* indicate all went well! */ } Copyright (c) 2001 DeLorme 51 Use the ThreadGroup class Threads can be tied to an instance of the ThreadGroup class. This instance is notified about any uncaught exceptions by one of its member threads: class java.lang.ThreadGroup { /* lots of other methods deleted… */ public void uncaughtException(Thread t, Throwable e); } /* The thread startup will change to this: */ new Thread(oMyThreadGroup, oMyThread).start(); You can derive from ThreadGroup, and make your own class with its own implementation of uncaughtException(). Then you can implement the listener pattern in your derived class and broadcast exception events to interested parties. Copyright (c) 2001 DeLorme 52 Exceptions and RMI Owing to the presenters limited experience with RMI, David Ezzio will speak for a moment on this interesting problem. Copyright (c) 2001 DeLorme 53 Questions for Section Four ???? Copyright (c) 2001 DeLorme 54 Bibliography Material for further reading Links to this material is at: http://www.backflip.com/members/cratliff Section One Goodenough, John B. Exception Handling: Issues and a Proposed Notation. Communications of the ACM, 18(12), Dec 1975. W. Kahan & J. Darcy. How Java’s Floating-Point Hurts Everyone Everywhere. ACM 1998 Workshop on Java for High-Performance Network Computing, March 1998. Copyright (c) 2001 DeLorme 56 Section Two R. Cartwright, E. Allen, and K. Charter. Intermediate Programming (C212) Lecture Notes. Department of Computer Science, Rice University, Spring 2001. M. Robillard and G. Murphy. Analyzing Exception Flow in Java Programs. Department of Computer Science, University of British Columbia, 2 March 1999. T. Suydam. Java Tip 91: Use nested exceptions in a multitiered environment. JavaWorld, April 2001. B. Venners. Under the Hood: Try-finally clauses defined and demonstrated. JavaWorld, February 1997. Copyright (c) 2001 DeLorme 57 Section Three J. Gosling, B. Joy, G. Steele & G. Bracha. The Java Language Specification, Second Edition. Sun Microsystems, 2000. Copyright (c) 2001 DeLorme 58 Section Four C. Austin & M. Pawlan. Advanced Programming for the Java2 Platform. Addison Wesley, September 2000. J-D. Choi, D. Grove, M. Hind, and V. Sarkar. Efficient and Precise Modeling of Exceptions for the Analysis of Java Programs. IBM T.J. Watson Research Center. M. Gupta, J-D. Choi, and M. Hind. Optimizing Java Programs in the Presence of Exceptions. European Conference on Object-Oriented Programming, Cannes France, June 14-16 2000. P. Haggar. Multithreaded Exception Handling in Java. IBM Corporation. V. Sarkar. Evolution of Optimization and Parallelization Technologies for Java. IBM T.J. Watson Research Center, May 2000. T. Suydam. Use nested exceptions in a multitiered environment. JavaWorld. Copyright (c) 2001 DeLorme 59