CS 261 Lab #4

February 7th

Goals:

This week you'll get some hands-on practice with catching and throwing exceptions, then move on to estimating complexity of methods.

Exercise 1: The Basics of Catching

  1. Take a moment to introduce yourself to your partner(s). After social pleasantries are complete, pick one member of the team to be the "typer". They'll share their screen while editing the lab code in BlueJ. (I think BlueJ works better for these interactions than Eclipse. It's easier to see when sharing screens, and makes it easier to quickly test individual methods than Eclipse does.) Group members should contribute equally while working through the problems below and discuss all code to be written, though only the "typer" will be able to edit code. Resist the temptation to have both members work simultaneously in BlueJ — you're much more likely to "drift apart" over the course of the lab if you do so. The goal here is to have a partner who's engaged on exactly the same step of the lab as you are.

    Consider letting the less experienced member of the group do the typing if it seems like there's a mismatch in experience or comfort levels. That will help make sure they don't get left behind. Worst case, flip a coin to see who does the typing. Or have Java generate a random boolean value. (Type (new java.util.Random()).nextBoolean() in the codepad.)

    At the conclusion of the lab, make arrangements for the typer to share a copy of the code with the other member(s) of the group. (E.g. email it, or put it on a shared Google drive, etc.) My solutions to the lab will get posted as well.

  2. Download the ExceptionsLab project and extract its contents (it's a .zip archive), then start BlueJ and open the project. You'll find our old friends, the Die classes, a mildly updated DieRoller class, and a Tester class that creates a HistoryDie instance and passes it to some DieRoller methods. Run its main method to see what it does.
  3. Next, let's intentionally introduce a bug to the HistoryDie constructor: Comment out the line that allocates the array holding the counts, so that the array reference will be null. After compiling, create an instance in the codepad (or by right-clicking on HistoryDie and creating a new one) and call its roll() method. Note that the constructor runs without an error, but we get into trouble once we roll the die instance:

    > HistoryDie d = new HistoryDie(6);
    > d.roll()
    Exception: java.lang.NullPointerException (null)
    

    BlueJ reports the error in the codepad, but it also gives a more complete summary of the error in the terminal window that includes details about where in the program the error occurred:

    java.lang.NullPointerException
        at HistoryDie.roll(HistoryDie.java:32)
    
  4. Next, run the Tester class's main method and look closely at the error report in the terminal window. What do the additional lines mean? What are they telling you?
  5. Let's catch that pesky exception! Edit Tester's main method: put the call to rollRepeatedly inside a try/catch block as shown below. (You'll have to make some other small changes too, before it will compile.) The "risky" part is moved into the try portion, and the "what to do if it blows up" code is in the catch part.

    try {
        int result = DieRoller.rollRepeatedly(d, NUM_ROLLS);
    }
    catch (Exception e) {
        System.out.println("I just caught "+e);
    }
    

    After getting it to compile, run it and verify that the exception was caught. Which pieces of code (if any) get executed after the code in the catch block?

  6. We'll call that a mixed success. We caught the error that occurred in rollRepeatedly, but our broken die blew up again in the rollUntil call below the try/catch. Let's move the three lines of code below the catch block up into the try and see what happens. In their place, leave a print statement that announces that the program is finished. (Make sure it's outside of the catch block, so that we can tell when something below the try/catch is executed.)

Exercise 2: Refining our Catches

  1. Our catch block was defined to handle Exception or any of its subclasses. However, we see that the actual exception being thrown is a NullPointerException, so let's write a catch block just to handle errors of this type. Copy the block below and add it to your code. Try it immediately above and immediately below the existing catch block, but leave the original catch block in your program as well. Does it make a difference where this new block goes? (Make sure both are there and compiling properly before moving to the next step.)

    catch (NullPointerException e) {
        System.out.println("Ooo!  A null pointer error: "+e);
    }
    
  2. Now let's see what happens with a different kind of exception. Go back to the HistoryDie code and edit the constructor so that it creates an array, but one that's slightly too small:

    history = new int[numSides-1];
    

    Play around with an instance of the modified class — how many rolls does it take before it blows up? Why?

  3. What do you think will happen when you run Tester's main method now that you've modified HistoryDie? Try it and see if you're right.
  4. Since the error in HistoryDie doesn't happen on every roll, we could try to continue in rollRepeatedly when the exception arises rather than stopping the program. Leave the existing try/catch code in your program, but add a new try/catch block in rollRepeatedly so that it's inside the loop and only watches the assignment statement in the loop body for exceptions. Have it catch errors of type ArrayIndexOutOfBoundsException, and set up a counter variable so that it keeps a count of how many times the error has been caught. Print this total right before the return statement.
  5. How many times does the exception get caught and handled? Is it what you'd expect? Does your program run to completion after the call to rollRepeatedly? What happens next?
  6. What happens if you add a catch block for ArrayIndexOutOfBoundsException to Tester's main method as well? Leave the try/catch in rollRepeatedly, but add a new (third) catch block in main. Where does the indexing exception get caught?
  7. Change the HistoryDie constructor back so that it doesn't create an array at all. What do you think will happen? Check and see. Where did it get caught and why?

Exercise 3: Learning to Throw

So far the lab has focused on catching exceptions — handling error conditions within our code so that the program doesn't die. There can be good reasons for generating these errors from within our code as well. For example, in class, we discussed using exceptions when things go wrong in a constructor. You can't just print a message and "refuse" to construct an instance. Once the constructor has started running it's too late — the object already exists. Sometimes the best course of action is to stop the execution by throwing an exception.
  1. In BasicDie (yes, BasicDie and not HistoryDie) add the following code to the one-argument constructor. The throw command in Java is much like a return statement: it leaves the current method and takes a "value" with it — the Exception object that's created by the call to new.

    if (numSides < 1) {
        throw new IllegalArgumentException("Negative # of sides ("+numSides+")");
    }
    

  2. Test it by creating instances of all three kinds of die instances in BlueJ (via the codepad or by pointing-and-clicking) and verifying that exceptions are thrown. Then change Tester's main method so that it tries to create a HistoryDie with negative sides and run the program. What should happen when the main method runs?

Exercise 4: Algorithm Efficiency

We're not done with our in-class conversation about estimating complexity (efficiency), but it will be good practice to apply the techniques we've been talking about to some methods in this project.
  1. Estimate how many computational steps are involved in calling rollRepeatedly on a BasicDie. That is, find the T(n) for rollRepeatedly, and be clear about what the problem size, n, is in this case. (Be sure to discuss how to handle the call to roll().)
  2. What's the simplified O(n) term for rollRepeatedly?
  3. Does it make a difference if we pass in a HistoryDie or CrookedDie instance rather than BasicDie? Why or why not?
  4. Now consider rollUntil: What is the problem size, n, for this method? Find T(n) for rollUntil, then simplify it to get the appropriate O(n).

Extras

If you finish early, consider trying the following:
  1. In the BasicDie constructor, throw a java.io.FileNotFoundException rather than an IllegalArgumentException. You'll get an error when you try to compile it. Why?
  2. How would you change the code in rollRepeatedly so that it stopped and returned the total rolled so far if an exception arose?
  3. Finish analyzing the last two methods in the efficiency examples from class. What's T(n) for countLetters and for sort? What are their corresponding O(n) terms?
  4. Estimate the number of computational steps for some of the other methods in the project. Can you find some that are constant time (that is, they don't depend on any n)? What about methods that are proportional to the size of an input?


Brad Richards, 2023