Index

Real-World Applications of Collections

Java Collections

16.1 Implementing a Simple Cache with Map

Understanding Caching and Why Maps Are Ideal

A cache is a data structure used to store frequently accessed data temporarily, improving performance by avoiding repeated expensive computations or data retrievals. The core idea is to trade memory usage for faster access times.

The Map interface in Java is naturally suited for caching:

Basic Cache Design Patterns

At its simplest, a cache can be a Map<K, V> where keys are used to store and retrieve values. However, real-world caches often need features like:

While Java doesn’t provide a built-in LRU cache directly in the standard Map, it allows creating one using LinkedHashMap by overriding removeEldestEntry.

Example: Basic LRU Cache Using LinkedHashMap

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

class LRUCache<K, V> extends LinkedHashMap<K, V> {
    private final int capacity;

    public LRUCache(int capacity) {
        super(capacity, 0.75f, true); // 'true' for access-order
        this.capacity = capacity;
    }

    @Override
    protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
        return size() > capacity;
    }
}

public class CacheDemo {
    public static void main(String[] args) {
        LRUCache<Integer, String> cache = new LRUCache<>(3);

        cache.put(1, "One");
        cache.put(2, "Two");
        cache.put(3, "Three");

        System.out.println("Cache before access: " + cache);

        cache.get(1); // Access key 1, making it most recently used
        cache.put(4, "Four"); // Evicts key 2 (least recently used)

        System.out.println("Cache after eviction: " + cache);
    }
}

Output:

Cache before access: {1=One, 2=Two, 3=Three}
Cache after eviction: {3=Three, 1=One, 4=Four}

Explanation and Performance Considerations

For more advanced scenarios (e.g., thread-safe or time-based expiration), third-party libraries like Caffeine or Guava are recommended, but for many cases, this LinkedHashMap-based approach is more than sufficient.

Conclusion

Caching is a vital performance optimization technique in software systems, and the Java Map interface provides an excellent foundation for implementing caches. By leveraging LinkedHashMap, developers can build lightweight LRU caches with minimal code. Understanding how to apply and customize this pattern is a valuable skill when working with collections in real-world applications.

Index

16.2 Using Collections in Data Processing Pipelines

Collections play a central role in building data processing pipelines—a sequence of operations applied to data to transform, filter, group, or aggregate it. Whether processing user records, log entries, or financial transactions, Java collections like List, Map, and Queue help structure each step of the pipeline.

These pipelines can be implemented using iterative loops or more modern functional approaches via the Streams API. Collections serve as the source, intermediate, and target containers throughout the pipeline.

Common Stages in a Pipeline Using Collections

  1. Ingestion/Collection – Use List or Queue to store incoming data.
  2. Filtering – Apply conditions to remove irrelevant elements.
  3. Transformation – Map one data format to another.
  4. Grouping – Use Map to group elements by a key.
  5. Aggregation/Output – Summarize or pass results to another system.

Example: Processing User Records

Let’s simulate a data pipeline that filters active users, groups them by role, and outputs the result.

import java.util.*;
import java.util.stream.Collectors;

class User {
    String name;
    String role;
    boolean isActive;

    User(String name, String role, boolean isActive) {
        this.name = name;
        this.role = role;
        this.isActive = isActive;
    }

    @Override
    public String toString() {
        return name;
    }
}

public class UserPipeline {
    public static void main(String[] args) {
        List<User> users = Arrays.asList(
            new User("Alice", "Admin", true),
            new User("Bob", "User", false),
            new User("Charlie", "User", true),
            new User("Diana", "Admin", true),
            new User("Eve", "Guest", false)
        );

        // Step 1: Filter active users
        List<User> activeUsers = users.stream()
            .filter(user -> user.isActive)
            .collect(Collectors.toList());

        // Step 2: Group by role
        Map<String, List<User>> groupedByRole = activeUsers.stream()
            .collect(Collectors.groupingBy(user -> user.role));

        // Output the grouped result
        groupedByRole.forEach((role, group) -> {
            System.out.println(role + ": " + group);
        });
    }
}

Expected Output:

Admin: [Alice, Diana]
User: [Charlie]

Explanation

This pipeline leverages the Streams API for readability and efficiency, but similar logic could be implemented using loops and conditionals, especially in older Java versions.

Other Use Cases

Conclusion

Collections are essential for organizing data flow in processing pipelines. They enable each stage—from ingestion to output—to operate on well-structured data. Whether using traditional loops or declarative streams, understanding how to combine and transform collections effectively is key to writing clean, performant data-driven code.

Index

16.3 Collections for Graph and Tree Structures

Collections such as List, Set, and Map form the backbone of modeling graph and tree data structures in Java. These structures are essential in domains like navigation systems, compilers, social networks, and hierarchical data modeling (e.g., organization charts).

Representing Graphs Using Collections

A graph is a set of nodes (vertices) connected by edges. It can be directed or undirected, and may include cycles.

The most common and efficient way to represent a graph in Java is via an adjacency list, typically implemented as:

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

Each key is a node, and the corresponding value is a list of its adjacent nodes.

Example: Simple Directed Graph

import java.util.*;

public class GraphExample {
    public static void main(String[] args) {
        Map<String, List<String>> graph = new HashMap<>();

        // Adding nodes and edges
        graph.put("A", Arrays.asList("B", "C"));
        graph.put("B", Arrays.asList("D"));
        graph.put("C", Arrays.asList("D"));
        graph.put("D", Collections.emptyList());

        // Print the graph
        graph.forEach((node, edges) -> {
            System.out.println(node + " -> " + edges);
        });
    }
}

Output:

A -> [B, C]
B -> [D]
C -> [D]
D -> []

Traversal can be done using Breadth-First Search (BFS) or Depth-First Search (DFS), implemented with Queue or recursion respectively.

Modeling Trees with Collections

A tree is a hierarchical structure with a root node and child nodes, where each child has exactly one parent. Common representations include:

  1. Custom Node class with a list of children
  2. Map of parent-child relationships

Example: Tree with Custom Node Class

import java.util.*;

class TreeNode {
    String name;
    List<TreeNode> children;

    TreeNode(String name) {
        this.name = name;
        this.children = new ArrayList<>();
    }

    void addChild(TreeNode child) {
        children.add(child);
    }
}

public class TreeExample {
    public static void main(String[] args) {
        TreeNode root = new TreeNode("Root");
        TreeNode a = new TreeNode("A");
        TreeNode b = new TreeNode("B");
        TreeNode c = new TreeNode("C");

        root.addChild(a);
        root.addChild(b);
        a.addChild(c);

        printTree(root, 0);
    }

    // Recursive DFS traversal
    static void printTree(TreeNode node, int depth) {
        System.out.println("  ".repeat(depth) + node.name);
        for (TreeNode child : node.children) {
            printTree(child, depth + 1);
        }
    }
}

Output:

Root
  A
    C
  B

Key Operations on Graphs and Trees

Conclusion

Java’s collection classes provide powerful tools for modeling complex structures like graphs and trees. By combining Map, List, and Set in intuitive ways, developers can efficiently represent and manipulate hierarchical and relational data. Whether modeling family trees, dependency graphs, or organizational hierarchies, understanding how to map these structures to collections is a vital programming skill.

Index