HashMap — Key-Value Pairs for Configuration

8 min read

ArrayList is great when you have a list of things. HashMap is the answer when you have named lookups — a configuration dictionary, an HTTP header bag, a lookup table from environment names to URLs. JavaScript objects ({ baseUrl: "..." }) and Python dictionaries do this job in those languages. Java's name for the same idea is HashMap. This lesson covers the everyday operations and the iteration patterns you'll use whenever code needs to ask, what value goes with this key?

Importing and creating

HashMap and the Map interface live in java.util:

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

Creation looks like ArrayList's — declare via the interface, build via the implementation:

Map<String, String> config = new HashMap<>();

Two generic parameters this time: <KeyType, ValueType>. Map<String, Integer> is a String-to-Integer map. Map<String, TestUser> maps names to user objects. Keys and values can be any object types (primitives go via wrappers, autoboxed automatically).

The everyday operations

import java.util.HashMap;
import java.util.Map;
 
public class ConfigDemo {
    public static void main(String[] args) {
        Map<String, String> config = new HashMap<>();
 
        config.put("baseUrl", "https://staging.myapp.com");
        config.put("env",     "staging");
        config.put("timeout", "10");
 
        System.out.println("baseUrl: " + config.get("baseUrl"));
        System.out.println("size:    " + config.size());
 
        System.out.println("has env?     " + config.containsKey("env"));
        System.out.println("has region?  " + config.containsKey("region"));
 
        String region = config.getOrDefault("region", "us-east-1");
        System.out.println("region:  " + region);
 
        config.put("env", "production");      // replaces — keys are unique
        System.out.println("env now: " + config.get("env"));
 
        config.remove("timeout");
        System.out.println("after remove: " + config);
    }
}

Output:

baseUrl: https://staging.myapp.com
size:    3
has env?     true
has region?  false
region:  us-east-1
env now: production
after remove: {baseUrl=https://staging.myapp.com, env=production}

The shape of the API:

  • put(key, value) — insert or replace. If the key already exists, the new value overwrites the old. Returns the previous value (or null).
  • get(key) — fetch the value, or null if the key is missing. The null is the source of many beginner bugs; prefer getOrDefault whenever a sensible default exists.
  • getOrDefault(key, fallback) — fetch, or return the fallback if absent. Beautiful for config defaults.
  • containsKey(key) / containsValue(value) — boolean membership tests. containsKey is fast (constant time); containsValue walks the whole map.
  • remove(key) — delete; returns the removed value or null.
  • size() — entry count.
  • isEmpty() — true when there are no entries.

The unique-keys rule is the headline: a HashMap cannot have two entries with the same key. put on an existing key is an update, not a duplicate. That's the behaviour you usually want for config and lookup tables.

Iterating — entrySet, keySet, values

Three ways to walk a map. The cleanest, when you need both key and value, is entrySet():

for (Map.Entry<String, String> entry : config.entrySet()) {
    System.out.println(entry.getKey() + " = " + entry.getValue());
}

When you only need the keys: for (String key : config.keySet()) { ... }.

When you only need the values: for (String value : config.values()) { ... }.

entrySet iteration is the right default — calling config.get(key) inside a keySet loop does a redundant lookup on every iteration. Pick entrySet once and forget the others exist for the simple case.

HashMap is unordered — and that matters

HashMap makes no promise about iteration order. Insert baseUrl, env, timeout in that order and you might iterate them as env, timeout, baseUrl. The order isn't random — it's a function of the keys' hash codes — but it isn't insertion order, and it isn't sorted.

Two siblings give you order if you need it:

  • LinkedHashMap — preserves insertion order. Drop-in replacement: Map<String, String> config = new LinkedHashMap<>();. Use it when you want predictable iteration for printing or reporting.
  • TreeMap — sorts entries by key. Map<String, String> sorted = new TreeMap<>(); iterates in alphabetical order. Slightly slower, but useful when "keys in alphabetical order" is the user-facing requirement.

For test code that prints config, LinkedHashMap is usually the right pick — your output order matches the order you populated the map.

A real QA example — a request-headers bag

HTTP requests are key-value parties — exactly the shape HashMap was made for:

import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
 
public class RequestHelper {
 
    public static Map<String, String> defaultHeaders() {
        Map<String, String> h = new LinkedHashMap<>();   // preserve order in logs
        h.put("Accept",        "application/json");
        h.put("Content-Type",  "application/json");
        h.put("User-Agent",    "qa-suite/1.0");
        return h;
    }
 
    public static Map<String, String> withAuth(Map<String, String> base, String token) {
        Map<String, String> h = new LinkedHashMap<>(base);  // copy
        h.put("Authorization", "Bearer " + token);
        return h;
    }
 
    public static void main(String[] args) {
        Map<String, String> baseHeaders = defaultHeaders();
        Map<String, String> authed = withAuth(baseHeaders, "abc123");
 
        for (Map.Entry<String, String> e : authed.entrySet()) {
            System.out.println(e.getKey() + ": " + e.getValue());
        }
 
        System.out.println("Token-aware? " + authed.containsKey("Authorization"));
        System.out.println("Default Content-Type: " +
                           authed.getOrDefault("Content-Type", "application/octet-stream"));
    }
}

Output:

Accept: application/json
Content-Type: application/json
User-Agent: qa-suite/1.0
Authorization: Bearer abc123
Token-aware? true
Default Content-Type: application/json

Notice the new LinkedHashMap<>(base) — many collection classes accept a Map (or Collection) in their constructor and copy it. That gives you an independent map you can mutate without surprising the caller. Mutate authed; baseHeaders is unchanged. Defensive copying pays off when methods return shared collections.

A map, visualised

Reading the table: each row is one entry. config.get("env") reaches into row 2 and returns "staging" in (amortised) constant time — that's the hash in HashMap. There's no scanning, no order to worry about. You ask for a key, you get its value.

⚠️ Common mistakes

  • get(missingKey) returning null and crashing later. The standard fix is getOrDefault(key, fallback). When null would actually be a value in your map (rare in test code), use containsKey for the existence check before get.
  • Iterating with keySet and calling get(key) inside. It works, but it's two lookups per entry — once to walk the key set, again to fetch the value. Use entrySet() and call entry.getValue() instead.
  • Counting on HashMap to preserve insertion order. It doesn't. If your test report prints headers in a flaky order, switch to LinkedHashMap. If you need alphabetical, switch to TreeMap. Both have the same Map API; only the order changes.

🎯 Practice task

Build an environment config registry. 25-30 minutes.

  1. Create EnvConfig.java. import java.util.HashMap; and import java.util.Map;.
  2. Build Map<String, Map<String, String>> envs = new HashMap<>(); — a map of environment names to inner maps of config keys.
  3. Populate three inner maps for dev, staging, production. Each should have baseUrl, timeout, retries.
  4. Write a method static String configFor(String env, String key) that:
    • returns the value if both the env and the key exist
    • returns "(unset)" if the key is missing
    • throws IllegalArgumentException if the env doesn't exist
  5. Call configFor("staging", "baseUrl") and configFor("production", "missingKey") and configFor("qa", "baseUrl") (the last should throw — wrap it in a try/catch from chapter 7.1, or just let it crash for now).
  6. Iterate over envs.entrySet(), and for each env iterate the inner entrySet(), printing staging.baseUrl = ... style.
  7. Switch the outer map's type from HashMap to LinkedHashMap. Re-run and confirm iteration is now in the order you put the entries.
  8. Stretch: add a Map<String, Integer> failureCounts = new HashMap<>(); that counts how many times each env's config has been read in a single run. Use failureCounts.merge(env, 1, Integer::sum) to increment — merge is a one-line increment-or-insert that beats if (containsKey) ... get + put.

You can now name and look up data. Lesson 3 introduces HashSet (uniqueness) and LinkedList (queue-like access patterns), the other two collections you'll see often.

// tip to track lessons you complete and pick up where you left off across devices.