1 What is an Exception?
1.1 Creating and Throwing Exceptions
1.2 Catching Exceptions
2 Handling Exceptions
2.1 A Couple of Notes
3 Checked Versus Unchecked Exceptions
4 Adding a Login Screen
5 Summary

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.

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. Two modifications are required:

  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 use 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.

  public Customer 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

    }

  }

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. We have yet to fill in the details of what to do, but this code fragment shows the idea. 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.

2 Handling Exceptions

So what should happen if the customer isn’t found? In this particular case, we have a login error: the user provided an invalid name for logging in. 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:

Let’s tackle the first before moving onto the second.

Logins can fail for two reasons: either the username or the password is incorrect. Good security design demands that systems not tell the user which reason caused the failure (revealing the reason can help attackers locate valid usernames on a system). The login method, therefore, should throw a single kind of exception regardless of which reason occured. To achieve this, we create a new exception class and have login throw that error to the (eventual) login screen when it gets a CustNotFoundException:

  class LoginFailedException extends Exception {

    LoginFailedException(){}

  }

  

  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 filled in what login should do if it catches a CustNotFoundException.

To finish off the login process, we also need to throw an exception if the given password is not valid. This requires modifying tryLogin 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();

    }

  }

  

  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;

      }

    }

  }

Here, it seems onerous to have to catch the second LoginFailedException just to pass it along. It would make more sense for login to not handle the LoginFailedException at all, instead letting Java continue to pass the exception back through previous method calls until it finds a meaningful handler for the method. Java supports this. Here, if we omit the catch on LoginFailedException, Java will just pass that exception along to the method that called login.

If a method wants to simply pass along exceptions of a particular type without handling them, that type must be listed in the method’s throws clause. Here, there is nothing to add because login already throws LoginFailedException. In other systems, however, you might need to add to throws to let an exception pass you by.

The final code for login is 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();

      }

    }

  }

If a method merely passes exceptions along, it 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 file shows two examples, including the getBalance method in the BankingService class:

  // this method should be allowed to assume that the acct num is valid

  // some other method has task of handling that problem

  public double getBalance(int forAcctNum) throws AcctNotFoundException {

    Account acct = accounts.findByNum(forAcctNum) ;

    return acct.getBalance();

  }

Sometimes, passing exceptions along means that a method will not

2.1 A Couple of Notes

Note that the new tryLogin method doesn’t return anything: if it doesn’t throw an exception, then the password check succeeded. In the login method, if the login attempt yields a LoginFailedException, login just passes it along to its calling method, rather than create a new exception.

In a more thorough banking system, we probably would have a separate exception class for password mismatch errors, rather than throw the LoginFailedException immediately. This would be important if we checked password validity in different contexts (attempting to change an existing password in addition to logging in, for example). We have done this version here to illustrate that sometimes the way to handle an exception is simply to pass it along to previous methods.

3 Checked Versus Unchecked Exceptions

Before we add a login screen that can prompt a user to try logging in again, let’s reflect on exceptions in the bigger picture.

You have seen two kinds of exceptions this term. Earlier in the term, we threw a RuntimeException to flag "cases that shouldn’t happen". We did this as a cheat, so we wouldn’t have to cover exceptions too early in the course. Now that we’ve had this lecture, we notice that seem to have gotten away with something – we were able to throw runtime exceptions without adding try/catch or throws statements. What’s going on?

Java supports two kinds of exceptions: checked and unchecked. Exceptions that we declare in throws clauses are checked – the compiler will tell you if you do not handle an exception that can arise in your code. Runtime exceptions are not checked. Java will simply stop your program if a runtime exception is thrown while the program is running. If your program (or larger project that uses your program) can recover from a problem without stopping the entire program, use a checked exception. If the problem is external to your program and will prevent the program from continuing to run (the machine ran out of memory, for example), an unchecked exception is acceptable.

Most of the cases in which we used runtime exceptions until now have been bad uses of runtime exceptions. In each case, either we should have used exceptions to fix the problem, or we should have redesigned our class hierarchy so that the seemingly erroneous case could not exist. If you are tempted to create a runtime exception, ask yourself whether it is there to avoid the Java typesystem. If so, you should adjust your class hierarchy to eliminate the problem, rather than throw a runtime exception.

4 Adding a Login Screen

Finally, we return to adding a login screen to our system. The login screen should prompt a user for a username and password, prompting them again if their username or password does not match the customer data in the system.

In general, the user interface for a system should be separate from its core data structures: this allows the interface to change (from text to graphics or the web) without editing the underlying core code. Here is a text-based user-interface for the banking system. It has a single method loginScreen that requests the username and password and prompts the user to login again if the attempt failed:

  class BankingConsole {

    private BankingService forService;

  

    public BankingConsole(BankingService forService){

      this.forService = forService;

    }

  

    Scanner keyboard = new Scanner(System.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.forService.login(username,password);

      } catch (LoginFailedException e) {

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

        this.loginScreen();

      }

    }

  }

Note how the catch clause for the LoginFailedException prints a message then can call the loginScreen method again. Note that this code cannot tell whether the login failed due to the username or the password. The distinction exists solely in the core banking system methods.

The final code shows how to connect the BankingConsole with the BankingService. We provide two versions of the final code: one without exceptions in findByNum (so you can try adding them yourself) and one with all of the exceptions, so you can check your work.

5 Summary

What should you be able to do after this lecture?