{"id":"equals-hashcode-approximate","slug":"equal-enough","title":"Equal enough: ignoring some values or capitals","text":"What counts as equal is up to the class (module 23). In this module so far, equal has meant that every value matched. But module 30's ClimbingRoute already left the grade and the ascents out of `equals`, and Role, in the same module, left out the student. When such objects are hash map keys, `hashCode` must leave the same values out. The same goes for text that should count as equal whatever its capital letters: `hashCode` has to follow the same idea of equal as `equals`.\n\n### Comparing only some values\n\nA paint shop mixes colors from red, green and blue values, and each mix has a name. Customers call the same color by different names: \"Sky\" and \"Sky blue\" are both 135, 206, 235. For the shop that's one color, so `equals` compares the three numbers and leaves the name out, and so does `hashCode`:\n\n```java\nimport java.util.HashMap;\n\npublic class Main {\n    public static void main(String[] args) {\n        HashMap<Color, Integer> cans = new HashMap<>();\n        cans.put(new Color(\"Sky\", 135, 206, 235), 4);\n\n        Color asked = new Color(\"Sky blue\", 135, 206, 235);\n        System.out.println(asked.equals(new Color(\"Sky\", 135, 206, 235)));\n        System.out.println(cans.get(asked));\n        System.out.println(cans.containsKey(new Color(\"Sky\", 135, 206, 200)));\n    }\n}\n// ==== Color.java ====\nimport java.util.Objects;\n\npublic class Color {\n    private String name;\n    private int red;\n    private int green;\n    private int blue;\n\n    public Color(String name, int red, int green, int blue) {\n        this.name = name;\n        this.red = red;\n        this.green = green;\n        this.blue = blue;\n    }\n\n    public boolean equals(Object compared) {\n        if (this == compared) {\n            return true;\n        }\n        if (!(compared instanceof Color)) {\n            return false;\n        }\n        Color other = (Color) compared;\n        return this.red == other.red && this.green == other.green && this.blue == other.blue;\n    }\n\n    public int hashCode() {\n        return Objects.hash(this.red, this.green, this.blue);\n    }\n\n    public String toString() {\n        return this.name + \" (\" + this.red + \", \" + this.green + \", \" + this.blue + \")\";\n    }\n}\n```\n\n```output\ntrue\n4\nfalse\n```\n\n- `equals` leaves the name out, and so does `hashCode`. If `Objects.hash` got the name too, \"Sky\" and \"Sky blue\" would be equal but have different hash codes, and the map wouldn't find the cans. That's what happened to the used stamp in the last challenge of step 3.\n- In short: `hashCode` may use only what `equals` compares.\n\n### Ignoring capital letters\n\nA stamp collector types countries as they come: `Chile`, `chile`, `CHILE`. To count them as the same country, compare the countries in lower case (`toLowerCase`, module 16), and give `hashCode` the lower-case country too:\n\n```java\nimport java.util.HashMap;\n\npublic class Main {\n    public static void main(String[] args) {\n        HashMap<Stamp, String> album = new HashMap<>();\n        album.put(new Stamp(\"Chile\", 1970), \"page 4\");\n        System.out.println(album.get(new Stamp(\"CHILE\", 1970)));\n        System.out.println(album.containsKey(new Stamp(\"chile\", 1971)));\n        System.out.println(\"Chile\".hashCode() == \"CHILE\".hashCode());\n    }\n}\n// ==== Stamp.java ====\nimport java.util.Objects;\n\npublic class Stamp {\n    private String country;\n    private int year;\n\n    public Stamp(String country, int year) {\n        this.country = country;\n        this.year = year;\n    }\n\n    public boolean equals(Object compared) {\n        if (this == compared) {\n            return true;\n        }\n        if (!(compared instanceof Stamp)) {\n            return false;\n        }\n        Stamp other = (Stamp) compared;\n        return this.country.toLowerCase().equals(other.country.toLowerCase()) && this.year == other.year;\n    }\n\n    public int hashCode() {\n        return Objects.hash(this.country.toLowerCase(), this.year);\n    }\n\n    public String toString() {\n        return this.country + \" \" + this.year;\n    }\n}\n```\n\n```output\npage 4\nfalse\nfalse\n```\n\n- `\"Chile\"` and `\"CHILE\"` are different Strings with different hash codes, as the last line shows. With `this.country` in `Objects.hash`, two stamps that `equals` calls equal would get different hash codes. `toLowerCase()` turns both into `\"chile\"` first, so their hash codes are the same.\n- Both methods use the same lower-case text, so they can't disagree. `equalsIgnoreCase` (module 16) would give the same answers in `equals` for letters like these, but `hashCode` still needs the lower-case text.\n- The stamp keeps the country as it was typed, so `toString` shows it that way.\n\n### Or tidied String keys\n\nWhen the key is one piece of text, there's a simpler way, which module 31 used: tidy the text with `trim()` and `toLowerCase()` (or `toUpperCase()`) before every `put` and every `get`. Then the keys are ordinary Strings, which already have `equals` and `hashCode`, and no key class is needed. The map keeps the keys in their tidied form, so if the spelling as typed matters for printing, keep it in the value.\n\nWhich to choose? When the key is one piece of text, tidied String keys are the simplest. When the key is several values that belong together, such as a stamp's country and year, or when some values shouldn't count, such as the name of a paint color, a class with `equals` and `hashCode` keeps the values together and says in one place what equal means.\n","task":"A rail pass counts the trips made between stations, and station names get typed with any mix of capitals. In `Trip.java`, write `equals` and `hashCode`: two trips are equal when they go from the same station to the same station, whatever the capital letters. Compare the lower-case names in `equals`, and give `Objects.hash` the lower-case names in `hashCode`. The check runs:\n\n```java\nHashMap<Trip, Integer> trips = new HashMap<>();\ntrips.put(new Trip(\"Oakdale\", \"Harbor\"), 3);\nSystem.out.println(trips.get(new Trip(\"OAKDALE\", \"harbor\")));\nSystem.out.println(trips.get(new Trip(\"Harbor\", \"Oakdale\")));\nSystem.out.println(new Trip(\"oakdale\", \"Harbor\").equals(new Trip(\"Oakdale\", \"HARBOR\")));\n```\n\nand expects:\n\n```\n3\nnull\ntrue\n```\n","kind":"code","seed":"// ==== Trip.java ====\nimport java.util.Objects;\n\npublic class Trip {\n    private String from;\n    private String to;\n\n    public Trip(String from, String to) {\n        this.from = from;\n        this.to = to;\n    }\n\n    // Write the equals and hashCode methods here.\n\n    public String toString() {\n        return this.from + \" to \" + this.to;\n    }\n}\n\n// ==== Main.java ====\nimport java.util.HashMap;\n\npublic class Main {\n    public static void main(String[] args) {\n        HashMap<Trip, Integer> trips = new HashMap<>();\n        trips.put(new Trip(\"Oakdale\", \"Harbor\"), 3);\n        System.out.println(trips.get(new Trip(\"OAKDALE\", \"harbor\")));\n        System.out.println(trips.get(new Trip(\"Harbor\", \"Oakdale\")));\n        System.out.println(new Trip(\"oakdale\", \"Harbor\").equals(new Trip(\"Oakdale\", \"HARBOR\")));\n    }\n}\n","solution":"// ==== Trip.java ====\nimport java.util.Objects;\n\npublic class Trip {\n    private String from;\n    private String to;\n\n    public Trip(String from, String to) {\n        this.from = from;\n        this.to = to;\n    }\n\n    public boolean equals(Object compared) {\n        if (this == compared) {\n            return true;\n        }\n        if (!(compared instanceof Trip)) {\n            return false;\n        }\n        Trip other = (Trip) compared;\n        return this.from.toLowerCase().equals(other.from.toLowerCase()) && this.to.toLowerCase().equals(other.to.toLowerCase());\n    }\n\n    public int hashCode() {\n        return Objects.hash(this.from.toLowerCase(), this.to.toLowerCase());\n    }\n\n    public String toString() {\n        return this.from + \" to \" + this.to;\n    }\n}\n\n// ==== Main.java ====\nimport java.util.HashMap;\n\npublic class Main {\n    public static void main(String[] args) {\n        HashMap<Trip, Integer> trips = new HashMap<>();\n        trips.put(new Trip(\"Oakdale\", \"Harbor\"), 3);\n        System.out.println(trips.get(new Trip(\"OAKDALE\", \"harbor\")));\n        System.out.println(trips.get(new Trip(\"Harbor\", \"Oakdale\")));\n        System.out.println(new Trip(\"oakdale\", \"Harbor\").equals(new Trip(\"Oakdale\", \"HARBOR\")));\n    }\n}\n","hints":["`equals` is the classic one, with Trip in place of Stamp. Its last line compares both names in lower case: `this.from.toLowerCase().equals(other.from.toLowerCase())`, then `&&` and the same for `to`.","`hashCode` gives the same lower-case names to `Objects.hash`: `return Objects.hash(this.from.toLowerCase(), this.to.toLowerCase());`. Then trips that `equals` calls equal get the same hash code."],"tests":[{"name":"Test 1","stdin":"","call":"HashMap<Trip, Integer> trips = new HashMap<>();\ntrips.put(new Trip(\"Oakdale\", \"Harbor\"), 3);\nSystem.out.println(trips.get(new Trip(\"OAKDALE\", \"harbor\")));\nSystem.out.println(trips.get(new Trip(\"Harbor\", \"Oakdale\")));\nSystem.out.println(new Trip(\"oakdale\", \"Harbor\").equals(new Trip(\"Oakdale\", \"HARBOR\")));\n","expect":"3\nnull\ntrue","hidden":false},{"name":"Test 2","stdin":"","call":"Trip trip = new Trip(\"Lakeside\", \"Mill Road\");\nSystem.out.println(trip.equals(new Trip(\"lakeside\", \"MILL ROAD\")));\nSystem.out.println(trip.equals(new Trip(\"Lakeside\", \"Mill Lane\")));\nSystem.out.println(trip.equals(new Trip(\"Mill Road\", \"Lakeside\")));\nSystem.out.println(trip.equals(\"Lakeside to Mill Road\"));\nSystem.out.println(trip.hashCode() == new Trip(\"LAKESIDE\", \"mill road\").hashCode());\n","expect":"true\nfalse\nfalse\nfalse\ntrue","hidden":true},{"name":"Test 3","stdin":"","call":"HashMap<Trip, Integer> trips = new HashMap<>();\nString[] typed = {\"Oakdale Harbor\", \"oakdale HARBOR\", \"Harbor Oakdale\", \"OAKDALE harbor\", \"Oakdale Hillside\"};\nfor (String line : typed) {\n    String[] parts = line.split(\" \");\n    Trip trip = new Trip(parts[0], parts[1]);\n    if (trips.containsKey(trip)) {\n        trips.put(trip, trips.get(trip) + 1);\n    } else {\n        trips.put(trip, 1);\n    }\n}\nSystem.out.println(trips.size() + \" \" + trips.get(new Trip(\"oakdale\", \"harbor\")) + \" \" + trips.get(new Trip(\"harbor\", \"oakdale\")));\n","expect":"3 3 1","hidden":true}],"require":[{"pattern":"\\bpublic\\s+boolean\\s+equals\\s*\\(\\s*Object\\s+\\w+\\s*\\)","message":"Write the header exactly as `public boolean equals(Object compared)`, so that the map uses your method."},{"pattern":"\\bpublic\\s+int\\s+hashCode\\s*\\(\\s*\\)","message":"Write the header exactly as `public int hashCode()`, so that the map uses your method."},{"pattern":"\\bObjects\\s*\\.\\s*hash\\s*\\(","message":"Work out the hash code with `Objects.hash`, from the lower-case names."},{"pattern":"\\.toLowerCase\\s*\\(","message":"Give `hashCode` the names in lower case, with `toLowerCase()`, so that it agrees with `equals`."}],"forbid":[],"more":[{"task":"`Station`'s `equals` ignores capital letters, but its `hashCode` doesn't. 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        ArrayList<Station> visited = new ArrayList<>();\n        visited.add(new Station(\"Oakdale\"));\n        HashMap<Station, Integer> fares = new HashMap<>();\n        fares.put(new Station(\"Oakdale\"), 450);\n\n        Station typed = new Station(\"OAKDALE\");\n        System.out.println(typed.equals(new Station(\"Oakdale\")));\n        System.out.println(visited.contains(typed));\n        System.out.println(fares.get(typed));\n        System.out.println(fares.get(new Station(\"Oakdale\")));\n    }\n}\n// ==== Station.java ====\npublic class Station {\n    private String name;\n\n    public Station(String name) {\n        this.name = name;\n    }\n\n    public boolean equals(Object compared) {\n        if (this == compared) {\n            return true;\n        }\n        if (!(compared instanceof Station)) {\n            return false;\n        }\n        Station other = (Station) compared;\n        return this.name.equalsIgnoreCase(other.name);\n    }\n\n    public int hashCode() {\n        return this.name.hashCode();\n    }\n}\n","solution":"import java.util.ArrayList;\nimport java.util.HashMap;\n\npublic class Main {\n    public static void main(String[] args) {\n        ArrayList<Station> visited = new ArrayList<>();\n        visited.add(new Station(\"Oakdale\"));\n        HashMap<Station, Integer> fares = new HashMap<>();\n        fares.put(new Station(\"Oakdale\"), 450);\n\n        Station typed = new Station(\"OAKDALE\");\n        System.out.println(typed.equals(new Station(\"Oakdale\")));\n        System.out.println(visited.contains(typed));\n        System.out.println(fares.get(typed));\n        System.out.println(fares.get(new Station(\"Oakdale\")));\n    }\n}\n// ==== Station.java ====\npublic class Station {\n    private String name;\n\n    public Station(String name) {\n        this.name = name;\n    }\n\n    public boolean equals(Object compared) {\n        if (this == compared) {\n            return true;\n        }\n        if (!(compared instanceof Station)) {\n            return false;\n        }\n        Station other = (Station) compared;\n        return this.name.equalsIgnoreCase(other.name);\n    }\n\n    public int hashCode() {\n        return this.name.hashCode();\n    }\n}\n","hints":["`equals` ignores capitals, so `OAKDALE` is equal to `Oakdale`, and the list's `contains`, which uses only `equals`, finds it.","`\"OAKDALE\"` and `\"Oakdale\"` have different hash codes, so the map looks for the typed station with the wrong hash code and never calls `equals`. A new `Oakdale` has the same hash code as the key, and is found."],"tests":[{"name":"Output","stdin":"","expect":"true\ntrue\nnull\n450","hidden":false}],"require":[],"forbid":[],"lines":["true","true","null","450"]},{"task":"Here's the stamp from the last challenge of step 3, done right. A dealer's price list is a `HashMap<Stamp, Integer>`, with prices in cents. A stamp has a country, a year and a condition, `mint` or `used`, and this dealer pays the same for either condition. In `Stamp.java`, write `equals` and `hashCode`: two stamps are equal when the country and the year match, whatever the condition. Then, in `Main.java`, write the method `albumValue(HashMap<Stamp, Integer> prices, ArrayList<Stamp> album)`, which returns what the stamps of an album are worth together: the price of each stamp, or 0 for a stamp that isn't on the list. The check runs:\n\n```java\nHashMap<Stamp, Integer> prices = new HashMap<>();\nprices.put(new Stamp(\"Chile\", 1970, \"mint\"), 250);\nprices.put(new Stamp(\"Peru\", 1985, \"mint\"), 400);\nArrayList<Stamp> album = new ArrayList<>();\nalbum.add(new Stamp(\"Chile\", 1970, \"used\"));\nalbum.add(new Stamp(\"Peru\", 1985, \"mint\"));\nalbum.add(new Stamp(\"Chile\", 1971, \"mint\"));\nSystem.out.println(albumValue(prices, album));\n```\n\nand expects:\n\n```\n650\n```\n","kind":"code","seed":"// ==== Stamp.java ====\npublic class Stamp {\n    private String country;\n    private int year;\n    private String condition;\n\n    public Stamp(String country, int year, String condition) {\n        this.country = country;\n        this.year = year;\n        this.condition = condition;\n    }\n\n    // Write the equals and hashCode methods here.\n\n    public String toString() {\n        return this.country + \" \" + this.year + \" (\" + this.condition + \")\";\n    }\n}\n\n// ==== Main.java ====\nimport java.util.ArrayList;\nimport java.util.HashMap;\n\npublic class Main {\n    public static void main(String[] args) {\n        HashMap<Stamp, Integer> prices = new HashMap<>();\n        prices.put(new Stamp(\"Chile\", 1970, \"mint\"), 250);\n        prices.put(new Stamp(\"Peru\", 1985, \"mint\"), 400);\n        ArrayList<Stamp> album = new ArrayList<>();\n        album.add(new Stamp(\"Chile\", 1970, \"used\"));\n        album.add(new Stamp(\"Peru\", 1985, \"mint\"));\n        album.add(new Stamp(\"Chile\", 1971, \"mint\"));\n        System.out.println(albumValue(prices, album));\n    }\n\n    // Write the method albumValue here.\n}\n","solution":"// ==== Stamp.java ====\nimport java.util.Objects;\n\npublic class Stamp {\n    private String country;\n    private int year;\n    private String condition;\n\n    public Stamp(String country, int year, String condition) {\n        this.country = country;\n        this.year = year;\n        this.condition = condition;\n    }\n\n    public boolean equals(Object compared) {\n        if (this == compared) {\n            return true;\n        }\n        if (!(compared instanceof Stamp)) {\n            return false;\n        }\n        Stamp other = (Stamp) compared;\n        return this.country.equals(other.country) && this.year == other.year;\n    }\n\n    public int hashCode() {\n        return Objects.hash(this.country, this.year);\n    }\n\n    public String toString() {\n        return this.country + \" \" + this.year + \" (\" + this.condition + \")\";\n    }\n}\n\n// ==== Main.java ====\nimport java.util.ArrayList;\nimport java.util.HashMap;\n\npublic class Main {\n    public static void main(String[] args) {\n        HashMap<Stamp, Integer> prices = new HashMap<>();\n        prices.put(new Stamp(\"Chile\", 1970, \"mint\"), 250);\n        prices.put(new Stamp(\"Peru\", 1985, \"mint\"), 400);\n        ArrayList<Stamp> album = new ArrayList<>();\n        album.add(new Stamp(\"Chile\", 1970, \"used\"));\n        album.add(new Stamp(\"Peru\", 1985, \"mint\"));\n        album.add(new Stamp(\"Chile\", 1971, \"mint\"));\n        System.out.println(albumValue(prices, album));\n    }\n\n    public static int albumValue(HashMap<Stamp, Integer> prices, ArrayList<Stamp> album) {\n        int total = 0;\n        for (Stamp stamp : album) {\n            total += prices.getOrDefault(stamp, 0);\n        }\n        return total;\n    }\n}\n","hints":["`equals` and `hashCode` leave the condition out, as the paint shop's Color leaves the name out: `equals` compares the country and the year, and `hashCode` gives `Objects.hash` the same two values (with `import java.util.Objects;` at the top).","`albumValue` goes through the album and adds up `prices.getOrDefault(stamp, 0)` for each stamp. A used Chile 1970 is equal to the mint one on the list, and has the same hash code, so the map finds its price."],"tests":[{"name":"Test 1","stdin":"","call":"HashMap<Stamp, Integer> prices = new HashMap<>();\nprices.put(new Stamp(\"Chile\", 1970, \"mint\"), 250);\nprices.put(new Stamp(\"Peru\", 1985, \"mint\"), 400);\nArrayList<Stamp> album = new ArrayList<>();\nalbum.add(new Stamp(\"Chile\", 1970, \"used\"));\nalbum.add(new Stamp(\"Peru\", 1985, \"mint\"));\nalbum.add(new Stamp(\"Chile\", 1971, \"mint\"));\nSystem.out.println(albumValue(prices, album));\n","expect":"650","hidden":false},{"name":"Test 2","stdin":"","call":"HashMap<Stamp, Integer> prices = new HashMap<>();\nprices.put(new Stamp(\"Japan\", 1964, \"used\"), 120);\nArrayList<Stamp> album = new ArrayList<>();\nSystem.out.println(albumValue(prices, album));\nalbum.add(new Stamp(\"Japan\", 1964, \"mint\"));\nalbum.add(new Stamp(\"Japan\", 1964, \"used\"));\nalbum.add(new Stamp(\"Japan\", 1965, \"used\"));\nalbum.add(new Stamp(\"Chile\", 1964, \"used\"));\nSystem.out.println(albumValue(prices, album));\n","expect":"0\n240","hidden":true},{"name":"Test 3","stdin":"","call":"Stamp mint = new Stamp(\"Peru\", 1985, \"mint\");\nString start = \"Pe\";\nStamp used = new Stamp(start + \"ru\", 1985, \"used\");\nSystem.out.println(mint.equals(used) + \" \" + (mint.hashCode() == used.hashCode()));\nSystem.out.println(mint.equals(new Stamp(\"Peru\", 1986, \"mint\")) + \" \" + mint.equals(\"Peru 1985 (mint)\"));\n","expect":"true true\nfalse false","hidden":true}],"require":[{"pattern":"\\bpublic\\s+boolean\\s+equals\\s*\\(\\s*Object\\s+\\w+\\s*\\)","message":"Write the header exactly as `public boolean equals(Object compared)`, so that the map uses your method."},{"pattern":"\\bpublic\\s+int\\s+hashCode\\s*\\(\\s*\\)","message":"Write the header exactly as `public int hashCode()`, so that the map uses your method."},{"pattern":"\\bpublic\\s+static\\s+int\\s+albumValue\\s*\\(\\s*HashMap<Stamp,\\s*Integer>\\s+\\w+\\s*,\\s*ArrayList<Stamp>\\s+\\w+\\s*\\)","message":"Write the method as `public static int albumValue(HashMap<Stamp, Integer> prices, ArrayList<Stamp> album)`."}],"forbid":[]}]}