# Java Maps

## Map implementations

**HashMap:** best performance for basic operations like get() and put(), one null key allowed, no fix order for entries

**LinkedHashMap:** worse performance, but fix order for entries

**TreeMap:** keys are sorted in natural order or by a custom Comparator, no null keys allowed

**ConcurrentHashMap:** thread-safe version of HashMap, no null keys and values allowed

**EnumMap:** for enum keys, good performance

**WeakHashMap:** holds keys via weak references, removes entries when keys are no longer strongly reachable, useful for caches

Code blocks in this cheatsheet use HashMap, but most operations work with all other Map implementations too.

## Define a Map

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

Map<String, Integer> scores = new HashMap<>();
```

Define an immutable Map with no entry:

```java
import java.util.Map;
import java.util.Collections;

Map<String, Integer> scores = Collections.emptyMap();
```

Define an immutable Map with only one entry:

```java
import java.util.Map;
import java.util.Collections;

Map<String, Integer> scores = Collections.singletonMap("Alice", 12);
```

Define an immutable Map with a maximum of 10 entries:

```java
import java.util.Map;

Map<String, Integer> scores = Map.of(
  "Alice", 12,
  "Bob", 34,
  "Charlie", 23,
  "Daniel", 35,
  "Ellie", 54
);
```

Define an immutable Map with any number of entries:

```java
import java.util.Map;

Map<String, Integer> scores = Map.ofEntries(
  Map.entry("Alice", 12),
  Map.entry("Bob", 34),
  Map.entry("Charlie", 23),
  Map.entry("Daniel", 35),
  Map.entry("Ellie", 54)
);
```

Define a synchronized (thread-safe) map based on a Map implementation (e.g. HashMap):

```java
import java.util.HashMap;
import java.util.Map;
import java.util.Collections;

Map<String, Integer> scores = Collections.synchronizedMap(new HashMap<>());
```

Define a synchronized (thread-safe) map based on a SortedMap implementation (e.g. TreeMap):

```java
import java.util.TreeMap;
import java.util.Map;
import java.util.Collections;

Map<String, Integer> scores = Collections.synchronizedSortedMap(new TreeMap<>());
```

## Add entry/entries to a Map

Add a single entry:

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

Map<String, Integer> scores = new HashMap<>();
scores.put("Alice", 95);
```

Add a single entry only if there isn't an entry yet with the same key:

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

Map<String, Integer> scores = new HashMap<>();
scores.put("Alice", 95);

scores.putIfAbsent("Alice", 67);
// "Alice" is still associated with 95
```

Add a key with the result of a specified operation as value:

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

Map<String, Integer> scores = new HashMap<>();
scores.put("Alice", 95);

scores.compute("Alice", (key, value) -> key.length());
// `scores` becomes {Alice=5} (because the length of "Alice" is calculated to be 5)

scores.compute("Bob", (key, value) -> key.length());
// `scores` becomes {Bob=3, Alice=5}
```

Add a key with the result of a specified operation as value, only if there's already an entry with the specified key:

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

Map<String, Integer> scores = new HashMap<>();
scores.put("Alice", 95);

scores.computeIfPresent("Alice", (key, value) -> key.length());
// `scores` becomes {Alice=5} (because the length of "Alice" is calculated to be 5)

scores.computeIfPresent("Bob", (key, value) -> key.length());
// `scores` remains {Alice=5} (because there was no entry with the key "Bob")
```

Add a key with the result of a specified operation as value, only if there isn't an entry yet with the same key:

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

Map<String, Integer> scores = new HashMap<>();
scores.put("Alice", 95);

scores.computeIfAbsent("Alice", key -> key.length());
// `scores` remains {Alice=95}

scores.computeIfAbsent("Bob", key -> key.length());
// `scores` becomes {Alice=95, Bob=3}

// Note that the lambda expression in computeIfAbsent() only has one parameter for the key, unlike in compute() and computeIfPresent()!
```

Add all entries of a Map to another Map:

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

Map<String, Integer> scores1 = new HashMap<>();
scores1.put("Alice", 95);

Map<String, Integer> scores2 = new HashMap<>();
scores2.put("Bob", 88);
scores2.put("Charlie", 90);

scores1.putAll(scores2);
// `scores1` becomes {Bob=88, Alice=95, Charlie=90}
```

## Query value of a key

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

Map<String, Integer> scores = new HashMap<>();
scores.put("Alice", 95);

Integer aliceScore = scores.get("Alice"); // 95
Integer unknown = scores.get("Charlie"); // null
```

Provide a default value used if key doesn't have a value:

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

Map<String, Integer> scores = new HashMap<>();
scores.put("Alice", 95);

Integer aliceScore = scores.getOrDefault("Alice", 67); // 95
Integer unknown = scores.getOrDefault("Charlie", 67); // 67
```

## Get number of entries

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

Map<String, Integer> scores = new HashMap<>();
scores.put("Alice", 95);
scores.put("Bob", 88);

int size = scores.size(); // 2
```

## Check if Map is empty

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

Map<String, Integer> scores = new HashMap<>();
boolean isEmpty = scores.isEmpty(); // true
```

## Get entries, keys, or values of Map

```java
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.Collection;

Map<String, Integer> scores = new HashMap<>();
scores.put("Alice", 95);
scores.put("Bob", 88);

Set<String> keys = scores.keySet(); // ["Alice", "Bob"]
Collection<Integer> values = scores.values(); // [95, 88]
Set<Map.Entry<String, Integer>> entries = scores.entrySet(); // [Bob=88, Alice=95]
```

## Check if Map contains a key or value

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

Map<String, Integer> scores = new HashMap<>();
scores.put("Alice", 95);

boolean hasAlice = scores.containsKey("Alice"); // true
boolean has95 = scores.containsValue(95); // true
```

## Check if Map contains all entries of another Map

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

Map<String, Integer> scores = new HashMap<>();
scores.put("Alice", 95);
scores.put("Bob", 88);

Map<String, Integer> subset = new HashMap<>();
subset.put("Alice", 95);

boolean containsSubset = scores.entrySet().containsAll(subset.entrySet()); // true
```

## Modify value of a key

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

Map<String, Integer> scores = new HashMap<>();
scores.put("Alice", 95);

scores.put("Alice", 97);
// or
scores.replace("Alice", 97);
// original value of "Alice" is overridden, `scores` becomes {Alice=97}
```

Modify value of a key only if it has a specific value:

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

Map<String, Integer> scores = new HashMap<>();
scores.put("Alice", 95);

scores.replace("Alice", 96, 97);
// `scores` remains {Alice=95}

scores.replace("Alice", 95, 97);
// `scores` becomes {Alice=97}
```

Merge new value with old value based on a specified logic:

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

Map<String, Integer> scores = new HashMap<>();
scores.put("Alice", 97);

scores.merge("Alice", 1, (oldValue, newValue) -> oldValue + newValue);
// `scores` becomes {Alice=98}
```

Modify all values based on a specified logic:

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

Map<String, Integer> scores = new HashMap<>();
scores.put("Alice", 95);

scores.replaceAll((key, value) -> value+1);
// `scores` becomes {Alice=96}
```

## Remove entries

Remove a single entry by key, return its value:

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

Map<String, Integer> scores = new HashMap<>();
scores.put("Alice", 95);
scores.put("Bob", 88);
scores.put("Charlie", 90);

Integer removed = scores.remove("Alice"); // 95
// `scores` becomes {Bob=88, Charlie=90}
Integer notFound = scores.remove("Daniel"); // null
// `scores` remains {Bob=88, Charlie=90}
```

Remove entry where both key and value match:

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

Map<String, Integer> scores = new HashMap<>();
scores.put("Alice", 95);
scores.put("Bob", 88);
scores.put("Charlie", 90);

boolean isItRemoved = scores.remove("Alice", 67); // false
// `scores` remains {Alice=95, Bob=88, Charlie=90}
boolean isItRemoved2 = scores.remove("Alice", 95); // true
// `scores` becomes {Bob=88, Charlie=90}
```

Remove all entries:

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

Map<String, Integer> scores = new HashMap<>();
scores.put("Alice", 95);
scores.put("Bob", 88);
scores.put("Charlie", 90);

scores.clear();
// `scores` becomes empty
```

Remove all entries, the key of which is present in the provided Collection:

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

Map<String, Integer> scores = new HashMap<>();
scores.put("Alice", 95);
scores.put("Bob", 88);
scores.put("Charlie", 90);

Set<String> toRemove = Set.of("Alice", "Bob");
scores.keySet().removeAll(toRemove);
// `scores` becomes {Charlie=90}
```

Remove all entries matching the specified condition:

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

Map<String, Integer> scores = new HashMap<>();
scores.put("Alice", 95);
scores.put("Bob", 88);
scores.put("Charlie", 90);

scores.entrySet().removeIf(entry -> entry.getValue() < 90);
// `scores` becomes {Alice=95, Charlie=90}
```

## Iterate over Map

Using forEach():

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

Map<String, Integer> scores = new HashMap<>();
scores.put("Alice", 95);
scores.put("Bob", 88);
scores.put("Charlie", 90);

scores.forEach((key, value) -> System.out.println(key + ": " + value));
```

Iterate over entries using enhanced for loop:

```java
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

Map<String, Integer> scores = new HashMap<>();
scores.put("Alice", 95);
scores.put("Bob", 88);
scores.put("Charlie", 90);

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

## Copy Map

Shallow copy (using constructor copy):

```java
import java.util.HashMap;
import java.util.Map;
import java.util.List;
import java.util.ArrayList;
import java.util.Arrays;

Map<String, List<Integer>> grades = new HashMap<>();
List<Integer> aliceGrades = new ArrayList<>(Arrays.asList(2, 4, 1));
grades.put("Alice", aliceGrades);

Map<String, List<Integer>> gradesCopy = new HashMap<>(grades);
List<Integer> bobGrades = new ArrayList<>(Arrays.asList(1, 5, 3));
gradesCopy.put("Bob", bobGrades);

// `grades` remains {Alice=[2, 4, 1]}
// `gradesCopy` becomes {Bob=[1, 5, 3], Alice=[2, 4, 1]}

aliceGrades.add(1);

// `grades` becomes {Alice=[2, 4, 1, 1]}
// `gradesCopy` becomes {Bob=[1, 5, 3], Alice=[2, 4, 1, 1]}
```

Shallow copy (using clone() method) - variable must be the same type as the stored object, can't be of superclass type!:

```java
import java.util.HashMap;
import java.util.Map;
import java.util.List;
import java.util.ArrayList;
import java.util.Arrays;

HashMap<String, List<Integer>> grades = new HashMap<>();
List<Integer> aliceGrades = new ArrayList<>(Arrays.asList(2, 4, 1));
grades.put("Alice", aliceGrades);

HashMap<String, List<Integer>> gradesCopy = (HashMap<String, List<Integer>>) grades.clone();
List<Integer> bobGrades = new ArrayList<>(Arrays.asList(1, 5, 3));
gradesCopy.put("Bob", bobGrades);

// `grades` remains {Alice=[2, 4, 1]}
// `gradesCopy` becomes {Bob=[1, 5, 3], Alice=[2, 4, 1]}

aliceGrades.add(1);

// `grades` becomes {Alice=[2, 4, 1, 1]}
// `gradesCopy` becomes {Bob=[1, 5, 3], Alice=[2, 4, 1, 1]}
```

Deep copy (using enhanced for loop):

```java
import java.util.HashMap;
import java.util.Map;
import java.util.List;
import java.util.ArrayList;
import java.util.Arrays;

Map<String, List<Integer>> grades = new HashMap<>();
List<Integer> aliceGrades = new ArrayList<>(Arrays.asList(2, 4, 1));
grades.put("Alice", aliceGrades);

Map<String, List<Integer>> gradesCopy = new HashMap<>();

for (Map.Entry<String, List<Integer>> entry : grades.entrySet()) {
  String key = entry.getKey();
  // ArrayList is only shallow copied because Integer elements are immutable
  // for more details about copying a List, see the Java Lists cheatsheet
  List<Integer> value = new ArrayList<>(entry.getValue());
  gradesCopy.put(key, value);
}

List<Integer> bobGrades = new ArrayList<>(Arrays.asList(1, 5, 3));
gradesCopy.put("Bob", bobGrades);

// `grades` remains {Alice=[2, 4, 1]}
// `gradesCopy` becomes {Bob=[1, 5, 3], Alice=[2, 4, 1]}

aliceGrades.add(1);

// `grades` becomes {Alice=[2, 4, 1, 1]}
// `gradesCopy` remains {Bob=[1, 5, 3], Alice=[2, 4, 1]}
```

Deep copy (using Stream - only for Java 8 and higher):

```java
import java.util.HashMap;
import java.util.Map;
import java.util.List;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.stream.Collectors;

Map<String, List<Integer>> grades = new HashMap<>();
List<Integer> aliceGrades = new ArrayList<>(Arrays.asList(2, 4, 1));
grades.put("Alice", aliceGrades);

Map<String, List<Integer>> gradesCopy = grades.entrySet().stream()
            .collect(Collectors.toMap(
                entry -> entry.getKey(),
                entry -> new ArrayList<>(entry.getValue())
            ));

List<Integer> bobGrades = new ArrayList<>(Arrays.asList(1, 5, 3));
gradesCopy.put("Bob", bobGrades);

// `grades` remains {Alice=[2, 4, 1]}
// `gradesCopy` becomes {Bob=[1, 5, 3], Alice=[2, 4, 1]}

aliceGrades.add(1);

// `grades` becomes {Alice=[2, 4, 1, 1]}
// `gradesCopy` remains {Bob=[1, 5, 3], Alice=[2, 4, 1]}
```

Unmodifiable copy:

```java
import java.util.HashMap;
import java.util.Map;
import java.util.Collections;

Map<String, List<Integer>> grades = new HashMap<>();
List<Integer> aliceGrades = new ArrayList<>(Arrays.asList(2, 4, 1));
grades.put("Alice", aliceGrades);

Map<String, List<Integer>> gradesLocked = Collections.unmodifiableMap(grades);
```
