How do I use LinkedHashMap for predictable iteration order?

In Java, a LinkedHashMap is a subtype of HashMap that maintains a predictable iteration order. It uses a doubly linked list to store the entries in insertion order (or, optionally, access order). Here’s how you can use LinkedHashMap for predictable iteration order:

1. Maintaining Insertion Order

By default, a LinkedHashMap iterates its entries in the order they were inserted. This is useful when you want to retrieve elements in the same order you added them.

Here’s an example:

package org.kodejava.util;

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

public class LinkedHashMapExample {
    public static void main(String[] args) {
        // Creating LinkedHashMap
        Map<String, Integer> map = new LinkedHashMap<>();

        // Adding elements (insertion order)
        map.put("One", 1);
        map.put("Two", 2);
        map.put("Three", 3);
        map.put("Four", 4);

        // Iterating through the map
        for (Map.Entry<String, Integer> entry : map.entrySet()) {
            System.out.println(entry.getKey() + " => " + entry.getValue());
        }
    }
}

Output:

One => 1
Two => 2
Three => 3
Four => 4

In this example, the elements are iterated in the same order they were inserted.


2. Maintaining Access Order

You can configure a LinkedHashMap to maintain access order, which means it reorders entries based on the most recent access. To enable access order, you must use the constructor that takes a boolean parameter for accessOrder.

Here’s an example:

package org.kodejava.util;

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

public class AccessOrderExample {
    public static void main(String[] args) {
        // Creating LinkedHashMap with access-order
        Map<String, Integer> map = new LinkedHashMap<>(16, 0.75f, true);

        // Adding elements
        map.put("One", 1);
        map.put("Two", 2);
        map.put("Three", 3);

        // Accessing some elements
        map.get("One");  // Access "One"
        map.get("Three"); // Access "Three"

        // Iterating through the map
        for (Map.Entry<String, Integer> entry : map.entrySet()) {
            System.out.println(entry.getKey() + " => " + entry.getValue());
        }
    }
}

Output:

Two => 2
One => 1
Three => 3

In this case:

  • Initially, the insertion order was One, Two, Three.
  • After accessing One and Three, they were moved to the end, making Two the first in the iteration order.

3. Removing the Oldest Entry with Access Order

If needed, you can use a LinkedHashMap in combination with its removeEldestEntry method to automatically remove the oldest entry (e.g., implementing a cache).

Here’s how:

package org.kodejava.util;

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

public class RemoveEldestExample {
    public static void main(String[] args) {
        // Create LinkedHashMap with override for removeEldestEntry
        LinkedHashMap<String, Integer> map = new LinkedHashMap<>(3, 0.75f, true) {
            @Override
            protected boolean removeEldestEntry(Map.Entry<String, Integer> eldest) {
                return size() > 3; // Remove oldest if size > 3
            }
        };

        // Adding elements
        map.put("One", 1);
        map.put("Two", 2);
        map.put("Three", 3);
        map.put("Four", 4); // "One" will be removed here

        // Accessing some elements
        map.get("Two");
        map.put("Five", 5); // "Three" will be removed here

        // Iterating through the map
        for (Map.Entry<String, Integer> entry : map.entrySet()) {
            System.out.println(entry.getKey() + " => " + entry.getValue());
        }
    }
}

Output:

Four => 4
Two => 2
Five => 5

Explanation:

  1. The map was set to remove the eldest (first) entry when its size exceeds 3.
  2. When "Four" was added, "One" was removed because the size limit was exceeded.
  3. When "Five" was added, "Three" was removed, as it was now the eldest entry after accessing "Two".

Summary of Key Points:

  1. Insertion Order: By default, the iteration order matches the insertion order.
  2. Access Order: Can be enabled using the LinkedHashMap constructor with accessOrder = true.
  3. Custom Behavior: Override the removeEldestEntry method to create a fixed-size cache or similar functionality.

LinkedHashMap is handy when you need consistent iteration order (e.g., for caches, ordering-sensitive collections).

How to create records in Java 17 for immutable data models

In Java 17, you can use the record feature to create immutable data models. Records are a new type of class in Java designed specifically to hold immutable data. Using records simplifies creating classes that are essentially data carriers. Here’s a step-by-step guide on how to create and use records in Java 17:

What is a Record?

A record is a special kind of class in Java introduced in Java 14 (as a preview) and became stable in Java 16+. It:

  • Is designed for immutability
  • Automatically generates boilerplate code like getters, equals(), hashCode(), and toString()

Syntax of a Record

Declaring a record is simple. Here’s the syntax:

public record RecordName(datatype field1, datatype field2, ...) {}

Key Features of Records

  1. Records automatically:
    • Generate getter methods for fields (no need for get prefix – field name itself is used).
    • Override toString(), hashCode(), and equals().
  2. Records are immutable (fields cannot be changed after initialization).

  3. Records can include custom methods.
  4. Records cannot extend other classes (inheritance is not allowed) but can implement interfaces.

An Example: Immutable Data Model with Records

package org.kodejava.basic;

public record Person(String name, int age) {
    // Custom constructor (optional)
    public Person {
        if (age < 0) {
            throw new IllegalArgumentException("Age cannot be negative");
        }
    }

    // Example of adding a custom method
    public String greet() {
        return "Hello, my name is " + name + " and I am " + age + " years old.";
    }
}

How to Use Records

You use a record just like any other class:

package org.kodejava.basic;

public class Main {
    public static void main(String[] args) {
        // Create a record instance
        Person person = new Person("John Doe", 30);

        // Access fields using getters
        System.out.println("Name: " + person.name());
        System.out.println("Age: " + person.age());

        // Use a custom method
        System.out.println(person.greet());

        // Immutability tested
        // person.name = "New Name"; // Compilation error because fields are final
    }
}

Output:

Name: John Doe
Age: 30
Hello, my name is John Doe and I am 30 years old.

Advantages of Using Records

  1. Less Boilerplate Code: You don’t need to write getters, setters, constructors, or methods like toString() and hashCode().
  2. Thread-Safety: Records are immutable, making them easy to use in concurrent environments.
  3. Better Readability: The succinct syntax improves code readability.

Restrictions of Records

  1. Records are final — you cannot extend them.
  2. Fields in a record are also final and cannot be changed.
  3. Records themselves cannot be mutable.

When Should You Use Records?

You should use records when:

  • You need a simple data model to hold immutable data.
  • You want to avoid the verbosity of writing boilerplate code for fields and methods (getters, toString(), etc.).

For mutable data, traditional classes or other patterns should be used instead of records.