Week 3 - Data Structures & Testing 

Collections

Generics

Enums

Stream APIs

Unit Testing with JUnit

Debugging in Java

Practice

Assignment

Back end Track

Content

Let’s Get Practical

<aside> 💭

Practice exercises are optional and do not need to be submitted

</aside>

Exercise 1 - Library Book Tracker

In this exercise, you will build a small book-tracking utility that groups books by genre. You will practice generics, ArrayList, HashMap, loops, and generic methods.

Tasks

  1. Create a generic class called Pair<K, V>.
  2. Add two private fields: key of type K and value of type V.
  3. Add a constructor that receives both values.
  4. Add getters named getKey() and getValue().
  5. Add a toString() method so printed pairs are readable.

In a main method, create an ArrayList<Pair<String, String>> to hold book-title and genre pairs. Add at least six books across at least three genres. For example:

Then complete the tracker:

  1. Create a HashMap<String, Integer> to count how many books belong to each genre.
  2. Loop through the book list and populate the map.
  3. Print the genre statistics.

Expected output for the sample data above:

Genre statistics:
Fantasy: 1 book(s)
Sci-Fi: 3 book(s)
Dystopian: 2 book(s)

<aside> 💡

HashMap has a method called getOrDefault(key, defaultValue). It is useful when you want to read the current count for a genre, but the genre might not be in the map yet.

</aside>

<aside> ⚠️

Use .equals() to compare String values, not ==. This is a common source of bugs when working with strings and collection keys.

</aside>

Bonus

Using @Override , implement the toString() method for your Pair<K,V> class. Then try printing the whole list in the console IO.println(books) or System.out.println(books)

Exercise 2A - Shopping Cart

Build a smart shopping cart that calculates totals and applies a discount based on item categories. This exercise combines enums, collections, the Stream API, records, and BigDecimal.

<aside> 💭

double can produce rounding errors, and money values need precision. In Java, common choices for money calculations are BigDecimal or a dedicated money library such as Joda-Money.

BigDecimal is more verbose because you use methods instead of arithmetic symbols, but it gives you stable calculations.

Operator BigDecimal equivalent
+ add(BigDecimal)
- subtract(BigDecimal)
* multiply(BigDecimal)
/ divide(BigDecimal)
</aside>

Resources:

Requirements:

GroceryType enum

Create an enum called GroceryType in net.hackyourfuture.models with below values:

ShoppingCart class

Create a ShoppingCart class in net.hackyourfuture. It should extend ArrayList<GroceryItem> and implement below methods:

package net.hackyourfuture;

public class ShoppingCart extends ArrayList<GroceryItem> {
  
  public List<GroceryItem> getItemsByType(GroceryType type) {
    throw new UnsupportedOperationException("Implement getItemsByType");
  }
  
  public Set<String> uniqueItemNames() {
    throw new UnsupportedOperationException("Implement uniqueItemNames");
  }
  
  public Integer totalItemsCount() {
    throw new UnsupportedOperationException("Implement totalItemsCount");
  }
  
  public BigDecimal calculateTotalPrice() {
    throw new UnsupportedOperationException("Implement calculateTotalPrice");
  }

  public String totalPriceFormatted() {
    return calculateTotalPrice()
        .setScale(GroceryItem.MONEY_SCALE, RoundingMode.HALF_UP)
        .toPlainString();
  }
}

Your methods should behave like this:

Method Required behavior
getItemsByType(GroceryType type) Return a List<GroceryItem> containing only items with the provided GroceryType. Use the Stream API.
uniqueItemNames() Return a Set<String> containing each grocery item name once. Use the Stream API.
totalItemsCount() Return the sum of all item quantities in the cart.
calculateTotalPrice() Calculate quantity * individualPrice for each item, apply a 20% discount to fruit items, and return the grand total with scale 2.
totalPriceFormatted() Return the total price as a plain string with two decimal places. This method is already provided above.

<aside> 💡

You can implement calculateTotalPrice() with a loop first. After that, try rewriting it with streams. Using reduce() is a good bonus challenge, but it is more advanced than the core Stream API operations from this week.

</aside>

Running the code below should produce the expected output shown after the code block:

import net.hackyourfuture.ShoppingCart;
import net.hackyourfuture.models.GroceryItem;
import net.hackyourfuture.models.GroceryType;

void main() {
  ShoppingCart cart = new ShoppingCart();
  cart.add(new GroceryItem("Bananas", GroceryType.FRUIT, 4, "1.25"));
  cart.add(new GroceryItem("Whole Milk", GroceryType.DAIRY, 1, "2.80"));
  cart.add(new GroceryItem("Sourdough Bread", GroceryType.BAKERY, 1, "3.50"));

  IO.println("Total: €" + cart.totalPriceFormatted());
  IO.println("Total Items in Cart: " + cart.totalItemsCount());
  IO.println("Fruit groceries: " + cart.getItemsByType(GroceryType.FRUIT));
  IO.println("Grocery item names: " + cart.uniqueItemNames());
}
/*=================================================================================================*/
//  Expected output:
//  Total: €10.30
//  Total Items in Cart: 6
//  Fruit groceries: [GroceryItem[name=Bananas, type=FRUIT, quantity=4, individualPrice=1.25]]
//  Grocery item names: [Bananas, Whole Milk, Sourdough Bread] <- // In any order
/*=================================================================================================*/

Exercise 2B - Unit Testing: ShoppingCartTest

Now that you have built ShoppingCart, prove that it works correctly using JUnit 5 tests and the Arrange-Act-Assert pattern from this week.

Task

Create a test class called ShoppingCartTest (place it in src/test/java/net/hackyourfuture/ so JUnit can find it).

Copy the below to your class and complete the test methods:

package net.hackyourfuture;

import static org.junit.jupiter.api.Assertions.fail;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

class ShoppingCartTest {
  private ShoppingCart cart;

  @BeforeEach
  void setUp() {
    cart = new ShoppingCart();
  }

  @Test
  void calculateTotalPrice_applies20PercentDiscountOnFruits() {
    fail("Arrange items, call calculateTotalPrice(), and assert the BigDecimal total.");
  }

  @Test
  void totalItemsCount_returnsSumOfAllQuantities() {
    fail("Arrange items, call totalItemsCount(), and assert the total quantity.");
  }

  @Test
  void getItemsByType_returnsOnlyMatchingItems() {
    fail("Arrange mixed items, call getItemsByType(), and assert only matching items are returned.");
  }

  @Test
  void uniqueItemNames_returnsAllUniqueNames() {
    fail("Arrange items with duplicate names, call uniqueItemNames(), and assert each name appears once.");
  }

  @Test
  void emptyCart_returnsZeroForTotalAndCount() {
    fail("Call calculateTotalPrice() and totalItemsCount() on an empty cart, then assert zero values.");
  }
}

Requirements:

  1. Use @Test for each test method.
  2. Use @BeforeEach to create a fresh ShoppingCart before every test.
  3. Follow the Arrange-Act-Assert pattern in every test.
  4. Use assertEquals(expected, actual) for totals, counts, lists, and sets.
  5. Use assertTrue() or assertFalse() when checking whether a result contains a specific item.
  6. Test the empty-cart case so you know your code handles an empty collection safely.

<aside> 💡

For BigDecimal, prefer expected values such as new BigDecimal("10.30"). Creating a BigDecimal from a String avoids the precision issues that happen with double.

</aside>

<aside> 🎉

If your tests pass, you have combined most of Week 3: collections, generics, enums, streams, unit testing, and debugging. That is exactly the toolkit you will use when you start building REST APIs in Week 4. Congratulations!

</aside>