Course outline · 0% complete

0/29 lessons0%

Course overview →

HashMap

lesson 7-2 · ~7 min · 20/29

HashMap, keys to values

Half of programming is looking things up by name: the price for a product code, the user for a session token, the count for a word.

Scanning a list for a match gets slower as data grows. A map answers by key in the same tiny time whether it holds ten entries or ten million, which is why some form of it sits inside almost every cache, index, and config system you will ever touch.

A HashMap<K, V> is Java's dictionary. It stores key-to-value pairs and looks them up in constant time, like Python's dict:

import java.util.HashMap;

HashMap<String, Integer> stock = new HashMap<>();
stock.put("apple", 3);          // insert or overwrite
stock.get("apple")              // 3, or null if missing
stock.getOrDefault("kiwi", 0)   // 0 instead of null
stock.containsKey("apple")      // true
stock.remove("apple");
stock.size();

Two differences from Python are worth flagging. A missing key returns null rather than raising an error, so prefer getOrDefault to dodge the null, and a HashMap has no promised order when you loop over it.

put on an existing key overwrites the old value rather than adding a second entry, which is what makes the counting idiom later in this lesson work.

An inventory map

Three puts, one of them a repeat, then four different reads.

import java.util.HashMap;

public class Main {
  public static void main(String[] args) {
    HashMap<String, Integer> stock = new HashMap<>();
    stock.put("apple", 3);
    stock.put("banana", 5);
    stock.put("apple", 4);
    System.out.println(stock.get("apple"));
    System.out.println(stock.getOrDefault("kiwi", 0));
    System.out.println(stock.containsKey("banana"));
    System.out.println(stock.size());
  }
}

Output

4
0
true
2

The second put for apple overwrote the 3 with a 4 instead of adding a second apple entry, which is why the final size is 2 after three puts.

getOrDefault("kiwi", 0) returned 0 rather than null, and that difference is what makes it safe to use the result in arithmetic immediately.

"banana"hashCode()→ bucket 1bucket 0banana → 5bucket 2apple → 4key
A HashMap hashes each key to pick a bucket, then stores the pair there. Lookup rehashes the key and jumps straight to the right bucket, no scanning.

Reading a key that was never put

When stock.get("pear") is called and "pear" was never put in the map, the call returns null.

Java maps return null for absent keys instead of raising an error, which is the opposite of Python's KeyError. Nothing complains at the moment of the lookup.

That silence is the danger. Code that uses the result immediately crashes later with a NullPointerException, often far from the lookup that produced the null:

int n = stock.get("pear");                 // throws while unboxing null
int safe = stock.getOrDefault("pear", 0);  // 0, no crash

For counters and totals, getOrDefault is the safer habit, and containsKey is the right tool when absence itself is meaningful rather than just inconvenient.

Looping over a map

A map is not a sequence, so looping takes one extra decision: whether you want the keys, the values, or both. Each view has a method:

for (String key : stock.keySet()) { ... }   // keys only
for (int count : stock.values()) { ... }    // values only
for (Map.Entry<String, Integer> e : stock.entrySet()) {
  System.out.println(e.getKey() + ": " + e.getValue());
}
ViewGives you
keySet()every key
values()every value
entrySet()every key-value pair

A Map.Entry<K, V> is one key-value pair, and entrySet() hands you all of them. Using it needs import java.util.Map; as well.

Prefer entrySet whenever you use both halves. Looping keySet and calling get(key) inside the body performs a second lookup on every pass for no benefit.

Entry order is not insertion order

The same map iterated twice, once for pairs and once for values.

import java.util.HashMap;
import java.util.Map;

public class Main {
  public static void main(String[] args) {
    HashMap<String, Integer> stock = new HashMap<>();
    stock.put("apple", 3);
    stock.put("banana", 5);
    stock.put("kiwi", 2);

    for (Map.Entry<String, Integer> e : stock.entrySet()) {
      System.out.println(e.getKey() + ": " + e.getValue());
    }

    int total = 0;
    for (int count : stock.values()) {
      total += count;
    }
    System.out.println("total items: " + total);
  }
}

Output

banana: 5
apple: 3
kiwi: 2
total items: 10

The printed order is banana, apple, kiwi, which is not the insertion order. A HashMap arranges entries by hash bucket, and that is what "no promised order" means in practice.

Never write code that depends on it, since the order can change between runs and between Java versions. When order matters, LinkedHashMap preserves insertion order and TreeMap keeps keys sorted.

Counting letters in a word

The canonical counting idiom, built on getOrDefault.

import java.util.HashMap;

public class Main {
  public static void main(String[] args) {
    String word = "banana";
    HashMap<Character, Integer> counts = new HashMap<>();
    for (int i = 0; i < word.length(); i++) {
      char c = word.charAt(i);
      counts.put(c, counts.getOrDefault(c, 0) + 1);
    }
    System.out.println("a: " + counts.get('a'));
    System.out.println("b: " + counts.get('b'));
    System.out.println("n: " + counts.get('n'));
  }
}

Output

a: 3
b: 1
n: 2

The one line counts.put(c, counts.getOrDefault(c, 0) + 1) handles both cases at once. A letter seen for the first time reads as 0 and becomes 1, and a repeat reads its current count and becomes one more.

The keys are Character because a map cannot hold the primitive char, and the reads use single quotes to match. Walking the string with charAt(i) is why this loop needs the index rather than the enhanced form.