What is the best way to combine two lists into a map (Java)?
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?
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.
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());
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.
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.