- Lab
-
Libraries: If you want this lab, consider one of these libraries.
- Core Tech
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 Info
Table of Contents
-
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-codedStockItem[10], every SKU set is aHashSetwhether 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. Opensrc/main/java/lab/StockItem.javaandsrc/main/java/lab/InventoryTracker.javanow and find the fixed-size array field and the manual shift-left removal logic -- you'll be replacing both. ## The migration aheadOver 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
ArrayListand remove items through anIteratorinstead of shifting indices by hand. - Step 3 -- pick
LinkedList,LinkedHashSet, andTreeSetfor 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 aWarehouseLocationkey class whose missingequals/hashCodeoverride silently breaks bin lookups. - Step 5 -- turn two reporting loops into stream pipelines, then lock the tracker's public API behind unmodifiable copies,
SequencedCollectionhelpers andCollectionsutility 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
// TODOcomment in a starter file undersrc/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// TODOhint 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_Mmeans 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
solutionfolder. - Step 2 -- swap the array for an
-
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
InventoryTrackercurrently stores items in aStockItem[10]field and tracks how many slots are filled with a separate counter. Any receiving day past ten pallets throwsArrayIndexOutOfBoundsException, 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, andaddnever needs a capacity check from you. ## Removing safely while you iterateWith 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-eachloop over aListcan't remove elements at all -- it throwsConcurrentModificationExceptionthe moment you try. The fix is to ask the list for anIteratorexplicitly and calliterator.remove(), which is the only removal path that keeps the loop's internal cursor consistent with the shrinking list. -
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
ArrayListisn'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 jobLinkedListdoes in constant time thatArrayListdoes by shifting every element. Opensrc/main/java/lab/DockQueue.java: the queue is still anArrayList, and you'll switch it to aLinkedList. ## Deduplicating without losing arrival orderThe 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
HashSetwould deduplicate correctly but scramble that arrival order on every run.LinkedHashSetkeeps both properties at once -- it rejects duplicates like anySet, but its iteration order always matches insertion order.src/main/java/lab/SkuAuditTrail.javacurrently builds its field as aHashSet; you'll swap in aLinkedHashSetso it dedupes and preserves order. ## Keeping the catalog alphabetical for freeThe 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
ArrayListafter every insert would be wasteful.TreeSetkeeps its elements in sorted order at all times, using each element's natural ordering (or aComparatoryou supply), so an alphabetical view falls out of iterating it directly.src/main/java/lab/CatalogIndex.javaalso starts with aHashSet; you'll swap in aTreeSet. -
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.javawas started as the replacement: its map field is declared, but its methods are still empty. ## Reading defaults and filling gaps safelyRestocking 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 ofnullwhen the key is absent, andMap.merge(key, value, remappingFunction)inserts a starting value only when nothing is there yet and combines it otherwise -- both without a separatecontainsKeycheck first. You'll use them together to restock any SKU that has dropped below its threshold. ## When a key class breaks its own contractBin lookups use
WarehouseLocationobjects asHashMapkeys, butWarehouseLocationoverrides neitherequalsnorhashCode-- it's still usingObject's identity-based versions. That means twoWarehouseLocationinstances built with the same aisle and bin number are neverequalto 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 insrc/main/java/lab/WarehouseLocation.java, because aMaprequires that any two equal objects also report the same hash code. -
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:
mapto convert each item to its line value, thenreduceto fold those values into a single total. Opensrc/main/java/lab/InventoryReports.javato find that loop; you'll replace it with a pipeline. ## Grouping without a manual map of listsThe 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'scollectmethod. Finish the second report method in the sameInventoryReports.java. ## Publishing state that callers can't corruptInventoryApiis the class other services call into, and right now it hands out its live internal list directly -- any caller that gets a reference can calladdorremoveon it and corrupt the warehouse's real inventory state without the tracker ever knowing. BothList.copyOf(source)andCollections.unmodifiableList(source)throwUnsupportedOperationExceptionon any mutation attempt, but they differ in one way:Collections.unmodifiableListis a live view that still shows later changes to the source, whileList.copyOfis 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 insrc/main/java/lab/InventoryApi.java. ## First and last, without index arithmeticShipment history is stored oldest-first, and
InventoryApianswers "the most recent shipment", "the very first shipment ever recorded" and "newest first, please" by reaching forget(size() - 1),get(0)and a copy passed throughCollections.reverse. Because the field implementsSequencedCollection, it already exposesgetFirst(),getLast(), andreversed()directly -- no index arithmetic, andreversed()gives you a live view in the opposite order without copying anything. Rewrite all three methods inInventoryApi.javausing those methods. ## Counting withCollectionsutilitiesThe dashboard also wants to know how many times a SKU has shipped and which SKU ships most often, and
InventoryApianswers both with hand-written loops. Thejava.util.Collectionsclass already has utility methods for each:Collections.frequency(collection, element)counts the elements equal toelement, andCollections.max(collection, comparator)returns the largest element under any ordering you supply. Replace both loops inInventoryApi.java. -
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.
Mainruns 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.shIn section 4, the caller's
addis stillrejected with UnsupportedOperationException, but the next line now readsthe 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 toreturn 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
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.