CMPU 102, Fall 2005 Lecture 5

In this lecture we will examine what happens when exceptions are thrown and handled.

Outline

The call stack

In order to understand what happens when an exception is thrown we first need to fully examine how method calls are handled when the program is executed.  Each call to a method creates a new stack frame; the stack frame holds the values of each local variable in the method, as well as the location in the code where the method was called from.

Here is a very simple Java program:

public class Example {
    private int count = 0;

    public static void main(String[] argv) {
        Example ex = new Example();
        ex.f();
        System.out.println(ex.count);
    }

    public void f() {
        add(2);
        add(3);
    }

    public void add(int val) {
        count += val;
    }

    public void mult(int val) {
        count *= val;
    }
}

We will represent each stack frame as a box showing the current values of each local variable.  Calls to other methods will be represented as arrows originating at the location of the call and pointing to the stack frame of the called method.

Note that the values of method parameters, and the special this value are also considered to be local variables.  We make this generalization because both parameters and this are values that are specific to a particular method call (and its stack frame).

Here is the stack frame representation of the program above:

The colored boxes with rounded corners are stack frames.  The reason the call stack is called a stack is because when a method is called, its stack frame is pushed onto the call stack, and when a method returns, its stack frame is popped off of the call stack.  We will examine the stack data structure in more detail in a future lecture.

Try tracing through the diagram to convince yourself that the arrows representing method calls and returns correctly represent the way the program will execute.  A method call creates a new stack frame and starts executing the called method within that stack frame.  A method return leaves the current stack frame and returns to the location just after the place where the method was called originally, within the context of the previous stack frame.

Exception Classes

In Java, an exception is an ordinary object that is thrown when an error prevents the program from continuing.  Exceptions are ordinary Java objects that are subclasses of the java.lang.Throwable class.  The following inheritance diagram shows how the various exception classes are organized:

Error and subclasses of Error represent very serious error conditions, such as running out of memory.  In general, you will not need to be concerned with Errors.

RuntimeException and subclasses signify bugs in the program.  For example, trying to call a method through a reference that is null results in a NullPointerException.  Another example is trying to access an array element that is either less than 0, or greater than or equal to the length of the array, which results in a ArrayIndexOutOfBoundsException.  These errors are not as serious as those that are subclasses of Error, and in some situations it may be appropriate to handle runtime exceptions.  However, a better approach is to fix the bug that is causing the runtime exception to be thrown.

Exception, and all of its subclasses (except RuntimeException and its subclasses) represent errors that are generally recoverable.  These exceptions are known as checked exceptions because in order for these exceptions to be thrown from a method, the method must declare them.  (We will see what that means shortly.)  One of the most common checked exceptions is IOException, which can occur when opening or closing a file, or reading or writing data using a stream, reader, or writer object.

How Exceptions Work

There are three general ways that an exception can be thrown in some block of code:

  1. The block of code called a method which threw an exception

  2. One of the statements in the block of code did something bad, such as call a method on a variable storing a null value, or access an array element at an invalid index.  Situations like these result in runtime exceptions.

  3. The block of code explicitly threw an exception using a throw statement.

When an exception is thrown in some block of code in a method, there are two possibilities.  Either the exception is caught, or the exeception is thrown out of the method.

Catching an exception means that an exception handler catches the exception, and afterwards execution continues normally.  Exception handlers are defined using a try/catch construct:

try {
    statements...
} catch (ExceptionType e) {
    code to handle ExceptionType
}

If any statements in the try block throw an exception of type ExceptionType (for any of the reasons mentioned above), the code specified in the catch block is executed.  Note that a try/catch may have multiple catch blocks:

try {
    statements...
} catch (ExceptionType e) {
    code to handle ExceptionType
} catch (AnotherExceptionType e) {
    code to handle AnotherExceptionType
}

When an execption is thrown in the try block, each catch block is tried in order.  The first catch block that handles the type of exception thrown, or a superclass of the exception type, is chosen to handle the exception.   If no catch block matches the exception type, then the exception is thrown out of the entire try/catch construct, and potentially out of the method containing the try/catch.

Throwing an exception out of the method happens when a block of code in a method throws an exception, but either

  1. The statement throwing the exception is not in a try/catch construct, or

  2. There is no catch block defined that catches the type of exception thrown

Throwing an exception out of method means causes the execution of the method to terminate, and control returns the caller.  However, the caller does not simply execute normally, as though a normal return statement had been executed.  To the caller, it is as though the method call statement threw the exception, which in a way is exactly what has happened. In our list of three reasons why exceptions can occur, the caller sees this as case number 1.

Let's visualize this process using a concrete example, a slightly modified version of our original example program:

public class Example {
    private int count = 0;

    public static void main(String[] argv) {
        try {
            Example ex = new Example();
            ex.f();
            System.out.println(ex.count);
        } catch (Exception e) {
            System.out.println("Boom!");
        }
    }

    public void f() throws Exception {
        add(2);
        add(3);
    }

    public void add(int val) throws Exception {
        if (val == 2)
            throw new Exception();
        count += val;
    }

    public void mult(int val) {
        count *= val;
    }
}

Note the addition of the two throws Exception clauses on the f and add methods.  These are now required because both methods may throw an object of type Exception, which is one of the checked exception classes.  The following rule applies:

Any method which can throw a checked exception out of the method must declare the type of that exception using a throws clause.

(If you write a method that may throw a checked exception, but do not declare it using a throws clause, Eclipse will signal an error using an error marker, which looks like this: Eclipse error marker).

When an exception is thrown out of a method, it unwinds the call stack.  What this means is that it will keep travelling down the call stack (towards the main method at the bottom of the call stack) popping off stack frames until a handler is found.  Note that it is possible for an exception to be thrown out of the main method: when this happens, the entire program ends.

Here is the control flow diagram showing the execution of the revised program:

When the exception is thrown out of the add method, it then thrown back to the f method.  However, f does not handle the exception either, and it is thrown out of f as well.  The main method does handle the exception.  Note that the exception caused some of the code in each method not to be executed (shown in gray).

Reading data from a file

Once you understand exceptions, you will have no trouble doing input and output in Java.

To read data from a file, you should create a FileInputStream object, passing a String containing the file name to FileInputStream's constructor.  The constructor for FileInputStream can throw IOException, which usually indicates that the file does not exist.  Because IOException is a checked exception, you will either need to handle the exception, or add a throws clause to the method indicating that the method can throw IOException.  In general, unless you have a specific reason to handle an exception, it is better to throw the exception out of the method and let the caller deal with it.

FileInputStream, like all of the subclasses of InputStream returns data as bytes.  If the file is a text file, then you will probably want to use a BufferedReader or a Scanner to read data out of it.

A BufferedReader is useful for reading entire lines of text out of a file:

String fileName = ...

InputStream fileIn = new FileInputStream(fileName);
BufferedReader reader = new BufferedReader(new InputStreamReader(fileIn));
while (true) {
    String line = reader.readLine();
    if (line == null) {
        break; // end of file
    }

    // do something with the line
}

A java.util.Scanner is useful for reading tokens out of the file, where each token is delimited by whitespace:

String fileName = ...

InputStream fileIn = new FileInputStream(fileName);
Scanner scanner = new Scanner(fileIn);
while (scanner.hasNext()) {
    String token = scanner.next();

    // do something with the token
}

Scanner makes it easy to read primitive values out of a file.  For example, if your file consists of integer values (represented in decimal form), you can read them with the following code:

while (scanner.hasNextInt()) {
    int value = scanner.nextInt();

    // do something with the value
}

Making sure a file is closed

It is good practice to make sure that a file is closed when you are done with it.  However, because of exceptions, this can be somewhat tricky to do.  The code that uses a FileInputStream may complete successfully, or it may be terminated by an exception, but in either case, we would like to make sure that the file is closed.

Fortunately, a try/finally construct allows us to specify a "cleanup" action for a block of code.  The finally block is guaranteed to execute, no matter what happens in the try try block.  Here is how to use a try/finally to ensure that a FileInputStream is closed, even if an exception occurs while reading from it:

String fileName = ...
FileInputStream fileIn = null;

try {
    fileIn = new FileInputStream(fileName);

    read data from the FileInputStream, possibly using a BufferedReader
} finally {
    try {
        if (fileIn != null) {
            fileIn.close();
        }
    } catch (IOException e) {
        // Ignore
    }
}

Note that the try/catch in the finally block is needed because the close method may throw an IOException.  However, there is very little point in trying to handle the exception: after all, we are calling the close method because we are done with the object and don't need it any more.  Therefore, we ignore the exception if it occurs.