Map #

HashMap<K, V> is the standard key-value collection in Rust — equivalent to dict in Python, Map in JavaScript, or HashMap in Java. It stores key-value pairs with an average access time of O(1). What distinguishes Rust’s HashMap from other languages is its strictness about ownership: when a value is inserted, the HashMap takes over ownership of it. And the way to update an existing value — through the Entry API — is far more elegant and idiomatic than what’s available in many other languages. This article covers HashMap thoroughly, including BTreeMap for sorted-order needs, HashSet as a valueless collection, and a guide to choosing among the three.

Creating a HashMap #

use std::collections::HashMap;

fn main() {
    // Empty — the type is inferred from the first insert
    let mut skor: HashMap<String, i32> = HashMap::new();

    // Insert key-value pairs
    skor.insert(String::from("Tim Merah"), 85);
    skor.insert(String::from("Tim Biru"), 92);
    skor.insert(String::from("Tim Hijau"), 78);

    println!("{:?}", skor);

    // From two Vecs with zip + collect
    let tim = vec!["Merah", "Biru", "Hijau"];
    let nilai = vec![85, 92, 78];
    let map: HashMap<_, _> = tim.iter().zip(nilai.iter()).collect();
    println!("{:?}", map);

    // From an array of tuples
    let konfigurasi: HashMap<&str, &str> = [
        ("host", "localhost"),
        ("port", "8080"),
        ("mode", "debug"),
    ].into_iter().collect();
    println!("{:?}", konfigurasi);

    // With initial capacity to avoid reallocation
    let mut cache: HashMap<u64, String> = HashMap::with_capacity(1000);
    println!("Initial capacity: {}", cache.capacity());
}

Accessing and Searching #

use std::collections::HashMap;

fn main() {
    let mut populasi: HashMap<&str, u64> = HashMap::new();
    populasi.insert("Jakarta", 10_500_000);
    populasi.insert("Surabaya", 2_900_000);
    populasi.insert("Bandung", 2_500_000);

    // get() — returns Option<&V>
    match populasi.get("Jakarta") {
        Some(p) => println!("Jakarta: {} people", p),
        None    => println!("Not found"),
    }

    // get() with if let — more concise
    if let Some(p) = populasi.get("Surabaya") {
        println!("Surabaya: {} people", p);
    }

    // unwrap_or — a default value if the key is missing
    let malang = populasi.get("Malang").copied().unwrap_or(0);
    println!("Malang: {} people", malang);

    // contains_key() — check existence without fetching the value
    println!("Has Jakarta: {}", populasi.contains_key("Jakarta")); // true
    println!("Has Malang: {}", populasi.contains_key("Malang"));   // false

    // Direct access via [] — panics if the key is missing
    // println!("{}", populasi["Malang"]); // panic!

    // CORRECT: use get() for safe access
    let kota = "Bandung";
    if let Some(&p) = populasi.get(kota) {
        println!("{}: {}", kota, p);
    }

    // len() and is_empty()
    println!("Number of cities: {}", populasi.len());
    println!("Empty: {}", populasi.is_empty());
}

The Entry API — Idiomatic Update Patterns #

The Entry API is one of the most elegant features of Rust’s HashMap. It handles three common scenarios — insert new, skip if present, update based on the old value — in a single expression without double lookups:

use std::collections::HashMap;

fn main() {
    let mut skor: HashMap<String, i32> = HashMap::new();

    // or_insert — insert if absent, return a mutable reference to the value
    skor.entry(String::from("Tim A")).or_insert(0);
    skor.entry(String::from("Tim A")).or_insert(999); // ignored, Tim A already exists
    skor.entry(String::from("Tim B")).or_insert(0);

    println!("{:?}", skor);  // {"Tim A": 0, "Tim B": 0}

    // or_insert_with — insert with a value from a closure (lazy evaluation)
    // The closure is only called if the key doesn't exist yet
    skor.entry(String::from("Tim C")).or_insert_with(|| {
        println!("Computing Tim C's initial score...");
        50  // an "expensive" initial value to compute
    });

    // Modify the value through the mutable reference returned by or_insert
    let nilai_a = skor.entry(String::from("Tim A")).or_insert(0);
    *nilai_a += 10;
    println!("{:?}", skor);  // Tim A: 10

    // Common pattern: word frequency counting
    let teks = "apel mangga apel jeruk mangga apel";
    let mut frekuensi: HashMap<&str, u32> = HashMap::new();

    for kata in teks.split_whitespace() {
        let counter = frekuensi.entry(kata).or_insert(0);
        *counter += 1;
    }

    println!("{:?}", frekuensi);  // {"apel": 3, "mangga": 2, "jeruk": 1}

    // Common pattern: group elements into Vecs
    let data = vec![
        ("kategori-a", "item-1"),
        ("kategori-b", "item-2"),
        ("kategori-a", "item-3"),
        ("kategori-b", "item-4"),
        ("kategori-c", "item-5"),
    ];

    let mut kelompok: HashMap<&str, Vec<&str>> = HashMap::new();
    for (kategori, item) in data {
        kelompok.entry(kategori).or_insert_with(Vec::new).push(item);
    }

    for (k, v) in &kelompok {
        println!("{}: {:?}", k, v);
    }
}

Updating and Removing #

use std::collections::HashMap;

fn main() {
    let mut harga: HashMap<&str, f64> = HashMap::new();
    harga.insert("kopi", 25_000.0);
    harga.insert("teh", 15_000.0);
    harga.insert("jus", 30_000.0);

    // insert() with the same key overwrites the old value
    // and returns the old value that was replaced
    let harga_lama = harga.insert("kopi", 28_000.0);
    println!("Old coffee price: {:?}", harga_lama);  // Some(25000.0)
    println!("New coffee price: {}", harga["kopi"]);  // 28000.0

    // remove() — remove and return the removed value
    let harga_teh = harga.remove("teh");
    println!("Tea removed: {:?}", harga_teh);  // Some(15000.0)
    println!("After removal: {:?}", harga);

    // retain() — keep only the pairs satisfying the condition
    let mut inventori: HashMap<&str, u32> = [
        ("apel", 50), ("mangga", 0), ("jeruk", 30), ("durian", 0),
    ].into_iter().collect();

    inventori.retain(|_, &mut stok| stok > 0);
    println!("Available stock: {:?}", inventori);

    // clear() — remove all pairs
    inventori.clear();
    println!("After clear: {:?}", inventori);
}

Iteration #

The iteration order of a HashMap is not guaranteed — this is a consequence of hashing. If you need a specific order, use BTreeMap or sort the results:

use std::collections::HashMap;

fn main() {
    let mut kota: HashMap<&str, u64> = [
        ("Jakarta", 10_500_000u64),
        ("Surabaya", 2_900_000),
        ("Bandung", 2_500_000),
        ("Medan", 2_100_000),
    ].into_iter().collect();

    // Iterate keys and values — order not guaranteed
    println!("=== All Cities ===");
    for (nama, pop) in &kota {
        println!("{}: {}", nama, pop);
    }

    // Iterate only the keys
    let mut nama_kota: Vec<&&str> = kota.keys().collect();
    nama_kota.sort();
    println!("\nCities (sorted): {:?}", nama_kota);

    // Iterate only the values
    let total: u64 = kota.values().sum();
    println!("Total population: {}", total);

    // Iterate with modification via values_mut()
    for pop in kota.values_mut() {
        *pop = (*pop as f64 * 1.02) as u64;  // increase by 2%
    }

    // Sorted iteration — sort by key
    let mut vec_kota: Vec<(&&str, &u64)> = kota.iter().collect();
    vec_kota.sort_by_key(|&(k, _)| *k);
    println!("\nSorted by name:");
    for (nama, pop) in vec_kota {
        println!("  {}: {}", nama, pop);
    }

    // Sort by value
    let mut vec_pop: Vec<(&&str, &u64)> = kota.iter().collect();
    vec_pop.sort_by(|a, b| b.1.cmp(a.1));  // descending
    println!("\nSorted by population (largest first):");
    for (nama, pop) in vec_pop {
        println!("  {}: {}", nama, pop);
    }
}

Ownership in a HashMap #

Types implementing Copy (integers, floats, bools) are copied on insert. Owned types like String are moved:

use std::collections::HashMap;

fn main() {
    let kunci = String::from("nama");
    let nilai = String::from("Budi");

    let mut map = HashMap::new();
    map.insert(kunci, nilai);  // kunci and nilai are MOVED into the map

    // ANTI-PATTERN: using the variables after they've been moved
    // println!("{}", kunci);  // error: kunci has been moved
    // println!("{}", nilai);  // error: nilai has been moved

    // CORRECT: use references if you still need the original variables
    let kunci2 = String::from("usia");
    let nilai2 = 30u32;  // Copy type
    let mut map2: HashMap<&str, u32> = HashMap::new();
    map2.insert(&kunci2, nilai2);   // &kunci2 = borrow
    println!("kunci2 still valid: {}", kunci2);  // ✓
    println!("nilai2 still valid: {}", nilai2);  // ✓ because Copy

    // References as keys — need a lifetime valid for as long as the HashMap lives
    let teks = String::from("hello world");
    let mut frekuensi: HashMap<&str, u32> = HashMap::new();
    for kata in teks.split_whitespace() {
        *frekuensi.entry(kata).or_insert(0) += 1;
    }
    println!("{:?}", frekuensi);
    // teks must live at least as long as frekuensi
}

Custom Keys #

A type used as a HashMap key must implement Hash and Eq. The easiest way is through #[derive]:

use std::collections::HashMap;

// Derive Hash and Eq — sufficient for common cases
#[derive(Debug, Hash, PartialEq, Eq)]
struct KoordinatGrid {
    x: i32,
    y: i32,
}

// A more complex custom type
#[derive(Debug, Hash, PartialEq, Eq)]
struct IdProduk {
    kategori: String,
    kode: u32,
}

fn main() {
    // HashMap with a struct key
    let mut peta: HashMap<KoordinatGrid, &str> = HashMap::new();
    peta.insert(KoordinatGrid { x: 0, y: 0 }, "Origin");
    peta.insert(KoordinatGrid { x: 1, y: 0 }, "Right");
    peta.insert(KoordinatGrid { x: 0, y: 1 }, "Up");

    let posisi = KoordinatGrid { x: 1, y: 0 };
    println!("{:?}", peta.get(&posisi));  // Some("Right")

    // HashMap with an enum key — derive Hash and Eq on the enum
    #[derive(Debug, Hash, PartialEq, Eq)]
    enum Mata {
        Matematika,
        IPA,
        Bahasa(String),
    }

    let mut nilai: HashMap<Mata, u8> = HashMap::new();
    nilai.insert(Mata::Matematika, 90);
    nilai.insert(Mata::IPA, 85);
    nilai.insert(Mata::Bahasa(String::from("Inggris")), 92);

    println!("{:?}", nilai.get(&Mata::Matematika));  // Some(90)
}

BTreeMap — A Sorted Map #

BTreeMap<K, V> stores key-value pairs in a B-tree structure that is always sorted by key. Its operations are O(log n) — slower than HashMap’s O(1), but it guarantees iteration order:

use std::collections::BTreeMap;

fn main() {
    let mut harga: BTreeMap<&str, f64> = BTreeMap::new();
    harga.insert("pisang", 5_000.0);
    harga.insert("apel", 15_000.0);
    harga.insert("mangga", 20_000.0);
    harga.insert("jeruk", 10_000.0);

    // Iteration is always sorted by key (alphanumeric)
    println!("=== Price List (Alphabetical) ===");
    for (nama, harga) in &harga {
        println!("  {:10}: Rp{:.0}", nama, harga);
    }

    // range() — key range queries
    println!("\nFruits between 'j' and 'p':");
    for (nama, h) in harga.range("j"..="p") {
        println!("  {}: Rp{:.0}", nama, h);
    }

    // first_key_value() and last_key_value()
    println!("\nFirst: {:?}", harga.first_key_value());
    println!("Last: {:?}", harga.last_key_value());

    // Real case: logs sorted by timestamp
    let mut log: BTreeMap<u64, String> = BTreeMap::new();
    log.insert(1_700_000_100, String::from("Server started"));
    log.insert(1_700_000_050, String::from("Configuration loaded"));
    log.insert(1_700_000_200, String::from("Connection accepted"));

    println!("\n=== Log (Chronological) ===");
    for (ts, pesan) in &log {
        println!("  {} | {}", ts, pesan);
    }
}

HashSet — A Collection of Unique Keys Without Values #

HashSet<T> is a simplified HashMap<T, ()> — it stores unique values without extra data. Useful for O(1) membership checks and set operations:

use std::collections::HashSet;

fn main() {
    let mut set: HashSet<i32> = HashSet::new();
    set.insert(1);
    set.insert(2);
    set.insert(3);
    set.insert(2);  // duplicate — ignored
    set.insert(1);  // duplicate — ignored
    println!("{:?}", set);  // {1, 2, 3} — order not guaranteed

    // contains() — O(1) membership check
    println!("Has 2: {}", set.contains(&2));  // true
    println!("Has 5: {}", set.contains(&5));  // false

    // From a Vec — an easy way to remove duplicates
    let angka_duplikat = vec![1, 2, 3, 2, 1, 4, 3, 5];
    let unik: HashSet<i32> = angka_duplikat.into_iter().collect();
    println!("Unique: {:?}", unik);

    // Set operations
    let a: HashSet<i32> = [1, 2, 3, 4, 5].into_iter().collect();
    let b: HashSet<i32> = [3, 4, 5, 6, 7].into_iter().collect();

    // Intersection — in both
    let irisan: HashSet<_> = a.intersection(&b).collect();
    println!("Intersection: {:?}", irisan);  // {3, 4, 5}

    // Union — in either or both
    let gabungan: HashSet<_> = a.union(&b).collect();
    println!("Union: {:?}", gabungan);  // {1, 2, 3, 4, 5, 6, 7}

    // Difference — in a but not in b
    let selisih: HashSet<_> = a.difference(&b).collect();
    println!("A \\ B: {:?}", selisih);  // {1, 2}

    // Subset and superset
    let c: HashSet<i32> = [3, 4].into_iter().collect();
    println!("c ⊆ a: {}", c.is_subset(&a));    // true
    println!("a ⊇ c: {}", a.is_superset(&c));  // true
}

Comparing HashMap, BTreeMap, and HashSet #

AspectHashMap<K,V>BTreeMap<K,V>HashSet<T>
ComplexityO(1) averageO(log n)O(1) average
Iteration orderNot guaranteedSorted by keyNot guaranteed
Key constraintsHash + EqOrdHash + Eq
Range queriesNoYes (.range())No
Memory usageMore efficientMoreMore efficient
When to useDefault mapWhen order mattersUnique membership checks
Use HashMap if:
  ✓ Standard map needs — lookup, insert, delete
  ✓ Iteration order doesn't matter
  ✓ Optimal O(1) performance

Use BTreeMap if:
  ✓ Need iteration in sorted order
  ✓ Need key range queries
  ✓ Keys must always be in a specific order

Use HashSet if:
  ✓ Only need to know whether an element exists
  ✓ Need set operations (union, intersection, difference)
  ✓ Removing duplicates from a collection

Summary #

  • The Entry API is the idiomatic way to update.entry(k).or_insert(v) handles insert-if-absent, .or_insert_with(||) for expensive-to-compute values, and the returned mutable reference for updates based on the old value.
  • HashMap takes ownership of keys and values — an inserted String can’t be used again afterward. Use &str as a key if you still need the original variable.
  • Iteration order isn’t guaranteed — if you need sorted iteration, use BTreeMap or collect into a Vec and sort.
  • Use get() instead of [] for safe accessmap[k] panics if the key is missing; map.get(k) returns an Option.
  • contains_key() for existence checks without fetching the value — more expressive than get().is_some().
  • retain() for in-place filtering — more efficient than filter + collect into a new HashMap.
  • Custom keys need Hash + Eq — use #[derive(Hash, PartialEq, Eq)] for simple structs or enums.
  • BTreeMap for guaranteed order — O(log n) slower but supports .range() and iteration is always sorted by key.
  • HashSet for unique membership — O(1) lookup and supports set operations (union, intersection, difference, subset).

← Previous: List   Next: Date & Time →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact