Java Arena
Step 4 of 5

Equal enough: ignoring some values or capitals

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.

Comparing only some values

A 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:

Main.java
import java.util.HashMap;

public class Main {
    public static void main(String[] args) {
        HashMap<Color, Integer> cans = new HashMap<>();
        cans.put(new Color("Sky", 135, 206, 235), 4);

        Color asked = new Color("Sky blue", 135, 206, 235);
        System.out.println(asked.equals(new Color("Sky", 135, 206, 235)));
        System.out.println(cans.get(asked));
        System.out.println(cans.containsKey(new Color("Sky", 135, 206, 200)));
    }
}
Color.java
import java.util.Objects;

public class Color {
    private String name;
    private int red;
    private int green;
    private int blue;

    public Color(String name, int red, int green, int blue) {
        this.name = name;
        this.red = red;
        this.green = green;
        this.blue = blue;
    }

    public boolean equals(Object compared) {
        if (this == compared) {
            return true;
        }
        if (!(compared instanceof Color)) {
            return false;
        }
        Color other = (Color) compared;
        return this.red == other.red && this.green == other.green && this.blue == other.blue;
    }

    public int hashCode() {
        return Objects.hash(this.red, this.green, this.blue);
    }

    public String toString() {
        return this.name + " (" + this.red + ", " + this.green + ", " + this.blue + ")";
    }
}
Output
true
4
false
  • 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.
  • In short: hashCode may use only what equals compares.

Ignoring capital letters

A 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:

Main.java
import java.util.HashMap;

public class Main {
    public static void main(String[] args) {
        HashMap<Stamp, String> album = new HashMap<>();
        album.put(new Stamp("Chile", 1970), "page 4");
        System.out.println(album.get(new Stamp("CHILE", 1970)));
        System.out.println(album.containsKey(new Stamp("chile", 1971)));
        System.out.println("Chile".hashCode() == "CHILE".hashCode());
    }
}
Stamp.java
import java.util.Objects;

public class Stamp {
    private String country;
    private int year;

    public Stamp(String country, int year) {
        this.country = country;
        this.year = year;
    }

    public boolean equals(Object compared) {
        if (this == compared) {
            return true;
        }
        if (!(compared instanceof Stamp)) {
            return false;
        }
        Stamp other = (Stamp) compared;
        return this.country.toLowerCase().equals(other.country.toLowerCase()) && this.year == other.year;
    }

    public int hashCode() {
        return Objects.hash(this.country.toLowerCase(), this.year);
    }

    public String toString() {
        return this.country + " " + this.year;
    }
}
Output
page 4
false
false
  • "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.
  • 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.
  • The stamp keeps the country as it was typed, so toString shows it that way.

Or tidied String keys

When 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.

Which 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.

Your task

Challenge 1 of 3

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:

HashMap<Trip, Integer> trips = new HashMap<>();
trips.put(new Trip("Oakdale", "Harbor"), 3);
System.out.println(trips.get(new Trip("OAKDALE", "harbor")));
System.out.println(trips.get(new Trip("Harbor", "Oakdale")));
System.out.println(new Trip("oakdale", "Harbor").equals(new Trip("Oakdale", "HARBOR")));

and expects:

3
null
true

Challenge 2

Station's equals ignores capital letters, but its hashCode doesn't. What does this program print?

Challenge 3

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:

HashMap<Stamp, Integer> prices = new HashMap<>();
prices.put(new Stamp("Chile", 1970, "mint"), 250);
prices.put(new Stamp("Peru", 1985, "mint"), 400);
ArrayList<Stamp> album = new ArrayList<>();
album.add(new Stamp("Chile", 1970, "used"));
album.add(new Stamp("Peru", 1985, "mint"));
album.add(new Stamp("Chile", 1971, "mint"));
System.out.println(albumValue(prices, album));

and expects:

650