1 What is an Exception?
1.1 Creating and Throwing Exceptions
1.2 Catching Exceptions
2 Handling Exceptions
2.1 Converting One Exception To Another
2.2 Passing Exceptions Along
3 Checked Versus Unchecked Exceptions

Exceptions

Kathi Fisler

Last class, we encapsulated the data structures for accounts and customers in our banking system. We were left with one last problem to resolve: what should the findByNum and findByName methods do if no customer or account matched the given input data?

Right now, both methods return null, as a way of saying "no answer". While this approach gets past the compiler, it is not a good solution because it clutters up the code of other methods that call findByNum and findByName. Think about getBalance. Ideally, we would like to write this method as follows:

  class BankingService {

    ...

    public double getBalance(int forAcctNum) {

      Account acct = accounts.findByNum(forAcctNum) ;

      return acct.getBalance();

    }

  }

However, if findByNum returns null (to indicate that no such account number exists), Java will raise an error at runtime when it tries to compute acct.getBalance(). To guard against that, we might modify the code to read:

  class BankingService {

    ...

    public double getBalance(int forAcctNum) {

      Account acct = accounts.findByNum(forAcctNum) ;

      if (acct != null)

        return acct.getBalance();

      else

        return ???; // some dummy value must go here

    }

  }

This code has two key flaws:

  1. The logic of the program has been obscured by the if-statement. The if-statement is really a check on the behavior of a different method (findByNum); it should not clutter up the code of this one.

  2. Returning dummy values is never a good idea, because the computation that receives the dummy value must be able to distinguish valid from invalid data. That requires something akin to the if-statement, which we argued against in the previous point.

Ideally, we need a way to clearly write methods that use findByNum and findByName, while letting these two methods alert methods that call them when something goes wrong. The appropriate programming construct for this is called an exception. As the name implies, exceptions are designed to help programs flag and handle situations that would otherwise complicate the normal logic of the program you are trying to write.

1 What is an Exception?

Exceptions (or some similar notion) exist in most mainstream programming languages. Intuitively, if a function encounters a situation that is not expected, it does not try to return a normal value. Instead, it announces that a problem occured (by throwing or raising an exception). Other methods watch for announcements and try to recover gracefully.

Isn’t returning null an announcement that something went wrong? Yes, but we’ve just seen that if we return null, the code that called findByNum has to check whether an announcement got made and handle it before calling getBalance. Exceptions use a separate communication channel (as it were) for announcements, This lets methods separate out the "normal" code from the "announcement-handling" code; it keeps the code cleaner and will help direct announcements to the part of the code that can best respond to them.

We’ll make this concept more concrete by working with findByName.

1.1 Creating and Throwing Exceptions

Our goal is to replace the return null statement from the current findByName code with an exception to alert to the rest of the code that something unexpected happened (in this case, the customer was not found). The Java construct that raises alerts is called throw. Our first step, then, is to replace the return null statement with a throw statement:

  class CustSet implements ICustSet {

    ...

    // return the Customer whose name matches the given name

    public Customer findByName(String name) {

      for (Customer cust:customers) {

        if (cust.getName() == name)

          return cust;

      }

      throw <some object indicating that the name was not found>

    }

  }

Next, we need to provide the specific exception to throw. In Java, an exception is an object in the Exception class. We create a subclass of Exception for each different type of alert that we want to raise in our program. In this case, we will create a new exception class for user-not-found errors:

  class CustNotFoundException extends Exception {

    String unfoundName;

  

    CustNotFoundException(String name) {

      this.unfoundName = name;

    }

  }

An exception subclass should store any information that might be needed later to respond to the exception. In this case, we store the name of the customer that could not be found. This info would be useful, for example, in printing an error message that indicated which specific customer name could not be found.

Finally, we modify findByName to throw a CustNotFoundException if it fails to find the customer. Three modifications are required:

  interface ICustSet {

    Customer findByName(String name) throws CustNotFoundException;

  }

  

  class CustSet implements ICustSet {

    ...

    // return the Customer whose name matches the given name

    public Customer findByName(String name)

      throws CustNotFoundException {

      for (Customer cust:customers) {

        if (cust.getName() == name)

          return cust;

      }

      throw new CustNotFoundException(name);

    }

  }

1.2 Catching Exceptions

Now that findByName throws an exception, we have to modify methods that call findByName to watch for and process that exception. In Java, we do this with a try/catch statement: the try portion contains the code that should execute no exceptions are thrown, while the catch portion shows what to do if an exception is thrown. Here’s what it looks like in the login method:

  public String login(String custname, int withPwd) {

    try {

      Customer cust = customers.findByName(custname);

      cust.tryLogin(withPwd);

      return cust;

    } catch (CustNotFoundException e) {

      // What to do when this alert has been raised.

      // For now, we just return a login-failed string, but

      //   we will do something smarter momentarily

      return "Try again";

    }

  }

Here, the try block is written assuming that findByName will locate the customer. This avoids cluttering the core logic of the method with the error handling. The catch block says what to do if the customer is indeed not found. In practice we usually want to do something more sophisticated than return a string, but we’ll get to that in just a moment.

At this point, you should understand that throw statements go hand-in-hand with try/catch blocks. Whenever a method declares that it can throw an exception, any method that calls it needs a try/catch statement to process the exception.

More generally, a try-catch block looks as follows:

  try {

    <the code to run, assuming no exceptions>

  } catch <Exception> {

    <how to recover from the exception>

  }

You can have multiple catch phrases, as we will see in later examples.

2 Handling Exceptions

Right now, we’re returning a string that reports a login error if the customer isn’t found. But think about systems you have used: if you provide an invalid username or password, you get an "invalid login" message of some kind, then a prompt to try again. We’d like to implement the same idea here. To get there, we need two modifications to our current code:

For starters, let’s add a loginScreen method that asks the user to enter a name and password and tries to log in. This builds off lab 4, which showed you how to do keyboard input/output in Java:

   // set up a Scanner to read input from the keyboard

   private Scanner keyboard = new Scanner(System.in);

  

   // the method that prompts for input then tries to log in

   public void loginScreen() {

      System.out.println("Welcome to the Bank.  Please log in.");

      System.out.print("Enter your username: ");

      String username = keyboard.next();

      System.out.print("Enter your password: ");

      int password = keyboard.nextInt();

      this.login(username,password);

  }

[Side note: we will NOT expect you to know/remember how to do keyboard input/output for either the programming exam or the final.]

In lab tomorrow (standard), you will learn how to make this method start automatically when you run the program. For today, we will just start it manually within Java:

  > new Examples.setUpGompei().loginScreen();

This compiles and runs fine, but the banking system doesn’t prompt us for the username and password again if login failed. So now let’s turn to that problem.

2.1 Converting One Exception To Another

Notice that the login method currently doesn’t throw any exceptions. It catches the CustNotFoundException, but doesn’t throw any alerts to other parts of the program (you can see this in two places: there are no throw statements in the code, and the method header doesn’t have a throws statement).

Thinking ahead a bit, we will want to report that logins have failed in two situations: either the customer isn’t found or the password didn’t match. Let’s introduce a second exception called LoginFailedException that we will send back to the login screen if the login fails. If that exception comes up, we will ask the user to try logging in again.

Concretely, we want the loginScreen method to look as follows:

  // set up a Scanner to read input from the keyboard

  private Scanner keyboard = new Scanner(System.in);

  

  // the method that prompts for input then tries to log in

  public void loginScreen() {

     System.out.println("Welcome to the Bank.  Please log in.");

     System.out.print("Enter your username: ");

     String username = keyboard.next();

     System.out.print("Enter your password: ");

     int password = keyboard.nextInt();

     try {

       this.login(username,password);

     } catch (LoginFailedException e) {

       System.out.println("Login failed -- please try again\n");

       this.loginScreen();

     }

   }

Now we need to make the rest of the code throw a LoginFailedException when the login fails. First, define the exception class:

  class LoginFailedException extends Exception {

    LoginFailedException(){}

  }

Now, let’s have the login method (not the screen version, the one that tries to find the customer) throw this exception when the customer isn’t found:

  class BankingService {

    ...

    public Customer login(String custname, int withPwd)

      throws LoginFailedException {

      try {

        Customer cust = customers.findByName(custname);

        cust.tryLogin(withPwd);

        return cust;

      } catch (CustNotFoundException e) {

        throw new LoginFailedException();

      }

    }

  }

Note that we have added a throws clause to the login header, and changed what login should do if it catches a CustNotFoundException.

To finish this off, we also need to throw an exception if the given password is not valid. This requires modifying tryLogin (which checks the password) to throw an exception if the passwords do not match, and handling that exception in the login method.

  class Customer {

    ...

    // check whether the given password matches the one for this user

    public void tryLogin(int withPwd) throws LoginFailedException {

      if (this.password != withPwd)

        throw new LoginFailedException();

    }

  }

Note that this method doesn’t do anything if the passwords match. That’s okay – the method has output type void, so Java doesn’t expect it to return anything. If the passwords match, the method will finish quietly and Java will proceed to return the customer found in the login method. But if the passwords don’t match, the exception will be thrown, rather than having the method finish quietly.

2.2 Passing Exceptions Along

Let’s look at the login method again, now that tryLogin is throwing an exception:

  class BankingService {

    ...

    public Customer login(String custname, int withPwd)

      throws LoginFailedException {

      try {

        Customer cust = customers.findByName(custname);

        cust.tryLogin(withPwd);

        return cust;

      } catch (CustNotFoundException e) {

        throw new LoginFailedException();

      }

    }

  }

This method does not catch the LoginFailedException; it is only catching the CustNotFoundException. This suggests that we should add a second catch clause, as follows:

  class BankingService {

    ...

    public Customer login(String custname, int withPwd)

      throws LoginFailedException {

      try {

        Customer cust = customers.findByName(custname);

        cust.tryLogin(withPwd);

        return cust;

      } catch (CustNotFoundException e) {

        throw new LoginFailedException();

      } catch (LoginFailedException e) {

        throw e;

      }

    }

  }

This is perfectly valid code. You can have multiple catch statements for the same try, each of which handles its exception in a different way.

But in this case, the additional catch clause is superfluous.

One of the best features about exceptions is that they keep "passing back" through the method calls that were waiting on the computation that threw the exception until some catch statement handles it. Let’s map this out more explicitly to understand what’s going on.

The call to login from within the loginScreen triggered a series of method calls:

  started with: loginScreen()

  which becomes (if "gompei" and 2102 entered):

       login("gompei", 2102)

  which becomes:

       Customer cust = customers.findByName("gompei");

       cust.tryLogin(2102);

  which becomes (after findByName finishes):

       if (this.password != withPwd)

          throw new LoginFailedException();

This shows us that tryLogin and login are waiting on the result from comparing the passwords. If the passwords match, then tryLogin will finish/return, which then allows login to finish/return.

Since both methods are waiting on the result of the password comparison, both methods could "hear" the alert raised by throwing the LoginFailedException. The exception is passed to each method in turn, starting from the one closest to where the exception was thrown. First, Java will check whether tryLogin has a catch for that kind of exception. If it does, that catch clause will capture it. But if it doesn’t, Java will check whether login, the next method waiting on the result, can catch the exception. If it doesn’t, Java will check whether loginScreen can catch it.

In this case, loginScreen has the catch on the LoginFailedException, so the exception gets handled there. The calls to tryLogin and login are aborted behind the scenes; any code that did not run in them before the exception was thrown will not execute.

The upshot of this is that login can simply let the LoginFailedException pass through without catching it. Java would still require login to say that it can throw that exception, but in this case it already does (as part of handling the CustNotFoundException).

class BankingService { ... public Customer login(String custname, int withPwd) throws LoginFailedException { try { Customer cust = customers.findByName(custname); cust.tryLogin(withPwd); return cust; } catch (CustNotFoundException e) { throw new LoginFailedException(); } } } }

The ability for a method to pass an exception along means that a method may have a throws clause, but no try/catch block (since a method should only catch exceptions to which it will respond).

The final code is available in this file.

3 Checked Versus Unchecked Exceptions

What we have done so far with try/catch and throw statements are called Checked Exceptions: exceptions that you are using within your application to respond to special situations within your code and what it needs to do. These are "checked" because Java analyzes your code at compile time to make sure that the exceptions will actually be caught.

Sometimes, however, your code has to fail because of a problem outside of your application (perhaps it relies on the network and the network is down, or the system you are running on ran out of memory, for example). In this case, Java also has something called a RuntimeException. These exceptions aren’t checked at compile time, but you can’t recover from them either. They will simply terminate your program. We mention them here in case you ever need these (or if you need a cheap way to silence the compiler until you get to addressing a flaw in your code), but for this course we will only expect you to work with checked exceptions.