将两个列表组合成地图(Java)的最简单方法?

时间:2009-12-03 12:46:02

标签: java collections

使用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中?

18 个答案:

答案 0 :(得分:39)

我经常使用以下习语。我承认它是否更清楚是值得商榷的。

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

它的优点是它也适用于集合和类似的东西,无需随机访问或没有有效的随机访问,如LinkedList,TreeSet或SQL ResultSet。例如,如果你在LinkedLists上使用原始算法,那么你有一个慢Shlemiel the painter algorithm,它实际上需要对长度为n的列表进行n * n次操作。

正如13ren指出的那样,如果在长度不匹配时尝试读取一个列表的结尾后,也可以使用Iterator.next抛出NoSuchElementException的事实。所以你会得到更好的但可能有点令人困惑的变种:

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

答案 1 :(得分:38)

自问这个问题以来已经有一段时间了但是这些天我偏爱:

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

对于那些不熟悉溪流的人来说,这样做会得到一个IntStream从0到长度,然后将其打包,使其成为Stream<Integer>,以便将其转换为对象,然后收集他们使用Collectors.toMap,其中有两个供应商,其中一个生成密钥,另一个生成密钥。

这可以进行一些验证(例如要求keys.size()小于values.size()),但它可以作为一个简单的解决方案。

编辑: 以上内容适用于任何具有恒定时间查找的内容,但如果您想要一些可以在同一个订单上工作的东西(并且仍然使用相同类型的模式),您可以执行以下操作:

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

输出相同(再次,缺少长度检查等),但时间复杂度不依赖于get方法对任何列表的实现。

答案 2 :(得分:19)

由于键值关系是通过列表索引隐式的,我认为明确使用列表索引的for循环解决方案实际上非常清楚 - 也很短。

答案 3 :(得分:9)

您的上述解决方案当然是正确的,但您的问题是关于清晰度,我会解决这个问题。

组合两个列表的最清晰方法是将组合放入一个名称清晰的方法中。我刚刚把你的解决方案解压缩到一个方法:

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

当然,你重构的主体现在看起来像这样:

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

我无法抗拒长度检查。

答案 4 :(得分:6)

就个人而言,我认为一个简单的for循环遍历索引是最清晰的解决方案,但这里还有两个可以考虑的方法。

另一种避免在boxed()上调用IntStream的Java 8解决方案是

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

答案 5 :(得分:5)

ArrayUtils#toMap()没有将两个列表组合到一个地图中,但是对于一个二维数组这样做了(所以不是你想要的,但可能有兴趣将来参考...)

答案 6 :(得分:5)

除了清晰度之外,我认为还有其他值得考虑的事情:

  • 正确拒绝非法参数,例如不同尺寸列表和null(如果问题代码中thingsnull,则会发生什么情况)。
  • 能够处理没有快速随机访问的列表。
  • 处理并发和同步集合的能力。

因此,对于库代码,可能是这样的:

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

(可能想检查不要放多个相等的键。)

答案 7 :(得分:4)

没有明确的方法。我仍然想知道Apache Commons或Guava是否有类似的东西。无论如何,我有自己的静态实用程序。但是这个意识到关键的碰撞!

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

答案 8 :(得分:2)

你甚至不需要限制自己的字符串。稍微修改CPerkins中的代码:

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;

}

答案 9 :(得分:1)

使用Clojure。只需一行;)

 (zipmap list1 list2)

答案 10 :(得分:1)

这可以使用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);

注意:我是Eclipse Collections的提交者。

答案 11 :(得分:1)

另一个Java 8解决方案:

如果您有权访问Guava库(最早支持版本21 [1] 中的流),则可以执行以下操作:

master

对我来说,这种方法的优点仅在于它是一个衬里,我发现这对我的用例很有帮助。

答案 12 :(得分:1)

使用vavr库:

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

答案 13 :(得分:0)

另一个观点是隐藏实现。您是否希望此功能的调用者能够享受Java的增强型for循环的外观和感觉?

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

如果是(Map.Entry被选为方便),那么这是完整的例子(注意:线程不安全):

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

打印(按要求):

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

答案 14 :(得分:0)

使用Java 8,我只需逐个遍历两者,然后填充地图:

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

或者您可以更进一步使用功能样式并执行:

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

答案 15 :(得分:0)

利用AbstractMapAbstractSet

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

用法:

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

输出:

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

我从java.util.stream.Collectors.Partition类中得到了这个想法,该类基本上具有相同的作用。

它提供封装和清晰的意图(压缩两个列表)以及可重用性和性能。

这比other answer更好,后者创建条目,然后立即将其解包以放入地图。

答案 16 :(得分:0)

我认为这很不言自明(假设列表大小相等)

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

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

答案 17 :(得分:0)

您可以利用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}");
}

当然,使用Kotlin语言更有趣:

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