【问题标题】:What is the best way to combine two lists into a map (Java)?将两个列表组合成地图(Java)的最佳方法是什么?
【发布时间】:2010-12-22 19:38:35
【问题描述】:

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

【问题讨论】:

  • 列表是使示例通用还是您的实际用例是从 String[] 数组开始的?
  • @PSpeed 我的真实用例使用Lists 而不是数组;只有一个是字符串。
  • 如果列表的长度可能不相等,您可能希望迭代次数仅为最短列表的长度:for(int i = 0; i
  • 如果您的列表是索引列表(如 ArrayList 和该数组包装器),那么我说坚持使用索引。如果它们是随机集合,那么迭代器方法总体上表现更好。如果你发现你经常使用这种模式,你甚至可以编写一个 Coiterator 包装器,它会从两个迭代器返回一个双值条目并包装错误检查等。
  • 您确定需要这样做吗?这是构建地图的一种非常糟糕的方式——容易出错,并且任何读者都难以理解什么映射到什么。每次与我交谈过的人认为他们需要这样做时,他们都会找到更好的方法。

标签: java data-structures collections


【解决方案1】:

另一个 Java 8 解决方案:

如果您可以访问 Guava 库(版本 21 中最早支持流 [1]),您可以这样做:

Streams.zip(keyList.stream(), valueList.stream(), Maps::immutableEntry)
       .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));

对我来说,这种方法的优势在于它是一个计算结果为 Map 的单个表达式(即一个衬里),我发现这对于我需要做的事情特别有用。

【讨论】:

  • 空值失败,可能需要也可能不需要。
【解决方案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;
}

【讨论】:

  • 查看我对 CPerkins 的评论 - 我也在考虑泛化它使用的 Map 类?也许Map&lt;M extends Map&lt;K, V&gt;, K, V&gt; map = new M&lt;K, V&gt;(); 但似乎 类型擦除 使这成为不可能,因为在运行时不知道类型:en.wikipedia.org/wiki/Generics_in_Java#Type_erasure
  • 哎呀,我的意思是代码是&lt;M extends Map&lt;K, V&gt;, K, V&gt; ... Map&lt;K, V&gt; map = new M&lt;K, V&gt;();
【解决方案3】:

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

【讨论】:

    【解决方案4】:

    我认为这是不言自明的(假设列表大小相同)

    Map<K, V> map = new HashMap<>();
    
    for (int i = 0; i < keys.size(); i++) {
        map.put(keys.get(i), vals.get(i));
    }
    

    【讨论】:

    • 此解决方案对于未实现 java.util.RandomAccess 的输入列表的性能很差,例如LinkedList
    • 同意,如果您更关心性能而不是代码可读性 - 最好不要使用此解决方案。但如果性能不是那么重要,这仍然是一个不错的选择。
    【解决方案5】:

    利用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 创建条目然后立即展开它们以放入地图要好。

    【讨论】:

      【解决方案6】:

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

      组合两个列表的最清晰的方法是将组合放入一个具有良好清晰名称的方法中。我刚刚采用了您的解决方案并将其提取到这里的方法中:

      Map combineListsIntoOrderedMap(List 键,List 值){ 如果 (keys.size() != values.size()) throw new IllegalArgumentException ("不能合并大小不同的列表"); Map map = new LinkedHashMap(); for (int i=0; i

      当然,重构后的 main 现在看起来像这样:

      静态公共无效主要(字符串[]参数){ List 名称 = Arrays.asList("apple,orange,pear".split(",")); List things = Arrays.asList("123,456,789".split(",")); Map map = combineListsIntoOrderedMap (names, things); System.out.println(map); }

      我无法抗拒长度检查。

      【讨论】:

      • ... 如果我们把它放在一个类中,我们可以将它泛化为除 String、String 之外的类型。也许,使它成为 LinkedHashMap 的子类,并将您的方法作为构造函数(因为这是它一直在做的事情)。能够泛化超类(以便特定的 Map 类成为参数)会很好,但我很确定 java 不允许这样做。它可以在一个单独的类中完成(不扩展 LinkedHashMap),采用三个类参数。 [在我的脑海中]
      • 该算法在长链表上效率很低,因为它们没有有效的随机访问。我认为如果你正在编写一个可重用的函数,你应该考虑到这一点。
      • @hstoerr 好点。我想我有考虑 ArrayLists 的习惯。
      【解决方案7】:

      vavr库:

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

      【讨论】:

        【解决方案8】:

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

        【讨论】:

          【解决方案9】:

          这可以使用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 的提交者。

          【讨论】:

            【解决方案10】:

            我个人认为一个简单的 for 循环遍历索引是最清晰的解决方案,但这里还有另外两种可能性需要考虑。

            避免在 IntStream 上调用 boxed() 的替代 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
                                               );
                                      );
            

            【讨论】:

              【解决方案11】:

              这个问题被问到已经有一段时间了,但这些天我偏向于:

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

              对于那些不熟悉流的人,它的作用是从 0 到长度获取一个 IntStream,然后将其装箱,使其成为 Stream&lt;Integer&gt;,以便可以将其转换为一个对象,然后使用 @987654324 收集它们@ 接受两个供应商,其中一个生成键,另一个生成值。

              这可以通过一些验证(例如要求 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 方法的实现,无论使用什么列表。

              【讨论】:

              • 像魅力一样工作......非常感谢@DrGodCarl
              • 我会有点担心性能。如果输入 List 是一个 ArrayList,那么应该没问题,因为您可以在 O(1) 中对 ArrayList 调用 .get()。但对于访问时间为 O(n) 的链表,此解决方案具有二次复杂度,这对于大型列表可能是个问题。
              • 是的,这绝对是我应该注意的。遗憾的是,Java 的迭代器在处理流时非常笨重。确实,streams 应该提供 zip 方法。
              【解决方案12】:

              ArrayUtils#toMap() 不会将两个列表组合成一个映射,但会为一个二维数组这样做(所以不是你要找的,但可能对未来的参考感兴趣......)

              【讨论】:

              【解决方案13】:

              对此的另一个观点是隐藏实现。您希望此功能的调用者享受 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}
              

              【讨论】:

              • 那个 dualiterator 看起来有点矫枉过正。为什么不使用两个普通的迭代器呢?
              • 我觉得有一个类似库的例程是为了从列表元素中获取地图。但也许这是矫枉过正。
              【解决方案14】:

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

              • 正确拒绝非法参数,例如不同大小的列表和nulls(看看问题代码中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;
              }
              

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

              【讨论】:

              • .toArray 是因为它是同步集合的原子操作,避免了并发操作的后台修改麻烦?一个很好的观点!
              【解决方案15】:

              没有明确的方法。我仍然想知道 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;
              }
              

              【讨论】:

              • +1 不错!我刚刚看到这个是因为对我的解决方案的评论。我不同意没有clear 方式——这种方式对我来说似乎很清楚。我喜欢它包含来自@hstoerr 的双迭代器解决方案和来自@fastcodejava 的泛型的方式。
              • 不错,但如果您在开始构建地图之前检查大小,您会更喜欢。
              • @tucuxi 在我看来,在特殊情况下没有优化。我的意思是,在大小不同的情况下,无论如何你都会得到 IllegalArgumentException。
              【解决方案16】:

              我经常使用以下成语。我承认它是否更清楚是有争议的。

              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、TreeSets 或 SQL ResultSets。例如,如果你在 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());
              

              【讨论】:

              • 这至少适用于大小不等的列表,无需任何预先检查。
              • @BalusC 这实际上可能不是您想要的。由于 index -> index 应该匹配,因此会在不匹配的列表上静默中断。 @hstoerr 在 OP 的示例中,迭代器将在内部使用基于索引的随机访问,因此您在那里并没有真正获得任何东西。当索引是两个列表之间的语义关系时,我不确定您是否可以使用它。
              • @PSpeed OP 在这里,如果第二个列表更长,我的示例代码也会默默地继续......我不担心有效性检查,但一种方法是 while(true),并且仅在 both 都没有剩余时才中断(如果只有一个没有剩余,那么它将继续,并在该列表的 next() 上抛出 NoSuchElementException)。
              • 如果需要的话,在上面的代码后面加上一个检查也不难……if (i1.hasNext() || i2.hasNext()) throw SomeException(...
              • 你也可以通过设置 count = Math.max(length1, length2) 来做到这一点。我仍然相信您当前的方式是最好的,因为它准确地说明了您在做什么:按索引同步两个列表。
              【解决方案17】:

              使用 Clojure。只需要一行;)

               (zipmap list1 list2)
              

              【讨论】:

              • 嗯,我猜这 java,在某种程度上;它相当于为任务提供了一个功能(参见 CPerkins 的回答)。 zipmap(list1, list2) 的等效 java 调用并没有什么不同,如果您已经拥有该函数的话。
              • 对。但是 Clojure 是一种很酷的语言,我无法抗拒它
              【解决方案18】:

              由于键值关系是通过列表索引隐含的,我认为显式使用列表索引的 for-loop 解决方案实际上非常清晰 - 也很简短。

              【讨论】:

              • 但是如果你结合使用 .get(i) 是一个坏主意,例如LinkedLists - 在这种情况下,它不再是一个恒定时间操作。
              猜你喜欢
              • 2010-11-15
              • 1970-01-01
              • 2011-04-06
              • 1970-01-01
              • 2015-02-04
              • 2011-05-13
              • 2017-01-27
              • 2010-12-23
              相关资源
              最近更新 更多