Collections Framework
Introduction to the Java Collections Framework, covering ArrayList, LinkedList, HashMap, HashSet, and other essential data structures.
HashMap in Java
Explanation of HashMap
A HashMap is a part of the Java Collections Framework and implements the Map
interface. It stores data in key-value pairs. Unlike other Map
implementations like TreeMap
which maintains elements in a sorted order, HashMap provides no guarantees as to the order of its elements. Crucially, it allows null
keys (only one) and null
values. The HashMap uses a technique called *hashing* to efficiently store and retrieve elements.
Key features of a HashMap:
- Unordered: Elements are not stored in any particular order.
- Key-Value Pairs: Stores data as key-value pairs. Keys must be unique within a HashMap.
- Hashing: Uses a hash function to calculate the index where the key-value pair is stored.
- Allows Null: Can store a single null key and multiple null values.
- Not Synchronized: Not thread-safe by default. Use
Collections.synchronizedMap(new HashMap(...))
orConcurrentHashMap
for thread-safe access.
Detailed Explanation of the HashMap Class
Creation
You can create a HashMap using one of its constructors:
HashMap()
: Creates an empty HashMap with the default initial capacity (16) and load factor (0.75).HashMap(int initialCapacity)
: Creates an empty HashMap with the specified initial capacity and the default load factor (0.75).HashMap(int initialCapacity, float loadFactor)
: Creates an empty HashMap with the specified initial capacity and load factor.HashMap(Map<? extends K, ? extends V> m)
: Creates a new HashMap with the same mappings as the specified Map.
import java.util.HashMap;
import java.util.Map;
public class HashMapCreationExample {
public static void main(String[] args) {
// Creating an empty HashMap with default capacity and load factor
HashMap<String, Integer> map1 = new HashMap<>();
// Creating a HashMap with an initial capacity of 20
HashMap<String, Integer> map2 = new HashMap<>(20);
// Creating a HashMap with an initial capacity of 20 and a load factor of 0.8
HashMap<String, Integer> map3 = new HashMap<>(20, 0.8f);
// Creating a HashMap from another Map
Map<String, Integer> existingMap = Map.of("A", 1, "B", 2);
HashMap<String, Integer> map4 = new HashMap<>(existingMap);
System.out.println("Map 1: " + map1);
System.out.println("Map 2: " + map2);
System.out.println("Map 3: " + map3);
System.out.println("Map 4: " + map4);
}
}
Adding Key-Value Pairs
You can add key-value pairs to a HashMap using the put(K key, V value)
method. If the key already exists, the old value is replaced with the new value, and the old value is returned. If the key doesn't exist, the new key-value pair is added, and null
is returned.
import java.util.HashMap;
public class HashMapPutExample {
public static void main(String[] args) {
HashMap<String, Integer> ages = new HashMap<>();
// Adding key-value pairs
ages.put("Alice", 30);
ages.put("Bob", 25);
ages.put("Charlie", 35);
System.out.println("HashMap after adding elements: " + ages);
// Adding a duplicate key (will overwrite the old value)
ages.put("Alice", 31); // Alice's age is updated
System.out.println("HashMap after updating Alice's age: " + ages);
}
}
Retrieving Values
You can retrieve values from a HashMap using the get(Object key)
method. This method returns the value associated with the specified key, or null
if the key is not found.
import java.util.HashMap;
public class HashMapGetExample {
public static void main(String[] args) {
HashMap<String, Integer> ages = new HashMap<>();
ages.put("Alice", 30);
ages.put("Bob", 25);
// Retrieving values
Integer aliceAge = ages.get("Alice");
Integer bobAge = ages.get("Bob");
Integer daveAge = ages.get("Dave"); // Key does not exist
System.out.println("Alice's age: " + aliceAge); // Output: 30
System.out.println("Bob's age: " + bobAge); // Output: 25
System.out.println("Dave's age: " + daveAge); // Output: null (Key not found)
}
}
Removing Entries
You can remove entries from a HashMap using the remove(Object key)
method. This method removes the entry associated with the specified key and returns the value that was associated with the key, or null
if the key is not found. There's also a `remove(Object key, Object value)` method which removes the entry only if the key is associated with the specified value.
import java.util.HashMap;
public class HashMapRemoveExample {
public static void main(String[] args) {
HashMap<String, Integer> ages = new HashMap<>();
ages.put("Alice", 30);
ages.put("Bob", 25);
// Removing an entry
Integer removedAge = ages.remove("Alice");
System.out.println("HashMap after removing Alice: " + ages); // Output: {Bob=25}
System.out.println("Removed age: " + removedAge); // Output: 30
// Removing a non-existent key
Integer nonExistentRemoval = ages.remove("Charlie");
System.out.println("Removed age when key doesn't exist: " + nonExistentRemoval); // Output: null
// Remove if and only if the key is associated with the specified value
ages.put("Bob", 25);
boolean removedCorrectly = ages.remove("Bob", 25);
System.out.println("Removed Bob successfully: " + removedCorrectly); // Output: true
System.out.println("HashMap after removing Bob: " + ages); // Output: {}
}
}
Iterating Through Keys and Values
You can iterate through the keys, values, or entries (key-value pairs) of a HashMap using various methods:
- Iterating through keys: Use the
keySet()
method, which returns aSet
of keys. - Iterating through values: Use the
values()
method, which returns aCollection
of values. - Iterating through entries (key-value pairs): Use the
entrySet()
method, which returns aSet
ofMap.Entry
objects.
import java.util.HashMap;
import java.util.Map;
public class HashMapIterationExample {
public static void main(String[] args) {
HashMap<String, Integer> ages = new HashMap<>();
ages.put("Alice", 30);
ages.put("Bob", 25);
ages.put("Charlie", 35);
System.out.println("Iterating through keys:");
for (String key : ages.keySet()) {
System.out.println("Key: " + key);
}
System.out.println("\nIterating through values:");
for (Integer value : ages.values()) {
System.out.println("Value: " + value);
}
System.out.println("\nIterating through entries (key-value pairs):");
for (Map.Entry<String, Integer> entry : ages.entrySet()) {
System.out.println("Key: " + entry.getKey() + ", Value: " + entry.getValue());
}
System.out.println("\nIterating through entries using forEach:");
ages.forEach((key, value) -> System.out.println("Key: " + key + ", Value: " + value));
}
}
Handling Collisions
Collisions occur when two or more keys have the same hash code. HashMap handles collisions using a technique called *separate chaining*. Each bucket in the HashMap's internal array (called the "table") points to a linked list (or, from Java 8 onwards, a tree node for larger buckets) of key-value pairs that have the same hash code.
When a collision occurs:
- The HashMap calculates the hash code of the key.
- It determines the index (bucket) in the internal array using the hash code.
- If the bucket is empty, a new linked list (or tree node) is created at that index, and the key-value pair is added.
- If the bucket is not empty (collision), the new key-value pair is added to the existing linked list (or tree node). If using Java 8 onwards, and the linked list gets too long (more than 8 nodes by default), it's converted to a self-balancing tree (e.g., a red-black tree) to improve performance.
When retrieving a value, the HashMap follows a similar process:
- Calculates the hash code of the key.
- Determines the index (bucket) in the internal array.
- If the bucket is empty, the key is not found.
- If the bucket is not empty, it traverses the linked list (or tree) in that bucket, comparing the key with the keys of the elements in the list/tree until a match is found.
The choice of a good hash function is crucial for minimizing collisions and maintaining good performance. A well-distributed hash function will distribute keys evenly across the buckets, reducing the length of the linked lists (or trees) and improving retrieval time.
Time Complexity of Operations
The time complexity of HashMap operations depends on the number of collisions that occur. In the best-case scenario, where there are no collisions, all operations have a time complexity of O(1) (constant time). However, in the worst-case scenario, where all keys hash to the same bucket, the time complexity can degrade to O(n) (linear time), where n is the number of entries in the HashMap.
- put(K key, V value): Average case: O(1), Worst case: O(n) (if all keys collide)
- get(Object key): Average case: O(1), Worst case: O(n) (if all keys collide)
- remove(Object key): Average case: O(1), Worst case: O(n) (if all keys collide)
- containsKey(Object key): Average case: O(1), Worst case: O(n) (if all keys collide)
- containsValue(Object value): Average case: O(n), Worst case: O(n)
- size(): O(1)
- isEmpty(): O(1)
- Iteration (keySet(), values(), entrySet()): O(n)
Note: In Java 8 and later, when the number of elements in a bucket exceeds a certain threshold (TREEIFY_THRESHOLD = 8), the linked list is replaced by a balanced tree (typically a red-black tree). This improves the worst-case time complexity for get()
, put()
, and remove()
to O(log n) in the case of many collisions within a single bucket. This makes the overall HashMap performance more predictable and efficient.