CPSC 225 | Intermediate Programming | Spring 2025 |
This lab deals with arrays storing collections. It provides opportunities to practice working out code for manipulating arrays and to practice debugging as well as providing an illustration of how code correctness techniques can be utilized in practice.
Labs are due at the start of lab on the date listed. It is OK to hand in your files at the very beginning of lab, but you should not be spending part or all of a lab period finishing up the previous week's lab.
To be eligible for revise-and-resubmit, you must turn in something by the due date. Late work and extensions are generally not accepted/granted; see the posted late policy for more information.
You may work with a partner if you wish. Both partners must actively contribute to the solution! You only need one handin for the group — be sure to put both teammates' names in every file.
Otherwise you may get help but you may not use other resources (friends, neighbors, websites, ChatGPT, etc) to produce answers. See the posted policy on academic integrity for more information.
To hand in your work:
Hand in a hardcopy of your before-and-after drawings and the drawings used for debugging. Make sure your name is on the paper!
For your code:
Make sure that your name is at the beginning of each file (use the @author tag in Java files) and that all your Java files have been auto-formatted and saved.
Copy the entire lab4 project directory from your workspace ~/cs225/workspace to your handin directory in /classes/cs225/handin.
Check that everything got handed in correctly — navigate to your handin directory and make sure it contains a directory lab4 with the debug-log file and a subdirectory src containing your Java files.
Create a new Java project called lab4 in Eclipse.
Import the Java files from /classes/cs225/lab4 into your project. Make sure they end up in the src directory!
Two classes have been provided: StringSet, an incomplete and buggy implementation of a set of strings, and StringSetTester, containing a collection of test cases for some of StringSet's methods.
A set is a collection of elements where the key operation is membership — whether or not an element belongs to the set. Duplicates are not allowed in a set, so inserting an element that is already in the set has no effect. Two other operations on sets are common: the union of two sets is a set containing every element present in one or both sets (without duplicates), and the intersection of two sets is a set containing every element present in both sets (without duplicates). For example, for sets { 1, 2, 3, 4 } and { 2, 4, 5 }, the union is { 1, 2, 3, 4, 5 } and the intersection is { 2, 4 }.
Internally StringSet uses an array to hold the elements in the set, and the elements are stored in lexicographic (alphabetical) order within the array to make the contains operation more efficient. This sorted order must be maintained when elements are inserted and removed.
Your task in this section is to implement the add operation for StringSet, following the methodology demonstrated in class:
Locate the header and comments for add in StringSet and note the behavior described there. In particular, note that nothing changes if the element is already present — it's not an error, and it shouldn't be added again.
Test cases for add have already been identified in StringSetTester. Draw before and after pictures for the "add" test case and identify what needs to change to get from the before picture to the after picture. (You'll be handing in these pictures, so use a piece of paper that you can hand in rather than a whiteboard.)
Implement add to handle this case. Use the compareTo method to compare strings — see section 2.3.3 in the textbook or look it up in the Java API.
What additional cases might you need to consider? Draw before and after pictures for the other add test cases in StringSetTester and trace through your code for each. Add to your implementation to handle the new cases if needed.
Add checks for preconditions and postconditions as appropriate. Postconditions for methods should include preservation of the class invariants, which you can find documented in comments where the instance variables are declared. Note that there's a private helper method isOrderedAndUnique to help with implementing these checks.
Run StringSetTester (make sure assertions are turned on) and make sure that all the tests for add pass. (Others may fail, that's OK.) Fix any bugs.
Create a new text file called debug-log in your lab4 project (not in src). Add your name at the beginning of the file and save it.
The provided StringSet contains bugs, and your task in this part is to find and fix the bugs in the intersection method:
Run StringSetTester, which is set up with one test case for intersection. You should find that it fails with an ArrayIndexOutOfBoundsException. What kind of problem does this indicate? (Write the answer in debug-log.)
The first step in debugging is to identify the immediate problem:
With an exception, you get a stack trace — click on the topmost line in the stack trace that refers to your code to go to where the error occurred. (This should be line 187 in StringSet.)
There are two array accesses on that line, elements_[i1] and other.elements_[i2]. Which is the problem? Immediately before line 187, add a System.out.println to print out the values of i1 and i2. Also print out elements_.length and other.elements_.length, since just knowing the value of the array index doesn't help you know if it is valid or not. Is it i1 or i2 that's the problem? (Write the answer in debug-log.)
The next step is to identify the root cause — why is the problem occurring? The goal is to figure out where what is actually happening in the code diverges from what is supposed to be handling. There are several strategies here: reading through the code and carefully reasoning about it, tracing through the code by hand with an example, and printing out values to assist with tracing through an example. Reading and reasoning is a good start, but that's not always enough to locate the problem. So, start with tracing through an example by hand:
Using the test case defined in StringSetTester as your example, draw the before picture showing the state of things just after intersection is called. (You'll be handing in these pictures, so use a piece of paper that you can hand in rather than a whiteboard.)
Starting from the beginning, trace through the code step by step, updating your picture as you go. Continue until you get to where the exception occurs. What's the problem to fix? (Write the answer in debug-log.)
Before you fix the bug, also print out information to help with the tracing — the goal is to understand the control flow, so print out the values of loop variables just inside the body of the loop and the values of if statement conditions just before the if statement. Printing values can be a helpful addition to or substitute for tracing by hand.
This should reveal what is actually happening and why the exception occurs. But what is supposed to happen? intersection is based on a merge operation: imagine two fingers, each starting pointing at the first element of their respective arrays. Then repeatedly compare the pointed-at elements, possibly take one of the elements as the next thing in the intersection, and move one or both of the fingers one spot to the right. Making sure the right things get added to combo and the fingers are moving along properly is the task of debugging.
Fix the problem, run StringSetTester again, and repeat until all of the intersection test cases pass. For each step, add an entry to debug-log describing what failed (an exception? a test case?), what you did to track down the problem (reasoned about the code? traced by hand? printed values?), and what you determined the problem to be.
If you have time, work on the following (in any order):
Implement union. The algorithm for union is similar to intersection — move fingers through both arrays, but now when do you take an element?
Fix the rest of the bugs turned up by the tester (remove and contains). Since both methods use the find helper method, start with the simpler one (contains) and get it to pass all of its tests before moving on to remove.
Add test cases to thoroughly test union and intersection. Only one test case was provided for each, but additional white box tests are important to fully test these operations.