الخرائط (Maps)
في هذا الدرس 11 موضوعات
هذا الدرس متاح حاليًا باللغة الإنجليزية فقط، ولم تُنشر نسخته العربية بعد.
Lesson Overview
Lists find things by position and sets answer "is it in there?". Very often you need something different: find the thing that goes with this key. The price for a product code, the customer for an id, the number of times a word appeared. That is the job of the Map, the phone book of the Collections Framework.
A map stores key-value pairs. Keys are unique; values may repeat. You give it a key and it gives back the value, quickly. The three main implementations mirror the sets you just met: HashMap (fast, no order), LinkedHashMap (insertion order), and TreeMap (sorted by key).
This lesson builds on Lessons 1 to 3, on null and NullPointerException from earlier modules, and on equals and hashCode from BEG-026: map keys are compared exactly the way set elements are.
Learning Objectives
By the end of this lesson you will be able to:
- Create a
Map<String, Integer>and useput,get,containsKey,remove,size, andgetOrDefault. - Explain what
putreturns and whatgetreturns for a missing key. - Choose between
HashMap,LinkedHashMap, andTreeMap. - Loop over a map with
keySet(),values(), andentrySet(). - Count occurrences with
getOrDefault(and recognizemergeas the shorter form). - Avoid the
NullPointerExceptionthat comes from unboxing a missing value.
Written Explanation
What a map promises
A Map<K, V> associates keys of type K with values of type V. The Java documentation says a map "cannot contain duplicate keys; each key can map to at most one value". Values may repeat freely: two products can cost the same.
Declare by interface, create by class, as always:
Map<String, Integer> stock = new HashMap<>();Read it as "a map from String to Integer". Both type arguments must be reference types (Lesson 1).
The core methods
| Method | What it does | Returns |
|---|---|---|
put(k, v) | Stores v under k, replacing any old value | the old value, or null if the key was new |
get(k) | Reads the value for k | the value, or null if there is no mapping |
getOrDefault(k, d) | Like get, but returns d if there is no mapping | value or d |
containsKey(k) | Is there a mapping for k? | boolean |
containsValue(v) | Does some key map to v? (scans all values) | boolean |
remove(k) | Removes the mapping for k | the old value, or null |
size(), isEmpty() | Number of pairs | int / boolean |
Two points deserve attention. put on an existing key replaces the value; it does not add a second entry. And get returns null for a missing key, which is a convenient signal but also the source of the most common map bug, below.
Choosing the implementation
| Class | Order of iteration | Lookup cost |
|---|---|---|
HashMap | None promised | About constant time with good hashing |
LinkedHashMap | Insertion order | About constant time |
TreeMap | Sorted by key | Logarithmic, guaranteed |
The HashMap documentation says it "makes no guarantees as to the order of the map", and that get and put take constant time if the hash function spreads the keys well. A TreeMap guarantees log(n) time for containsKey, get, put, and remove, and keeps keys in ascending order using compareTo or a comparator. The pattern is the same as for sets: start with HashMap; reach for LinkedHashMap when you need insertion order and TreeMap when you need sorted keys.
The keys follow the rules of sets. In a HashMap or LinkedHashMap, two keys are the same when equals says so and their hashCode values agree, so key classes need both methods (BEG-026). In a TreeMap, two keys are the same when the comparison returns 0.
Looping over a map
A map is not a Collection, so it is not directly usable in a for-each. It gives you three views that are collections:
keySet()gives the keys.values()gives the values.entrySet()gives the key-value pairs, each as aMap.Entry<K, V>withgetKey()andgetValue().
for (Map.Entry<String, Integer> e : stock.entrySet()) {
System.out.println(e.getKey() + "=" + e.getValue());
}Use entrySet() when you need both key and value. Looping over keySet() and calling get(k) inside the loop works, but does an extra lookup each time and is more to write.
The missing-key trap
get returns null when the key is absent. With a Map<String, Integer>, the returned type is Integer, a reference. If you then use it where an int is needed, Java unboxes it automatically, and unboxing null throws a NullPointerException:
int n = stock.get("cap"); // "cap" was never addedThe real message on JDK 21 is Cannot invoke "java.lang.Integer.intValue()" because the return value of "java.util.Map.get(Object)" is null. It looks mysterious until you remember what get returns for a missing key. The fixes:
stock.getOrDefault("cap", 0)when a default is natural.containsKeyfirst when absence needs different handling.- Store the result in an
Integervariable and compare withnull.
Counting with a map
Counting is the classic map job: the key is the thing, the value is the count.
counts.put(w, counts.getOrDefault(w, 0) + 1);If w is new, getOrDefault gives 0 and we store 1; otherwise it gives the old count and we store one more. A shorter form is counts.merge(w, 1, Integer::sum). The documentation says merge stores the given value if the key is absent, otherwise it combines the old and new values with the function you pass. The Integer::sum part is a method reference, a way to pass a function that BEG-035 teaches properly; for now read it as "add the old count and the new 1". You may use either form. Lesson 8 builds a full word-frequency program on this idea.
Values can be collections
A value can itself be a collection, which gives "one key, many values":
Map<String, List<String>> byInitial = new TreeMap<>();Each key leads to a list. You create the list the first time you see the key, then add to it. Note that the nested angle brackets are exactly as they read.
Examples
Example 1: put, get, getOrDefault, remove (simple)
Map<String, Integer> stock = new HashMap<>();
System.out.println(stock.put("pen", 10));
System.out.println(stock.put("pen", 12));
stock.put("ink", 3);
System.out.println(stock.get("pen") + " " + stock.get("cap") + " " + stock.getOrDefault("cap", 0));
System.out.println(stock.containsKey("ink") + " " + stock.containsValue(12) + " " + stock.size());
System.out.println(stock.remove("ink") + " " + stock.remove("ink") + " " + stock);
int n = stock.get("cap");Output:
null
10
12 null 0
true true 2
3 null {pen=12}
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "java.lang.Integer.intValue()" because the return value of "java.util.Map.get(Object)" is null
at L04a.main(L04a.java:11)The first put returned null (new key). The second returned the old value 10 and replaced it with 12. The last line triggers the missing-key trap. (Your stack trace will show your own class name and line number.)
Example 2: three maps, same data (practical)
String[] keys = {"pear", "apple", "fig", "kiwi"};
Map<String, Integer> h = new HashMap<>();
Map<String, Integer> l = new LinkedHashMap<>();
Map<String, Integer> t = new TreeMap<>();
for (String k : keys) {
h.put(k, k.length()); l.put(k, k.length()); t.put(k, k.length());
}
System.out.println(h); System.out.println(l); System.out.println(t);
for (Map.Entry<String, Integer> e : t.entrySet()) {
System.out.println(e.getKey() + " -> " + e.getValue());
}
System.out.println(t.keySet() + " " + t.values());Output on the JDK 21 used here:
{apple=5, pear=4, kiwi=4, fig=3}
{pear=4, apple=5, fig=3, kiwi=4}
{apple=5, fig=3, kiwi=4, pear=4}
apple -> 5
fig -> 3
kiwi -> 4
pear -> 4
[apple, fig, kiwi, pear] [5, 3, 4, 4]The HashMap order is arbitrary and may differ on another Java version. LinkedHashMap shows insertion order. TreeMap shows sorted keys, and values() follows the key order. The values 4 appear twice: values may repeat, keys may not.
Example 3: counting words and grouping names (realistic)
Map<String, Integer> counts = new TreeMap<>();
for (String w : "to be or not to be".split(" ")) {
counts.put(w, counts.getOrDefault(w, 0) + 1);
}
System.out.println(counts);
Map<String, List<String>> byInitial = new TreeMap<>();
for (String name : List.of("Ana", "Adam", "Omar", "Olga", "Sam")) {
String key = name.substring(0, 1);
if (!byInitial.containsKey(key)) byInitial.put(key, new ArrayList<>());
byInitial.get(key).add(name);
}
System.out.println(byInitial);Output:
{be=2, not=1, or=1, to=2}
{A=[Ana, Adam], O=[Omar, Olga], S=[Sam]}Running the merge(w, 1, Integer::sum) version on the same words printed the same counts. In the grouping loop, containsKey guards the get, so the list always exists before we add to it.
Backend Insight
Maps are everywhere in backend code. A cache is a map from a key (such as a user id) to a stored result. HTTP headers and query parameters are maps from names to values. JSON objects map directly onto Map<String, Object> when you do not have a class for them. Configuration, feature flags, and "lookup this customer by id" all use the same shape.
The performance lesson from Lesson 2 pays off here: finding a customer by id in a List<Customer> scans the list, while a Map<Long, Customer> jumps straight to it. A service handling thousands of requests per second cannot afford a scan per request over a large list. And the "missing key returns null" behavior shows up in real systems as NullPointerException in production; treating "not found" as an explicit case (with containsKey or getOrDefault) is the habit that prevents it. Later modules introduce Optional for expressing "maybe a value"; the principle is the same.
Common Mistakes
Mistake 1: Unboxing a missing value
- Incorrect:
int count = counts.get(word) + 1;for a word not yet in the map (throwsNullPointerException). - Correct:
int count = counts.getOrDefault(word, 0) + 1;.
Mistake 2: Expecting put to add a second value
put("pen", 12) when pen exists replaces the value. If you need several values per key, make the value a list.
Mistake 3: Using a list of pairs instead of a map
Searching a List of key-value objects scans the list every time. If you look things up by key, use a map.
Mistake 4: Relying on HashMap iteration order
Do not write code or tests that depend on the order of a HashMap. Use LinkedHashMap or TreeMap when order matters.
Mistake 5: Keys without equals and hashCode, or keys that change
A HashMap key class must have consistent equals and hashCode. And if a key object is changed after it was stored, in a way that changes its hash, the map may no longer find it. Prefer immutable keys such as String, Integer, or records.
Exercises
Try each exercise yourself before opening the solutions.
Level A: Foundation
A1. What does this print?
Map<String, Integer> m = new HashMap<>();
m.put("a", 1); m.put("b", 2); m.put("a", 3);
System.out.println(m.size() + " " + m.get("a") + " " + m.get("z"));أظهر الحل A1
A1. 2 3 null. The second put("a", ...) replaced the value, so there are two keys, and a maps to 3. get("z") is null.
A2. What does get return for a missing key, and what does put return for a new key?
أظهر الحل A2
A2. get returns null for a missing key. put returns null for a new key (and the previous value when it replaces one).
A3. Which map class would you use for a lookup where keys must be printed in alphabetical order?
أظهر الحل A3
A3. TreeMap, because it iterates in sorted key order.
Level B: Application
B1. Given Map<String, Integer> prices containing pen=2, book=10, bag=25 (in a LinkedHashMap), write a loop over entrySet() that prints the sum of the values.
أظهر الحل B1
B1.
int total = 0;
for (Map.Entry<String, Integer> e : prices.entrySet()) {
total += e.getValue();
}
System.out.println(total);It prints 37.
B2. Given Map<Integer, String> ids holding 7=Ana and 3=Omar, write one line that gets the name for id 5 and prints unknown when absent.
أظهر الحل B2
B2. System.out.println(ids.getOrDefault(5, "unknown")); prints unknown.
B3. This code throws. Say why, and fix it two ways.
Map<String, Integer> price = new HashMap<>();
price.put("pen", 2);
int p = price.get("cap");أظهر الحل B3
B3. price.get("cap") is null, and assigning it to an int unboxes null, which throws NullPointerException. Fix 1: int p = price.getOrDefault("cap", 0);. Fix 2: int p = price.containsKey("cap") ? price.get("cap") : -1; (the -1 is a made-up "missing" marker; pick one that suits your data).
Level C: Challenge
C1. Write static Map<Integer, List<String>> byLength(List<String> words) that groups words by length, with lengths in ascending order. For fig, pear, kiwi, pen, apple print the result.
أظهر الحل C1
C1.
static Map<Integer, List<String>> byLength(List<String> words) {
Map<Integer, List<String>> out = new TreeMap<>();
for (String w : words) {
List<String> group = out.get(w.length());
if (group == null) {
group = new ArrayList<>();
out.put(w.length(), group);
}
group.add(w);
}
return out;
}The result is {3=[fig, pen], 4=[pear, kiwi], 5=[apple]}. The null check is safe here because we compare the list, not unbox a number.
C2. For each case choose HashMap, LinkedHashMap, or TreeMap and justify it in one line: (a) a cache of users by id where order is irrelevant, (b) a report that lists countries alphabetically with their totals, (c) a settings map that must be saved to a file in the order the settings were defined.
أظهر الحل C2
C2. (a) HashMap: fastest general lookup, order not needed. (b) TreeMap: keys come out sorted by country name. (c) LinkedHashMap: it remembers insertion order.
Mini Challenge
Write an inventory program with a Map<String, Integer> from product name to quantity:
- Add four products with quantities, using a
LinkedHashMap. - Write
static void sell(Map<String, Integer> stock, String item, int qty)that printsunknown itemif the item is absent andnot enough stockif the quantity is too high; otherwise it reduces the stock. - Call
sellfor a known item, an unknown one, and one with too large a quantity. - Print all entries with a
entrySet()loop, and print the total number of units.
Check your work: no NullPointerException for the unknown item, and the printed order matches insertion order.
Summary & Next Lesson
Key takeaways
- A
Mapstores key-value pairs; keys are unique, values may repeat, and each key has at most one value. putreplaces on an existing key and returns the old value;getreturnsnullfor a missing key.getOrDefaultandcontainsKeyhandle "missing" safely; unboxing a missingIntegerthrowsNullPointerException.HashMapis fast with no order,LinkedHashMapkeeps insertion order,TreeMapsorts by key.- Loop with
entrySet()for key and value together; count withgetOrDefaultormerge.
Next lesson
The next lesson, "Queues and deques", moves from lookup to processing order: first in, first out, and stacks.
Self-check
- Can a map hold duplicate keys? Duplicate values?
- What does
getreturn when the key is missing? - Which loop gives you both key and value?
- Why can
int n = map.get("x")throw aNullPointerException? - Which map class returns keys in sorted order?
أظهر الإجابات
- No for keys (a repeated
putreplaces the value); yes for values. null.- A for-each over
entrySet(), usinggetKey()andgetValue(). - If the key is missing,
getreturnsnull, and automatic unboxing ofnulltointthrows. TreeMap.
References
- Oracle, Java SE 21 API,
Map. Supports unique keys,getreturningnullfor no mapping,getOrDefault, and theentrySetview. - Oracle, Java SE 21 API,
HashMap. Supports no order guarantee, null key and values permitted, constant-timeget/putwith good hashing, and the behavior ofmerge. - Oracle, Java SE 21 API,
TreeMap. Supports sorted keys, log(n) cost, and comparison bycompareToor a comparator. - Oracle, Java SE 21 API,
LinkedHashMap. Supports that iteration order is normally insertion order, and that re-inserting a key does not change its position. - Output and the
NullPointerExceptionmessage were produced by compiling and running the examples with JDK 21. TheHashMaporder shown is one real run and not guaranteed.
التطبيق
طبّق هذا الدرس بمختبر الوحدة وتمارينها واختبارها.
تعليم الدرس كمكتمل اختياري ويمكنك التراجع عنه في أي وقت. يبقى تقدمك محفوظًا في هذا المتصفح.