Review Topics
- Inheritance
- Overriding
- Interface
- Generic class
Topic Coverage
- Bounded Type Parameter
- HashMap
- Optional
- Functional Interfaces
Problem Description
In this lab, we are going to see how we can useΒ OptionalΒ to simplify our tt, allowing us to focus on the core application logic instead of worrying about edge cases. This lab also involves using theΒ HashMapΒ class from Java Collections and more functional interfaces from Java:Β Consumer,Β Supplier, andΒ Runnable.
Task
Your task is to read in a roster of students, the modules they take, the assessments they have completed, and the grade for each assessment. Then, given a query consisting of a triplet: a student, a module, and an assessment, retrieve the corresponding grade.
For instance, if the input is:
Steve CS1010 Lab3 A Steve CS1231 Test A+ Bruce CS2030 Lab1 C
and the query isΒ Steve CS1231 Test, the program should printΒ A+.
In our scenario, a roster has zero or more students; a student takes zero or more modules, a module has zero or more assessments, and each assessment has exactly one grade. Each of these entities can be uniquely identified by a string.
Preamble
Map
Map<K,V>Β is a generic interface from the Java Collection Framework, the implementation of which is useful for storing a collection of items and retrieving an item. It maintains a map (aka dictionary) between keys (of typeΒ K) and values (of typeΒ V). The two core methods are (i)Β put, which stores a (key, value) pair into the map, and (ii)Β get, which returns the value associated with a given key if the key is found or returnsΒ nullΒ otherwise.
The following examples show how theΒ Map<K,V>Β interface and its implementationΒ HashMap<K,V>Β can be used.
jshell> Map<String,Integer> map = new HashMap<>();
jshell> map.put("one", 1);
$.. ==> null
jshell> map.put("two", 2);
$.. ==> null
jshell> map.put("three", 3);
$.. ==> null
jshell> map.get("one")
$.. ==> 1
jshell> map.get("four")
$.. ==> null
jshell> map.entrySet()
$.. ==> [one=1, two=2, three=3]
jshell> for (Map.Entry e: map.entrySet()) {
...> System.out.println(e.getKey() + ":" + e.getValue());
...> }
one:1
two:2
three:3
Level 1
AssessmentΒ class andΒ KeyableΒ interface
We shall start by writing theΒ AssessmentΒ class that implements the followingΒ KeyableΒ interface.
interface Keyable {
String getKey();
}
Include aΒ getGradeΒ method that returns the grade of an assessment.
jshell> new Assessment("Lab1", "B")
$.. ==> {Lab1: B}
jshell> new Assessment("Lab1", "B").getGrade()
$.. ==> "B"
jshell> new Assessment("Lab1", "B").getKey()
$.. ==> "Lab1"
jshell> /exit
Check the format correctness of the output by running the following on the command line:
$ javac -Xlint:rawtypes *.java $ jshell -q your_java_files_in_bottom-up_dependency_order < test1.jsh
Check your styling by issuing the following
$ checkstyle *.java
Level 2
ModuleΒ class
Write theΒ ModuleΒ class to store (via theΒ putΒ method) the assessments of a module in a map for easy retrieval as part of answering queries. A module can have zero or more assessments, with each assessment having a title as a key β a unique identifier.
jshell> new Module("CS2040")
$.. ==> CS2040: {}
jshell> new Module("CS2040").getKey();
$.. ==> "CS2040"
jshell> new Module("CS2040").put(new Assessment("Lab1", "B"))
$.. ==> CS2040: {{Lab1: B}}
jshell> new Module("CS2040").put(new Assessment("Lab1", "B")).put(new Assessment("Lab2","A+"))
$.. ==> CS2040: {{Lab1: B}, {Lab2: A+}}
jshell> new Module("CS2040").put(new Assessment("Lab1", "B")).put(new Assessment("Lab2","A+")).get("Lab1")
$.. ==> {Lab1: B}
jshell> new Module("CS2040").put(new Assessment("Lab1", "B")).put(new Assessment("Lab2","A+")).get("Lab2")
$.. ==> {Lab2: A+}
jshell> new Module("CS2040").put(new Assessment("Lab1", "B")).put(new Assessment("Lab2","A+")).get("Lab3")
$.. ==> null
jshell> /exit
Check the format correctness of the output by running the following on the command line:
$ javac -Xlint:rawtypes *.java $ jshell -q your_java_files_in_bottom-up_dependency_order < test2.jsh
Check your styling by issuing the following
$ checkstyle *.java
Level 3
StudentΒ class and generic classΒ KeyableMap
Write aΒ StudentΒ class that stores the modules he/she reads in a map via theΒ putΒ method. A student can read zero or more modules, with each module having a unique module code as its key. You will notice that the implementations of theΒ StudentΒ andΒ ModuleΒ classes are very similar. Hence, by applying theΒ abstraction principle, write a generic classΒ KeyableMapΒ to reduce the duplication.
Hint:Β KeyableMap<V>Β is a generic class that wraps around aΒ StringΒ key (i.e. implementsΒ Keyable) and aΒ Map<String,V>.Β KeyableMapΒ models an entity that contains a map, but is also itself contained in another container (e.g. a student contains a map of modules but could be contained in a roster). The parameter typeΒ VΒ is the type of the value of items stored in the map;Β VΒ must be a subtype ofΒ Keyable.
The classΒ KeyableMapΒ is a mutable class — we made this decision since theΒ MapΒ implementation in Java Collection Framework is mutable.Β KeyableMapΒ provides two core methods:
- get(String key)Β which returns the item with the givenΒ key;
- put(V item)Β which adds the key-value pair (item.getKey(),item) into the map. TheΒ putΒ method returns aΒ KeyableMap. To avoid type mismatch when chainingΒ putΒ method calls together, each sub-class ofΒ KeyableMapΒ should override theΒ putΒ method fromΒ KeyableMapΒ and change the return type to the corresponding sub-classes. E.g.,Β StudentΒ should overrideΒ putΒ to return aΒ StudentΒ through a (guaranteed to be safe) type casting.
jshell> new Module("CS2040").put(new Assessment("Lab1", "B")).get("Lab1")
$.. ==> {Lab1: B}
jshell> new Module("CS2040").put(new Assessment("Lab1", "B")).get("Lab1").getGrade()
$.. ==> "B"
jshell> new Student("Tony").put(new Module("CS2040").put(new Assessment("Lab1", "B")))
$.. ==> Tony: {CS2040: {{Lab1: B}}}
jshell> new Student("Tony").put(new Module("CS2040").put(new Assessment("Lab1", "B"))).get("CS2040")
$.. ==> CS2040: {{Lab1: B}}
jshell> Student natasha = new Student("Natasha");
jshell> natasha.put(new Module("CS2040").put(new Assessment("Lab1", "B")))
$.. ==> Natasha: {CS2040: {{Lab1: B}}}
jshell> natasha.put(new Module("CS2030").put(new Assessment("PE", "A+")).put(new Assessment("Lab2", "C")))
$.. ==> Natasha: {CS2030: {{Lab2: C}, {PE: A+}}, CS2040: {{Lab1: B}}}
jshell> Student tony = new Student("Tony");
jshell> tony.put(new Module("CS1231").put(new Assessment("Test", "A-")))
$.. ==> Tony: {CS1231: {{Test: A-}}}
jshell> tony.put(new Module("CS2100").put(new Assessment("Test", "B")).put(new Assessment("Lab1", "F")))
$.. ==> Tony: {CS1231: {{Test: A-}}, CS2100: {{Lab1: F}, {Test: B}}}
jshell> /exit
Check the format correctness of the output by running the following on the command line:
$ javac -Xlint:rawtypes *.java $ jshell -q your_java_files_in_bottom-up_dependency_order < test3.jsh
Check your styling by issuing the following
$ checkstyle *.java
Level 4
AvoidingΒ nullΒ withΒ Optional
So far we have been avoiding invalid retrievals such as
new Module("CS2040").put(new Assessment("Lab1", "B")).get("Lab2")
which would result in aΒ nullΒ value. Furthermore, chaining methods like
new Student("Tony").put(new Module("CS2040").put(new Assessment("Lab1", "B"))).get("CS2030" ).get("Lab1")
would result in aΒ NullPointerExceptionΒ being thrown due to callingΒ get("Lab1")Β on aΒ nullΒ value. Let’s deal with the generation ofΒ nullΒ values first, and worry about chainingΒ getΒ methods in the next level.
Take a look at theΒ Java documentation of OptionalΒ to familiarize yourself with the APIs available. Modify theΒ KeyableMapΒ generic class such that each call toΒ getΒ returns anΒ Optional<V>Β whereΒ VΒ is a subtype ofΒ Keyable.
jshell> new Module("CS2040").put(new Assessment("Lab1", "B")).get("Lab1")
$.. ==> Optional[{Lab1: B}]
jshell> new Student("Tony").put(new Module("CS2040").put(new Assessment("Lab1", "B")))
$.. ==> Tony: {CS2040: {{Lab1: B}}}
jshell> new Student("Tony").put(new Module("CS2040").put(new Assessment("Lab1", "B"))).get("CS2040")
$.. ==> Optional[CS2040: {{Lab1: B}}]
jshell> Student natasha = new Student("Natasha");
jshell> natasha.put(new Module("CS2040").put(new Assessment("Lab1", "B")))
$.. ==> Natasha: {CS2040: {{Lab1: B}}}
jshell> natasha.put(new Module("CS2030").put(new Assessment("PE", "A+")).put(new Assessment("Lab2", "C")))
$.. ==> Natasha: {CS2030: {{Lab2: C}, {PE: A+}}, CS2040: {{Lab1: B}}}
jshell> Student tony = new Student("Tony");
jshell> tony.put(new Module("CS1231").put(new Assessment("Test", "A-")))
$.. ==> Tony: {CS1231: {{Test: A-}}}
jshell> tony.put(new Module("CS2100").put(new Assessment("Test", "B")).put(new Assessment("Lab1", "F")))
$.. ==> Tony: {CS1231: {{Test: A-}}, CS2100: {{Lab1: F}, {Test: B}}}
jshell> /exit
As you can see, the only difference is that each value returned from an invocation of theΒ getΒ method is wrapped in anΒ Optional.
Check the format correctness of the output by running the following on the command line:
$ javac -Xlint:rawtypes *.java $ jshell -q your_java_files_in_bottom-up_dependency_order < test4.jsh
Check your styling by issuing the following
$ checkstyle *.java
Level 5
“Chaining”Β Optionals
ChainingΒ OptionalsΒ together as such
new Student("Tony").put(new Module("CS2040").put(new Assessment("Lab1", "B"))).get("CS2040" ).get("Lab1")
will now result in a compilation error instead. This is because theΒ OptionalΒ class does not have aΒ get(String)Β method defined (although it does define aΒ get()Β method which, other than for debugging purposes, should typically be avoided).
Rather than chaining in the usual way, we do it through aΒ mapΒ orΒ flatMap. Let’s start withΒ map.
jshell> new Module("CS2040").put(new Assessment("Lab1", "B")).get("Lab1")
$.. ==> Optional[{Lab1: B}]
jshell> new Module("CS2040").put(new Assessment("Lab1", "B")).get("Lab1").getGrade()
| Error:
| cannot find symbol
| symbol: method getGrade()
| new Module("CS2040").put(new Assessment("Lab1", "B")).get("Lab1").getGrade()
| ^------------------------------------------------------------------------^
jshell> new Module("CS2040").put(new Assessment("Lab1", "B")).get("Lab1").map(x -> x.getGrade())
$.. ==> Optional[B]
jshell> new Module("CS2040").put(new Assessment("Lab1", "B")).get("Lab1").map(Assessment::getGrade)
$.. ==> Optional[B]
As expected, invokingΒ getGrade()Β on anΒ OptionalΒ results in a compilation error. However, we can perform a similar chaining effect by passing in the functionality ofΒ getGradeΒ (either in the form of a lambda or method reference) toΒ Optional‘sΒ mapΒ method. Notice the return value is actually wrapped in anotherΒ Optional. When usingΒ map, you can think of the operation as “taking the value out of theΒ OptionalΒ box, transforming it via the function passed to map, and wrap the transformed value back in anotherΒ Optional“.
Now this is where things start to get interesting! Look at the following:
jshell> new Student("Tony").put(new Module("CS2040").put(new Assessment("Lab1", "B"))).get("CS2040" ).map(x -> x.get("Lab1"))
$.. ==> Optional[Optional[{Lab1: B}]]
Observe that the return value is anΒ OptionalΒ wrapped around anotherΒ OptionalΒ that wraps around the desired value! Why is this so? The difference lies in the return type ofΒ Assessment::getGradeΒ (readΒ getGradeΒ method of theΒ AssessmentΒ class) andΒ Module::get. The former returns aΒ String, while the latter returns anΒ Optional.
InΒ x -> x.getGrade()Β (orΒ Assessment::getGrade), the transformed value is simply the grade, and this is wrapped in an Optional. However, passingΒ x -> x.get("Lab1")Β in the above code snippet results in a transformed value ofΒ Optional. And this transformed value is wrapped around anotherΒ OptionalΒ via theΒ mapΒ operation!
As such, we use theΒ flatMapΒ method instead. You may think ofΒ flatMapΒ as flattening theΒ Optionals into a single one.
jshell> new Student("Tony").put(new Module("CS2040").put(new Assessment("Lab1", "B"))).get("CS2040" ).flatMap(x -> x.get("Lab1"))
$.. ==> Optional[{Lab1: B}]
jshell> new Student("Tony").put(new Module("CS2040").put(new Assessment("Lab1", "B"))).get("CS2040" ).flatMap(x -> x.get("Lab1")).map(Assessment::getGrade)
$.. ==> Optional[B]
Now you are ready to create a roster. Define aΒ RosterΒ class that stores the students in a map via theΒ putΒ method. A roster can have zero or more students, with each student having a unique id as its key. Once again, notice the similarities betweenΒ Roster,Β StudentΒ andΒ Module.
Define a method calledΒ getGradeΒ inΒ RosterΒ to answer the query from the user. The method takes in threeΒ StringΒ parameters, corresponds to the student id, the module code, and the assessment title, and returns the corresponding grade.
In cases where there are no such student, or the student does not read the given module, or the module does not have the corresponding assessment, then outputΒ No such recordΒ followed by details of the query. Here, you might findΒ Optional::orElseΒ useful.
jshell> Student natasha = new Student("Natasha");
jshell> natasha.put(new Module("CS2040").put(new Assessment("Lab1", "B")))
$.. ==> Natasha: {CS2040: {{Lab1: B}}}
jshell> natasha.put(new Module("CS2030").put(new Assessment("PE", "A+")).put(new Assessment("Lab2", "C")))
$.. ==> Natasha: {CS2030: {{Lab2: C}, {PE: A+}}, CS2040: {{Lab1: B}}}
jshell> Student tony = new Student("Tony");
jshell> tony.put(new Module("CS1231").put(new Assessment("Test", "A-")))
$.. ==> Tony: {CS1231: {{Test: A-}}}
jshell> tony.put(new Module("CS2100").put(new Assessment("Test", "B")).put(new Assessment("Lab1", "F")))
$.. ==> Tony: {CS1231: {{Test: A-}}, CS2100: {{Lab1: F}, {Test: B}}}
jshell> Roster roster = new Roster("AY1920").put(natasha).put(tony)
jshell> roster
roster ==> AY1920: {Natasha: {CS2030: {{Lab2: C}, {PE: A+}}, CS2040: {{Lab1: B}}}, Tony: {CS1231: {{Test: A-}}, CS2100: {{Lab1: F}, {Test: B}}}}
jshell> roster.get("Tony").flatMap(x -> x.get("CS1231")).flatMap(x -> x.get("Test")).map(Assessment::getGrade)
$.. ==> Optional[A-]
jshell> roster.get("Natasha").flatMap(x -> x.get("CS2040")).flatMap(x -> x.get("Lab1")).map(Assessment::getGrade)
$.. ==> Optional[B]
jshell> roster.get("Tony").flatMap(x -> x.get("CS1231")).flatMap(x -> x.get("Exam")).map(Assessment::getGrade)
$.. ==> Optional.empty
jshell> roster.get("Steve").flatMap(x -> x.get("CS1010")).flatMap(x -> x.get("Lab1")).map(Assessment::getGrade)
$.. ==> Optional.empty
jshell> roster.getGrade("Tony","CS1231","Test")
$.. ==> "A-"
jshell> roster.getGrade("Natasha","CS2040","Lab1")
$.. ==> "B"
jshell> roster.getGrade("Tony","CS1231","Exam");
$.. ==> "No such record: Tony CS1231 Exam"
jshell> roster.getGrade("Steve","CS1010","Lab1");
$.. ==> "No such record: Steve CS1010 Lab1"
jshell> /exit
Check the format correctness of the output by running the following on the command line:
$ javac -Xlint:rawtypes *.java $ jshell -q your_java_files_in_bottom-up_dependency_order < test5.jsh
Check your styling by issuing the following
$ checkstyle *.java
Level 6
TheΒ MainΒ class
Now use the classes that you have built and write aΒ MainΒ class to solve the following:
Read the following from the standard input:
- The first token read is an integerΒ N, indicating the number of records to be read.
- The subsequent inputs consist ofΒ NΒ records, each record consists of four words, separated by one or more spaces. The four words correspond to the student id, the module code, the assessment title, and the grade, respectively.
- The subsequent inputs consist of zero or more queries. Each query consists of three words, separated by one or more spaces. The three words correspond to the student id, the module code, and the assessment title.
For each query, if a match in the input is found, print the corresponding grade to the standard output. Otherwise, print “No Such Record:” followed by the three words given in the query, separated by exactly one space.
See sample input and output below. Inputs are underlined.
$ java Main 12 Jack CS2040 Lab4 B Jack CS2040 Lab6 C Jane CS1010 Lab1 A Jane CS2030 Lab1 A+ Janice CS2040 Lab1 A+ Janice CS2040 Lab4 A+ Jim CS1010 Lab9 A+ Jim CS2010 Lab1 C Jim CS2010 Lab2 B Jim CS2010 Lab8 A+ Joel CS2030 Lab3 C Joel CS2030 Midterm A Jack CS2040 Lab4 Jack CS2040 Lab6 Janice CS2040 Lab1 Janice CS2040 Lab4 Joel CS2030 Midterm Jason CS1010 Lab1 Jack CS2040 Lab5 Joel CS2040 Lab3 B C A+ A+ A No such record: Jason CS1010 Lab1 No such record: Jack CS2040 Lab5 No such record: Joel CS2040 Lab3




