Featured resource
2026 Tech Forecast
2026 Tech Forecast

1,500+ tech insiders, business leaders, and Pluralsight Authors share their predictions on what’s shifting fastest and how to stay ahead.

Download the forecast
  • Lab
    • Libraries: If you want this lab, consider one of these libraries.
    • Core Tech
Labs

Fixed Arrays to Expressive Collections in Java

A single-site distribution warehouse runs an inventory tracker that was prototyped with fixed-size arrays: stock items live in a hard-coded StockItem[10], every SKU set is a HashSet whether or not its workflow needs a particular order, and the reports are hand-written loops. As shipments grow, the array-based code throws ArrayIndexOutOfBoundsException on overstock days and silently corrupts data when items are removed mid-loop. You are the engineer assigned to migrate the tracker's storage layer to the Java Collections Framework: swapping arrays for ArrayList with safe iterator removal, picking the right List and Set implementation for each workflow (dock queue, SKU audit trail, alphabetical catalog), tracking quantities in a HashMap (and fixing a location-lookup bug caused by a missing equals/hashCode override), rewriting reporting loops as stream pipelines, and finally locking down the public API with unmodifiable copies, sequenced-collection helpers and Collections utility methods so downstream code can't corrupt warehouse state.

Lab platform
Lab Info
Level
Intermediate
Last updated
Nov 12, 2026
Duration
45m

Contact sales

By clicking submit, you agree to our Privacy Policy and Terms of Use, and consent to receive marketing emails from Pluralsight.
Table of Contents
  1. Challenge

    Step 1: Tour the fixed-array inventory tracker

    Tour the fixed-array inventory tracker

    The warehouse tracks everything that comes off the dock in a class called InventoryTracker. It was written fast, during the warehouse's first month, and it shows: stock items live in a hard-coded StockItem[10], every SKU set is a HashSet whether or not it needs a particular order, and the reports are hand-written loops. It worked while the warehouse was small.

    It does not work anymore. On a busy receiving day the eleventh pallet throws ArrayIndexOutOfBoundsException. When a picker removes an expired item mid-loop, the array shifts underneath the loop counter and a neighboring item silently disappears from the count. Open src/main/java/lab/StockItem.java and src/main/java/lab/InventoryTracker.java now and find the fixed-size array field and the manual shift-left removal logic -- you'll be replacing both. ## The migration ahead

    Over the next four steps you will move the tracker's storage layer onto the Java Collections Framework, one workflow at a time:

    • Step 2 -- swap the array for an ArrayList and remove items through an Iterator instead of shifting indices by hand.
    • Step 3 -- pick LinkedList, LinkedHashSet, and TreeSet for three real workflows (a dock queue, a SKU audit trail, and an alphabetical catalog), based on the ordering guarantee each one needs.
    • Step 4 -- finish the half-built HashMap<String, Integer> quantity ledger, and repair a WarehouseLocation key class whose missing equals/hashCode override silently breaks bin lookups.
    • Step 5 -- turn two reporting loops into stream pipelines, then lock the tracker's public API behind unmodifiable copies, SequencedCollection helpers and Collections utility methods so no caller can corrupt warehouse state from outside.

    Each step edits a different file and leaves you with code that a test suite can actually verify, because the array-based version keeps breaking in ways that are hard to write a stable test against in the first place. ## How you'll work

    Every task you complete replaces a // TODO comment in a starter file under src/main/java/lab/. Each task names the file and method to change and walks you through the change one numbered step at a time, and the // TODO hint in the starter file points at the technique to use. Run the full test suite at any point with:

    bash runTest.sh 'Step*Test'
    

    A passing test for task_N_M means the behavior that task describes now works end to end -- not just that the code compiles. Move on to Step 2 to start with the tracker's core storage.

    info> If you get stuck, you can refer to the provided solution code for each task, available in the solution folder.

  2. Challenge

    Step 2: Replace the fixed array with a growable, iterator-safe list

    Step 2: Replace the fixed array with a growable, iterator-safe list

    InventoryTracker currently stores items in a StockItem[10] field and tracks how many slots are filled with a separate counter. Any receiving day past ten pallets throws ArrayIndexOutOfBoundsException, and there's no way to grow the backing store without rewriting the array-copy logic by hand. ArrayList<StockItem> gives you that growth for free: it resizes itself, and add never needs a capacity check from you. ## Removing safely while you iterate

    With the array gone, the next problem is expiration sweeps. The old code shifted every element after a removed index one slot to the left, and got that shift wrong whenever two expired items sat next to each other. A for-each loop over a List can't remove elements at all -- it throws ConcurrentModificationException the moment you try. The fix is to ask the list for an Iterator explicitly and call iterator.remove(), which is the only removal path that keeps the loop's internal cursor consistent with the shrinking list.

  3. Challenge

    Step 3: Pick List and Set implementations by their ordering behavior

    Step 3: Pick List and Set implementations by their ordering behavior

    Not every collection in the tracker needs the same ordering guarantee, and ArrayList isn't the right choice for all of them. The dock crew needs to push urgent shipments to the front of the unloading queue without touching the rest -- a job LinkedList does in constant time that ArrayList does by shifting every element. Open src/main/java/lab/DockQueue.java: the queue is still an ArrayList, and you'll switch it to a LinkedList. ## Deduplicating without losing arrival order

    The warehouse's compliance team wants a SKU audit trail: every SKU that has ever been received, each one listed exactly once, in the order it first arrived. A plain HashSet would deduplicate correctly but scramble that arrival order on every run. LinkedHashSet keeps both properties at once -- it rejects duplicates like any Set, but its iteration order always matches insertion order. src/main/java/lab/SkuAuditTrail.java currently builds its field as a HashSet; you'll swap in a LinkedHashSet so it dedupes and preserves order. ## Keeping the catalog alphabetical for free

    The printed catalog handed to warehouse staff needs SKUs listed alphabetically, and it needs to stay that way as new SKUs are added throughout the day -- re-sorting an ArrayList after every insert would be wasteful. TreeSet keeps its elements in sorted order at all times, using each element's natural ordering (or a Comparator you supply), so an alphabetical view falls out of iterating it directly. src/main/java/lab/CatalogIndex.java also starts with a HashSet; you'll swap in a TreeSet.

  4. Challenge

    Step 4: Track quantities with Map and repair hash-based lookup

    Step 4: Track quantities with Map and repair hash-based lookup

    The original prototype kept quantities in a second array, indexed in lockstep with the SKU array -- any bug that let the two arrays drift out of sync corrupted every quantity lookup after it. A HashMap<String, Integer> keyed directly by SKU removes that whole failure mode: the quantity for a SKU lives with its key, not at a position you have to keep synchronized. src/main/java/lab/QuantityLedger.java was started as the replacement: its map field is declared, but its methods are still empty. ## Reading defaults and filling gaps safely

    Restocking logic needs to ask "how many of this SKU do we have" even for a SKU the ledger has never seen, and it should not have to null-check every read. Map.getOrDefault(key, fallback) returns the fallback instead of null when the key is absent, and Map.merge(key, value, remappingFunction) inserts a starting value only when nothing is there yet and combines it otherwise -- both without a separate containsKey check first. You'll use them together to restock any SKU that has dropped below its threshold. ## When a key class breaks its own contract

    Bin lookups use WarehouseLocation objects as HashMap keys, but WarehouseLocation overrides neither equals nor hashCode -- it's still using Object's identity-based versions. That means two WarehouseLocation instances built with the same aisle and bin number are never equal to each other, and a lookup with a freshly constructed key always misses, even when the exact same location was stored moments earlier. Fixing this means overriding both methods together in src/main/java/lab/WarehouseLocation.java, because a Map requires that any two equal objects also report the same hash code.

  5. Challenge

    Step 5: Stream the reports and lock down the public API

    Step 5: Stream the reports and lock down the public API

    The warehouse's nightly valuation report currently loops over every stock item, multiplying quantity by unit price and accumulating a running total by hand. A stream pipeline expresses the same computation as a sequence of transformations: map to convert each item to its line value, then reduce to fold those values into a single total. Open src/main/java/lab/InventoryReports.java to find that loop; you'll replace it with a pipeline. ## Grouping without a manual map of lists

    The supplier breakdown report needs every stock item bucketed by its supplier name -- the kind of thing the old code built with a HashMap<String, List<StockItem>> and an if-absent-create-the-list check inside the loop. Collectors.groupingBy(classifier) does exactly that bucketing in one call, handed to a stream's collect method. Finish the second report method in the same InventoryReports.java. ## Publishing state that callers can't corrupt

    InventoryApi is the class other services call into, and right now it hands out its live internal list directly -- any caller that gets a reference can call add or remove on it and corrupt the warehouse's real inventory state without the tracker ever knowing. Both List.copyOf(source) and Collections.unmodifiableList(source) throw UnsupportedOperationException on any mutation attempt, but they differ in one way: Collections.unmodifiableList is a live view that still shows later changes to the source, while List.copyOf is a frozen point-in-time copy. A snapshot should not change after it is taken, so you'll use the copy, and your internal list stays fully mutable. Finish the snapshot method in src/main/java/lab/InventoryApi.java. ## First and last, without index arithmetic

    Shipment history is stored oldest-first, and InventoryApi answers "the most recent shipment", "the very first shipment ever recorded" and "newest first, please" by reaching for get(size() - 1), get(0) and a copy passed through Collections.reverse. Because the field implements SequencedCollection, it already exposes getFirst(), getLast(), and reversed() directly -- no index arithmetic, and reversed() gives you a live view in the opposite order without copying anything. Rewrite all three methods in InventoryApi.java using those methods. ## Counting with Collections utilities

    The dashboard also wants to know how many times a SKU has shipped and which SKU ships most often, and InventoryApi answers both with hand-written loops. The java.util.Collections class already has utility methods for each: Collections.frequency(collection, element) counts the elements equal to element, and Collections.max(collection, comparator) returns the largest element under any ordering you supply. Replace both loops in InventoryApi.java.

  6. Challenge

    Step 6: Watch the warehouse's collections at work

    Every task is done and graded. Now run the whole tracker once and watch each collection do the job you chose it for: storage that grows, a sweep that misses nothing, orders that prove their implementation, and an API callers can't corrupt. Main runs your code from each step in turn and prints one section per step.

    Open the Terminal tab and run:

    bash runApp.sh
    ``` ### Storage that grows, and orders that prove each choice
    
    In section 1, `Received 15 pallets, tracker holds 15` is the receiving day that made the old `StockItem[10]` throw `ArrayIndexOutOfBoundsException` on pallet 11. Your `ArrayList` from Task 2.1 just grew. The sweep starts with three expired pallets side by side, `PALLET-03`, `PALLET-04` and `PALLET-05`, and ends with `5 pallets, still expired: none`. The index loop you wrote in Task 2.1 would have printed `still expired: [PALLET-04]`, because `PALLET-04` slides into the slot that was just emptied and the loop moves past it. Your `Iterator` from Task 2.2 removes all three.
    
    Section 2 shows SKUs in three different orders, and each order comes from the implementation you picked. The dock unloads `[SKU-URGENT, SKU-A, SKU-B]` because your `LinkedList` from Task 3.1 put the priority shipment first. The audit trail is `[PALLET-9, BOX-12, CRATE-3, DRUM-41]`: duplicates dropped and first-arrival order kept, which is your `LinkedHashSet` from Task 3.2. The catalog is `[BOX-12, CRATE-3, DRUM-41, PALLET-9]`, alphabetical with no sort call, from your `TreeSet` in Task 3.3. ### Lookups by value, and an API callers can't corrupt
    
    Section 3 starts with `on hand 15` after receiving 40 and shipping 25, then `on hand 65` once `restockIfBelow` sees that 15 is under 20 and merges in 50 (Tasks 4.1 and 4.2). The bin line ends with `PALLET-07`, not `miss`. The lookup uses a brand-new `WarehouseLocation` key, and it finds the stored entry only because your `equals` and `hashCode` from Task 4.3 compare aisle and bin instead of object identity.
    
    In section 4, `Total inventory value: 90.0` and `By supplier: {Acme=[SKU-1, SKU-2], Globex=[SKU-3]}` come from your `map`/`reduce` and `groupingBy` pipelines in Tasks 5.1 and 5.2. The caller's `add` is `rejected with UnsupportedOperationException`, and the snapshot still has `2 items` after the tracker receives SKU-3 (Task 5.3). The newest-first list `[SKU-C, SKU-B, SKU-A, SKU-B]` becomes `[SKU-B, SKU-C, SKU-B, SKU-A, SKU-B]` after one more shipment without being asked again, because `reversed()` from Task 5.4 is a live view. The last line, `SKU-B shipped 3 times, busiest SKU: SKU-B`, is `Collections.frequency` and `Collections.max` from Task 5.5. ### Swap the snapshot's copy for a view
    
    Task 5.3 told you to use `List.copyOf` rather than `Collections.unmodifiableList`. Here is why. Open the **InventoryApi.java** editor tab and find this line in `currentStockSnapshot()`:
    
    ```java
    return List.copyOf(stockList);
    

    Change it to:

    return Collections.unmodifiableList(stockList);
    

    Save, then run the tour again:

    bash runApp.sh
    

    In section 4, the caller's add is still rejected with UnsupportedOperationException, but the next line now reads the snapshot has 3 items. The view blocks changes from the caller, yet it shows every change the tracker makes, so it was never a snapshot. Change the line back to return List.copyOf(stockList); and save.

    You now have a storage layer that grows on demand, uses each collection for the order its workflow needs, and hands callers data they can't corrupt. To rerun every graded check at once, use bash runTest.sh 'Step*Test'.

About the author

Pluralsight’s AI authoring technology is designed to accelerate the creation of hands-on, technical learning experiences. Serving as a first-pass content generator, it produces structured lab drafts aligned to learning objectives defined by Pluralsight’s Curriculum team. Each lab is then enhanced by our Content team, who configure the environments, refine instructions, and conduct rigorous technical and quality reviews. The result is a collaboration between artificial intelligence and human expertise, where AI supports scale and efficiency, and Pluralsight experts ensure accuracy, relevance, and instructional quality, helping learners build practical skills with confidence.

Real skill practice before real-world application

Hands-on Labs are real environments created by industry experts to help you learn. These environments help you gain knowledge and experience, practice without compromising your system, test without risk, destroy without fear, and let you learn from your mistakes. Hands-on Labs: practice your skills before delivering in the real world.

Learn by doing

Engage hands-on with the tools and technologies you’re learning. You pick the skill, we provide the credentials and environment.

Follow your guide

All labs have detailed instructions and objectives, guiding you through the learning process and ensuring you understand every step.

Turn time into mastery

On average, you retain 75% more of your learning if you take time to practice. Hands-on labs set you up for success to make those skills stick.

Get started with Pluralsight