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 (ornull).get(key)— fetch the value, ornullif the key is missing. Thenullis the source of many beginner bugs; prefergetOrDefaultwhenever 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.containsKeyis fast (constant time);containsValuewalks the whole map.remove(key)— delete; returns the removed value ornull.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
Map<String,String> config — keys, values, and lookup
| key | value | |
|---|---|---|
| row 1 | "baseUrl" | "https://staging.myapp.com" |
| row 2 | "env" | "staging" |
| row 3 | "timeout" | "10" |
| row 4 | "retries" | "3" |
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)returningnulland crashing later. The standard fix isgetOrDefault(key, fallback). Whennullwould actually be a value in your map (rare in test code), usecontainsKeyfor the existence check beforeget.- Iterating with
keySetand callingget(key)inside. It works, but it's two lookups per entry — once to walk the key set, again to fetch the value. UseentrySet()and callentry.getValue()instead. - Counting on
HashMapto preserve insertion order. It doesn't. If your test report prints headers in a flaky order, switch toLinkedHashMap. If you need alphabetical, switch toTreeMap. Both have the sameMapAPI; only the order changes.
🎯 Practice task
Build an environment config registry. 25-30 minutes.
- Create
EnvConfig.java.import java.util.HashMap;andimport java.util.Map;. - Build
Map<String, Map<String, String>> envs = new HashMap<>();— a map of environment names to inner maps of config keys. - Populate three inner maps for
dev,staging,production. Each should havebaseUrl,timeout,retries. - 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
IllegalArgumentExceptionif the env doesn't exist
- Call
configFor("staging", "baseUrl")andconfigFor("production", "missingKey")andconfigFor("qa", "baseUrl")(the last should throw — wrap it in a try/catch from chapter 7.1, or just let it crash for now). - Iterate over
envs.entrySet(), and for each env iterate the innerentrySet(), printingstaging.baseUrl = ...style. - Switch the outer map's type from
HashMaptoLinkedHashMap. Re-run and confirm iteration is now in the order youputthe entries. - 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. UsefailureCounts.merge(env, 1, Integer::sum)to increment —mergeis a one-line increment-or-insert that beatsif (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.