将两个列表合并为一个映射(Java)的最佳方法是什么?

如果使用 for (String item: list)就好了,但是它只能迭代一个列表,并且您需要一个显式的迭代器来迭代另一个列表。或者,您可以对两者都使用显式迭代器。

下面是这个问题的一个例子,以及一个使用索引 for循环的解决方案:

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

产出:

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

有没有更清晰的方法? 也许在集合 API 的某个地方?

118240 次浏览

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.

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

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

Use Clojure. one line is all it takes ;)

 (zipmap list1 list2)

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.

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

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

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

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}

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.

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

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.

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

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.

With vavr library:

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

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.

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

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