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

JavaData StructuresCollections

Java Problem Overview


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?

Java Solutions


Solution 1 - Java

Been a while since this question was asked but these days I'm partial to something like:

public static <K, V> Map<K, V> zipToMap(List<K> keys, List<V> values) {
    return IntStream.range(0, keys.size()).boxed()
            .collect(Collectors.toMap(keys::get, values::get));
}

For those unfamiliar with streams, what this does is gets an IntStream from 0 to the length, then boxes it, making it a Stream<Integer> so that it can be transformed into an object, then collects them using Collectors.toMap which takes two suppliers, one of which generates the keys, the other the values.

This could stand some validation (like requiring keys.size() be less than values.size()) but it works great as a simple solution.

EDIT: The above works great for anything with constant time lookup, but if you want something that will work on the same order (and still use this same sort of pattern) you could do something like:

public static <K, V> Map<K, V> zipToMap(List<K> keys, List<V> values) {
    Iterator<K> keyIter = keys.iterator();
    Iterator<V> valIter = values.iterator();
    return IntStream.range(0, keys.size()).boxed()
            .collect(Collectors.toMap(_i -> keyIter.next(), _i -> valIter.next()));
}

The output is the same (again, missing length checks, etc.) but the time complexity isn't dependent on the implementation of the get method for whatever list is used.

Solution 2 - Java

I'd often use the following idiom. I admit it is debatable whether it is clearer.

Iterator<String> i1 = names.iterator();
Iterator<String> i2 = things.iterator();
while (i1.hasNext() && i2.hasNext()) {
    map.put(i1.next(), i2.next());
}
if (i1.hasNext() || i2.hasNext()) complainAboutSizes();

It has the advantage that it also works for Collections and similar things without random access or without efficient random access, like LinkedList, TreeSets or SQL ResultSets. For example, if you'd use the original algorithm on LinkedLists, you've got a slow Shlemiel the painter algorithm which actually needs n*n operations for lists of length n.

As 13ren pointed out, you can also use the fact that Iterator.next throws a NoSuchElementException if you try to read after the end of one list when the lengths are mismatched. So you'll get the terser but maybe a little confusing variant:

Iterator<String> i1 = names.iterator();
Iterator<String> i2 = things.iterator();
while (i1.hasNext() || i2.hasNext()) map.put(i1.next(), i2.next());

Solution 3 - Java

Since the key-value relationship is implicit via the list index, I think the for-loop solution that uses the list index explicitly is actually quite clear - and short as well.

Solution 4 - Java

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.

Solution 5 - Java

Your solution above is correct of course, but your as question was about clarity, I'll address that.

The clearest way to combine two lists would be to put the combination into a method with a nice clear name. I've just taken your solution and extracted it to a method here:

Map<String,String> combineListsIntoOrderedMap (List<String> keys, List<String> values) {
if (keys.size() != values.size())
throw new IllegalArgumentException ("Cannot combine lists with dissimilar sizes");
Map<String,String> map = new LinkedHashMap<String,String>();
for (int i=0; i<keys.size(); i++) {
map.put(keys.get(i), values.get(i));
}
return map;
}

And of course, your refactored main would now look like this:

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 = combineListsIntoOrderedMap (names, things);
System.out.println(map);
}

I couldn't resist the length check.

Solution 6 - Java

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
                                   );
                          );

Solution 7 - Java

ArrayUtils#toMap() doesn't combine two lists into a map, but does do so for a 2 dimensional array (so not quite what your looking for, but maybe of interest for future reference...)

Solution 8 - Java

As well as clarity, I think there are other things that are worth considering:

  • Correct rejection of illegal arguments, such as different sizes lists and nulls (look what happens if things is null in the question code).
  • Ability to handle lists that do not have fast random access.
  • Ability to handle concurrent and synchronized collections.

So, for library code, perhaps something like this:

@SuppressWarnings("unchecked")
public static <K,V> Map<K,V> linkedZip(List<? extends K> keys, List<? extends V> values) {
    Object[] keyArray = keys.toArray();
    Object[] valueArray = values.toArray();
    int len = keyArray.length;
    if (len != valueArray.length) {
        throwLengthMismatch(keyArray, valueArray);
    }
    Map<K,V> map = new java.util.LinkedHashMap<K,V>((int)(len/0.75f)+1);
    for (int i=0; i<len; ++i) {
        map.put((K)keyArray[i], (V)valueArray[i]);
    }
    return map;
}

(May want to check not putting multiple equal keys.)

Solution 9 - Java

There's no clear way. I still wonder if Apache Commons or Guava has something similar. Anyway I had my own static utility. But this one is aware of key collisions!

public static <K, V> Map<K, V> map(Collection<K> keys, Collection<V> values) {
	
	Map<K, V> map = new HashMap<K, V>();
	Iterator<K> keyIt = keys.iterator();
	Iterator<V> valueIt = values.iterator();
	while (keyIt.hasNext() && valueIt.hasNext()) {
		K k = keyIt.next();
		if (null != map.put(k, valueIt.next())){
			throw new IllegalArgumentException("Keys are not unique! Key " + k + " found more then once.");
		}
	}
	if (keyIt.hasNext() || valueIt.hasNext()) {
		throw new IllegalArgumentException("Keys and values collections have not the same size");
	};
	
	return map;
}

Solution 10 - Java

With vavr library:

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

Solution 11 - Java

You need not even limit yourself to Strings. Modifying the code from CPerkins a little :

 Map<K, V> <K, V> combineListsIntoOrderedMap (List<K> keys, List<V> values) {
          if (keys.size() != values.size())
              throw new IllegalArgumentException ("Cannot combine lists with dissimilar sizes");
    Map<K, V> map = new LinkedHashMap<K, V>();
    for (int i=0; i<keys.size(); i++) {
      map.put(keys.get(i), values.get(i));
    }
    return map;
}

Solution 12 - Java

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));
}

Solution 13 - Java

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.

Solution 14 - Java

Another perspective to this is to hide implementation. Would you like the caller of this functionality to enjoy the look and feel of Java's enhanced for-loop?

public static 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 HashMap<>(4);
    for (Map.Entry<String, String> e : new DualIterator<>(names, things)) {
        map.put(e.getKey(), e.getValue());
    }
    System.out.println(map);
}

If yes (Map.Entry is chosen as a convenience), then here is the complete example (note: it is thread unsafe):

import java.util.*;
/** <p>
    A thread unsafe iterator over two lists to convert them 
    into a map such that keys in first list at a certain
    index map onto values in the second list <b> at the same index</b>.
    </p>
    Created by kmhaswade on 5/10/16.
 */
public class DualIterator<K, V> implements Iterable<Map.Entry<K, V>> {
    private final List<K> keys;
    private final List<V> values;
    private int anchor = 0;

    public DualIterator(List<K> keys, List<V> values) {
        // do all the validations here
        this.keys = keys;
        this.values = values;
    }
    @Override
    public Iterator<Map.Entry<K, V>> iterator() {
        return new Iterator<Map.Entry<K, V>>() {
            @Override
            public boolean hasNext() {
                return keys.size() > anchor;
            }

            @Override
            public Map.Entry<K, V> next() {
                Map.Entry<K, V> e = new AbstractMap.SimpleEntry<>(keys.get(anchor), values.get(anchor));
                anchor += 1;
                return e;
            }
        };
    }

    public static 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<>(4);
        for (Map.Entry<String, String> e : new DualIterator<>(names, things)) {
            map.put(e.getKey(), e.getValue());
        }
        System.out.println(map);
    }
}

It prints (per requirement):

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

Solution 15 - Java

With Java 8, I would simply iterate over both, one by one, and fill the map:

public <K, V> Map<K, V> combineListsIntoOrderedMap (Iterable<K> keys, Iterable<V> values) {

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

    Iterator<V> vit = values.iterator();
    for (K k: keys) {
        if (!vit.hasNext())
            throw new IllegalArgumentException ("Less values than keys.");
        
        map.put(k, vit.next());
    }
    
    return map;
}

Or you could go one step further with the functional style and do:

/**
 * Usage:
 *
 *     Map<K, V> map2 = new LinkedHashMap<>();
 *     combineListsIntoOrderedMap(keys, values, map2::put);
 */
public <K, V> void combineListsIntoOrderedMap (Iterable<K> keys, Iterable<V> values, BiConsumer<K, V> onItem) {
    Iterator<V> vit = values.iterator();
    for (K k: keys) {
        if (!vit.hasNext())
            throw new IllegalArgumentException ("Less values than keys.");
        onItem.accept(k, vit.next());
    }
}

Solution 16 - Java

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.

Solution 17 - Java

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"))
}

Solution 18 - Java

Use Clojure. one line is all it takes ;)

 (zipmap list1 list2)

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
Question13renView Question on Stackoverflow
Solution 1 - JavaDrGodCarlView Answer on Stackoverflow
Solution 2 - JavaHans-Peter StörrView Answer on Stackoverflow
Solution 3 - JavaMichael BorgwardtView Answer on Stackoverflow
Solution 4 - Javasmac89View Answer on Stackoverflow
Solution 5 - JavaCPerkinsView Answer on Stackoverflow
Solution 6 - JavaPaul BoddingtonView Answer on Stackoverflow
Solution 7 - JavaJoelView Answer on Stackoverflow
Solution 8 - JavaTom Hawtin - tacklineView Answer on Stackoverflow
Solution 9 - JavaPlapView Answer on Stackoverflow
Solution 10 - JavaTomasz WerszkoView Answer on Stackoverflow
Solution 11 - JavafastcodejavaView Answer on Stackoverflow
Solution 12 - JavaVitaliy MoskalyukView Answer on Stackoverflow
Solution 13 - JavaDonald RaabView Answer on Stackoverflow
Solution 14 - JavaKedar MhaswadeView Answer on Stackoverflow
Solution 15 - JavaOndra ŽižkaView Answer on Stackoverflow
Solution 16 - JavawilmolView Answer on Stackoverflow
Solution 17 - JavaFrank NeblungView Answer on Stackoverflow
Solution 18 - JavaGabiMeView Answer on Stackoverflow