Skip to main content

Section 4.5 MyIterator Class (Design Recipe)

In previous sections, we used raw loops (for or while) to traverse arrays. But what if we want an object that encapsulates iteration state and behavior, so other code can ask “Is there a next element?” or “Reset to the start?” without fiddling with array indices? In this section, we’ll apply the Design Recipe step by step to create such a class: MyIterator.
We’ll build this step by step using the Java features we’ve learned so far. By the end, you’ll see how systematically walking through the design process (clarifying the problem, enumerating test cases, writing a skeleton, etc.) yields a robust, well-tested solution. This systematic approach will serve you well as you tackle more complex programming challenges in the future.

Subsection 4.5.1 An Authentic Example: Building MyIterator With the Design Recipe

Subsubsection 4.5.1.1 Step 0: Understand & Restate the Problem

Problem Prompt (Paraphrased): “We want a Java class that can traverse an array of integers, returning one element at a time. Instead of writing a for loop everywhere, the rest of our code should just ask: ’Is there a next element?’ and ’Give it to me if so.’ The class should track where we are in the array and avoid out-of-bounds errors.
We also want a ’reset’ capability, so we can start over at the first element. If the array is empty or we run out of elements, we want predictable behavior—no accidental exceptions. Later, we might integrate this concept with Java’s for–each loops, but for now we’ll just rely on standard features like arrays and simple classes.”
Key Details & Clarifications:
  • Data: An int array, which we’ll store internally as data.
  • State: An integer currentIndex that marks which element to return next.
  • Behavior
    • hasNext() — checks if more elements remain.
    • next() — retrieves the current element and advances the index.
    • reset() — starts iteration over from the first element.
  • Edge Cases
    • If data is empty, hasNext() returns false from the start.
    • If next() is called when no elements remain, we either throw an exception or handle it gracefully—our design choice. We’ll opt to throw a RuntimeException.
We’ll assume arr passed in isn’t null to keep things simple.

Subsubsection 4.5.1.2 Step 1: Data Definition

We identify which fields or data elements this class needs:
  • private int[] data; The integer array we’re iterating over. We do not modify it.
  • private int currentIndex; The “bookmark.” Ranges from 0 to data.length inclusive. Once currentIndex == data.length, no more elements remain.
By bundling the array and the index in one place, we minimize off-by-one mistakes scattered across the code. Instead, iteration logic lives inside MyIterator, so callers just call hasNext() / next().

Subsubsection 4.5.1.3 Step 2: Method Signatures & Purpose Statements

We’ll provide four key methods:
Table 4.5.1. Iterator Methods
Method Details
Constructor
public MyIterator(int[] arr)
Save arr in data and set currentIndex to 0.
hasNext()
public boolean hasNext()
Check if currentIndex < data.length. Return true if yes, false otherwise.
next()
public int next()
Return the current element data[currentIndex] and then increment currentIndex. If we’re at the end, we’ll throw an exception.
reset()
public void reset()
Move currentIndex back to 0 so iteration restarts at the first element.
Any additional methods (e.g., “peek” at current index) can be added later. These four satisfy the basics of iteration.

Subsubsection 4.5.1.4 Step 3: Examples & Test Cases

Before coding, we plan out examples—especially edge cases. We’ll likely turn these examples into actual main checks or JUnit tests.
Test Case Table:
Table 4.5.2. Iterator Test Cases
Property Description
Test Case 1: Non-empty Array (Normal Usage)
Data {10, 20, 30}
Operation Check hasNext() at start
Expected Outcome Returns true since currentIndex = 0 < 3
Notes We confirm that if data has 3 items, iteration isn’t done yet.
Test Case 1 (continued): Iteration Through Array
Data {10, 20, 30}
Operation Call next() 3 times
Expected Outcome Returns 10, then 20, then 30 in sequence, and updates index each time
Notes After 3 calls, currentIndex should be 3. No items remain.
Test Case 1 (continued): End of Iteration
Data {10, 20, 30}
Operation hasNext() after 3 calls to next
Expected Outcome false, because currentIndex == 3
Notes No more elements left to return.
Test Case 2: Reset Scenario
Data {10, 20, 30}
Operation Iterate to end, then call reset()
Expected Outcome Calling hasNext() after reset ⟹ true again, index=0
Notes We can start from the beginning again as if new iteration.
Test Case 3: Empty Array
Data {}
Operation hasNext() from the start
Expected Outcome false immediately
Notes No elements exist at all. Next calls to next() can throw or handle gracefully.
Test Case 4: Next() Beyond End
Data {10, 20, 30}
Operation 4th call to next()
Expected Outcome Throw RuntimeException
Notes We’ve chosen to fail fast with an exception if we call next() illegally.
These scenarios ensure we cover typical, boundary, and error conditions. For instance, an empty array is a corner case some might forget if they only tested an array of size 3.

Subsubsection 4.5.1.5 Step 4: Skeleton / Method Template

We now outline the class structure, leaving placeholders for final logic. This helps us verify we haven’t forgotten any method or data field.
public class MyIterator {
    
    private int[] data;        // The array we're iterating over
    private int currentIndex;  // Current position in the array

    public MyIterator(int[] arr) {
        // Initialize data with arr
        // Set currentIndex to 0
    }

    public boolean hasNext() {
        // Check if currentIndex is less than data.length
        // Return true if there are more elements, false otherwise
        return false;  // Placeholder return
    }

    public int next() {
        // If currentIndex is out of bounds:
        //     Throw a RuntimeException
        // Otherwise:
        //     Get the element at currentIndex
        //     Increment currentIndex
        //     Return the element
        return 0;  // Placeholder return
    }

    public void reset() {
        // Reset currentIndex to 0
    }
}
At a glance, we see each method that the test table references. Next, we fill in the logic and then validate with examples from Step 3.

Subsubsection 4.5.1.6 Step 5: Implementation & Testing

We now fill in real Java code. Because we haven’t learned about exceptions yet, we’ll keep things simpler by returning a sentinel value (like -999) if next() is called with no elements remaining, rather than throwing an error. In more advanced Java, we might throw an exception instead, but for now let’s avoid that. The rest of the design steps remain the same.
Try pressing “Run”. You’ll see the output from main in the console. Then click “Show Tests” to see if everything passes. If anything fails, revisit the logic or the design steps.
Notice how we print a small message in next() whenever we return -999, so we can observe in the console that we’ve gone beyond the last element. This is only an interim solution until we learn about exceptions later.

Subsubsection 4.5.1.7 Step 6: Reflection & Possible Revisions

With all tests passing, we might be “done.” But design is an iterative process. Let’s imagine new or changing requirements—these might send us back to earlier steps:
  • What if we truly want an error if next() goes too far? We’ve returned -999 as a sentinel, but in real Java code, a thrown error can be more explicit. Once we learn about exceptions, we might adjust Step 0’s problem statement (“We throw an error if out-of-bounds”), update Step 3’s test (“Expect an error, not a sentinel”), and re-implement Step 5’s next().
  • What if we want a generic type (not just int)? We’d need to revise Step 0 (“We want any type, not just ints!”), Step 1’s data definition, and Step 2’s method signatures to use generics. We’re not ready for that yet, but that’s how you’d do it.
  • What if arr can be null? Then Step 0 and Step 3 must define how to handle it—maybe MyIterator prints an error or reassigns arr to an empty array. That’s an example of how real designs expand over time when new edge cases appear.
In short, each design step is open to “walkbacks” if new info arises. Because we explicitly wrote out our data definitions, method specs, and test scenarios, it’s easy to see what to revise. This clarity is why the Design Recipe is so powerful.
With MyIterator in hand, we’ve proven that iteration logic can live in an object that manages state (currentIndex) and behavior (hasNext, next). We no longer have to manually maintain a loop variable everywhere. In the upcoming chapters, we’ll see how Java’s Iterator and Iterable interfaces give you the for–each loop for free once you adopt their patterns. For now, enjoy our simpler, sentinel-based version—and keep the door open for refactoring once you learn about exceptions and generics!

Subsection 4.5.2 Summary

By applying the Design Recipe, we built a MyIterator class that can “walk through” an integer array. We separated:
  • Data: The array itself (data) plus an index (currentIndex).
  • Behavior: hasNext(), next(), and reset().
  • Edge-Case Handling: We tested empty arrays, normal usage, and a scenario where iteration goes beyond the last element.
This systematic approach ensures robust code that others (or you, in a few weeks!) can use without re-implementing the same iteration logic. The next step is to see how we can adapt this into Java’s standard Iterable framework and for–each loops once we’ve learned about interfaces and inheritance. That’s coming soon!
You have attempted of activities on this page.