493 views
# Lecture 16 -- MVC and Exceptions ## Motivating Questions How can we handle errors gracefully across systems with multiple components? ## Conceptual Study Questions - What is an exception? - How is `throw` different from `return`? - What does the `throws` annotation do? - Where is a `throws` annotation needed? ## Handling Failed Login Attempts In the original banking code, if a login attempt fails, the system prints that login has failed then thanks the user for logging in. This is obviously broken. Ideally, we should check whether the login has been successful and prompt the user to try again if it was not. Let's work through some ways to try doing that: Here is our current code: ```=java= public class BankingService { CustomerList customers; public String login(String custname, String withPwd) { Customer cust = this.customers.findCustomer(custname); if (cust.checkPwd(withPwd)) { return "Welcome"; } else { return "Try Again"; } } } public class CustomerList { LinkedList<Customer> customers; public Customer findCustomer(String custname) { for (Customer cust:customers) { if (cust.name.equals(custname)) return cust; } return null; } } public class Main { public static void main(String[] args) { BankingService B = new BankingService(new AccountsList()); B.login("kathi", "cs200"); } } ``` ## Relogin Poorly with If-statements Right now, `findCustomer` returns `null` if the customer is not found. This means that we would get a NullPointerException on line 6 (`cust.checkPwd`) if the customer was not found. Perhaps we should check whether the customer is not null before calling `checkPwd`? ```=java= public String login(String custname, String withPwd) { Customer cust = this.customers.findCustomer(custname); if (cust != null) { if (cust.checkPwd(withPwd)) { return "Welcome"; } else { return "Try Again"; } } else { return "Try Again"; } } ``` It works, but it is getting messy. The core logic of logging in should be as we originally wrote (find the customer, check the password), but now all of this error checking obscures the essential steps. And to make things worse, the `main` method would now need to check whether the login was successful. ## Exceptions: Manage Errors while Maintaining Core Logic Exceptions are a mechanism common to most modern programming languages. Put simply, they let one part of your code report a problem, leaving it for another part of the code to handle *only if a problem occurs*. With the if-statement approach, every use of login has to explicitly check whether the customer wasn't found. With exceptions, we write our code to assume that nothing has gone wrong, then deal with the situation if it does. You have used some exceptions before, such as `RunTimeExceptions`. In general, `RunTimeExceptions` are meant to say "crash the program, this is an unrecoverable error". But that isn't our situation here: if a customer isn't found during login, we want to prompt them to log in again. In other words, failure to find a customer is a *recoverable* error. We just need to write our code to do the recovery. ### Adding an interactive login method Our current code doesn't ask a user to log in at all -- we just have the `main` method trying to log someone in. Let's first fix that. We introduce a class called `BankingConsole` that provides a textual interactive interface that a user can use to log in (if you haven't seen user input in Java yet, you will in an upcoming lab): ```=java= public class BankingConsole { public void loginScreen() { Scanner keyboard = new Scanner(System.in); 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: "); String password = keyboard.next(); controller.login(username, password)) System.out.println("Thanks for logging in!"); } } ``` With this class, we'll also adapt our `main` method to use this. The changes are on lines 3 (create a console) and 7 (use the Console method to log in): ```=java= public static void main(String[] args) { BankingService B = new BankingService(new AccountsList()); BankingConsole C = new BankingConsole(); Customer kCust = B.addCustomer("kathi", "cs200"); Account kAcct = new Account(100465, kCust, 150); B.addAccount(kAcct); C.loginScreen(); ``` ## Creating and Throwing Exceptions Our goal is to replace the `return null` statement from the current `findCustomer` 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: ```=java= public Customer findCustomer(String custname) { for (Customer cust : customers) { if (cust.nameMatches(custname)) { return cust; } } // replace return null with a report of an error throw new CustomerNotFoundException(custname); } ``` `CustomerNotFoundException` is a new class that we will create to hold information relevant to the error that occured (in this case, which customer couldn't be found -- we might want to print this information out as part of error message later). We create a subclass of `Exception` for each different type of alert that we want to raise in our program. ```=java= 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. Summarizing: we modify `findCustomer` to throw a `CustNotFoundException` if it fails to find the customer. Three modifications are required: - The `throw` statement needs to be given an object of the `CustNotFoundException` class to throw. - The `findCustomer` method must declare that it can throw that exception (the compiler needs this information). This occurs in a new `throws` declaration within the method header, as shown below. - The `ICustomerSet` interface, which has the `findCustomer` method header, must also include the `throws` statement. ```=java= interface ICustomerSet { Customer findCustomer(String name) throws CustNotFoundException; } class CustomerList implements ICustomerSet { ... // return the Customer whose name matches the given name public Customer findCustomer(String custname) throws CustomerNotFoundException { for (Customer cust : customers) { if (cust.nameMatches(custname)) { return cust; } } // replace return null with a report of an error throw new CustomerNotFoundException(custname); } } ``` ## Catching Exceptions Exceptions are neat because they let us (as programmers) control what part of the code handles the errors that exceptions report. Think about what happens when you encounter a login error when using a modern web-based application: the webpage (your user interface) tells you that your username or password was incorrect and prompts you to try logging in again. That's the same behavior we want to implement here. To do this at the level of code, we will use another new construct in Java called a `try-catch block`. We \"try\" running some method that might result in an exception. If the exception is thrown, we \"catch\" it and handle it. Here's a `try-catch` pair within the `loginScreen` (which is where we already said we want to handle the error: ```=java= public void loginScreen() { Scanner keyboard = new Scanner(System.in); 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: "); String password = keyboard.next(); try { controller.login(username, password); System.out.println("Thanks for logging in!"); } catch (CustomerNotFoundException e) { // what to do when this happens System.out.println("No user " + e.custname); this.loginScreen(); } } ``` Notice the `try` encloses both the call to `login` and the `println` that login succeeded. When you set up a try, you have it enclose the entire sequence of statements that should happen if the exception does NOT get thrown. As Java runs your program, if any statement in the `try` block yields an exception, Java ignores the rest of the `try` block and hops down to the `catch` block. Java runs the code in the `catch` block, and continues from there. The `e` after `CustomerNotFoundException` in the `catch` line refers to the exception object that got thrown. As the code shows, we could look inside that object to retrieve information that is useful for handling the error (like printing an error message). **Note**: if you've only typed in the code to this point and try to compile, you will get errors regarding the `login` method -- hang on -- we're getting to those by way of the next section. ### Understanding Exceptions by Understanding Call Stacks To understand how exceptions work, you need to understand a bit more about how Java evaluates your programs. Exceptions aside, what happens \"under the hood\" when Java runs your program and someone tries to log in? Our `main` method started by calling the `loginScreen` method; this method calls other methods in turn, with methods often waiting on the results of other methods to continue their own computations. Java maintains a *stack* (we discussed those briefly in the data structures lectures) of method calls that haven't yet completed. When we kick off `loginScreen`, this stack contains just the call to that method. Separately from the stack, Java starts running the code in your method statement by statement. Switch now to the PDF linked next to these notes on the lectures page ("How Exeptions Work"), which walks through how Java executes programs with `try/catch` blocks, showing how the exceptions work. The slideshow simplifies a couple of details. There may be multiple `try` markers on the stack (because you can have multiple `try` blocks), and the stack has ways of \"remembering\" where it left off in pending method calls. We ignore those details here in the hopes of giving you the bigger picture. ### Housekeeping: annotating intermediate methods As our demonstration of the stack just showed, the `CustomerNotFoundException` \"passes through\" certain classes as it comes back from the `findCustomer` method. The Java compiler needs every method to acknowledge what exceptions might get thrown while it is running. We therefore have to add the same `throws` annotations to each method that does not want to catch the exception as it passes through on the way to the marker. For example, the `login` method needs to look as follows: ```=java= public String login(String custname, String withPwd) throws CustomerNotFoundException, LoginFailedException { Customer cust = customers.findCustomer(custname); if (cust.checkPwd(withPwd)) { System.out.println(''Login Successful''); this.loggedIn.addFirst(cust); } else { System.out.println(''Login Failed); } } ``` Once you put these additional `throws` annotations on the code, the code should compile and Java will report failed logins through the loginScreen. ## Summarizing `Try/Catch` blocks 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: ```=java= try { <the code to run, assuming no exceptions> } catch <Exception> { <how to recover from the exception> } ``` You can have multiple catch phrases, one for each kind of exception that you need to handle differently (or you can have two kinds of exceptions get handled the same way, as we will show shortly). ## Handling Incorrect Passwords Now that you've seen one example of exceptions, let's try another. As an exercise for yourself, change the `login` method in the `CustomerList` class (which checks the password) so that it throws an exception called `LoginFailedException` if the passwords don't match. Try it before reading further. You should have ended up with the following: ```=java= public String login(String custname, String withPwd) throws CustomerNotFoundException, LoginFailedException { Customer cust = customers.findCustomer(custname); if (!cust.checkPwd(withPwd)) throw new LoginFailedException(custname); this.loggedIn.addFirst(cust); } ``` In addition, we have to add this exception to the `catch` used to prompt a user to log in again. ```=java= public void loginScreen() { Scanner keyboard = new Scanner(System.in); 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: "); String password = keyboard.next(); try { controller.login(username, password); System.out.println("Thanks for logging in!"); } catch (CustomerNotFoundException|LoginFailedException e) { // what to do when this happens System.out.println("Login Failed''); this.loginScreen(); } } ``` As with the `CustNotFoundException`, you have to put `throws` annotations on all methods that can either throw or pass along the `FailedLoginException`. You'll see this in the final posted code. ## 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 that arise within your code. With checked exceptions, Java analyzes your code at compile time to make sure that the exceptions will actually be caught (and handled). Sometimes, however, your code fails because of a bug in your code. Null pointer exceptions, array index out of bounds exceptions, and division by zero exceptions are examples of exceptions that alert you to bugs when they are thrown. You don't want to catch these -- you want to fix your code so that they can't happen again. Put differently, the fix for such situations is to debug your code before you run it. Checked exceptions, in contrast, are for situations you can't control (because they arise from user behavior, for example) that arise while your application is running. These "code-error" exceptions are special cases of `RuntimeExceptions`, which we saw earlier in the course. When you throw a runtime exception, you shouldn't catch and manage it (and the compiler won't expect you to). Runtime exceptions are handy when you are working on your code and just want to throw something so you can get past the compiler and test the code you're currently working on. In production code, you only use them for situations in which the program needs to terminate. As a general rule, you should write (and catch) checked exceptions from here on out in homeworks and projects.