Convert List to Map in Java: toMap(), Duplicate Keys and Order

Convert List to Map in Java with Collectors.toMap() and groupingBy(), fix the duplicate key IllegalStateException with a merge function, keep order.

Converting a list of books to a map with toMap by id, toMap with a merge function by author, and groupingBy by author

To convert a List to a Map in Java, we stream the list and collect it with Collectors.toMap(keyMapper, valueMapper), which turns every element into one key-value entry. When several elements can produce the same key, we either pass a merge function to toMap() or use Collectors.groupingBy() to get a Map<K, List<V>>.

We convert lists to maps to look up elements by a key in constant time, for example a book by its id, or to group elements, such as all books of one author.

The following example builds both kinds of map from a list of fruit names.

List<String> fruits = List.of("apple", "kiwi", "banana", "pear");
Map<String, Integer> lengths = fruits.stream().collect(Collectors.toMap(Function.identity(), String::length));
Integer apple = lengths.get("apple");               // 5
Map<Integer, List<String>> byLength = fruits.stream().collect(Collectors.groupingBy(String::length));
List<String> fourLetters = byLength.get(4);         // [kiwi, pear]

We can see that “kiwi” and “pear” share the key 4. A plain toMap(String::length, …) would throw IllegalStateException for them, while groupingBy() keeps both. We cover the loop version, toMap() with duplicate keys, ordering and null values, grouping, and the Apache Commons and Guava helpers.

1. List and Map Basics for the Conversion

A List keeps elements in order and allows duplicates, whereas a Map stores key-value pairs with unique keys and allows duplicate values. Converting a list into a map therefore needs two decisions. We pick the key of each element, and we decide what happens when two elements produce the same key.

The following example uses a record for the books of an online bookstore. The ids are unique, but two books share the author Austen, so the author works as a key only with a duplicate strategy.

record Book(int id, String title, String author) {

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

static List<Book> books() {
    return List.of(
            new Book(1, "Dune", "Herbert"),
            new Book(2, "Emma", "Austen"),
            new Book(3, "Persuasion", "Austen"),
            new Book(4, "Ubik", "Dick"));
}
Converting a list of books to a map with toMap by id, toMap with a merge function by author, and groupingBy by author
A unique key needs plain toMap(), a repeated key needs a merge function or groupingBy()

2. Different Ways to Convert a List to a Map

The JDK offers a loop and two collectors, and Apache Commons Collections and Guava add one helper each. All of them produce the same lookup for unique keys and differ in how they treat duplicates.

2.1. Using a for Loop

The loop creates a HashMap and calls put() for each element. It needs no streams and works on every Java version.

Map<Integer, Book> byId = new HashMap<>();
for (Book book : books()) {
    byId.put(book.id(), book);
}
Book dune = byId.get(1);                            // Dune

For duplicate keys, put() replaces the earlier element, so the last book with a key wins without a warning. The Map interface offers better methods for the other two strategies. The method putIfAbsent() keeps the first element, and computeIfAbsent() collects all elements per key in a list.

Map<String, Book> lastByAuthor = new HashMap<>();
Map<String, Book> firstByAuthor = new HashMap<>();
Map<String, List<Book>> allByAuthor = new HashMap<>();
for (Book book : books()) {
    lastByAuthor.put(book.author(), book);
    firstByAuthor.putIfAbsent(book.author(), book);
    allByAuthor.computeIfAbsent(book.author(), author -> new ArrayList<>()).add(book);
}
Book last = lastByAuthor.get("Austen");             // Persuasion
Book first = firstByAuthor.get("Austen");           // Emma
List<Book> all = allByAuthor.get("Austen");         // [Emma, Persuasion]

2.2. Using Collectors.toMap()

A bookstore API receives an order with 50 book ids and must show each book’s title. Searching the list of books for every id scans the whole list 50 times. Converting the list to a Map<Integer, Book> once makes every lookup a single get() call.

The collector Collectors.toMap() takes two functions, one that computes the key and one that computes the value. The method Function.identity() returns the element itself, so the value is the whole book.

Map<Integer, Book> byId = books().stream().collect(Collectors.toMap(Book::id, Function.identity()));
Book emma = byId.get(2);                            // Emma
Map<Integer, String> titles = books().stream().collect(Collectors.toMap(Book::id, Book::title));
String ubik = titles.get(4);                        // "Ubik"

The second map stores only the titles, which uses less memory when the screen needs nothing else. The post on collecting a stream to a Map shows the same collector on other sources than lists.

2.3. Handling Duplicate Keys With a Merge Function

When two elements produce the same key, the two-argument toMap() throws IllegalStateException. The message names the key and both values, so it shows which data is duplicated.

Map<String, Book> byAuthor = books().stream().collect(Collectors.toMap(Book::author, Function.identity()));   // IllegalStateException: Duplicate key Austen (attempted merging values Emma and Persuasion)

The third argument of toMap() is a merge function, which receives the existing value and the new value for a repeated key and returns the value to keep. With a list as the source, “existing” means the element that came first in the list.

Map<String, Book> firstBook = books().stream().collect(Collectors.toMap(Book::author, Function.identity(), (existing, replacement) -> existing));
Book keptFirst = firstBook.get("Austen");           // Emma
Map<String, Book> lastBook = books().stream().collect(Collectors.toMap(Book::author, Function.identity(), (existing, replacement) -> replacement));
Book keptLast = lastBook.get("Austen");             // Persuasion
Map<String, String> joinedTitles = books().stream().collect(Collectors.toMap(Book::author, Book::title, (a, b) -> a + ", " + b));
String austen = joinedTitles.get("Austen");         // "Emma, Persuasion"
Map<String, Integer> bookCount = books().stream().collect(Collectors.toMap(Book::author, b -> 1, Integer::sum));
Integer austenCount = bookCount.get("Austen");      // 2

The merge function can also combine values, as the last two maps show. Summing with Integer::sum counts the books per author, and joining titles builds a display string.

2.4. Keeping the List Order in the Map

A HashMap does not keep the order of the list, so the entries of a toMap() result come out in no particular order. The four-argument toMap() takes a map supplier. A LinkedHashMap supplier keeps the list order, and a TreeMap supplier sorts by key.

Map<String, Integer> inListOrder = books().stream().collect(Collectors.toMap(Book::title, Book::id, (a, b) -> a, LinkedHashMap::new));   // {Dune=1, Emma=2, Persuasion=3, Ubik=4}
Map<String, Integer> sortedByAuthor = books().stream().collect(Collectors.toMap(Book::author, b -> 1, Integer::sum, TreeMap::new));   // {Austen=2, Dick=1, Herbert=1}

The supplier version always needs a merge function in the third position. When keys are unique, (a, b) -> a is a placeholder that never runs.

2.5. Handling null Values and Unmodifiable Results

The toMap() collector throws NullPointerException when the value function returns null, even though a HashMap accepts null values. We filter those elements out first or map null to a default value.

List<String> names = Arrays.asList("ann", null, "bo");
Map<String, Integer> broken = names.stream().collect(Collectors.toMap(n -> String.valueOf(n), n -> n == null ? null : n.length()));   // NullPointerException
Map<String, Integer> safe = names.stream().filter(Objects::nonNull).collect(Collectors.toMap(Function.identity(), String::length));
Integer bo = safe.get("bo");                        // 2

When the map must not change after we build it, Java 10’s Collectors.toUnmodifiableMap() returns an unmodifiable map. It has the same duplicate-key rules as toMap() and also rejects null keys.

Map<Integer, String> catalog = books().stream().collect(Collectors.toUnmodifiableMap(Book::id, Book::title));
String title = catalog.get(3);                      // "Persuasion"
catalog.put(5, "Solaris");                          // UnsupportedOperationException

2.6. Using Collectors.groupingBy()

When every element matters and keys repeat, Collectors.groupingBy() is the right tool. It puts all elements with the same key into a list, so it never throws on duplicates and never drops an element.

Map<String, List<Book>> byAuthor = books().stream().collect(Collectors.groupingBy(Book::author));
List<Book> austenBooks = byAuthor.get("Austen");    // [Emma, Persuasion]
Map<String, List<String>> titlesByAuthor = books().stream().collect(Collectors.groupingBy(Book::author, TreeMap::new, Collectors.mapping(Book::title, Collectors.toList())));   // {Austen=[Emma, Persuasion], Dick=[Ubik], Herbert=[Dune]}
Map<String, Long> countByAuthor = books().stream().collect(Collectors.groupingBy(Book::author, Collectors.counting()));
Long austenTotal = countByAuthor.get("Austen");     // 2

The second argument, a downstream collector such as mapping() or counting(), changes what is stored per key. In the bookstore, the author page uses the titles map, and the author list shows the counts.

2.7. Apache Commons Collections MapUtils.populateMap()

The MapUtils class of Apache Commons Collections 4.5.0 fills an existing map from an Iterable and a key function. Like the loop, it calls put(), so a duplicate key keeps the last element.

Map<String, Book> byAuthor = new LinkedHashMap<>();
MapUtils.populateMap(byAuthor, books(), Book::author);
Book austen = byAuthor.get("Austen");               // Persuasion
int authors = byAuthor.size();                      // 3

Because we pass the target map, we choose its type, such as a LinkedHashMap for list order. An overload also takes a value function.

2.8. Guava Maps.uniqueIndex() and Multimaps.index()

Guava 33.7.2 has a pair of helpers for the two cases. Maps.uniqueIndex() builds an ImmutableMap in list order and throws IllegalArgumentException on a duplicate key, and Multimaps.index() builds an ImmutableListMultimap that keeps every element.

ImmutableMap<Integer, Book> byId = Maps.uniqueIndex(books(), Book::id);
Book dune = byId.get(1);                            // Dune
ImmutableMap<String, Book> byAuthor = Maps.uniqueIndex(books(), Book::author);   // IllegalArgumentException
ImmutableListMultimap<String, Book> allByAuthor = Multimaps.index(books(), Book::author);
List<Book> austen = allByAuthor.get("Austen");      // [Emma, Persuasion]

Note that Guava throws IllegalArgumentException where Collectors.toMap() throws IllegalStateException, which matters when a catch block expects one of them.

3. Choosing the Right Conversion

The choice depends mostly on whether keys can repeat and what should happen when they do. Each option from section 2 treats a repeated key differently, so the last column is the one to compare first.

GoalApproachOn duplicate key
Unique key, lookup maptoMap(keyFn, Function.identity())IllegalStateException
Keep first or last elementtoMap(keyFn, valueFn, (a, b) -> a) or (a, b) -> bMerge function decides
Combine values (count, sum, join)toMap(keyFn, valueFn, Integer::sum)Merge function combines
Keep list ordertoMap(…, LinkedHashMap::new)Merge function decides
Read-only resulttoUnmodifiableMap(keyFn, valueFn)IllegalStateException
Keep every elementgroupingBy(keyFn)Added to the list
No streamsfor loop with put(), putIfAbsent() or computeIfAbsent()Last, first or all

For the reverse direction, the post on converting a Map to a List covers keys, values and entries, and inverting a map swaps keys and values. The Java Collections guide explains when a map is the better structure in the first place.

4. List to Map Conversion FAQs

Developers who convert lists to maps in real projects ask about the map type, index keys, speed and Java 8 support.

4.1. Which Map Implementation Does Collectors.toMap() Return?

The current JDK returns a HashMap, but the collector gives no guarantee on the type, mutability or thread safety. When the type matters, we pass a map supplier such as LinkedHashMap::new, TreeMap::new or ConcurrentHashMap::new.

4.2. How Do We Use the List Index as the Map Key?

We stream the indexes with IntStream.range() and look up each element.

List<String> steps = List.of("wash", "cut", "cook");
Map<Integer, String> byIndex = IntStream.range(0, steps.size()).boxed().collect(Collectors.toMap(i -> i, steps::get));
String second = byIndex.get(1);                     // "cut"

4.3. Is Collectors.toMap() Faster Than a for Loop?

Not in a way that matters for typical lists. Both visit every element once and call the same map operations, so we choose by readability and by how each one treats duplicate keys.

4.4. How Do We Convert a List to a Map in Java 8?

The same Collectors.toMap() and groupingBy() calls work since Java 8. Only toUnmodifiableMap() (Java 10) and Stream.toList() (Java 16) need newer versions.

5. Conclusion

Converting a list to a map is one Collectors.toMap() call when the keys are unique. When keys repeat, the merge function decides which value to keep or how to combine them, and groupingBy() keeps every element in a list per key.

A map supplier such as LinkedHashMap::new controls the order, toUnmodifiableMap() makes the result read-only, and null values need a filter before toMap(). For code bases that already use Apache Commons Collections or Guava, MapUtils.populateMap(), Maps.uniqueIndex() and Multimaps.index() do the same job, but with different duplicate-key behavior.

6. References

Happy Learning !!

Source Code on Github

About Us

HowToDoInJava provides tutorials and how-to guides on Java and related technologies.

It also shares the best practices, algorithms & solutions and frequently asked interview questions.