18.6HashMap

Last updated June 13, 2026

A Vec looks values up by position; a HashMap<K, V> looks them up by key. It stores key-value pairs, a phone book mapping names to numbers, a tally mapping words to counts, and lets you fetch a value by its key quickly. It's the third essential collection, and this lesson covers inserting, getting, the elegant entry API for "update or insert," and how ownership applies to keys and values.

Creating, inserting, getting

HashMap isn't in the prelude (unlike Vec and String), so you bring it into scope with use (lesson 13.2). Then insert adds a pair and get looks one up:

use std::collections::HashMap;

fn main() {
    let mut scores = HashMap::new();
    scores.insert(String::from("Blue"), 10);
    scores.insert(String::from("Red"), 50);

    let blue = scores.get("Blue");
    println!("{blue:?}");

    let purple = scores.get("Purple");
    println!("{purple:?}");
}
Some(10)
None

HashMap<K, V> is generic over the key type K and value type V (lesson 15.3); here it's inferred as HashMap<String, i32> from the first insert. get returns Option<&V>, Some(&value) if the key is present, None if not, exactly like Vec::get (lesson 18.2) and for the same reason: the key might not be there, so the type forces you to handle absence (lesson 11.3). Inserting with a key that already exists overwrites the old value.

The entry API: update or insert

A constant need with maps is "give me the value for this key, or insert a default if it's missing, then update it." Doing that with get and a match is clumsy; the entry API does it cleanly. entry(key) returns a handle to that slot, and or_insert(default) returns a mutable reference to the value, inserting the default first if the key was absent:

use std::collections::HashMap;

fn main() {
    let text = "the quick brown the lazy the";
    let mut counts: HashMap<&str, i32> = HashMap::new();

    for word in text.split_whitespace() {
        let count = counts.entry(word).or_insert(0);
        *count += 1;
    }

    println!("{counts:?}");
}
{"the": 3, "quick": 1, "brown": 1, "lazy": 1}

(The order of a HashMap's entries is unspecified and varies between runs, so your line may list the words differently; only the counts are meaningful.) This is the classic word-frequency counter, and the entry line is the heart of it. counts.entry(word).or_insert(0) means "for this word's slot, insert 0 if it's not there yet, and give me a mutable reference to the value either way." Then *count += 1 increments through that reference. The first time a word appears, it's inserted as 0 then bumped to 1; later appearances find the existing count and bump it. One line replaces a get-check-insert-update dance, which is why entry is one of the most-used HashMap methods.

Key insight

entry(key).or_insert(default) is the "update-or-insert" idiom: it hands you a mutable reference to the value for a key, creating it with a default if needed. It's the right tool whenever you're accumulating into a map, counting, grouping, tallying, because it collapses "is the key present? if not, add it; then modify it" into a single expression. Reach for it instead of get followed by insert whenever you'd otherwise check-then-modify.

Ownership of keys and values

The ownership rules (chapter 8) apply to what goes into a map. For types that are Copy (like i32), values are copied in. For owned types (like String), they're moved into the map, which then owns them:

use std::collections::HashMap;

fn main() {
    let name = String::from("Blue");
    let mut map = HashMap::new();
    map.insert(name, 10);
    // println!("{name}");   // refused: name was moved into the map
}

Inserting name (a String) moves it into map, so name can't be used afterward (lesson 8.4), the map now owns that text and will drop it when the map is dropped. If you need to keep using the key after inserting, insert a clone (lesson 8.7) or use references as keys (which brings lifetimes, chapter 17, into play, since the map must not outlive the borrowed keys). This is the same ownership story as everywhere else, here applied to collection contents: putting an owned value into a collection gives the collection ownership of it.

Notice that get("Blue") earlier took a &str even though the keys are String: looking up doesn't require an owned key, you can query with a borrowed form, which is the deref-coercion convenience from lesson 9.7. You insert owned keys but can look up with borrowed ones.

Quiz time

Question #1

What does HashMap::get return, and why isn't it just the value?

Show solution

It returns Option<&V>: Some(&value) if the key is present, None if not. It's an Option because the key might not be in the map, so the type forces you to handle the missing case (lesson 11.3), just like Vec::get. It's a reference to the value (you're borrowing what the map owns), not the value itself.

Question #2

What does map.entry(key).or_insert(0) do, and why is it better than a get-then-insert?

Show solution

It returns a mutable reference to the value for key, inserting 0 first if the key wasn't present. It collapses "check if the key exists, insert a default if not, then get a mutable reference to modify" into one expression, which is exactly what you need when accumulating (counting, tallying) into a map. Doing it with get then insert is several clumsier steps.

Question #3

You insert a String key into a HashMap and then try to use that String variable. What happens, and why?

Show solution

It's refused: inserting a String moves it into the map (the map takes ownership), so the original variable can't be used afterward (lesson 8.4, E0382). To keep using it, insert key.clone() instead, or use a borrowed key type with appropriate lifetimes. Putting an owned value into a collection transfers ownership to the collection.

You now have the three core collections. The next lesson revisits the fixed-size cousins, arrays and slices, in depth: when a fixed length beats a Vec, and why slices are the universal way to write functions that accept either.