Abstract Collectables and Dynamic Registries (Completed Homework)
Build a data-driven FantasyPersona and FantasyStudent, then sort, order and group the roster with a Catalog and Registry.
Categories: CS112 CS113 Oct 26, 2026 • Anika Marathe (based on John Mortensen’s lesson) • 12 min read
Abstract Collectables & Dynamic Registries: Homework
Model the OCS survey’s Fantasy Persona as a Collectable, compose it into a FantasyStudent, and use a Registry to sort, order and group the roster. Purpose of my hack: make student profiles sortable, projectable and groupable by any key, then test the edge cases.
Run the cells top to bottom (they share state). Save each runner (💾) before submitting.
Reference Guide
| Term | Meaning | In this lesson |
|---|---|---|
| Interface | A contract of methods | Collectable |
| Abstract class | Shared code, cannot be instantiated | AbstractCollectable (derives getKeys, getValue, toString from getMap) |
| Natural order | Default order from compareTo |
Student by name, Standing by weight |
| Bounded generics / wildcard | Type limited to a family of types | Catalog<T extends Collectable>, Registry(Catalog<? extends Collectable>) |
| Map projection | Fields exposed as key/value pairs | getMap() with a LinkedHashMap |
| Decorator | Wrapper that adds behavior | ProjectedCollectable moves the sort key to column 1 |
| Stable sort | Equal items keep their original order | List.sort |
Standards: CS112-OOP-1/OOP-2 (interfaces, generics), CS112-DS-1 (LinkedHashMap, computeIfAbsent), CS112-ALG-1 (sorting), CS113-DS-2 (decorator), CS113-TEST-1 (edge cases), CS113-PBL-1 (domain entities to inspectable data).
Real-Life Application
| Situation | What went wrong | What the pattern fixes |
|---|---|---|
Roster sorted with plain String.compareTo |
isaac Newton lands after every capital letter |
One case-insensitive compareTo, reused everywhere |
| Table that only sorts its hard-coded column | Every new column needs new code | sortByField(key) sorts by any key |
| Survey dashboard grouping by persona, standing, pathway | Copy-pasted loops | groupBy(key) partitions any collection |
Takeaway: write the record once (getMap, compareTo) and let the registry sort and group every record type.
Tech Talk
Learning objective: model data as Collectable records, store them in a type-safe Catalog, and process them with a Registry.
Success criteria: I can write getMap() and compareTo() for a new class; sort naturally, sort by a key and group by a key; and test empty, singleton, duplicate and missing-key cases.
Student --getMap()--> {Name, Standing, Pathway Year, ...} --orderBy("Pathway Year")--> {Pathway Year, Name, Standing, ...}
Collectable (interface) <-- AbstractCollectable <-- Animal, Student, FantasyStudent, Pathway, Standing, FantasyPersona
A. Building blocks (provided, do not change)
// CODE_RUNNER: Run to compile Collectable, AbstractCollectable, Catalog and Registry. Read the comments: they are your reference.
import java.util.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* Collectable: the contract every row-like record must follow.
* - Polymorphism: Registry/Catalog treat Student, Animal, Pathway... all as "Collectable".
* - Extends Comparable<Collectable>: forces every record to define a natural order.
* - getMap(): exposes fields as key/value pairs so code can ask for a field by *name*.
*/
interface Collectable extends Comparable<Collectable> {
Map<String, Object> getMap();
List<String> getKeys();
Object getValue(String key);
}
/**
* AbstractCollectable: shares code that every Collectable can derive from getMap().
* Template Method idea: subclasses only supply getMap() and compareTo().
*/
abstract class AbstractCollectable implements Collectable {
@Override public List<String> getKeys() { return new ArrayList<>(getMap().keySet()); }
@Override public Object getValue(String key) { return getMap().get(key); }
@Override public String toString() { return getMap().toString(); }
}
/** Catalog<T>: a type-safe container (like a database table) for any Collectable subtype. */
class Catalog<T extends Collectable> {
private List<T> items = new ArrayList<>();
public void add(T item) { items.add(item); }
public List<T> getItems() { return items; }
public void display() { for (T item : items) System.out.println(item); }
}
/**
* Registry: sorting and grouping services over ANY catalog of Collectables.
* - Wildcard generics (? extends Collectable) lets it accept Catalog<Student>, Catalog<Animal>, ...
* - sortByField uses key strings, so one method sorts every kind of record.
* - groupBy uses Map.computeIfAbsent to build "one list per category".
*/
class Registry {
private List<Collectable> items;
public Registry(Catalog<? extends Collectable> catalog) {
this.items = new ArrayList<>(catalog.getItems());
}
/** Decorator: same data, but the primaryKey is moved to the first column. */
public record ProjectedCollectable(Collectable delegate, String primaryKey) implements Collectable {
@Override
public Map<String, Object> getMap() {
Map<String, Object> original = delegate.getMap();
Map<String, Object> reordered = new LinkedHashMap<>();
if (original.containsKey(primaryKey)) {
reordered.put(primaryKey, original.get(primaryKey));
}
original.forEach((k, v) -> {
if (!k.equals(primaryKey)) reordered.put(k, v);
});
return reordered;
}
@Override public List<String> getKeys() { return getMap().keySet().stream().toList(); }
@Override public Object getValue(String key) { return getMap().get(key); }
@Override public int compareTo(Collectable o) { return delegate.compareTo(o); }
@Override public String toString() { return getMap().toString(); }
}
public List<Collectable> sortNatural() {
List<Collectable> sorted = new ArrayList<>(items);
Collections.sort(sorted);
return sorted;
}
@SuppressWarnings({"unchecked", "rawtypes"})
public List<Collectable> sortByField(String key) {
List<Collectable> sorted = new ArrayList<>(items);
sorted.sort((a, b) -> {
Object valA = a.getValue(key);
Object valB = b.getValue(key);
if (valA instanceof Comparable && valB instanceof Comparable) {
return ((Comparable) valA).compareTo(valB);
}
return 0;
});
return sorted;
}
public List<ProjectedCollectable> orderBy(String key) {
return sortByField(key).stream()
.map(item -> new ProjectedCollectable(item, key))
.toList();
}
public Map<Object, List<Collectable>> groupBy(String key) {
Map<Object, List<Collectable>> groupedMap = new LinkedHashMap<>();
for (Collectable item : items) {
Object groupKey = item.getValue(key);
groupedMap.computeIfAbsent(groupKey, k -> new ArrayList<>()).add(item);
}
return groupedMap;
}
}
B. Simple data set: Animals
Run the demo and match each output section to Animals.main.
Code Runner Challenge
Run the Animals demo: display, natural sort, orderBy Age, groupBy Species.
View IPYNB Source
// CODE_RUNNER: Run the Animals demo: display, natural sort, orderBy Age, groupBy Species.
import java.util.*;
import java.util.*;
/** Animal (from the lesson): used here to prove the comparator works across different Collectable types. */
class Animal extends AbstractCollectable {
private String name;
private String species;
private int age;
public Animal(String name, String species, int age) {
this.name = name;
this.species = species;
this.age = age;
}
@Override
public Map<String, Object> getMap() {
Map<String, Object> map = new LinkedHashMap<>();
map.put("Name", name);
map.put("Species", species);
map.put("Age", age);
return map;
}
@Override
public int compareTo(Collectable o) { return this.name.compareTo((String) o.getValue("Name")); }
}
/** Animals: demo driver for the simple Animal data set (no Student code needed). */
class Animals {
public static void main(String[] args) {
Catalog<Animal> animalCatalog = new Catalog<>();
animalCatalog.add(new Animal("Zebra", "Mammal", 5));
animalCatalog.add(new Animal("Aardvark", "Mammal", 3));
animalCatalog.add(new Animal("Falcon", "Bird", 2));
animalCatalog.add(new Animal("Baboon", "Mammal", 4));
Registry registry = new Registry(animalCatalog);
System.out.println("=== 1. Catalog Display (Insertion Order) ===");
animalCatalog.display();
System.out.println("\n=== 2. Natural Sorting (Alphabetical by Name) ===");
registry.sortNatural().forEach(System.out::println);
System.out.println("\n=== 3. Alternate Sorting (Dynamic Sort by Age) ===");
registry.orderBy("Age").forEach(System.out::println);
System.out.println("\n=== 4. Categorized Grouping by Species ===");
registry.groupBy("Species").forEach((species, list) -> {
System.out.println("Species Group: " + species);
list.forEach(animal -> System.out.println(" - " + animal));
});
}
}
Animals.main(null);
C. OCS data set: Student (original)
Kept unchanged so the original output stays reproducible next to your FantasyStudent output. Notice: isaac Newton sorts last because String.compareTo is case-sensitive. You fix that in Hack 2.
Code Runner Challenge
Run the ORIGINAL Pathway/Standing/Student demo and study where 'isaac Newton' lands in the natural sort.
View IPYNB Source
// CODE_RUNNER: Run the ORIGINAL Pathway/Standing/Student demo and study where 'isaac Newton' lands in the natural sort.
import java.util.*;
/** Pathway: a curriculum track (year 0-3). Natural order = by year. */
class Pathway extends AbstractCollectable {
private int year;
private String projectCapability;
private String scrumTeamCapability;
private String problemSolving;
private String initiative;
public Pathway(int year, String projectCapability, String scrumTeamCapability, String problemSolving, String initiative) {
this.year = year;
this.projectCapability = projectCapability;
this.scrumTeamCapability = scrumTeamCapability;
this.problemSolving = problemSolving;
this.initiative = initiative;
}
public int getYear() { return year; }
public String getProjectCapability() { return projectCapability; }
@Override
public Map<String, Object> getMap() {
Map<String, Object> map = new LinkedHashMap<>();
map.put("Year", year);
map.put("Project Capability", projectCapability);
map.put("Scrum Team", scrumTeamCapability);
map.put("Problem Solving", problemSolving);
map.put("Initiative", initiative);
return map;
}
@Override public int compareTo(Collectable o) { return Integer.compare(this.year, (Integer) o.getValue("Year")); }
public static Catalog<Pathway> catalog() {
Catalog<Pathway> catalog = new Catalog<>();
catalog.add(new Pathway(0, "Fundamental", "Solo", "Remembering", "Passive"));
catalog.add(new Pathway(1, "Project", "Pair (2-3)", "Analyzing", "Self-starter"));
catalog.add(new Pathway(2, "System", "Group (~6)", "Applying", "Shaper"));
catalog.add(new Pathway(3, "Research", "Team (~10)", "Creating", "Driver"));
return catalog;
}
}
/** Standing: academic level with a numeric weight so levels can be compared. */
class Standing extends AbstractCollectable {
private String label;
private int weight;
public Standing(String label, int weight) { this.label = label; this.weight = weight; }
public String getLabel() { return label; }
public int getWeight() { return weight; }
@Override
public Map<String, Object> getMap() {
Map<String, Object> map = new LinkedHashMap<>();
map.put("Label", label);
map.put("Weight", weight);
return map;
}
@Override public int compareTo(Collectable o) { return Integer.compare(this.weight, (Integer) o.getValue("Weight")); }
public static Catalog<Standing> catalog() {
Catalog<Standing> catalog = new Catalog<>();
catalog.add(new Standing("Freshman", 0));
catalog.add(new Standing("Sophomore", 1));
catalog.add(new Standing("Junior", 2));
catalog.add(new Standing("Senior", 3));
return catalog;
}
}
/** Student (original): composes Standing + Pathway and flattens them into one map. */
class Student extends AbstractCollectable {
private String name;
private Standing standing;
private Pathway pathway;
private String archetype;
public Student(String name, Standing standing, Pathway pathway, String archetype) {
this.name = name;
this.standing = standing;
this.pathway = pathway;
this.archetype = archetype;
}
public String getName() { return name; }
public Standing getStanding() { return standing; }
public Pathway getPathway() { return pathway; }
public String getArchetype() { return archetype; }
@Override
public Map<String, Object> getMap() {
Map<String, Object> map = new LinkedHashMap<>();
map.put("Name", name);
map.put("Standing", standing.getLabel());
map.put("Standing Weight", standing.getWeight());
map.put("Pathway Year", pathway.getYear());
map.put("Technical Capability", pathway.getProjectCapability());
map.put("Archetype", archetype);
return map;
}
@Override
public int compareTo(Collectable o) {
return this.name.compareTo((String) o.getValue("Name"));
}
}
/** Students: the ORIGINAL driver. Kept so we can compare old output vs FantasyStudent output. */
class Students {
public static Catalog<Student> catalog() {
Catalog<Pathway> pathwayCatalog = Pathway.catalog();
Catalog<Standing> standingCatalog = Standing.catalog();
Catalog<Student> studentCatalog = new Catalog<>();
studentCatalog.add(new Student("Thomas Edison", standingCatalog.getItems().get(3), pathwayCatalog.getItems().get(3), "Scrummer"));
studentCatalog.add(new Student("Nikola Tesla", standingCatalog.getItems().get(3), pathwayCatalog.getItems().get(2), "Technologist"));
studentCatalog.add(new Student("Grace Hopper", standingCatalog.getItems().get(2), pathwayCatalog.getItems().get(3), "Planner"));
studentCatalog.add(new Student("Marie Curie", standingCatalog.getItems().get(2), pathwayCatalog.getItems().get(1), "Analyst"));
studentCatalog.add(new Student("Alan Turing", standingCatalog.getItems().get(2), pathwayCatalog.getItems().get(1), "Technologist"));
studentCatalog.add(new Student("Hedy Lamarr", standingCatalog.getItems().get(2), pathwayCatalog.getItems().get(2), "Planner"));
studentCatalog.add(new Student("isaac Newton", standingCatalog.getItems().get(1), pathwayCatalog.getItems().get(0), "Analyst"));
studentCatalog.add(new Student("Bill Gates", standingCatalog.getItems().get(0), pathwayCatalog.getItems().get(0), "Analyst"));
return studentCatalog;
}
public static void main(String[] args) {
Catalog<Student> studentCatalog = catalog();
Registry registry = new Registry(studentCatalog);
System.out.println("=== 1. Student toString() & Catalog Display ===");
studentCatalog.display();
System.out.println("\n=== 2. Natural Sorting (compareTo by Name) ===");
registry.sortNatural().forEach(System.out::println);
System.out.println("\n=== 3. Alternate Sorting (by Pathway Year) ===");
registry.orderBy("Pathway Year").forEach(System.out::println);
System.out.println("\n=== 4. Grouping by Archetype ===");
Map<Object, List<Collectable>> grouped = registry.groupBy("Archetype");
grouped.forEach((key, value) -> {
System.out.println("Archetype: " + key);
value.forEach(item -> System.out.println(" - " + item));
});
}
}
Students.main(null);
Hacks & Practice Tasks
Submission template
Name: <your name>
Hack title (purpose): <what you built>
Runners saved: Popcorn, Hack 1, Hack 2, Hack 3, Hack 4
What I learned: <2-3 sentences>
Safety: submit only your own work, never paste passwords or tokens into a runner, and save every runner (💾) first.
Popcorn Hack (5 min)
Predict first, run second. Each line should print true.
Code Runner Challenge
Popcorn Hack: add your own Animal, write your three predictions, then run. All lines should print true.
View IPYNB Source
// CODE_RUNNER: Popcorn Hack: add your own Animal, write your three predictions, then run. All lines should print true.
import java.util.*;
/**
* Popcorn Hack: PREDICT before you run.
* Fill in the three predictions, then run. Each check should print true.
*/
public class AnimalsPopcorn {
public static void main(String[] args) {
Catalog<Animal> catalog = new Catalog<>();
catalog.add(new Animal("Zebra", "Mammal", 5));
catalog.add(new Animal("Aardvark", "Mammal", 3));
catalog.add(new Animal("Falcon", "Bird", 2));
catalog.add(new Animal("Baboon", "Mammal", 4));
catalog.add(new Animal("Newt", "Amphibian", 1)); // our extra animal
Registry registry = new Registry(catalog);
// Predictions (written BEFORE running)
String firstByName = "Aardvark"; // natural order = alphabetical by Name
String firstByAge = "Newt"; // orderBy("Age") = youngest first
int numberOfSpeciesGroups = 3; // Mammal, Bird, Amphibian
System.out.println(registry.sortNatural().get(0).getValue("Name").equals(firstByName));
System.out.println(registry.orderBy("Age").get(0).getValue("Name").equals(firstByAge));
System.out.println(registry.groupBy("Species").size() == numberOfSpeciesGroups);
}
}
AnimalsPopcorn.main(null);
MCQ Check
1. Why does Registry take Catalog<? extends Collectable> instead of Catalog<Collectable>?
- A. Wildcards are faster
- B. A
Catalog<Student>is not aCatalog<Collectable>, so the wildcard is needed to accept any subtype - C.
Catalogis an interface - D. It blocks null values
2. What does registry.groupBy("Persona") return?
- A. A sorted
List<Collectable> - B. A
Map<Object, List<Collectable>>from each persona to its members - C. A
Set<String>of persona names - D. A new
Catalog
3. orderBy("Pathway Year") keeps Marie Curie before Alan Turing (both Year 1). Why?
- A.
compareToranks names - B.
List.sortis stable, so equal keys keep their original order - C.
HashMappreserves order - D. Pathway Year is unique
Answer key: 1-B, 2-B, 3-B
Homework Hack
Finish each hack, then run the self-check at the end. Run the Tech Talk cells first.
Hack 1: Data-driven attribute class (15 min)
Create FantasyPersona (Speedster, Powerhouse, Mastermind, Visionary): extends AbstractCollectable; private name, superpower, powerLevel; getMap() keys Persona, Superpower, Power Level; compareTo() strongest first (tie-break by name); static catalog().
Code Runner Challenge
Hack 1: build FantasyPersona (fields, getMap, compareTo, catalog) and run main to display it.
View IPYNB Source
// CODE_RUNNER: Hack 1: build FantasyPersona (fields, getMap, compareTo, catalog) and run main to display it.
import java.util.*;
/**
* FantasyPersona: one option from the survey's "Fantasy Persona" question, modeled as data.
*
* Concepts:
* - Inheritance: extends AbstractCollectable, so getKeys/getValue/toString come for free.
* - Data projection: getMap() exposes fields under readable String keys.
* - Natural ordering: strongest persona first (powerLevel descending), name as a tie-breaker,
* so compareTo is a consistent total order even if two personas share a power level.
*/
public class FantasyPersona extends AbstractCollectable {
private final String name;
private final String superpower;
private final int powerLevel;
public FantasyPersona(String name, String superpower, int powerLevel) {
this.name = name;
this.superpower = superpower;
this.powerLevel = powerLevel;
}
public String getName() { return name; }
public String getSuperpower() { return superpower; }
public int getPowerLevel() { return powerLevel; }
/** LinkedHashMap keeps insertion order, so printed output is predictable. */
@Override
public Map<String, Object> getMap() {
Map<String, Object> map = new LinkedHashMap<>();
map.put("Persona", name);
map.put("Superpower", superpower);
map.put("Power Level", powerLevel);
return map;
}
@Override
public int compareTo(Collectable o) {
int otherLevel = (Integer) o.getValue("Power Level");
int byLevel = Integer.compare(otherLevel, this.powerLevel); // swapped args => descending
if (byLevel != 0) return byLevel;
return this.name.compareTo((String) o.getValue("Persona")); // tie-breaker
}
/** Master catalog of all personas students can pick from. */
public static Catalog<FantasyPersona> catalog() {
Catalog<FantasyPersona> catalog = new Catalog<>();
catalog.add(new FantasyPersona("Speedster", "Super speed", 7));
catalog.add(new FantasyPersona("Powerhouse", "Super strength", 9));
catalog.add(new FantasyPersona("Mastermind", "Genius-level planning", 8));
catalog.add(new FantasyPersona("Visionary", "Sees possible futures", 6));
return catalog;
}
public static void main(String[] args) {
Catalog<FantasyPersona> personas = FantasyPersona.catalog();
System.out.println("=== Persona catalog (insertion order) ===");
personas.display();
System.out.println("\n=== Persona natural order (strongest first) ===");
new Registry(personas).sortNatural().forEach(System.out::println);
}
}
FantasyPersona.main(null);
Hack 2: Compose it into FantasyStudent (15 min)
Create FantasyStudent holding a Standing, a Pathway and a FantasyPersona. getMap() returns Name, Standing, Standing Weight, Pathway Year, Technical Capability, then every persona attribute (hint: putAll). compareTo() sorts by name ignoring case.
// CODE_RUNNER: Hack 2: build FantasyStudent (composition, getMap with the persona's attributes, case-insensitive compareTo).
import java.util.*;
/**
* FantasyStudent: a Student whose "archetype" is now a full FantasyPersona object.
*
* Concepts:
* - Composition (has-a): holds Standing, Pathway AND FantasyPersona objects.
* - Flattening: getMap() merges child maps into one level so Registry can look up any key.
* persona.getMap() is merged with putAll(), so if FantasyPersona gains a field later,
* FantasyStudent exposes it automatically (no duplicate bookkeeping).
* - Natural ordering: by Name, ignoring case (compareToIgnoreCase).
*/
public class FantasyStudent extends AbstractCollectable {
private final String name;
private final Standing standing;
private final Pathway pathway;
private final FantasyPersona persona;
public FantasyStudent(String name, Standing standing, Pathway pathway, FantasyPersona persona) {
this.name = name;
this.standing = standing;
this.pathway = pathway;
this.persona = persona;
}
public String getName() { return name; }
public Standing getStanding() { return standing; }
public Pathway getPathway() { return pathway; }
public FantasyPersona getPersona() { return persona; }
@Override
public Map<String, Object> getMap() {
Map<String, Object> map = new LinkedHashMap<>();
map.put("Name", name);
map.put("Standing", standing.getLabel());
map.put("Standing Weight", standing.getWeight());
map.put("Pathway Year", pathway.getYear());
map.put("Technical Capability", pathway.getProjectCapability());
map.putAll(persona.getMap()); // adds Persona, Superpower, Power Level
return map;
}
@Override
public int compareTo(Collectable o) {
return this.name.compareToIgnoreCase((String) o.getValue("Name"));
}
}
Hack 3: Registry and enhanced output (20 min)
Build FantasyStudents with at least 4 students (different personas, standings, pathways) and show natural sort, orderBy("Pathway Year"), orderBy("Standing Weight") and groupBy("Persona"). Compare with the original Students output.
Code Runner Challenge
Hack 3: build the roster and print natural sort, two orderBy views and groupBy Persona.
View IPYNB Source
// CODE_RUNNER: Hack 3: build the roster and print natural sort, two orderBy views and groupBy Persona.
import java.util.*;
/** FantasyStudents: driver that shows the enhanced FantasyStudent output. */
public class FantasyStudents {
public static Catalog<FantasyStudent> catalog() {
// Reference data, each pulled from its own master catalog
Catalog<Pathway> pathways = Pathway.catalog();
Catalog<Standing> standings = Standing.catalog();
Catalog<FantasyPersona> personas = FantasyPersona.catalog();
// Persona indexes: 0 Speedster, 1 Powerhouse, 2 Mastermind, 3 Visionary
Catalog<FantasyStudent> roster = new Catalog<>();
roster.add(new FantasyStudent("Thomas Edison", standings.getItems().get(3), pathways.getItems().get(3), personas.getItems().get(2)));
roster.add(new FantasyStudent("Nikola Tesla", standings.getItems().get(3), pathways.getItems().get(2), personas.getItems().get(3)));
roster.add(new FantasyStudent("Grace Hopper", standings.getItems().get(2), pathways.getItems().get(3), personas.getItems().get(2)));
roster.add(new FantasyStudent("Marie Curie", standings.getItems().get(2), pathways.getItems().get(1), personas.getItems().get(1)));
roster.add(new FantasyStudent("Alan Turing", standings.getItems().get(2), pathways.getItems().get(1), personas.getItems().get(3)));
roster.add(new FantasyStudent("Hedy Lamarr", standings.getItems().get(2), pathways.getItems().get(2), personas.getItems().get(0)));
roster.add(new FantasyStudent("isaac Newton", standings.getItems().get(1), pathways.getItems().get(0), personas.getItems().get(1)));
roster.add(new FantasyStudent("Bill Gates", standings.getItems().get(0), pathways.getItems().get(0), personas.getItems().get(0)));
return roster;
}
public static void main(String[] args) {
Catalog<FantasyStudent> roster = catalog();
Registry registry = new Registry(roster);
System.out.println("=== 1. FantasyStudent Catalog Display (insertion order) ===");
roster.display();
System.out.println("\n=== 2. Natural Sorting (Name, case-insensitive) ===");
registry.sortNatural().forEach(System.out::println);
System.out.println("\n=== 3a. orderBy Pathway Year ===");
registry.orderBy("Pathway Year").forEach(System.out::println);
System.out.println("\n=== 3b. orderBy Standing Weight ===");
registry.orderBy("Standing Weight").forEach(System.out::println);
System.out.println("\n=== 4. Grouping by Persona ===");
registry.groupBy("Persona").forEach((persona, members) -> {
System.out.println("Persona: " + persona + " (" + members.size() + ")");
members.forEach(m -> System.out.println(" - " + m.getValue("Name") + " | " + m.getValue("Standing") + " | Year " + m.getValue("Pathway Year")));
});
}
}
FantasyStudents.main(null);
Self-check for Hacks 1-3
Every line should say PASS.
Code Runner Challenge
Self-check: run after Hacks 1-3. Every line should say PASS.
View IPYNB Source
// CODE_RUNNER: Self-check: run after Hacks 1-3. Every line should say PASS.
import java.util.*;
/** One self-check for Hacks 1-3. Every line should say PASS. */
public class HackChecks {
static void check(String label, boolean ok) { System.out.println((ok ? "PASS" : "FAIL") + " - " + label); }
public static void main(String[] args) {
// ---- Hack 1: FantasyPersona
Catalog<FantasyPersona> personas = FantasyPersona.catalog();
check("Hack 1: catalog has 4 personas", personas.getItems().size() == 4);
check("Hack 1: extends AbstractCollectable", personas.getItems().get(0) instanceof AbstractCollectable);
check("Hack 1: keys are Persona, Superpower, Power Level",
personas.getItems().get(0).getKeys().equals(List.of("Persona", "Superpower", "Power Level")));
List<Collectable> byPower = new Registry(personas).sortNatural();
boolean desc = true;
for (int i = 1; i < byPower.size(); i++)
if ((Integer) byPower.get(i - 1).getValue("Power Level") < (Integer) byPower.get(i).getValue("Power Level")) desc = false;
check("Hack 1: natural order is strongest first", desc);
// ---- Hack 2: FantasyStudent
FantasyPersona power = new FantasyPersona("Powerhouse", "Super strength", 9);
FantasyStudent isaac = new FantasyStudent("isaac Newton", new Standing("Sophomore", 1),
new Pathway(0, "Fundamental", "Solo", "Remembering", "Passive"), power);
FantasyStudent marie = new FantasyStudent("Marie Curie", new Standing("Junior", 2),
new Pathway(1, "Project", "Pair (2-3)", "Analyzing", "Self-starter"), power);
check("Hack 2: persona attributes exposed in getMap", isaac.getValue("Persona").equals("Powerhouse")
&& isaac.getValue("Power Level").equals(9) && isaac.getKeys().contains("Superpower"));
check("Hack 2: standing/pathway still exposed", isaac.getValue("Pathway Year").equals(0));
check("Hack 2: compareTo ignores case (isaac before Marie)", isaac.compareTo(marie) < 0);
// ---- Hack 3: FantasyStudents roster + registry
Catalog<FantasyStudent> roster = FantasyStudents.catalog();
Registry registry = new Registry(roster);
check("Hack 3: at least 4 students", roster.getItems().size() >= 4);
check("Hack 3: groupBy Persona makes 3+ groups", registry.groupBy("Persona").size() >= 3);
check("Hack 3: orderBy moves the key to column 1",
registry.orderBy("Pathway Year").get(0).getKeys().get(0).equals("Pathway Year"));
List<Collectable> sorted = registry.sortNatural();
boolean ok = true;
for (int i = 1; i < sorted.size(); i++)
if (((String) sorted.get(i - 1).getValue("Name")).compareToIgnoreCase((String) sorted.get(i).getValue("Name")) > 0) ok = false;
check("Hack 3: natural sort is alphabetical", ok);
}
}
HackChecks.main(null);
Hack 4: Boundary tests (15 min)
Write a check(label, condition) helper and test: empty catalog, one item, duplicates (stable order, one group), missing key (sortByField keeps order, groupBy makes a null group) and case-insensitive natural order. Finish with Summary: X passed, Y failed. Goal: 0 failed.
Code Runner Challenge
Hack 4: implement check(...) and test the five edge-case categories. Goal: 0 failed.
View IPYNB Source
// CODE_RUNNER: Hack 4: implement check(...) and test the five edge-case categories. Goal: 0 failed.
import java.util.*;
/** EdgeCases: tiny test harness (no JUnit needed) for Registry + FantasyStudent invariants. */
public class EdgeCases {
static int passed = 0, failed = 0;
static void check(String label, boolean ok) {
System.out.println((ok ? "PASS" : "FAIL") + " - " + label);
if (ok) passed++; else failed++;
}
static List<String> names(List<? extends Collectable> list) {
List<String> out = new ArrayList<>();
for (Collectable c : list) out.add((String) c.getValue("Name"));
return out;
}
public static void main(String[] args) {
Standing senior = new Standing("Senior", 3);
Pathway year3 = new Pathway(3, "Research", "Team (~10)", "Creating", "Driver");
FantasyPersona speedster = new FantasyPersona("Speedster", "Super speed", 7);
// ---- 1. Empty catalog
Registry empty = new Registry(new Catalog<FantasyStudent>());
check("empty: sortNatural is empty", empty.sortNatural().isEmpty());
check("empty: sortByField is empty", empty.sortByField("Name").isEmpty());
check("empty: orderBy is empty", empty.orderBy("Pathway Year").isEmpty());
check("empty: groupBy is empty", empty.groupBy("Persona").isEmpty());
// ---- 2. Singleton
Catalog<FantasyStudent> one = new Catalog<>();
one.add(new FantasyStudent("Solo", senior, year3, speedster));
Registry single = new Registry(one);
check("singleton: natural sort has 1 item", single.sortNatural().size() == 1);
check("singleton: exactly 1 group", single.groupBy("Persona").size() == 1);
// ---- 3. Duplicates (equal keys + equal names)
Catalog<FantasyStudent> dup = new Catalog<>();
dup.add(new FantasyStudent("Twin", senior, year3, speedster)); // inserted first
dup.add(new FantasyStudent("Twin", senior, year3, speedster)); // inserted second
dup.add(new FantasyStudent("Alpha", new Standing("Freshman", 0), new Pathway(0, "Fundamental", "Solo", "Remembering", "Passive"), speedster));
Registry dups = new Registry(dup);
List<Collectable> byYear = dups.sortByField("Pathway Year");
check("duplicates: lowest year first", "Alpha".equals(byYear.get(0).getValue("Name")));
check("duplicates: stable sort keeps both Twins", names(byYear).equals(List.of("Alpha", "Twin", "Twin")));
check("duplicates: same persona -> one group of 3", dups.groupBy("Persona").get("Speedster").size() == 3);
check("duplicates: equal names compare to 0", dup.getItems().get(0).compareTo(dup.getItems().get(1)) == 0);
// ---- 4. Missing attribute key
Registry roster = new Registry(FantasyStudents.catalog());
List<String> original = names(FantasyStudents.catalog().getItems());
check("missing key: sortByField leaves order unchanged", names(roster.sortByField("Nope")).equals(original));
Map<Object, List<Collectable>> missing = roster.groupBy("Nope");
check("missing key: groupBy -> single null group", missing.size() == 1 && missing.containsKey(null));
check("missing key: null group has all 8", missing.get(null).size() == 8);
Collectable first = roster.orderBy("Nope").get(0);
check("missing key: orderBy keeps key order", first.getKeys().equals(FantasyStudents.catalog().getItems().get(0).getKeys()));
// ---- 5. Natural order is case-insensitive (the isaac Newton bug)
List<String> expected = List.of("Alan Turing", "Bill Gates", "Grace Hopper", "Hedy Lamarr",
"isaac Newton", "Marie Curie", "Nikola Tesla", "Thomas Edison");
check("natural order ignores case", names(roster.sortNatural()).equals(expected));
// ---- 6. Projection invariants
Collectable top = roster.orderBy("Pathway Year").get(0);
check("orderBy: sort key is the first column", top.getKeys().get(0).equals("Pathway Year"));
check("orderBy: projection loses no keys", top.getKeys().size() == FantasyStudents.catalog().getItems().get(0).getKeys().size());
check("persona keys exposed on student", FantasyStudents.catalog().getItems().get(0).getKeys().containsAll(List.of("Persona", "Superpower", "Power Level")));
System.out.println("\nSummary: " + passed + " passed, " + failed + " failed");
}
}
EdgeCases.main(null);
Reflection (2-3 sentences each)
- Why is
Collectablean interface butAbstractCollectablean abstract class? - What are the pros and cons of
map.putAll(persona.getMap())inFantasyStudent.getMap()? getValue(key)callsgetMap()every time. What does that cost when sorting, and how could you reduce it?
Answers
1. The interface is the contract: any class can promise getMap/getKeys/getValue/compareTo, so Registry treats every record polymorphically. The abstract class is shared code: getKeys, getValue and toString all derive from getMap(), so subclasses write only getMap() and compareTo().
2. Pros: no duplicate bookkeeping; a new persona field shows up in FantasyStudent and in groupBy automatically. Cons: the student’s keys are now controlled by another class, and two maps using the same key (e.g. "Name") would overwrite each other, which is why the persona’s key is "Persona".
3. Each getValue builds a new LinkedHashMap, so a sort costs about O(n log n x fields) with many short-lived objects. Reduce it by caching the map, switching on the key for hot fields, or extracting sort keys once before sorting.
Grading plan (1.0 point)
| Item | Points |
|—|—|
| Popcorn Hack | 0.20 |
| MCQ Check | 0.20 |
| Hack 1: FantasyPersona | 0.15 |
| Hack 2: FantasyStudent | 0.15 |
| Hack 3: FantasyStudents output + self-check | 0.20 |
| Hack 4: edge-case tests | 0.10 |
| Total | 1.00 |
Quick validation: every runner executes; the self-check prints only PASS; EdgeCases prints 0 failed; original Students and new FantasyStudents output both visible; every runner saved (💾).
LxD Cycle Process
Empathize: students must model data in Java before moving it through Spring (POJO to JPA to API). Define: CSA students need interfaces, abstract classes and inheritance to build reusable data types; goal: store, manage and present a collection by sorting, grouping and ordering. Ideate: how do we manage cognitive load using meaningful OCS examples? Prototype: a simple data set (Animal) plus an OCS data set (Student / FantasyStudent). Test: evidence comes from runner output, the self-checks, the MCQ, AI grading of submitted code, and the reflection.
References
- Mortensen, J. (2026, Oct 18). Abstract Collectables & Dynamic Registries. Open Coding Society, CS112/CS113.
- Oracle. Java SE 21 API documentation:
Comparable,Comparator,Map.computeIfAbsent.
Submit Assignment
Use Submit Code on the OCS page to send your saved runners for automatic review, add optional notes, and check the submissions dashboard. (The button comes from the OCS page template.)
Notes (optional): ________
Submit Assignment
Need to update a submission later? Open the submissions dashboard.