Java Arena
Step 5 of 5

Objects as keys in practice

Everything is in place now: a key class with equals and hashCode, and a HashMap that uses it. This step puts them to work in a small program, and deals with one more thing: the order in which to print what's in the map.

A game board

A board game is played on a grid of squares, with columns A to F and rows 1 to 6, and each player has a token of a color. The game keeps the tokens in a HashMap<Square, String>, from a square to the color of the token on it. Square is the class from steps 1 and 3, now with both methods. The class Board places tokens, looks up squares, moves tokens and prints them all:

Main.java
public class Main {
    public static void main(String[] args) {
        Board board = new Board();
        board.place(new Square("C", 2), "red");
        board.place(new Square("A", 1), "blue");
        board.place(new Square("B", 3), "green");
        board.move(new Square("B", 3), new Square("D", 4));
        System.out.println(board.tokenAt(new Square("D", 4)));
        System.out.println(board.tokenAt(new Square("B", 3)));
        board.printTokens();
    }
}
Board.java
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;

public class Board {
    private HashMap<Square, String> tokens;

    public Board() {
        this.tokens = new HashMap<>();
    }

    public void place(Square square, String color) {
        this.tokens.put(square, color);
    }

    public String tokenAt(Square square) {
        if (this.tokens.containsKey(square)) {
            return this.tokens.get(square);
        }
        return "empty";
    }

    public void move(Square from, Square to) {
        if (this.tokens.containsKey(from)) {
            String color = this.tokens.get(from);
            this.tokens.remove(from);
            this.tokens.put(to, color);
        }
    }

    public void printTokens() {
        ArrayList<String> lines = new ArrayList<>();
        for (Square square : this.tokens.keySet()) {
            lines.add(square + " " + this.tokens.get(square));
        }
        Collections.sort(lines);
        for (String line : lines) {
            System.out.println(line);
        }
    }
}
Square.java
public class Square {
    private String column;
    private int row;

    public Square(String column, int row) {
        this.column = column;
        this.row = row;
    }

    public boolean equals(Object compared) {
        if (this == compared) {
            return true;
        }
        if (!(compared instanceof Square)) {
            return false;
        }
        Square other = (Square) compared;
        return this.column.equals(other.column) && this.row == other.row;
    }

    public int hashCode() {
        return 31 * this.column.hashCode() + this.row;
    }

    public String toString() {
        return this.column + this.row;
    }
}
Output
green
empty
A1 blue
C2 red
D4 green
  • Every method is given a Square made with new, never the object that was put in. containsKey, get, put and remove still find the right key, because Square has both equals and hashCode.
  • remove(key) takes a key and its value out of the map (module 31). move gets the color, removes the old square, and puts the token on the new one.
  • tokenAt checks containsKey first, so an empty square gives empty instead of null.

The order of the keys

printTokens goes through the keys with keySet(), as in module 31. A hash map goes through its keys in an order set by their hash codes, not in the order they were added, and it's no order to rely on. With keys of your own class, it depends on how hashCode is written. So printTokens makes its own order: it builds a line for each token, puts the lines in a list, and sorts the list with Collections.sort (module 28). Square names sort well as Strings on this board: the column's letter first, then the row, which is always one digit.

Two other ways to get an order you can count on:

  • Keep the keys in an ArrayList too, next to the map, in the order they were added. Go through the list, and get each value from the map.
  • Go through the keys you want in the order you want, such as the squares of one row from A to F, and look each one up with a new Square and containsKey or get.

One warning: never go through, or print, a hash map whose keys have no hashCode of their own. Their hash codes are Java's default ones, which can change from run to run, and the order would change with them.

Keys that don't change

A key goes into the bucket of the hash code it has when it's put in. If the key object changed later, its hash code would change too, and the map would look for it in the wrong bucket. So key classes usually have no setters: none of the key classes in this module can be changed once an object is made. That's why move doesn't change a Square: it removes the old key and puts in a new one.

Your task

Challenge 1 of 3

A theater keeps its bookings in a HashMap<Seat, String>, from a seat to the name it's booked for, and the box office wants to see one row at a time. Seat.java is ready, with equals, hashCode and toString. Write the class Bookings, with:

  • a constructor without parameters, which creates the map
  • book(Seat seat, String name), which books the seat for the name (booking a seat again replaces the name)
  • printRow(String row, int seats), which prints the seats of one row, from number 1 up to seats, one per line: the seat, a colon, and the name it's booked for, or free

printRow can't go through the map's keys: they come in the map's own order, and a free seat isn't a key at all. Go through the seat numbers in order instead, make each seat with new Seat(row, number), and look it up. The check runs:

Bookings bookings = new Bookings();
bookings.book(new Seat("B", 2), "Ada");
bookings.book(new Seat("B", 3), "Ben");
bookings.book(new Seat("C", 1), "Cy");
bookings.printRow("B", 4);

and expects:

B1: free
B2: Ada
B3: Ben
B4: free

Challenge 2

Here Coin's hashCode uses only the country, so every Italian coin has the same hash code. Does the map still work? What does this program print?

Challenge 3

The board game from the lesson wants to know which squares get the most traffic. Read the squares that tokens land on, such as C4, one per line, until an empty line, and count the landings on each square in a HashMap<Square, Integer>. After the empty line comes one more line: the number of a row. Print that row's squares from column A to F, one per line, each with how many times a token landed there, like C4: 2. A square nobody landed on gets 0.

Square.java has the column, the row and a toString that gives names like C4: write its equals and hashCode. In Main.java, a line such as C4 gives the column with line.substring(0, 1) and the row with Integer.valueOf(line.substring(1)). For the output, don't go through the map: go through the columns A to F (an array {"A", "B", "C", "D", "E", "F"} works well) and look each square up with a new Square. For the input shown, the program prints:

A4: 0
B4: 1
C4: 2
D4: 0
E4: 0
F4: 1