How HashMap works
Problem
The seat was sold twice
The theater box office keeps the sold seats in a dictionary: seat → name of the customer. A seat is a class with a row and a number:
class Seat { int row; int number;
Seat(int row, int number) { this.row = row; this.number = number; }}Map<Seat, String> bookings = new HashMap<>();bookings.put(new Seat(5, 12), "Maria");
System.out.println(bookings.get(new Seat(5, 12)));System.out.println(bookings.size());bookings.put(new Seat(5, 12), "Ivan");System.out.println(bookings.size());null12Maria bought seat 5/12, but get with the same seat returns null. Then Ivan buys the same seat, and the dictionary now has two 5/12 keys. Why?
The idea
Hash code: a number from the key
A HashMap does not compare the key with all the others. It computes one whole number from the key, and from it, it knows where to look. This number is called a hash code, and the hashCode() method returns it. Every object has it, because it is in Object.
System.out.println("Maria".hashCode());System.out.println("Maria".hashCode());System.out.println("Marie".hashCode());System.out.println("maria".hashCode());741137507411375074113754103666422- The same text gives the same number, every time and on every computer.
- A small change gives a different number: "Maria", "Marie" and "maria" are three different hash codes.
- A hash code is fast to compute, no matter how many keys are in the dictionary.
The method that makes the number is called a hash function.
More precisely, if you are curious
- For
Stringthe formula is defined in the Java documentation:s[0]*31^(n-1) + s[1]*31^(n-2) + … + s[n-1], wheres[i]is the code of each character. The result is anint, so on overflow it becomes negative.
- Cryptographic hash functions like SHA-256 are something else. They give a long result, and it is practically impossible to find two texts with the same hash. That is why they are used to check that a file has not been tampered with and to store passwords.
hashCode()is for fast lookups and gives no such protection.
See it
The buckets
Inside, a HashMap is an array of buckets. The number of the bucket for a key is the remainder of the hash divided by the number of buckets:
String[] keys = { "Boris", "Yana", "Ivan", "Emil" };int buckets = 8;for (String key : keys) { int hash = key.hashCode(); int index = Math.floorMod(hash, buckets); System.out.println(key + " -> " + hash + " -> bucket " + index);}Step by step, in words
- Four keys and eight empty buckets, numbered 0 to 7.
- "Boris".hashCode() is 64372111. The remainder when divided by 8 is 7: that is the bucket.
- "Yana" gives 2748123, bucket 3.
- "Ivan" gives 2291258, bucket 2.
- "Emil" is also in bucket 3: a collision. The two keys are in one bucket: "Emil" hangs after "Yana" like a chain.
- Looking up "Emil": the same number, the same bucket 3. equals with "Yana": no. With "Emil": yes.
putcomputes the bucket and puts the pair there.getcomputes the same bucket and looks only in it. That is why it is fast even with a million keys.Math.floorModis a remainder of division that is never negative. A hash code can be negative, but a bucket number cannot.- "Yana" and "Emil" landed in bucket 3. This is called a collision. The next card is about it.
More precisely, if you are curious
- The real
HashMapcomputes the index a little differently:(h ^ (h >>> 16)) & (n - 1). The number of bucketsnis always a power of two, and& (n - 1)is a fast remainder of division byn. So the numbers there are different, but the principle is the same.
Try it
Which bucket?
The hash code of an Integer is the number itself. The order numbers go into a dictionary with 4 buckets.
What will the program print?
int[] orderIds = { 6, 10, 13, 21 };for (int id : orderIds) { Integer key = id; System.out.println(id + " -> bucket " + Math.floorMod(key.hashCode(), 4));}The remainders when divided by 4: 6 and 10 give 2, and 13 and 21 give 1. Four keys, two collisions, two empty buckets. With more buckets, the keys spread out more evenly.
The idea
Collision: hashCode finds the bucket, equals finds the key
Different keys can have the same hash code. The two best-known examples:
System.out.println("Aa".hashCode() + " " + "BB".hashCode());
Map<String, Integer> codes = new HashMap<>();codes.put("Aa", 1);codes.put("BB", 2);System.out.println(codes.get("Aa") + " " + codes.get("BB"));2112 21121 2The two keys are in one bucket, but the dictionary does not mix them up. The bucket holds a small linked list of pairs. get goes along it and compares with equals until it finds the key.
So the two steps work like this:
hashCode()says which bucket to look in.equals()says which of the keys in the bucket is the one you want.
Now it is clear what happened with the theater seat. Seat overrides neither of them, so it uses the versions from Object. There, equals is ==, and hashCode is usually different for every object. The two new Seat(5, 12) objects are two different objects, so they are also two different keys.
The idea
The contract: equals and hashCode together
Seat has to say when two seats are the same seat, and give equal seats the same hash code:
class Seat { int row; int number;
Seat(int row, int number) { this.row = row; this.number = number; }
@Override public boolean equals(Object other) { if (!(other instanceof Seat)) { return false; } Seat s = (Seat) other; return row == s.row && number == s.number; }
@Override public int hashCode() { return 31 * row + number; }}With the same code in main as in the first card:
Map<Seat, String> bookings = new HashMap<>();bookings.put(new Seat(5, 12), "Maria");
System.out.println(bookings.get(new Seat(5, 12)));System.out.println(bookings.size());bookings.put(new Seat(5, 12), "Ivan");System.out.println(bookings.size());Maria11The rules Java expects:
- If
a.equals(b), thena.hashCode() == b.hashCode(). Always. - If the hash codes are equal, the objects may be different. That is a collision.
- If you override
equals, overridehashCodetoo, with the same fields.
More precisely, if you are curious
- Why 31? It is an odd prime number, and it mixes well. Multiplying by 31 makes the hash depend on the order: seat 5/12 and seat 12/5 have different hash codes. With
row + number, the two would land in the same bucket. - Instead of the formula, you can write
Objects.hashfromjava.util.Objects. It computes the same thing from the fields you give it:
@Overridepublic int hashCode() { return Objects.hash(row, number);}equalsgets anObject, so it first checks withinstanceofwhether the other one is aSeatat all, and then casts it. This is the downcast from the "Inheritance" module.
Try it
Find the bug
HashSet. Two products with the same code are the same product, even if the price has changed. But contains with the new price returns false:Set<Product> catalog = new HashSet<>();catalog.add(new Product("A-100", 25.0));// The price went down, but it is the same product.System.out.println(catalog.contains(new Product("A-100", 19.9)));falseWhich line in the class has the bug?
Pick the line with the bug.
equals compares only the code, but hashCode also uses the price. The two products are equal according to equals, but they have different hash codes. This breaks the contract, so the HashSet looks in a different bucket and never compares them at all. hashCode has to use the same fields as equals: return code.hashCode();. Then contains returns true.The idea
The buckets fill up
When there are many pairs, the buckets get long and lookups slow down. That is why a HashMap grows:
- At the start there are 16 buckets.
- When there are more pairs than 75% of the number of buckets, it makes twice as many buckets and redistributes all the keys. The bucket number depends on the number of buckets, so every key is computed again.
- That is why, on average,
putandgetare fast, but now and then oneputtakes longer.
There is a trap too. A key must not change while it is in a dictionary or a set:
Seat seat = new Seat(5, 12);Set<Seat> sold = new HashSet<>();sold.add(seat);
seat.number = 13;System.out.println(sold.contains(seat));System.out.println(sold.contains(new Seat(5, 12)));System.out.println(sold.size());falsefalse1contains(seat) looks in the bucket for 5/13 and does not find it. contains(new Seat(5, 12)) looks in the right bucket, but the object there is no longer equal to 5/12. The element is in the set (the size is 1), but it cannot be found. That is why it is a good idea for the fields of a key to be final.More precisely, if you are curious
- That 75% is called the load factor, and you can set it in the constructor:
new HashMap<>(64, 0.75f). If you know how many keys there will be, give the starting number of buckets, and the dictionary will not have to grow along the way. - From Java 8 on, a bucket with more than 8 pairs (
TREEIFY_THRESHOLD) turns from a list into a balanced tree, if there are at least 64 buckets. If a redistribution leaves 6 or fewer pairs in such a bucket (UNTREEIFY_THRESHOLD), the tree becomes a list again. So with many collisions, a lookup in a bucket with n pairs takes about log n comparisons instead of n.
Exercise
Your own hash codes
Solve the tasks in your editor.
1. A hash code for a person. A Person class with a first name, a last name and an age. Override equals and hashCode. For hashCode, use the recipe: start from 17, and for each field, result = 31 * result + the hash of the field. For an int field, the hash is the number itself. Create Ivan Petrov at 30 twice and once at 31. Print the hash codes and how many different people are left in a HashSet.
-1104532243-1104532243-1104532242Different people: 22. The robot in the warehouse. The robot starts at cell (0, 0) and makes the moves NNEESWWN: N is up (y + 1), S is down, E is right (x + 1), W is left. How many different cells has it visited, counting the starting one? Keep them in a HashSet<Point>. With this class the program says 9, but it should say 7. Fix it:
class Point { int x; int y;
Point(int x, int y) { this.x = x; this.y = y; }}Moves: 8Different cells: 73. A hash of a file. A hashFileContent(String filePath) method reads the file and returns the hash code of its content as hexadecimal text: Integer.toHexString(content.hashCode()). Write three reports: a.txt and b.txt with "October report: €1250", and c.txt with "October report: €1260". Compare their hashes. Different hashes mean the files are certainly different. Equal hashes mean "almost certainly the same": with an int a collision is possible, so for a real check you use SHA-256.
a.txt: c2192f1cb.txt: c2192f1cc.txt: c21932dda and b: samea and c: differentMore tasks
- Your own hash table. A follow-up to the user table from the last lesson, this time without
HashMap. Make an array of 8 buckets, each anArrayListof username and email pairs. The hash function is the sum of the character codes in the name. Writeadd,get, and a method that prints how many pairs are in each bucket. - An ID from an email.
generateUserId(String email)returns"U" + Integer.toHexString(email.toLowerCase().hashCode()). Is the ID the same forIvan@Example.comandivan@example.com? Can two different emails get the same ID? How would you find out? - Counting collisions. Compute the bucket of the keys
user1…user1000with 16, 64 and 1024 buckets, and count how many keys land in a bucket that already has a key. - The shoe box. A
Boxclass with a width, a height and a depth. Two boxes are the same if their sizes are the same, in the same order. WriteequalsandhashCodewithObjects.hash, and check the boxes 30×20×10 and 10×20×30 in aHashSet.
Test