CS 3230 - Chap 11-12 Notes

Exceptions, Debugging & File I/O

Some Background on Error Handling

Review: 3 Types of Errors

  1. Compile-time errors - These are our favorite because the big, programmer's spell-checker will tell us where the problems are.
  2. Run-time errors - These occur while the program is running; no error messages are generated at compile time. The user will be made aware of the error, however, usually by seeing the program crash. Maybe an error message if he's lucky.
  3. Logic errors - You multiplied when you should have added. No error occurs, ever. You simply get incorrect output and have to track down the offending line(s) of code. These are our least favorite kind of errors.

Chapter 11 focuses on run-time errors. Numerous different operations can cause exceptions: file I/O, networking, threads, even parsing a string to make an integer (see p.558). Your book it gives a little bulleted list on p.557 of some things you should do when an unrecoverable error occurs.

Approaches to Runtime Error Handling

Different programming languages have different approaches to handling exceptions. The following list gives some common techniques.

Do nothing - A popular approach. Silently ignore the error and wait for the program to crash in some spectacular fashion.

Return an error code (p.558) - Make the user check the return value of a function and see if an error code was returned. The problem with this approach is that a user can happily ignore the return value (see "Do Nothing").

Install a callback function - Before engaging in potentially hazardous code, you install a function that will be called when something goes wrong. This approach makes it harder to ignore errors. (But you do have to worry about problems with re-entrancy...)

Throw an exception - This is the approach used in most object-oriented languages, including C++, Java, and Python. When a problem occurs, an exception is "thrown" by the called function which you must "catch" in the calling function. Example:

MediaTracker tracker = new MediaTracker(imgPanel);
Image img = getImage(getCodeBase(), imgName);
tracker.addImage(img, 1);
try {
	tracker.waitForAll();
} catch (InterruptedException ex) {
	JOptionPane.showMessageDialog(imgPanel,
			"Interrupted while downloading image");
}
If the download gets interrupted, an exception is thrown by the tracker.waitForAll() method. Said exception is caught in the catch block.

Java Error Classifications p.559

Java uses different classes for different kinds of errors. See p.559 for part of the class hierarchy.

Errors vs. Exceptions (p.559) - Two large branches exist in the "Throwable" class hierarchy:

  1. An Error is a problem with the Java Runtime Environment, these are usually fatal and unrecoverable. Don't bother handling them, just turn out the lights, the party's over.
  2. An Exception is a problem that occurs at runtime. We can catch these and deal with them.

Checked vs. Unchecked Exceptions (p.559) - Furthermore, two large branches exist within the exception hierarchy:

  1. A Checked Exception is one that occurs in a very specific situation and must be caught; if you do not write a try..catch block the compiler will yell at you. (This is a good thing.) Most exceptions fall into this category, including: file I/O, networking, threading, and database-related exceptions.
  2. An Unchecked Exception is a problem that is potentially so common that it would be ridiculous to try to catch them all. Examples include: "null pointer" and "array index out of bounds" exceptions. (Errors are also unchecked.)

Exception Handling in Java

Catching Exceptions p.564

Catching one exception - Put the dangerous code in a try block and the handling code in the catch block. Code example in your book on p.564.

Catching more than one exception (p.566) - Just use multiple catch blocks, one for each exception type. (This is similar to an if..else if..else if.. construct.) Code example in your book on p.566. Alternatively, you can cheat and consolodate all your exception handling code like so:

try {
	dangerous_code();
} catch (Exception e) {
	System.err.println(e);
}
Exceptions know how to print themselves so the message that gets printed on the console will tell you what happened.

The finally clause (p.567) - This occasionally useful clause of the try construct will execute everything in its block whether an exception occurs or not. The inquisitive student might ask: "Why not just put the code you want to execute after the try block?" Answer: Because it will not be executed; the exception halts execution of the program.

The wrong way to handle an exception (p.577) - Some programmers do this just to silence the compiler error:

try {
	dangerous_code();
} catch (Exception e) { }
Then their program will crash and they will have no idea why it happened. It is a good thing that Java requires you to handle exceptions; do not squelch them.

The right way to handle an exception - The following commented code illustrates:

String str = "Ned Flanders";
int index = 8;
char ch;
try {
	// First, perform a simple test to prevent an exception
	// from occurring in the first place (See p.576)
	if (index < str.length()) {
		ch = str.charAt(index); // (this line could generate the exception)
	}
} catch (StringIndexOutOfBoundsException e) {

	// Second, print a diagnostic message for yourself plus
	// the exception itself
	System.err.println("Couldn't get string index: " + e);
	// (Alternatively, you could write this to a logger --
	//  more on that below)

	// Third, print the call stack that led up to this
	// exception (See p.570, top)
	e.printStackTrace();
}

Throwing Exceptions p.562

You probably won't do this as often, but you can throw your own exceptions if you want to.

The throw keyword (p.562 bottom) - Pick an exception class and throw it. This will usually follow a conditional statement.

	throw new IOException();

The throws keyword (p.560) - The next thing you need to do is change your method signature to indicate what exception(s) you are throwing in that method.

int readBytes(File f) throws IOException {
	...rest of method body...
The compiler will use the method signature to spew errors if calling code does not handle the exceptions you throw. The javadoc program will also include a list of the exceptions thrown by a method in the documentation.

Custom exception classes (p.563) - If you don't want to throw one of the canned exceptions, you can easily make your own. Just derive one from an existing Exception class, instantiate one, and throw it.

Final words on exceptions

Source code (p.572) - ExceptTest.java - Demonstrates generating numerous different types of exceptions and the messages printed by each one.

Tips (p.576) - Lots of great advice packed into a small space. Some of these were discussed previously.

Debugging Techniques

Logging p.578

Often, you want to get a view of what's going on inside your program as it's running. The most well-known, well-loved, time-tested tool for doing this is the "print" statement. A slightly more formalized way of printing output is to write it to a log file. Java provides the Logger class to help you accomplish this. There are two flavors of loggin: basic and advanced.

Basic Logging (p.579) - Can be done easily by writing to the global log file. BasicLogExample.java shows how to use this. Note that by default it prints messages to stdout. To log messages to a file, add a "file handler" as mentioned on p.583 (middle).

Advanced Logging: (p.579) - This is the approach you use for big-time apps that will be logging a lot of messages. LoggingImageViewer.java (p.587) Illustrates how to use advanced logging.

There is tons more info in this chapter on logging, but I'm not going to cover it all here. I'll just call your attention to the "Logging Cookbook" on p.586, which give some good logging guidelines. Also, look at the selected API docs on p.590. (I love the entering and exiting methods.)

Assertions p.593

The assert() macro is a venerable tool used by C programmers since time immemorial. In a nutshell, it takes a boolean expression and if it evaluates to false, generates a run-time error. This causes the program to halt and lets you see where it failed. This technique is commonly used to validate parameters passed to functions. Example:

void sortArray(String[] ary, int index) {
	assert(ary != null);
	assert(index >= 0);
	...

Source code: AssertExample.java shows using an assertion to validate a parameter. Note how you can add an expression that gets printed with the exception. Note also that you must compile this example like:

	javac -source 1.4 AssertExample.java
(See p.594 in your book, and note that it says "MyClass.class" where it should say "MyClass.java".)

Next, compare the "Exception" messages that are printed when you run it like:

	java -ea AssertExample
versus:
	java -ea AssertExample
(Again, see p.594)

Page 595 in your book gives some good guidelines on when you should use assertions.

Using a Debugger

There are numerous Java debuggers you can use. Your book mentions these:

jdb (p.614) - This is a command-line debugger similar to gdb (the GNU debugger). Most of you will probably cringe at the idea of using this one.

Use an IDE (p.619) - Sun ONE (screenshot p.619), Symantec Cafe, Borland's JBuilder, IBM's Visual Age for Java, etc. These let you set breakpoints, view the call stack, have a watch window, etc.

File I/O

Why so complicated?

File I/O in Java is an admittedly hairy beast. (Recall on Lab2 how we had to make a class to read from stdin?) Purists / apologists will tell you that they have taken the high ground of "seperating output from formatting" (this is why Java doesn't have a printf) and other such platitudes. Newcomers / level-headed programmers will take one look at the Java stream zoo (p.625) and pine for the golden days of C with it's FILE* ADT and fprintf function.

So really, why is File I/O so complicated?

Here's some possible answers:

If there is one positive side to Java's file I/O it's that it follows the UNIX philosophy of "everything is a file": file I/O works just like network I/O, works just like memory-mapped I/O... and so forth. It also keeps with the UNIX spirit of "filtering" (i.e. a pipeline of commands like cat /etc/services | grep irc | wc -l), so get ready to "nest" a bunch of constructors together.

I'll attempt to demystify some of this stuff and give some examples of common solutions to common problems.

Also, Java file streams will throw exceptions like crazy, so get ready to catch them.

Classifications of Streams

A "stream" is a flow of data. Java classifies streams into several categories.

Character vs. Byte: Two large categories of streams in the I/O hierarchy:

  1. Character Streams - Used for text files that will contain human-readable text strings. The 2 superclasses for this category are Reader and Writer. See the abbreviated class hierarchy on p.626.
  2. Byte Streams - Used for for "data" files that will contain non-human-readable binary data. The 2 superclasses for this category are InputStream and OutputStream. See the abbreviated class hierarchy on p.625.

Data vs. Processing - Furthermore, there are two other large categories of streams, orthagonal to the previous two.

  1. Data Streams - This represents a source or destination for data, such as a file. FileReader is an example.
  2. Processing Streams - These are classes which sit between a source and a destination and provide "convenience" methods for the programmer. BufferedReader or PushbackReader are examples.

Text Files (Character Streams)

I imagine most of you will be using text files if you're doing any file I/O at all, so here's how you do it:

Writing (p.638) - 2 classes: a FileWriter to open an output stream to a file, passed to a PrintWriter, which provides the println() method (remember this one?) to make writing more convenient.

try {
	PrintWriter outFile = new PrintWriter(
			new FileWriter("myfile.txt"), true);
	...
	outFile.println("string");
} catch (IOException e) {
	System.err.println("Couldn't write to file: " + e);
}

Note that adding true to the PrintWriter constructor will make it flush after each call to println() (this is a good idea).

Reading (p.640) - 2 classes: a FileReader to open an input stream to the file passed to a BufferedReader, which provides the readLine() method to make reading more convenient.

try {
	BufferedReader inFile = new BufferedReader(
			new FileReader("myfile.txt"));

	String line;
	while ((line = inFile.readLine()) != null) {
		process line
		...
	}
} catch (FileNotFoundException e) {
	System.err.println("Couldn't find file: " + e);
} catch (IOException e) {
	System.err.println("Couldn't read from file: " + e);
}

String Tokenizing (p.650) - If you are reading from delimited text files, you might want to find a way to parse it all. Take a look at the StringTokenizer, which can help you decompose a delimited string into its smaller parts.

Source code: DataFileTest.java from your book on p.651. Look at the employee.dat file that gets created.

Serializing Objects (Byte Stream)

"Serializing" means writing a whole object out to a file so you can read it back in later. This is sometimes called "persistence".

Writing (p.662) - 2 classes: a FileOutputStream to open a stream to a file where the object will be written, passed to an ObjectOutputStream.

// The class must implement the Serializable
// interface to be persistent
class Dinosaur implements Serializable {
	...
}

Dinosaur dino = new Dinosaur();
ObjectOutputStream outStream = new ObjectOutputStream(
	new FileOutputStream("obj.ser"));
outStream.writeObject(dino);

Reading (p.640) - 2 classes: a FileInputStream to open a stream to a file containing a serialized object, passed to an ObjectInputStream.

ObjectInputStream inStream = new ObjectInputStream(
	new FileInputStream("obj.ser"));
Dinosaur dino = (Dinosaur)inStream.readObject();

Source code: ObjectFileTest.java from your book on p.663. Look at the employee.dat file it creates and compare it with the previous.

The Gory Details (p.665) - If you're interested, the serialization file format is described in your book on p.665-668. Starting on p.668 it describes the problem with serializing objects and on p.671 it says that all these considerations have been taken care of for you.

Generic Byte Streams (p.627) - Object serialization is merely one example of a byte stream. If you want just a "generic" byte stream that allows you to dump a bunch of bytes to a file, you can just make a DataOutputStream and DataInputStream.

The File Class (p.684)

The useful File class can be used to test if a file exists, check last modified time, add / remove directories, create temporary files, and so forth. Have a look at the selected API docs on p.687-9.

Source code: FindDirectories.java from your book on p.686.