On this page:
18.1 The Setup
18.2 Stream On
18.3 Collector
8.6

Assignment 18: Streams

Goals: Practice using streams and collectors visitor pattern.

You should submit one .java file containing the solution to this problem.

Be sure to properly test your code and write purpose statements for your methods. A lack of tests and documentation will result in a lower grade! Remember that testing requires you to make some examples of data in an examples class.

18.1 The Setup

You have just begun your first day of your summer internship at Generyc, a very exciting new startup in Boston’s booming tech scene. You have been tasked with enhancing the company’s internal tools that allow them to track their customers and sales. The developer who began to build this, but has since quit to pursue their dreams of joining the circus, left you with the following classes.

Note that all of the fields are marked as private final, indicating they cannot be accessed outside their declaring class, and may never be reassigned once they’ve been initialized in the constructors. We’ll discuss what these words mean in more depth later, but basically, they’re (mostly) enforcing their immutability.

A line item is akin to a single entry on a receipt at a grocery store.

class Customer {
private final int id;
private final String name;
 
public Customer(int id, String name) {
this.id = id;
this.name = name;
}
 
public int getId() { return this.id; }
public String getName() { return this.name; }
}
 
class Inventory {
private final int id;
private final String description;
private final int costPerUnit;
 
public Inventory(int id, String description, int costPerUnit) {
this.id = id;
this.description = description;
this.costPerUnit = costPerUnit;
}
 
public int getId() { return this.id; }
public String getDescription() { return this.description; }
public int getCostPerUnit() { return this.costPerUnit; }
}
 
class Purchase {
private final int id;
private final List<LineItem> lineItems;
private final int customerIdOfPurchaser;
private final int numberOfDaysAgo;
 
public Purchase(int id, List<LineItem> lineItems, int customerIdOfPurchaser, int numberOfDaysAgo) {
this.id = id;
this.lineItems = new ArrayList<>(lineItems);
this.customerIdOfPurchaser = customerIdOfPurchaser;
this.numberOfDaysAgo = numberOfDaysAgo;
}
 
public int getId() { return this.id; }
public List<LineItem> getLineItems() { return this.lineItems; }
public int getCustomerIdOfPurchaser() { return this.customerIdOfPurchaser; }
public int getNumberOfDaysAgo() { return this.numberOfDaysAgo; }
}
 
class LineItem {
private final int inventoryId;
private final int numberOfItemsPurchased;
 
public LineItem(int inventoryId, int numberOfItemsPurchased) {
this.inventoryId = inventoryId;
this.numberOfItemsPurchased = numberOfItemsPurchased;
}
 
public int getInventoryId() { return this.inventoryId; }
public int getNumberOfItemsPurchased() { return this.numberOfItemsPurchased; }
}

Java’s built in libraries around dates are clunky at best, so for simplicity’s sake, you can assume the databse properly instantiates the numberOfDaysAgo field in the Purchase class. Working robustly with dates can be surprisingly difficult.

The developer also created a database to store this information, as well as some interfaces with which to interact with the database:
interface CustomerDB {
Optional<Customer> getCustomerById(int id);
List<Customer> getAllCustomers();
}
 
interface InventoryDB {
Optional<Inventory> getInventoryById(int id);
List<Inventory> getAllInventory();
}
 
interface PurchaseDB {
Optional<Purchase> getPurchaseById(int id);
// get purchases made numberOfDaysAgo, numberOfDaysAgo - 1... today List<Purchase> getPurchasesSince(int numberOfDaysAgo);
List<Purchase> getPurchasesFor(int customerId);
List<Purchase> getPurchasesFor(List<Integer> customerIds);
List<Purchase> getPurchasesForSince(int customerId, int numberOfDaysAgo);
List<Purchase> getPurchasesForSince(List<Integer> customerIds, int numberOfDaysAgo);
}

Retrieving the information for multiple customers at once from a databse can have strong performance improvements over querying the database once per customer.

As the developer didn’t take CS2, however, there is neither a single example nor test to be found!

18.2 Stream On

Now that you have data to properly test on, it’s time to implement the features that have been requested. Be sure to test your methods as you go, and use streams where appropriate.

18.3 Collector

Design a method that given a customer id, produces the list of ids of the pieces of inventory they have purchased the most times (which is not necessarily the same as the pieces of inventory they have purchased the largest quantities of). The method should error if such a customer does not exist. Hint: Since Stream::max only returns one optional value, you should create your own Collector in a util class which will make this method as readable as possible. The inputs to the method that returns the collector should likely be very similar to the inputs given to Stream::max.