What is the best way to combine two lists into a map (Java)?

Viewed 113721

It would be nice to use for (String item: list), but it will only iterate through one list, and you'd need an explicit iterator for the other list. Or, you could use an explicit iterator for both.

Here's an example of the problem, and a solution using an indexed for loop instead:

import java.util.*;
public class ListsToMap {
  static public void main(String[] args) {
    List<String> names = Arrays.asList("apple,orange,pear".split(","));
    List<String> things = Arrays.asList("123,456,789".split(","));
    Map<String,String> map = new LinkedHashMap<String,String>();  // ordered

    for (int i=0; i<names.size(); i++) {
      map.put(names.get(i), things.get(i));    // is there a clearer way?
    }

    System.out.println(map);
  }
}

Output:

{apple=123, orange=456, pear=789}

Is there a clearer way? Maybe in the collections API somewhere?

18 Answers

Another Java 8 solution:

If you have access to the Guava library (earliest support for streams in version 21 [1]), you can do:

Streams.zip(keyList.stream(), valueList.stream(), Maps::immutableEntry)
       .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));

For me the advantage of this method simply lies in the fact that it is a single expression (i.e. one liner) that evaluates to a Map and I found that particularly useful for what I needed to do.

Personally I consider a simple for loop iterating over the indices to be the clearest solution, but here are two other possibilities to consider.

An alternative Java 8 solution that avoids calling boxed() on an IntStream is

List<String> keys = Arrays.asList("A", "B", "C");
List<String> values = Arrays.asList("1", "2", "3");

Map<String, String> map = IntStream.range(0, keys.size())
                                   .collect(
                                        HashMap::new, 
                                        (m, i) -> m.put(keys.get(i), values.get(i)), 
                                        Map::putAll
                                   );
                          );

With vavr library:

List.ofAll(names).zip(things).toJavaMap(Function.identity());

I think this is quite self-explanatory (assuming that lists have equal size)

Map<K, V> map = new HashMap<>();

for (int i = 0; i < keys.size(); i++) {
    map.put(keys.get(i), vals.get(i));
}

This works using Eclipse Collections.

Map<String, String> map =
        Maps.adapt(new LinkedHashMap<String, String>())
                .withAllKeyValues(
                        Lists.mutable.of("apple,orange,pear".split(","))
                                .zip(Lists.mutable.of("123,456,789".split(","))));

System.out.println(map);

Note: I am a committer for Eclipse Collections.

Leverage AbstractMap and AbstractSet:

import java.util.AbstractMap;
import java.util.AbstractSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;

public class ZippedMap<A, B> extends AbstractMap<A, B> {

  private final List<A> first;

  private final List<B> second;

  public ZippedMap(List<A> first, List<B> second) {
    if (first.size() != second.size()) {
      throw new IllegalArgumentException("Expected lists of equal size");
    }
    this.first = first;
    this.second = second;
  }

  @Override
  public Set<Entry<A, B>> entrySet() {
    return new AbstractSet<>() {
      @Override
      public Iterator<Entry<A, B>> iterator() {
        Iterator<A> i = first.iterator();
        Iterator<B> i2 = second.iterator();
        return new Iterator<>() {

          @Override
          public boolean hasNext() {
            return i.hasNext();
          }

          @Override
          public Entry<A, B> next() {
            return new SimpleImmutableEntry<>(i.next(), i2.next());
          }
        };
      }

      @Override
      public int size() {
        return first.size();
      }
    };
  }
}

Usage:

public static void main(String... args) {
  Map<Integer, Integer> zippedMap = new ZippedMap<>(List.of(1, 2, 3), List.of(1, 2, 3));
  zippedMap.forEach((k, v) -> System.out.println("key = " + k + " value = " + v));
}

Output:

key = 1 value = 1
key = 2 value = 2
key = 3 value = 3

I got this idea from the java.util.stream.Collectors.Partition class, which does basically the same thing.

It provides encapsulation and clear intent (the zipping of two lists) as well as reusability and performance.

This is better than the other answer which creates entries and then immediately unwraps them for putting in a map.

You can leverage kotlin-stdlib

@Test
void zipDemo() {
    List<String> names = Arrays.asList("apple", "orange", "pear");
    List<String> things = Arrays.asList("123", "456", "789");
    Map<String, String> map = MapsKt.toMap(CollectionsKt.zip(names, things));
    assertThat(map.toString()).isEqualTo("{apple=123, orange=456, pear=789}");
}

Of course, it's even more fun to use kotlin language:

@Test
fun zipDemo() {
    val names = listOf("apple", "orange", "pear");
    val things = listOf("123", "456", "789");
    val map = (names zip things).toMap()
    assertThat(map).isEqualTo(mapOf("apple" to "123", "orange" to "456", "pear" to "789"))
}
Related