Updated Aug 5, 2026

10. HashMap

📋 Jump to Takeaways

🎁 You can store any single type in a Vec. But what if you need to associate keys with values, count occurrences, or check membership in O(1) time?

Key-Value Storage with HashMap

Need to look things up by name instead of position? That's a HashMap: key-value pairs with O(1) average lookups.

use std::collections::HashMap;

fn main() {
    let mut scores = HashMap::new();
    scores.insert("Alice", 95);
    scores.insert("Bob", 87);

    // Access with .get() — returns Option<&V>
    if let Some(score) = scores.get("Alice") { // score is &i32 (reference into the map)
        println!("Alice scored {}", score); // Alice scored 95
    }

    // Iteration — name and score are references (&str, &i32)
    for (name, score) in &scores {
        println!("{}: {}", name, score);
    }
}

.get() returns Option<&V>, a reference to the value inside the map. The value stays in the map — you're just peeking at it. That's why score above is &i32, not i32. If you want to take the value out (removing it from the map), use .remove():

use std::collections::HashMap;

fn main() {
    let mut scores = HashMap::from([("Alice", 95), ("Bob", 87)]);

    let peeked = scores.get("Alice");      // Option<&i32> — map still has it
    println!("{:?}", peeked);              // Some(95)

    let taken = scores.remove("Alice");    // Option<i32> — gone from the map
    println!("{:?}", taken);               // Some(95)
    println!("{:?}", scores.get("Alice")); // None — removed
}

You must use std::collections::HashMap, it's not in the prelude. Keys must implement Eq and Hash.

If you already know the initial values, use HashMap::from with an array of tuples:

use std::collections::HashMap;

fn main() {
    let scores = HashMap::from([
        ("Alice", 95),
        ("Bob", 87),
        ("Carol", 92),
    ]);

    println!("{:?}", scores.get("Bob")); // Some(87)
}

There's no literal syntax like Python's {"key": value}. HashMap::from([...]) is the closest equivalent.

The Entry API

Counting things is where the entry API shines. It inserts a value when the key is missing, or lets you tweak the one that's already there, all in one call.

use std::collections::HashMap;

fn main() {
    let text = "hello world hello rust hello";
    let mut word_count = HashMap::new();

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

    println!("{:?}", word_count);
    // {"hello": 3, "world": 1, "rust": 1} (key order varies between runs)
}

.entry(key).or_insert(default) returns a mutable reference to the value. If the key didn't exist, it inserts the default first. The *count += 1 dereferences that reference to modify the actual value in the map — same pattern as the &mut iteration in the previous lesson.

Collecting Iterators into Collections

.collect() is the bridge from iterators back to collections. Run any iterator through it and you get a Vec, a HashMap, or whatever type you ask for. This works because .collect() is generic over any type that implements the FromIterator trait, which is what defines how that type gets built from an iterator. Vec, HashMap, HashSet, and String all implement it. That's why you need to tell .collect() which type you want, it has no way to guess which FromIterator implementation to use.

fn main() {
    // Collect a range into a Vec — type annotation tells collect what to build
    let numbers: Vec<i32> = (1..=5).collect();
    println!("{:?}", numbers); // [1, 2, 3, 4, 5]

    // Same thing with "turbofish" syntax — type goes on collect itself
    let numbers = (1..=5).collect::<Vec<i32>>();
    println!("{:?}", numbers); // [1, 2, 3, 4, 5]

    // Filter and collect
    let evens: Vec<i32> = (1..=10).filter(|n| n % 2 == 0).collect();
    println!("{:?}", evens); // [2, 4, 6, 8, 10]
}

Both styles (type annotation on the variable vs turbofish on .collect::< >()) do the same thing — they tell Rust what collection to build.

You can also collect into a HashMap from tuples:

use std::collections::HashMap;

fn main() {
    let pairs: HashMap<&str, i32> = vec![("a", 1), ("b", 2)]
        .into_iter()
        .collect();
    println!("{:?}", pairs); // {"a": 1, "b": 2} (key order varies between runs)
}

.into_iter() consumes the vec, moving ownership of each element into the iterator. By contrast, .iter() just borrows elements. Here we need into_iter() because HashMap wants to own the key-value pairs, not borrow them.

Bounded Collections

On standard machines Vec grows freely until the OS says no. On embedded systems, IoT devices, or safety-critical targets (rockets, satellites) that's not acceptable — you need a hard cap at compile time.

The heapless crate gives you familiar collection APIs with fixed capacity baked in as a const generic:

use heapless::Vec;

fn main() {
    let mut buf: Vec<u8, 256> = Vec::new(); // capacity is 256, fixed forever

    match buf.push(42) {
        Ok(()) => println!("pushed"),
        Err(val) => println!("full, couldn't push {}", val), // no panic, just Err
    }

    println!("len: {}", buf.len()); // len: 1
}

The 256 is resolved at compile time — no heap allocation, no surprise growth. If you try to push beyond capacity you get an Err, not a panic.

The same pattern works for maps with heapless::FnvIndexMap (a fixed-capacity HashMap alternative):

use heapless::FnvIndexMap;

fn main() {
    let mut map: FnvIndexMap<&str, i32, 8> = FnvIndexMap::new(); // max 8 entries

    map.insert("alice", 95).ok();
    map.insert("bob", 87).ok();

    println!("{:?}", map.get("alice")); // Some(95)
    println!("len: {}", map.len());     // len: 2
}

For standard Vec when you're not sure memory is available, try_reserve lets you check before committing:

fn main() {
    let mut items: Vec<u32> = Vec::new();
    if items.try_reserve(1_000_000).is_err() {
        eprintln!("not enough memory");
        return;
    }
    // safe to push up to 1_000_000 items now
}

try_reserve returns Err if the allocator can't fulfill the request instead of panicking. Useful when you want to fail gracefully rather than crash.

Key Takeaways

  • HashMap provides O(1) key-value lookups, keys must implement Eq and Hash
  • The entry API (entry(key).or_insert(default)) handles insert-or-update in one call
  • .collect() transforms any iterator into a Vec, HashMap, or other collection that implements FromIterator
  • .into_iter() consumes and moves; .iter() borrows
  • Turbofish (.collect::<Vec<i32>>()) or type annotation both tell .collect() what to build
  • heapless::Vec gives fixed-capacity collections with no heap, ideal for embedded/safety-critical targets
  • try_reserve checks memory availability before pushing, returning Err instead of panicking

🎁 You've got Vec and HashMap, but what about guaranteed uniqueness, sorted keys, or a priority queue? Rust's standard library has specialized collections for each of these, and picking the right one makes your code faster and cleaner.

📝 Ready to test your knowledge?

Answer the quiz below to mark this lesson complete.

Spot something off? Report an issue
© 2026 ByteLearn.dev. Free courses for developers. · Privacy