انتقل إلى المحتوى
أمين أبوزيد
منشورBEG-030-L04

الخرائط (Maps)

  • الدرس 4 من 9
  • 12 دقيقة قراءة
  • مبتدئ
في هذا الدرس 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 use put, get, containsKey, remove, size, and getOrDefault.
  • Explain what put returns and what get returns for a missing key.
  • Choose between HashMap, LinkedHashMap, and TreeMap.
  • Loop over a map with keySet(), values(), and entrySet().
  • Count occurrences with getOrDefault (and recognize merge as the shorter form).
  • Avoid the NullPointerException that 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:

java
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

MethodWhat it doesReturns
put(k, v)Stores v under k, replacing any old valuethe old value, or null if the key was new
get(k)Reads the value for kthe value, or null if there is no mapping
getOrDefault(k, d)Like get, but returns d if there is no mappingvalue 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 kthe old value, or null
size(), isEmpty()Number of pairsint / 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

ClassOrder of iterationLookup cost
HashMapNone promisedAbout constant time with good hashing
LinkedHashMapInsertion orderAbout constant time
TreeMapSorted by keyLogarithmic, 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 a Map.Entry<K, V> with getKey() and getValue().
java
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:

java
int n = stock.get("cap");   // "cap" was never added

The 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.
  • containsKey first when absence needs different handling.
  • Store the result in an Integer variable and compare with null.

Counting with a map

Counting is the classic map job: the key is the thing, the value is the count.

java
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":

java
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)

java
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:

text
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)

java
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:

text
{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)

java
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:

text
{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 (throws NullPointerException).
  • 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?

java
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.

java
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.

java
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.

java
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:

  1. Add four products with quantities, using a LinkedHashMap.
  2. Write static void sell(Map<String, Integer> stock, String item, int qty) that prints unknown item if the item is absent and not enough stock if the quantity is too high; otherwise it reduces the stock.
  3. Call sell for a known item, an unknown one, and one with too large a quantity.
  4. 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 Map stores key-value pairs; keys are unique, values may repeat, and each key has at most one value.
  • put replaces on an existing key and returns the old value; get returns null for a missing key.
  • getOrDefault and containsKey handle "missing" safely; unboxing a missing Integer throws NullPointerException.
  • HashMap is fast with no order, LinkedHashMap keeps insertion order, TreeMap sorts by key.
  • Loop with entrySet() for key and value together; count with getOrDefault or merge.

Next lesson

The next lesson, "Queues and deques", moves from lookup to processing order: first in, first out, and stacks.

Self-check

  1. Can a map hold duplicate keys? Duplicate values?
  2. What does get return when the key is missing?
  3. Which loop gives you both key and value?
  4. Why can int n = map.get("x") throw a NullPointerException?
  5. Which map class returns keys in sorted order?
أظهر الإجابات
  1. No for keys (a repeated put replaces the value); yes for values.
  2. null.
  3. A for-each over entrySet(), using getKey() and getValue().
  4. If the key is missing, get returns null, and automatic unboxing of null to int throws.
  5. TreeMap.

References

  • Oracle, Java SE 21 API, Map. Supports unique keys, get returning null for no mapping, getOrDefault, and the entrySet view.
  • Oracle, Java SE 21 API, HashMap. Supports no order guarantee, null key and values permitted, constant-time get/put with good hashing, and the behavior of merge.
  • Oracle, Java SE 21 API, TreeMap. Supports sorted keys, log(n) cost, and comparison by compareTo or 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 NullPointerException message were produced by compiling and running the examples with JDK 21. The HashMap order shown is one real run and not guaranteed.

التطبيق

طبّق هذا الدرس بمختبر الوحدة وتمارينها واختبارها.

المختبر والتمارين الاختبار

تعليم الدرس كمكتمل اختياري ويمكنك التراجع عنه في أي وقت. يبقى تقدمك محفوظًا في هذا المتصفح.

اكتب للبحث في المستويات والمراحل والوحدات والدروس.