开发者

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

开发者 https://www.devze.com 2022-12-13 15:44 出处:网络
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.

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);
  }
开发者_Python百科}

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.


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.


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


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


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


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


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


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


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.


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}


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


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


Use Clojure. one line is all it takes ;)

 (zipmap list1 list2)
0

精彩评论

暂无评论...
验证码 换一张
取 消