Exceptions

Assertions (Appendix A) provide one way to verify assumptions we make during our programs. Unfortunately, if an assertion check fails, the program crashes. This is useful for debugging, but it is not acceptable behavior for a polished program. Exceptions enable us to recover gracefully from errors.

An exception is an unusual event that occurs during the operation of a program, such as an attempt to divide by zero, to follow a null reference, or to look at element 1 of an array. The operation that causes the exception is said to throw the exception. Once an exception is thrown, it must be caught by some method or the program will crash.

In Java, exceptions are represented by objectsinstances of classes which descend from the built-in Exception class. Each class corresponds to a different kind of exception. A few of the built-in exception classes are shown in Figure 4-18.

Figure 4-18. A few of the built-in exception classes. There are many more, and these three subclasses shown are not actually direct subclasses of Exception (see Figure 4-28).

An exception class doesn't have to be very complicated. All it needs is a constructor, and since we can have an implicit zero-argument constructor, the body of the class can be empty. An example is the IllegalMoveException class shown in Figure 4-19.

Figure 4-19. The body of the IllegalMoveException class is empty.

1 /** Thrown when a player attempts an illegal move in a game. */ 2 public class IllegalMoveException extends Exception { 3 }

We want the deal() method from IdiotsDelight to throw an IllegalMoveException if it is invoked when deck is empty. We must do three things:

  1. Add the declaration tHRows IllegalMoveException to the end of the method's signature. This warns any method calling deal() that such an exception might occur.
  2. Mention this fact in the comment for deal(), explaining under what conditions an exception is thrown.
  3. Add code to the body of deal() to check if deck is empty and throw an IllegalMove exception if it is.

The revised deal() method is shown in Figure 4-20. The special notation @throws in the comment helps javadoc make a link in the documentation to the page describing the IllegalMoveException class.

Figure 4-20. The deal() method can throw an IllegalMoveException.

1 /** 2 * Deal one Card from the Deck onto each Stack. 3 * @throws IllegalMoveException if the Deck is empty. 4 */ 5 public void deal() throws IllegalMoveException { 6 if (deck.isEmpty()) { 7 throw new IllegalMoveException(); 8 } 9 for (Stack s : stacks) { 10 s.push(deck.deal()); 11 } 12 }

When this method is invoked, if deck is empty, it immediately stops and throws an exception. The code on lines 911 is not executed. This is similar to a return statement, but the method doesn't even return normally. It passes the exception to the next frame down in the call stack, which must handle it one way or another.

If we try to compile the program now, Java will complain with messages like this:

IdiotsDelight.java:19: unreported exception IllegalMoveException; must be caught or declared to be thrown deal(); ^ IdiotsDelight.java:63: unreported exception IllegalMoveException; must be caught or declared to be thrown deal(); ^

The problem is that when the constructor and the play() method invoke deal(), they don't deal with the possible exception. There are two ways to deal with an exception. The first is to pass the buck. If we declare that play() might throw an IllegalMoveException, then if play() receives such an exception, it is passed on to the next call frame downan invocation of the method that invoked play(), namely main().

We can get the program to compile and run by adding throws IllegalMoveException to the signatures of the constructor, play(), and main().

If we try to deal from an empty deck, the deal() method throws an IllegalMoveException. The play() method receives it and passes it on to main(). Finally, main() passes the exception on to the Java system (which invoked main()), causing the program to crash. The stack trace in Figure 4-21 is printed. This is slightly better than the one in Figure 4-17, because it tells us that an illegal move was attempted.

Figure 4-21. The stack trace from the new program is slightly more enlightening.

1 Exception in thread "main" IllegalMoveException 2 at IdiotsDelight.deal(IdiotsDelight.java:30) 3 at IdiotsDelight.play(IdiotsDelight.java:63) 4 at IdiotsDelight.main(IdiotsDelight.java:124)

Rather than passing on an exception, a method can catch it. This is done with a try/catch block. As shown in Figure 4-22, this consists of the keyword try, a bunch of statements between curly braces, the keyword catch, the declaration of a variable of an exception type in parentheses, and another bunch of statements between curly braces.

Figure 4-22. Revised version of play() including a try/catch block.

1 /** Play the game. */ 2 public void play() { 3 while (true) { 4 try { 5 // Print game state 6 System.out.println(" " + this); 7 // Check for victory 8 boolean done = true; 9 for (Stack s : stacks) { 10 if (!(s.isEmpty())) { 11 done = false; 12 break; 13 } 14 } 15 if (done) { 16 System.out.println("You win!"); 17 return; 18 } 19 // Get command 20 System.out.print 21 ("Your command (pair, suit, deal, or quit)? "); 22 String command = INPUT.nextLine(); 23 // Handle command 24 if (command.equals("pair")) { 25 removePair(); 26 } else if (command.equals("suit")) { 27 removeLowCard(); 28 } else if (command.equals("deal")) { 29 deal(); 30 } else { 31 return; 32 } 33 } catch (IllegalMoveException e) { 34 System.out.println("I'm sorry, that's not a legal move."); 35 } 36 } 37 }

When this method is run, it tries to do everything in the try block (lines 432). If no exception occurs, the catch block (lines 3234) is ignored. If an exception does occur, the method immediately jumps down to the catch block, executes the code there, and then picks up after the end of the catch block. In this example, the next thing after the catch block is the right curly brace on line 35, so after handling the exception the method begins another pass through the while loop.

There is sometimes no reasonable way to handle an exception. For example, in the constructor for IdiotsDelight, if the invocation of deal() throws an IllegalMoveException, something is seriously wrong with the programthe deck shouldn't be empty at this point! We could declare that the constructor and any methods which invoke it (such as main()) are capable of throwing IllegalMoveExceptions, but this would be a lot of clutter in our code for a situation which we never expect to occur. A better alternative is to simply catch the exception, print a stack trace (so we'll know that the exception occurred if there is a bug in our program), and bring the program to a screeching halt with System.exit(1). The stack trace can be printed by invoking the printStackTrace() method on the instance e (Figure 4-23).

Figure 4-23. We never expect deal() to throw an exception when invoked from the constructor. If it does, a stack trace is printed and the program crashes.

1 /** Create and shuffle the Deck. Deal one Card to each Stack. */ 2 public IdiotsDelight() { 3 deck = new Deck(); 4 deck.shuffle(); 5 stacks = new Stack[4]; // This causes a compiler warning 6 for (int i = 0; i < 4; i++) { 7 stacks[i] = new ArrayStack(); 8 } 9 try { 10 deal(); 11 } catch (IllegalMoveException e) { 12 e.printStackTrace(); 13 System.exit(1); 14 } 15 }

The removePair() method might also throw an IllegalMoveException. A revised version of this method is shown in Figure 4-24. On line 14, we assume Card has an equals() method which returns true for two cards of the same rank, ignoring suit. This is reasonable, because while many card games compare the ranks of cards, only a few unusual games involving multiple decks check to see if two cards have the same rank and suit.

Figure 4-24. The revised removePair() method verifies that the cards chosen by the user have the same rank. If not, an exception is thrown.

1 /** 2 * Remove two Cards of the same rank, as specified by the user. 3 * @throws IllegalMoveException if the cards are not of the same 4 * rank. 5 */ 6 public void removePair() throws IllegalMoveException { 7 System.out.print("Location (1-4) of first card? "); 8 int i = INPUT.nextInt(); 9 System.out.print("Location (1-4) of second card? "); 10 int j = INPUT.nextInt(); 11 INPUT.nextLine(); // To clear out input 12 Card card1 = stacks[i - 1].peek(); 13 Card card2 = stacks[j - 1].peek(); 14 if (!(card1.equals(card2))) { 15 throw new IllegalMoveException(); 16 } 17 stacks[i - 1].pop(); 18 stacks[j - 1].pop(); 19 }

We don't have to modify play(), because a single try/catch block can deal with an exception occurring anywhere in the block.

The removeLowCard() method (Figure 4-25) can also throw an IllegalMoveException. This invokes the getSuit() and geTRank() methods from the Card class.

The behavior of the improved Idiot's Delight program is illustrated in Figure 4-26.

There are some classes of exceptions that can be thrown in so many places that catching them or passing them on would be an enormous nuisance. For example, any method which performs arithmetic might conceivably throw an ArithmeticException. Java does not require that exceptions of these classes, which descend from the RuntimeException subclass of Exception, be declared in method signatures. The built-in classes ArithmeticException, NullPointerException, and ArrayIndexOutOfBoundsException all descend from RuntimeException.

Figure 4-25. The revised removeLowCard() method can also throw an IllegalMoveException.

1 /** 2 * Remove the lower of two cards of the same suit, as specified by 3 * the user. 4 * @throws IllegalMoveException if the low card is not of the same 5 * suit as, and of lower rank than, the high card. 6 */ 7 public void removeLowCard() throws IllegalMoveException { 8 System.out.print("Location (1-4) of low card? "); 9 int i = INPUT.nextInt(); 10 System.out.print("Location (1-4) of high card? "); 11 int j = INPUT.nextInt(); 12 INPUT.nextLine(); // To clear out input 13 Card lowCard = stacks[i - 1].peek(); 14 Card highCard = stacks[j - 1].peek(); 15 if ((lowCard.getSuit() != highCard.getSuit()) 16 // (lowCard.getRank() > highCard.getRank())) { 17 throw new IllegalMoveException(); 18 } 19 stacks[i - 1].pop(); 20 }

Figure 4-26. Now, when we make an illegal move, the program neither crashes nor lets us get away with it.

1 Welcome to Idiot's Delight. 2 3 7h Tc Th 4h 4 48 cards left in the deck 5 Your command (pair, suit, deal, or quit)? suit 6 Location (1-4) of low card? 1 7 Location (1-4) of high card? 2 8 I'm sorry, that's not a legal move. 9 10 7h Tc Th 4h 11 48 cards left in the deck 12 Your command (pair, suit, deal, or quit)?

RuntimeExceptions are preventable, so they should never occur in a program which checks for valid input. For example, by checking whether deck is empty before invoking its deal() method on line 8 of Figure 4-20, we prevent deck.deal() from throwing an ArrayIndexOutOfBoundsException. If a RuntimeException does occur, it is normally passed all the way back to the system, causing a program crash.

Returning to the Stack interface (Figure 4-2), we see that the comments for peek() and pop() mention that they might throw EmptyStructureExceptions, but the method signatures do not declare such a possibility. This is because the EmptyStructureException class extends RuntimeException. An EmptyStructureException is preventable, because we can use isEmpty() to check whether a Stack is empty before peeking at it or popping it. The EmptyStructureException class is shown in Figure 4-27.

Figure 4-27. The EmptyStructureException class.

1 /** 2 * Thrown when an attempt is made to access an element in a 3 * structure which contains no elements. 4 */ 5 public class EmptyStructureException extends RuntimeException { 6 }

Figure 4-28 shows the inheritance hierarchy of all of the exception classes we have seen.

Figure 4-28. Since RuntimeExceptions are preventable, the possibility of throwing them does not have to be declared in method signatures.

 

Exercises

4.7

Discuss whether it makes sense for a method to be capable of throwing more than one kind of exception.

4.8

On line 12 of Figure 4-23, the constructor invokes the printStackTrace() method on e, which is an instance of IllegalMoveException. Speculate on how Java knows that e has such a method.

 
4.9

The Idiot's Delight program can still be crashed if the user specifies an invalid stack number such as 0 or 5. Describe how to fix the program.

4.10

The Idiot's Delight program does not prevent the user from giving the command pair and then entering the same stack number twice. What happens if the user does this? Describe how to fix the program.

4.11

Can the user cheat in Idiot's Delight by giving the command suit and then entering the same stack number twice? If so, fix the program. If not, explain why not.

Категории