Back to Stream: map & Transform
Q3Question 3 of 34

Collect employees into a LinkedHashMap preserving insertion order using Collectors.toMap with a map supplier

Key Concepts
  • The default `Collectors.toMap` returns a `HashMap`, which gives no guarantee about iteration order.
  • The four-argument `toMap(key, value, merge, supplier)` lets you choose the concrete map type via a supplier.
  • Passing `LinkedHashMap::new` makes the result iterate in the order elements arrived in the stream.
  • The four-argument overload forces you to also provide a merge function, even if duplicates are not expected.
  • `LinkedHashMap` preserves insertion order because it links entries in a doubly linked list alongside the hash table.
  • Use `TreeMap::new` instead when you want keys sorted rather than kept in insertion order.

Definition

`toMap(Employee::getId, Employee::getName, (a, b) -> a, LinkedHashMap::new)` builds an id-to-name map whose iteration order matches the order the employees appeared in the stream.

The four-argument Collectors.toMap controls the concrete Map implementation.

  • Key mapper – extracts the key, here Employee::getId.
  • Value mapper – extracts the value, here Employee::getName.
  • Merge function – a BinaryOperator required by this overload; it resolves duplicate keys.
  • Map supplier – a factory such as LinkedHashMap::new that decides which Map type holds the result.

The default two-argument form uses a HashMap, so iteration order is unspecified. Supplying LinkedHashMap::new keeps entries in the order they were inserted.

At National Bank of India, Rajesh records new account openings in a ledger. A plain HashMap is like tossing the slips into a drawer: they are all there, but when you reopen the drawer they come out in an unpredictable jumble. A LinkedHashMap is like a bound ledger book where each new account is written on the next line, so reading the book back always follows the order accounts were opened. The map supplier is Rajesh choosing the bound ledger instead of the loose drawer.

Understanding the Concepts

Why does the default toMap lose order?

The two-argument and three-argument toMap overloads create a HashMap. A HashMap places entries in buckets based on their hash codes, so the iteration order has no relationship to the order elements entered the stream. Running the same code twice can even produce different orders across Java versions.

JAVA
package com.bank.hr;

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

public class UnorderedMapDemo {
    public static void main(String[] args) {
        List<Employee> employees = List.of(
            new Employee(30L, "Rajesh"),
            new Employee(10L, "Priya"),
            new Employee(20L, "Pankaj"));
        Map<Long, String> map = employees.stream()
            .collect(Collectors.toMap(Employee::getId, Employee::getName));
        System.out.println(map); // order is not guaranteed
    }
}
TEXT
{20=Pankaj, 10=Priya, 30=Rajesh}

How do you preserve insertion order?

Use the four-argument overload and pass LinkedHashMap::new as the map supplier. This overload also requires a merge function, so provide one even when you do not expect duplicate keys.

JAVA
package com.bank.hr;

import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

public class OrderedMapDemo {
    public static Map<Long, String> idToName(List<Employee> employees) {
        return employees.stream()
            .collect(Collectors.toMap(
                Employee::getId,
                Employee::getName,
                (existing, replacement) -> existing, // required merge
                LinkedHashMap::new));                // the map supplier
    }
}
TEXT
{30=Rajesh, 10=Priya, 20=Pankaj}

The keys now appear in the exact order the employees sat in the original list.

How does LinkedHashMap keep order internally?

A LinkedHashMap extends HashMap but adds a doubly linked list threading through all entries. Lookups still use the hash buckets for speed, while the linked list records insertion order for iteration.

TEXT
  Hash buckets (for fast get/put)      Linked list (for ordered iteration)
  bucket[3] -> (10, Priya)             head -> (30,Rajesh) -> (10,Priya) -> (20,Pankaj) -> tail
  bucket[5] -> (20, Pankaj)
  bucket[7] -> (30, Rajesh)

Because the list is maintained as entries are inserted, iteration follows insertion order while get remains constant time.

How do the toMap overloads compare?

| Overload | Merge function | Map supplier | Resulting map | Iteration order |
|----------|----------------|--------------|---------------|-----------------|
| toMap(k, v) | not allowed | not allowed | HashMap | unspecified |
| toMap(k, v, merge) | required | not allowed | HashMap | unspecified |
| toMap(k, v, merge, supplier) | required | required | whatever you supply | depends on supplier |

JAVA
// Same data, three different ordering outcomes
map1 = stream.collect(Collectors.toMap(Employee::getId, Employee::getName));                 // HashMap
map2 = stream.collect(Collectors.toMap(Employee::getId, Employee::getName, (a,b)->a, LinkedHashMap::new)); // insertion order
map3 = stream.collect(Collectors.toMap(Employee::getId, Employee::getName, (a,b)->a, java.util.TreeMap::new)); // sorted keys

How is insertion order different from sorted order?

Insertion order follows the sequence elements arrived. Sorted order follows the natural ordering of the keys. They are only the same when the input happened to already be sorted.

JAVA
// Insertion order: 30, 10, 20 as they appeared
Map<Long, String> inserted = employees.stream()
    .collect(Collectors.toMap(Employee::getId, Employee::getName, (a,b)->a, LinkedHashMap::new));

// Sorted by id: 10, 20, 30
Map<Long, String> sorted = employees.stream()
    .collect(Collectors.toMap(Employee::getId, Employee::getName, (a,b)->a, java.util.TreeMap::new));
TEXT
inserted = {30=Rajesh, 10=Priya, 20=Pankaj}
sorted   = {10=Priya, 20=Pankaj, 30=Rajesh}

What are the common mistakes?

The first mistake is expecting the two-argument toMap to keep order and being surprised when it does not.

JAVA
// WRONG: assumes HashMap preserves insertion order
Map<Long, String> map = employees.stream()
    .collect(Collectors.toMap(Employee::getId, Employee::getName));
JAVA
// CORRECT: ask for a LinkedHashMap explicitly
Map<Long, String> map = employees.stream()
    .collect(Collectors.toMap(Employee::getId, Employee::getName,
             (a, b) -> a, LinkedHashMap::new));

The second mistake is forgetting that the four-argument overload makes the merge function mandatory, which causes a compile error if you try to skip it.

JAVA
// WRONG: does not compile, missing the merge function
.collect(Collectors.toMap(Employee::getId, Employee::getName, LinkedHashMap::new));

// CORRECT: merge function comes before the supplier
.collect(Collectors.toMap(Employee::getId, Employee::getName, (a, b) -> a, LinkedHashMap::new));

What are the edge cases with duplicate keys in an ordered map?

When two employees share an id, the merge function decides the value, but the position in the LinkedHashMap stays where the key was first inserted. A later merge updates the value in place, not the order.

JAVA
package com.bank.hr;

import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

public class OrderWithDuplicates {
    public static void main(String[] args) {
        List<Employee> employees = List.of(
            new Employee(30L, "Rajesh"),
            new Employee(10L, "Priya"),
            new Employee(30L, "Rajesh Kumar")); // duplicate id 30, keep last
        Map<Long, String> map = employees.stream()
            .collect(Collectors.toMap(Employee::getId, Employee::getName,
                     (a, b) -> b, LinkedHashMap::new));
        System.out.println(map);
    }
}
TEXT
{30=Rajesh Kumar, 10=Priya}

Notice id 30 keeps its original first position even though its value was replaced.

When should you use a map supplier, and which one?

Choose the supplier based on the iteration contract you need. Use LinkedHashMap::new to echo the source order, TreeMap::new to sort keys, and leave the default HashMap when order is irrelevant and you want the fastest option.

JAVA
// Statement that lists accounts in the order they were opened
Map<Long, String> ledger = openings.stream()
    .collect(Collectors.toMap(Account::getId, Account::getHolder,
             (a, b) -> a, LinkedHashMap::new));

What should a senior engineer know?

  • Parallel streams and order. LinkedHashMap records the order entries are merged, which in a parallel stream is the encounter order of the source if the stream is ordered. For predictable order prefer an ordered source like a List.
  • Access-order mode. LinkedHashMap also has an access-order constructor used to build LRU caches, but toMap always uses the default insertion-order mode.
  • Immutability. Wrap with collectingAndThen(toMap(...), Collections::unmodifiableMap) to return a read-only ordered view.
  • Memory cost. LinkedHashMap carries two extra references per entry for the linked list, a small cost paid for predictable iteration.

What are common interview follow-up questions?

  • "Does toMap guarantee order by default?" Answer: no, the default is a HashMap.
  • "Which overload lets you choose the map type?" Answer: the four-argument toMap(key, value, merge, supplier).
  • "Why must you pass a merge function when you only want ordering?" Answer: the supplier overload always requires all four arguments, so you give a trivial merge like (a, b) -> a.
  • "Insertion order versus sorted order?" Answer: LinkedHashMap keeps arrival order; TreeMap keeps keys sorted by natural ordering.

Visual Overview

DIAGRAM
  List<Employee>: [ (30,Rajesh), (10,Priya), (20,Pankaj) ]
        │
        │  toMap(getId, getName, merge, SUPPLIER)
        ▼
   choose SUPPLIER
        │
   ┌────┴───────────────┬──────────────────────┐
   ▼                    ▼                        ▼
 HashMap::new      LinkedHashMap::new        TreeMap::new
 (default)         (insertion order)         (sorted keys)
   │                    │                        │
   ▼                    ▼                        ▼
 {20=Pankaj,       {30=Rajesh,              {10=Priya,
  10=Priya,         10=Priya,                20=Pankaj,
  30=Rajesh}        20=Pankaj}               30=Rajesh}
 unspecified        source order             key order
DIAGRAM
  LinkedHashMap entry layout
  ┌──────────── hash buckets (fast lookup) ────────────┐
  │ bucket[a] -> (30,Rajesh)                            │
  │ bucket[b] -> (10,Priya)                             │
  │ bucket[c] -> (20,Pankaj)                            │
  └─────────────────────────────────────────────────────┘
            │ also linked in insertion order
            ▼
  head -> (30,Rajesh) -> (10,Priya) -> (20,Pankaj) -> tail
         iteration follows this chain

Key Takeaway

When Rajesh collects employees into a Map, the quick two-argument Collectors.toMap gives him a HashMap. Everything is in there, but the iteration order is unpredictable, the way loose slips tossed in a drawer come out jumbled. For a statement or ledger where the reader expects the same order the accounts were opened, that is not good enough.

The fix is the four-argument overload. You keep the same key mapper Employee::getId and value mapper Employee::getName, add a merge function to satisfy the overload, and pass LinkedHashMap::new as the map supplier. Now the result iterates in the exact order the employees appeared in the stream, like reading entries down the lines of a bound ledger.

LinkedHashMap achieves this by keeping a doubly linked list of entries alongside the usual hash buckets. Lookups stay fast because they still use the buckets, while iteration follows the linked list, which records insertion order. If you need sorted keys instead of arrival order, swap the supplier for TreeMap::new.

Two details trip people up. The supplier overload makes the merge function mandatory, so you must supply something like (a, b) -> a even when duplicates are not expected. And when a duplicate key does occur, the merge updates the value but the key stays in its original position.

In our banking work the rule is to pick the map type on purpose: default HashMap when order does not matter, LinkedHashMap to mirror the source, and TreeMap to sort.

Remember one sentence: pass LinkedHashMap::new as the fourth argument to toMap to keep entries in the order the stream produced them.

---

Share
Use ← → arrow keys to navigate