{"id":"grouping-fast-lookups","slug":"fast-lookups","title":"Fast lookups, and the end of part 8","text":"Before part 8, finding a value meant searching a list or an array. Module 28's linear search looks at the elements one at a time, and so do a list's `contains` and `indexOf`, which compare the value with each element using `equals`. To find a value near the end of a list of a million elements, or to find out that it isn't there at all, they make about a million comparisons. Binary search needs far fewer, but only on data that's kept sorted. A hash map needs neither: module 32 said that it finds a key about as fast among a million keys as among ten. This step shows that happen, and then ends part 8.\n\n### Where the map looks\n\nAs module 32 showed, a hash map keeps its entries in many small groups, called **buckets**, and the key's hash code decides which bucket an entry goes into. `get` and `containsKey` work out the hash code of the key they're given, go straight to that bucket, and look only at the keys there. They call `equals` only when two hash codes are the same. The rest of the map isn't looked at.\n\nWhat keeps this fast as the map grows is that Java's `HashMap` adds more buckets as it fills up, and spreads its entries out again, so that each bucket keeps only a few entries. Twice as many keys means about twice as many buckets, not twice as many keys in each bucket.\n\n### Seeing the difference\n\nA small experiment shows it. A `Card` is a student card with a number. Its `equals` compares the numbers, the way modules 23 and 32 wrote it, and its `hashCode` returns the number as it is, the way module 32's Seat used its seat number. For this experiment only, `equals` also counts how many times it's called. The program puts the same cards in a list and in a map, and then looks for the last card in both, each time with a new Card object that has its number:\n\n```java\nimport java.util.ArrayList;\nimport java.util.HashMap;\n\npublic class Main {\n    public static void main(String[] args) {\n        experiment(100);\n        experiment(1000);\n        experiment(10000);\n    }\n\n    public static void experiment(int size) {\n        ArrayList<Card> list = new ArrayList<>();\n        HashMap<Card, Integer> map = new HashMap<>();\n        for (int i = 1; i <= size; i++) {\n            Card card = new Card(i);\n            list.add(card);\n            map.put(card, i);\n        }\n\n        Card wanted = new Card(size);\n        Card again = new Card(size);\n        if (list.contains(wanted) && map.containsKey(again)) {\n            System.out.println(size + \" cards: list \" + wanted.getComparisons() + \", map \" + again.getComparisons());\n        }\n    }\n}\n// ==== Card.java ====\npublic class Card {\n    private int number;\n    private int comparisons;\n\n    public Card(int number) {\n        this.number = number;\n        this.comparisons = 0;\n    }\n\n    public int getComparisons() {\n        return this.comparisons;\n    }\n\n    public boolean equals(Object compared) {\n        this.comparisons++;\n        if (this == compared) {\n            return true;\n        }\n        if (!(compared instanceof Card)) {\n            return false;\n        }\n        Card other = (Card) compared;\n        return this.number == other.number;\n    }\n\n    public int hashCode() {\n        return this.number;\n    }\n}\n```\n\n```output\n100 cards: list 100, map 1\n1000 cards: list 1000, map 1\n10000 cards: list 10000, map 1\n```\n\n- Why do the counts end up in `wanted` and `again`? `contains` and `containsKey` call `equals` on the card you give them, with a card from the collection as the parameter. So every comparison adds one to the searched-for card's count.\n- `list.contains(wanted)` compared the card with every card in the list before it reached the last one. Ten times the cards, ten times the comparisons.\n- `map.containsKey(again)` went straight to the right bucket, and called `again.equals(...)` once, with the stored card that has the same number. Here that's one comparison at every size.\n- Comparisons aren't the whole cost. Working out a hash code takes a moment too (a String's hash code goes through all its characters), but that work depends on the key, not on how many keys the map holds.\n\nThis relies on good hash codes. If many keys had the same hash code, they'd all land in the same bucket, and finding one of them would mean comparing it with many of the others: the map would lose much of its speed. That's why module 32's Seat multiplies the row's hash code by 31 before it adds the number: without the 31, B7, C6 and D5 would all share one hash code. Card's hash code is its number, so no two cards with different numbers share one. Java's own String and Integer already have good hash codes.\n\n### A list or a map?\n\nSpeed isn't the only thing that matters. Each collection is good at different things:\n\n| What the program needs | Use |\n| --- | --- |\n| Things in order, and each one by its place: the first, the last, the one at index 3 | a list |\n| The same value more than once, like every card scanned at a desk during a day | a list |\n| A thing found by a key, like a student by card number | a map |\n| One value for each key, where a new value replaces the old one | a map |\n| Counts or groups by a key | a map, with Integer or list values |\n\nA map pays for its speed: it keeps each key only once, and it goes through its keys in an order of its own.\n\nOften a program uses both. A list keeps the objects in the order they came, and a map finds them by a key. Building the map takes one loop over the list. After that, every lookup goes straight to its key, however many lookups there are.\n\n### The end of part 8\n\nThis step ends the material of the MOOC's eighth part, the first part of Java Programming II. It started with a recap of parts 1 to 7, and then brought the hash map, which finds a value by its key. Module 32 showed how Java decides that two objects are the same, with `equals` and `hashCode`, so that objects of your own classes can be keys too. This module put lists in a map to group data, and showed why finding a key stays fast however big the map gets. Next, part 9 brings inheritance and interfaces. Part 8 closes with [the MOOC's page on fast data fetching and grouping information](https://java-programming.mooc.fi/part-8/5-fast-data-fetching-and-grouping-information) on its own site, for reading only.\n","task":"A school library's desk scans student cards all day. `Student.java` is ready: a student has a card number and a name, with the getters `getCard()` and `getName()`. In `Main.java`, write the method `namesFor(ArrayList<Student> students, ArrayList<String> scans)`. It returns a new list with the name of the student for each scanned card, in the order of the scans, and `unknown` for a card that no student has. Build a `HashMap` from card number to student once, and then look up each scan in it. The check runs:\n\n```java\nArrayList<Student> students = new ArrayList<>();\nstudents.add(new Student(\"S-104\", \"Amira\"));\nstudents.add(new Student(\"S-231\", \"Jonas\"));\nstudents.add(new Student(\"S-087\", \"Priya\"));\nArrayList<String> scans = new ArrayList<>();\nscans.add(\"S-231\");\nscans.add(\"S-500\");\nscans.add(\"S-104\");\nscans.add(\"S-231\");\nSystem.out.println(namesFor(students, scans));\n```\n\nand expects:\n\n```\n[Jonas, unknown, Amira, Jonas]\n```\n","kind":"code","seed":"// ==== Main.java ====\nimport java.util.ArrayList;\nimport java.util.HashMap;\n\npublic class Main {\n    public static void main(String[] args) {\n        ArrayList<Student> students = new ArrayList<>();\n        students.add(new Student(\"S-104\", \"Amira\"));\n        students.add(new Student(\"S-231\", \"Jonas\"));\n        students.add(new Student(\"S-087\", \"Priya\"));\n        ArrayList<String> scans = new ArrayList<>();\n        scans.add(\"S-231\");\n        scans.add(\"S-500\");\n        scans.add(\"S-104\");\n        scans.add(\"S-231\");\n        System.out.println(namesFor(students, scans));\n    }\n\n    // Write the method namesFor here.\n}\n\n// ==== Student.java ====\npublic class Student {\n    private String card;\n    private String name;\n\n    public Student(String card, String name) {\n        this.card = card;\n        this.name = name;\n    }\n\n    public String getCard() {\n        return this.card;\n    }\n\n    public String getName() {\n        return this.name;\n    }\n\n    public String toString() {\n        return this.name + \" (\" + this.card + \")\";\n    }\n}\n","solution":"// ==== Main.java ====\nimport java.util.ArrayList;\nimport java.util.HashMap;\n\npublic class Main {\n    public static void main(String[] args) {\n        ArrayList<Student> students = new ArrayList<>();\n        students.add(new Student(\"S-104\", \"Amira\"));\n        students.add(new Student(\"S-231\", \"Jonas\"));\n        students.add(new Student(\"S-087\", \"Priya\"));\n        ArrayList<String> scans = new ArrayList<>();\n        scans.add(\"S-231\");\n        scans.add(\"S-500\");\n        scans.add(\"S-104\");\n        scans.add(\"S-231\");\n        System.out.println(namesFor(students, scans));\n    }\n\n    public static ArrayList<String> namesFor(ArrayList<Student> students, ArrayList<String> scans) {\n        HashMap<String, Student> byCard = new HashMap<>();\n        for (Student student : students) {\n            byCard.put(student.getCard(), student);\n        }\n\n        ArrayList<String> names = new ArrayList<>();\n        for (String card : scans) {\n            Student student = byCard.get(card);\n            if (student == null) {\n                names.add(\"unknown\");\n            } else {\n                names.add(student.getName());\n            }\n        }\n        return names;\n    }\n}\n\n// ==== Student.java ====\npublic class Student {\n    private String card;\n    private String name;\n\n    public Student(String card, String name) {\n        this.card = card;\n        this.name = name;\n    }\n\n    public String getCard() {\n        return this.card;\n    }\n\n    public String getName() {\n        return this.name;\n    }\n\n    public String toString() {\n        return this.name + \" (\" + this.card + \")\";\n    }\n}\n","hints":["First build the map, with one loop over the students: `HashMap<String, Student> byCard = new HashMap<>();`, and `byCard.put(student.getCard(), student);` for each one.","Then go through the scans. `byCard.get(card)` gives the student, or `null` for a card no student has: add `\"unknown\"` then, and the student's `getName()` otherwise. Return the list of names at the end."],"tests":[{"name":"Test 1","stdin":"","call":"ArrayList<Student> students = new ArrayList<>();\nstudents.add(new Student(\"S-104\", \"Amira\"));\nstudents.add(new Student(\"S-231\", \"Jonas\"));\nstudents.add(new Student(\"S-087\", \"Priya\"));\nArrayList<String> scans = new ArrayList<>();\nscans.add(\"S-231\");\nscans.add(\"S-500\");\nscans.add(\"S-104\");\nscans.add(\"S-231\");\nSystem.out.println(namesFor(students, scans));\n","expect":"[Jonas, unknown, Amira, Jonas]","hidden":false},{"name":"Test 2","stdin":"","call":"ArrayList<Student> students = new ArrayList<>();\nfor (int i = 1; i <= 500; i++) {\n    students.add(new Student(\"S-\" + i, \"Student \" + i));\n}\nArrayList<String> scans = new ArrayList<>();\nscans.add(\"S-377\");\nscans.add(\"S-0\");\nscans.add(\"S-500\");\nscans.add(\"s-12\");\nscans.add(\"S-1\");\nSystem.out.println(namesFor(students, scans));\nSystem.out.println(students.size() + \" \" + scans.size());\n","expect":"[Student 377, unknown, Student 500, unknown, Student 1]\n500 5","hidden":true},{"name":"Test 3","stdin":"","call":"ArrayList<Student> students = new ArrayList<>();\nArrayList<String> scans = new ArrayList<>();\nSystem.out.println(namesFor(students, scans));\nscans.add(\"K-9\");\nscans.add(\"K-9\");\nSystem.out.println(namesFor(students, scans));\nstudents.add(new Student(\"K-9\", \"Mei\"));\nSystem.out.println(namesFor(students, scans));\n","expect":"[]\n[unknown, unknown]\n[Mei, Mei]","hidden":true}],"require":[{"pattern":"\\bpublic\\s+static\\s+ArrayList\\s*<\\s*String\\s*>\\s+namesFor\\s*\\(\\s*ArrayList\\s*<\\s*Student\\s*>\\s+\\w+\\s*,\\s*ArrayList\\s*<\\s*String\\s*>\\s+\\w+\\s*\\)","message":"Write the method as `public static ArrayList<String> namesFor(ArrayList<Student> students, ArrayList<String> scans)`."},{"pattern":"\\bHashMap\\s*<","message":"Build a `HashMap` from card number to student once, and look up each scan in it."}],"forbid":[],"more":[{"task":"The library desk keeps every scan in a list, counts the scans of each card in a map, and remembers, in a second map, the index of each card's latest scan. What does this program print?\n","kind":"predict","seed":"import java.util.ArrayList;\nimport java.util.HashMap;\n\npublic class Main {\n    public static void main(String[] args) {\n        String[] scans = {\"S-104\", \"S-231\", \"S-104\", \"S-087\", \"S-104\"};\n        ArrayList<String> history = new ArrayList<>();\n        HashMap<String, Integer> counts = new HashMap<>();\n        HashMap<String, Integer> lastScan = new HashMap<>();\n        for (int i = 0; i < scans.length; i++) {\n            history.add(scans[i]);\n            counts.put(scans[i], counts.getOrDefault(scans[i], 0) + 1);\n            lastScan.put(scans[i], i);\n        }\n        System.out.println(history.size() + \" \" + counts.size());\n        System.out.println(history.get(2) + \" \" + counts.get(\"S-104\"));\n        System.out.println(history.indexOf(\"S-104\") + \" \" + lastScan.get(\"S-104\"));\n        System.out.println(history.contains(\"S-500\") + \" \" + counts.containsKey(\"S-087\"));\n    }\n}\n","solution":"import java.util.ArrayList;\nimport java.util.HashMap;\n\npublic class Main {\n    public static void main(String[] args) {\n        String[] scans = {\"S-104\", \"S-231\", \"S-104\", \"S-087\", \"S-104\"};\n        ArrayList<String> history = new ArrayList<>();\n        HashMap<String, Integer> counts = new HashMap<>();\n        HashMap<String, Integer> lastScan = new HashMap<>();\n        for (int i = 0; i < scans.length; i++) {\n            history.add(scans[i]);\n            counts.put(scans[i], counts.getOrDefault(scans[i], 0) + 1);\n            lastScan.put(scans[i], i);\n        }\n        System.out.println(history.size() + \" \" + counts.size());\n        System.out.println(history.get(2) + \" \" + counts.get(\"S-104\"));\n        System.out.println(history.indexOf(\"S-104\") + \" \" + lastScan.get(\"S-104\"));\n        System.out.println(history.contains(\"S-500\") + \" \" + counts.containsKey(\"S-087\"));\n    }\n}\n","hints":["The list keeps every scan, so it has 5 elements. Each map keeps every card once, as a key: there are 3 different cards.","`indexOf` finds the first scan of S-104. In `lastScan`, each new scan of a card replaces the index stored before, so the map keeps the latest one."],"tests":[{"name":"Output","stdin":"","expect":"5 3\nS-104 3\n0 4\nfalse true","hidden":false}],"require":[],"forbid":[],"lines":["5 3","S-104 3","0 4","false true"]},{"task":"To end part 8: a photo library. `Photo.java` is ready: a photo has a file name, a place and a year, with the getters `getFile()`, `getPlace()` and `getYear()`, and its `toString` gives the file name and the year. File names are unique. In `PhotoLibrary.java`, write:\n\n- the constructor,\n- `add(Photo photo)`, which adds a photo to the library,\n- `find(String file)`, which returns the photo with that file name, or `null` if there's none,\n- `photosAt(String place)`, which returns a list of the photos taken at that place, in the order they were added, or an empty list for a place with no photos,\n- `places()`, which returns a new list of the places that have photos, in alphabetical order.\n\nKeep the photos in two hash maps: one from a file name to its photo, so that `find` goes straight to it, and one from a place to the list of its photos. `add` puts each photo in both. `Main.java` is a test drive, and it prints:\n\n```\n[Bergen, Hanoi, Namib]\n[dunes.jpg (2023), sunset.jpg (2024)]\nfjord.jpg (2022)\nnull\n0\n```\n","kind":"code","seed":"// ==== PhotoLibrary.java ====\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.HashMap;\n\npublic class PhotoLibrary {\n\n    public PhotoLibrary() {\n\n    }\n\n    public void add(Photo photo) {\n\n    }\n\n    public Photo find(String file) {\n        return null;\n    }\n\n    public ArrayList<Photo> photosAt(String place) {\n        return null;\n    }\n\n    public ArrayList<String> places() {\n        return null;\n    }\n}\n\n// ==== Photo.java ====\npublic class Photo {\n    private String file;\n    private String place;\n    private int year;\n\n    public Photo(String file, String place, int year) {\n        this.file = file;\n        this.place = place;\n        this.year = year;\n    }\n\n    public String getFile() {\n        return this.file;\n    }\n\n    public String getPlace() {\n        return this.place;\n    }\n\n    public int getYear() {\n        return this.year;\n    }\n\n    public String toString() {\n        return this.file + \" (\" + this.year + \")\";\n    }\n}\n\n// ==== Main.java ====\npublic class Main {\n    public static void main(String[] args) {\n        PhotoLibrary library = new PhotoLibrary();\n        library.add(new Photo(\"dunes.jpg\", \"Namib\", 2023));\n        library.add(new Photo(\"wharf.jpg\", \"Bergen\", 2021));\n        library.add(new Photo(\"sunset.jpg\", \"Namib\", 2024));\n        library.add(new Photo(\"fjord.jpg\", \"Bergen\", 2022));\n        library.add(new Photo(\"market.jpg\", \"Hanoi\", 2024));\n        System.out.println(library.places());\n        System.out.println(library.photosAt(\"Namib\"));\n        System.out.println(library.find(\"fjord.jpg\"));\n        System.out.println(library.find(\"glacier.jpg\"));\n        System.out.println(library.photosAt(\"Lima\").size());\n    }\n}\n","solution":"// ==== PhotoLibrary.java ====\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.HashMap;\n\npublic class PhotoLibrary {\n    private HashMap<String, Photo> byFile;\n    private HashMap<String, ArrayList<Photo>> byPlace;\n\n    public PhotoLibrary() {\n        this.byFile = new HashMap<>();\n        this.byPlace = new HashMap<>();\n    }\n\n    public void add(Photo photo) {\n        this.byFile.put(photo.getFile(), photo);\n        String place = photo.getPlace();\n        if (!this.byPlace.containsKey(place)) {\n            this.byPlace.put(place, new ArrayList<>());\n        }\n        this.byPlace.get(place).add(photo);\n    }\n\n    public Photo find(String file) {\n        return this.byFile.get(file);\n    }\n\n    public ArrayList<Photo> photosAt(String place) {\n        return this.byPlace.getOrDefault(place, new ArrayList<>());\n    }\n\n    public ArrayList<String> places() {\n        ArrayList<String> places = new ArrayList<>(this.byPlace.keySet());\n        Collections.sort(places);\n        return places;\n    }\n}\n\n// ==== Photo.java ====\npublic class Photo {\n    private String file;\n    private String place;\n    private int year;\n\n    public Photo(String file, String place, int year) {\n        this.file = file;\n        this.place = place;\n        this.year = year;\n    }\n\n    public String getFile() {\n        return this.file;\n    }\n\n    public String getPlace() {\n        return this.place;\n    }\n\n    public int getYear() {\n        return this.year;\n    }\n\n    public String toString() {\n        return this.file + \" (\" + this.year + \")\";\n    }\n}\n\n// ==== Main.java ====\npublic class Main {\n    public static void main(String[] args) {\n        PhotoLibrary library = new PhotoLibrary();\n        library.add(new Photo(\"dunes.jpg\", \"Namib\", 2023));\n        library.add(new Photo(\"wharf.jpg\", \"Bergen\", 2021));\n        library.add(new Photo(\"sunset.jpg\", \"Namib\", 2024));\n        library.add(new Photo(\"fjord.jpg\", \"Bergen\", 2022));\n        library.add(new Photo(\"market.jpg\", \"Hanoi\", 2024));\n        System.out.println(library.places());\n        System.out.println(library.photosAt(\"Namib\"));\n        System.out.println(library.find(\"fjord.jpg\"));\n        System.out.println(library.find(\"glacier.jpg\"));\n        System.out.println(library.photosAt(\"Lima\").size());\n    }\n}\n","hints":["Two instance variables: `private HashMap<String, Photo> byFile;` and `private HashMap<String, ArrayList<Photo>> byPlace;`, both created in the constructor. `find` is then one line, `return this.byFile.get(file);`.","`add` puts the photo in `byFile` under `photo.getFile()`, and on its place's list in `byPlace`, creating the list for the place's first photo. `photosAt` and `places` work like `dishesAt` and `tables` in step 3."],"tests":[{"name":"The test drive","stdin":"","expect":"[Bergen, Hanoi, Namib]\n[dunes.jpg (2023), sunset.jpg (2024)]\nfjord.jpg (2022)\nnull\n0","hidden":false},{"name":"Test 2","stdin":"","call":"PhotoLibrary library = new PhotoLibrary();\nSystem.out.println(library.places() + \" \" + library.photosAt(\"Oslo\") + \" \" + library.find(\"a.jpg\"));\nlibrary.add(new Photo(\"a.jpg\", \"Oslo\", 2020));\nlibrary.add(new Photo(\"b.jpg\", \"Cusco\", 2021));\nlibrary.add(new Photo(\"c.jpg\", \"Oslo\", 2019));\nlibrary.add(new Photo(\"d.jpg\", \"Accra\", 2022));\nlibrary.add(new Photo(\"e.jpg\", \"Zagreb\", 2023));\nSystem.out.println(library.places());\nSystem.out.println(library.photosAt(\"Oslo\") + \" \" + library.photosAt(\"Cusco\").size());\nSystem.out.println(library.find(\"d.jpg\").getPlace() + \" \" + library.find(\"z.jpg\"));\n","expect":"[] [] null\n[Accra, Cusco, Oslo, Zagreb]\n[a.jpg (2020), c.jpg (2019)] 1\nAccra null","hidden":true},{"name":"Test 3","stdin":"","call":"PhotoLibrary library = new PhotoLibrary();\nfor (int i = 1; i <= 300; i++) {\n    library.add(new Photo(\"img\" + i + \".jpg\", \"Place \" + (i % 4), 2000 + i % 20));\n}\nSystem.out.println(library.places());\nSystem.out.println(library.photosAt(\"Place 2\").size() + \" \" + library.photosAt(\"Place 1\").get(0));\nSystem.out.println(library.find(\"img250.jpg\").getPlace() + \" \" + library.find(\"img301.jpg\"));\n","expect":"[Place 0, Place 1, Place 2, Place 3]\n75 img1.jpg (2001)\nPlace 2 null","hidden":true}],"require":[{"pattern":"\\bHashMap\\s*<\\s*String\\s*,\\s*Photo\\s*>","message":"Keep a `HashMap<String, Photo>` from a file name to its photo, so that `find` goes straight to the photo."},{"pattern":"\\bHashMap\\s*<\\s*String\\s*,\\s*ArrayList\\s*<\\s*Photo\\s*>\\s*>","message":"Keep the photos of each place in a `HashMap<String, ArrayList<Photo>>`."}],"forbid":[{"pattern":"\\bclass\\s+PhotoLibrary\\b[\\s\\S]*\\bSystem\\s*\\.\\s*out\\b[\\s\\S]*\\bclass\\s+Photo\\b","message":"PhotoLibrary doesn't print. Its methods return their answers, and `main` prints them."}]}]}