Mastering Maps, Concurrent Collections, and Advanced Java Collections

Mastering Maps, Concurrent Collections, and Advanced Java Collections

In the previous weeks articles, we explored the List, Set, and Queue interfaces and their implementations. We understood their structures, performance characteristics, and real-world use cases. Now, let’s dive into the Map interface, which is used to store key-value pairs, and explore advanced topics like concurrent collections, custom sorting, and Stream API integration. This article will help you master the more advanced aspects of the Java Collection Framework.


1. Introduction to the Map Interface

The Map interface represents a collection of key-value pairs. Unlike List, Set, and Queue, the Map interface does not extend the Collection interface because it operates on pairs of objects (keys and values) rather than individual elements.

Key Features of Map

  • Unique Keys: Each key in a Map must be unique.
  • Null Keys/Values: Some implementations allow null keys and values (e.g., HashMap), while others do not (e.g., TreeMap, which does not allow null keys but allows null values).
  • Ordering: Some implementations maintain order (e.g., LinkedHashMap maintains insertion order, while TreeMap sorts keys). Others, like HashMap, have an unpredictable iteration order based on internal hashing.


2. Map Implementations

The Map interface has several implementations, each with its own use cases and performance characteristics.

2.1 HashMap

  • Description: HashMap is an unordered collection that uses hashing to store key-value pairs.
  • Use Case: Ideal for scenarios where you need fast access to values by key and don’t care about order.
  • Performance:

Access/Insertion/Deletion: O(1) (average case), but O(n) in the worst case due to hash collisions.

Example: Using HashMap

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

public class HashMapExample {
    public static void main(String[] args) {
        Map<String, Integer> fruitPrices = new HashMap<>();
        fruitPrices.put("Apple", 50);
        fruitPrices.put("Banana", 20);
        fruitPrices.put("Cherry", 30);

        System.out.println("Fruit Prices: " + fruitPrices);
    }
}        

2.2 LinkedHashMap

  • Description: LinkedHashMap maintains insertion order while storing key-value pairs.
  • Use Case: Ideal for scenarios where you need to maintain the order of insertion.
  • Performance:

Access/Insertion/Deletion: O(1) (average case), but slightly more memory-intensive than HashMap.

Example: Using LinkedHashMap

import java.util.LinkedHashMap;
import java.util.Map;

public class LinkedHashMapExample {
    public static void main(String[] args) {
        Map<String, Integer> orderedFruitPrices = new LinkedHashMap<>();
        orderedFruitPrices.put("Apple", 50);
        orderedFruitPrices.put("Banana", 20);
        orderedFruitPrices.put("Cherry", 30);

        System.out.println("Ordered Fruit Prices: " + orderedFruitPrices); 
        // Output: {Apple=50, Banana=20, Cherry=30}
    }
}        

2.3 TreeMap

  • Description: TreeMap is a sorted map that uses a Red-Black Tree for storage.
  • Use Case: Ideal for scenarios where you need key-value pairs in sorted order.
  • Performance:

Access/Insertion/Deletion: O(log n)

TreeMap does not allow null keys, but allows null values.

Example: Using TreeMap

import java.util.TreeMap;
import java.util.Map;

public class TreeMapExample {
    public static void main(String[] args) {
        Map<String, Integer> sortedFruitPrices = new TreeMap<>();
        sortedFruitPrices.put("Banana", 20);
        sortedFruitPrices.put("Apple", 50);
        sortedFruitPrices.put("Cherry", 30);

        System.out.println("Sorted Fruit Prices: " + sortedFruitPrices); 
        // Output: {Apple=50, Banana=20, Cherry=30}
    }
}        

3. Concurrent Collections

Concurrent collections are designed for use in multi-threaded environments. They provide thread-safe operations without the need for external synchronization.

3.1 ConcurrentHashMap

  • Description: ConcurrentHashMap is a thread-safe version of HashMap.
  • Use Case: Ideal for scenarios where multiple threads need to read/write to a map concurrently.
  • Performance:

Access/Insertion/Deletion: O(1) (average case)

Uses segmented locking (pre-Java 8) and CAS-based updates (Java 8+) for improved concurrency.

Example: Using ConcurrentHashMap

import java.util.concurrent.ConcurrentHashMap;
import java.util.Map;

public class ConcurrentHashMapExample {
    public static void main(String[] args) {
        Map<String, Integer> concurrentFruitPrices = new ConcurrentHashMap<>();
        concurrentFruitPrices.put("Apple", 50);
        concurrentFruitPrices.put("Banana", 20);

        System.out.println("Concurrent Fruit Prices: " + concurrentFruitPrices); 
        // Output: {Apple=50, Banana=20}
    }
}        

3.2 CopyOnWriteArrayList

  • Description: CopyOnWriteArrayList is a thread-safe version of ArrayList.
  • Use Case: Ideal for scenarios where reads are more frequent than writes.
  • Performance:

Access: O(1)

Insertion/Deletion: O(n) (due to array copying on modification)

Trade-off: It is efficient for read-heavy use cases but not suitable for frequent modifications due to copying overhead.

Example: Using CopyOnWriteArrayList

import java.util.concurrent.CopyOnWriteArrayList;
import java.util.List;

public class CopyOnWriteArrayListExample {
    public static void main(String[] args) {
        List<String> threadSafeFruits = new CopyOnWriteArrayList<>();
        threadSafeFruits.add("Apple");
        threadSafeFruits.add("Banana");

        System.out.println("Thread-Safe Fruits: " + threadSafeFruits); 
        // Output: [Apple, Banana]
    }
}        

4. Custom Sorting

Java provides two ways to sort collections: Comparable and Comparator.

4.1 Comparable

  • Description: The Comparable interface is used to define the natural ordering of objects.
  • Use Case: Ideal for scenarios where you want to define a default sorting order for a class.

Example: Using Comparable

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

class Fruit implements Comparable<Fruit> {
    String name;
    int price;

    Fruit(String name, int price) {
        this.name = name;
        this.price = price;
    }

    @Override
    public int compareTo(Fruit other) {
        return this.price - other.price;
    }

    @Override
    public String toString() {
        return name + "=" + price;
    }
}

public class ComparableExample {
    public static void main(String[] args) {
        List<Fruit> fruits = new ArrayList<>();
        fruits.add(new Fruit("Apple", 50));
        fruits.add(new Fruit("Banana", 20));
        fruits.add(new Fruit("Cherry", 30));

        Collections.sort(fruits);
        System.out.println("Sorted Fruits: " + fruits); 
        // Output: [Banana=20, Cherry=30, Apple=50]
    }
}        

4.2 Comparator

  • Description: The Comparator interface is used to define custom sorting logic.
  • Use Case: Ideal for scenarios where you need multiple sorting criteria or want to sort objects without modifying their class.

Example: Using Comparator

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;

class Fruit {
    String name;
    int price;

    Fruit(String name, int price) {
        this.name = name;
        this.price = price;
    }

    @Override
    public String toString() {
        return name + "=" + price;
    }
}

public class ComparatorExample {
    public static void main(String[] args) {
        List<Fruit> fruits = new ArrayList<>();
        fruits.add(new Fruit("Apple", 50));
        fruits.add(new Fruit("Banana", 20));
        fruits.add(new Fruit("Cherry", 30));

        Comparator<Fruit> priceComparator = (f1, f2) -> f1.price - f2.price;
        Collections.sort(fruits, priceComparator);
        System.out.println("Sorted Fruits: " + fruits); 
        // Output: [Banana=20, Cherry=30, Apple=50]
    }
}        

5. Stream API with Collections

The Stream API (introduced in Java 8) allows you to process collections in a functional style.

Example: Using Stream API

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class StreamAPIExample {
    public static void main(String[] args) {
        List<String> fruits = Arrays.asList("Apple", "Banana", "Cherry");

        // Filter and collect
        List<String> filteredFruits = fruits.stream()
                                            .filter(f -> f.startsWith("A"))
                                            .collect(Collectors.toList());

        System.out.println("Filtered Fruits: " + filteredFruits); 
        // Output: [Apple]
    }
}        

6. Performance Considerations

Here’s a quick comparison of the performance of common Map implementations:

Article content

7. Real-World Use Cases

  • HashMap: Storing user sessions or caching data.
  • LinkedHashMap: Implementing an LRU (Least Recently Used) cache.
  • TreeMap: Storing sorted configurations or settings.
  • ConcurrentHashMap: Managing shared resources in multi-threaded applications.
  • CopyOnWriteArrayList: Storing read-heavy data like configuration lists.


8. Conclusion

In this article, we explored the Map interface, concurrent collections, and advanced topics like custom sorting and the Stream API. These concepts are essential for writing efficient and scalable Java applications. With this knowledge, you can confidently choose the right collection for your use case and leverage advanced features to optimize your code.

To further enhance your Java skills, consider exploring:

  • Immutable Collections (e.g., Collections.unmodifiableMap(), Google Guava Immutable Collections)
  • WeakHashMap for caching with automatic garbage collection cleanup
  • ConcurrentSkipListMap for thread-safe sorted maps


For a complete learning journey, follow me: Eugene Koshy

To view or add a comment, sign in

More articles by Eugene Koshy

Others also viewed

Explore content categories