【问题标题】:Nifty way to iterate over parallel arrays in Java using foreach使用 foreach 在 Java 中迭代并行数组的好方法
【发布时间】:2011-07-29 13:41:26
【问题描述】:

我继承了一堆代码,这些代码大量使用并行数组来存储键/值对。这样做实际上是有道理的,但是编写遍历这些值的循环有点尴尬。我真的很喜欢新的 Java foreach 构造,但似乎没有办法使用它来迭代并行列表。

使用普通的for 循环,我可以轻松做到这一点:

for (int i = 0; i < list1.length; ++i) {
    doStuff(list1[i]);
    doStuff(list2[i]);
}

但在我看来,这在语义上并不纯粹,因为我们没有在迭代期间检查 list2 的边界。是否有一些类似于 for-each 的巧妙语法可以用于并行列表?

【问题讨论】:

  • Re:语义纯度——您可以在迭代期间检查 list2 边界 ((i &lt; list1.length) &amp;&amp; (i &lt; list2.length)),或者如果您知道列表在迭代期间不会被修改,您可以在循环之前检查 @987654325 @ 和 list2 的长度相等,在这种情况下,您可以在迭代过程中不检查两者的界限,并且问心无愧。
  • 如果一个线程修改了list2而不是list1,那我就完蛋了。
  • 就像我说的那样——“如果你知道列表在迭代过程中不会被修改”。而且,这不会让你比现在更糟。如果您不得不担心多个线程会访问这些列表,那么您需要担心的不仅仅是这个循环。
  • 我不确定我的代码将来会适应哪些用例。我不想引入一个可能很难找到的错误。
  • +1 表示酷标题 :-)

标签: java foreach iteration


【解决方案1】:

我自己会使用Map。但是相信您的话,一对数组在您的情况下是有意义的,那么一个实用方法可以获取您的两个数组并返回一个 Iterable 包装器吗?

概念上:

for (Pair<K,V> p : wrap(list1, list2)) {
    doStuff(p.getKey());
    doStuff(p.getValue());
}

Iterable&lt;Pair&lt;K,V&gt;&gt; 包装器会隐藏边界检查。

【讨论】:

  • Pair解决一切。没想到,不错是不是有一个 JSR 请求将其包含在语言中?
  • 小心这个答案。如果第一个列表有重复值会怎样?生成的 Map 将覆盖这些键的值。所以如果您的列表可能包含重复值,请不要使用这种方法
  • @jfcorugedo 您是正确的,地图不能包含重复的键。我的回答实际上不需要使用地图。
【解决方案2】:

来自增强型 for 循环的 Oracle 官方页面:

最后,它不能用于循环 必须迭代多个 并行收集。这些 缺点是众所周知的 有意识的设计师 决定去一个干净,简单 涵盖伟大的建筑 大多数情况。

基本上,您最好使用普通的 for 循环。

如果您使用这些数组对来模拟 Map,您总是可以编写一个类来实现带有两个数组的 Map 接口;这可以让你抽象出大部分循环。

如果不查看您的代码,我无法告诉您此选项是否是最好的方法,但您可以考虑。

【讨论】:

    【解决方案3】:

    这是一个有趣的练习。我创建了一个名为 ParallelList 的对象,它接受可变数量的类型列表,并且可以迭代每个索引处的值(作为值列表返回):

    public class ParallelList<T> implements Iterable<List<T>> {
    
        private final List<List<T>> lists;
    
        public ParallelList(List<T>... lists) {
            this.lists = new ArrayList<List<T>>(lists.length);
            this.lists.addAll(Arrays.asList(lists));
        }
    
        public Iterator<List<T>> iterator() {
            return new Iterator<List<T>>() {
                private int loc = 0;
    
                public boolean hasNext() {
                    boolean hasNext = false;
                    for (List<T> list : lists) {
                        hasNext |= (loc < list.size());
                    }
                    return hasNext;
                }
    
                public List<T> next() {
                    List<T> vals = new ArrayList<T>(lists.size());
                    for (int i=0; i<lists.size(); i++) {
                        vals.add(loc < lists.get(i).size() ? lists.get(i).get(loc) : null);
                    }
                    loc++;
                    return vals;
                }
    
                public void remove() {
                    for (List<T> list : lists) {
                        if (loc < list.size()) {
                            list.remove(loc);
                        }
                    }
                }
            };
        }
    }
    

    示例用法:

    List<Integer> list1 = Arrays.asList(new Integer[] {1, 2, 3, 4, 5});
    List<Integer> list2 = Arrays.asList(new Integer[] {6, 7, 8});
    ParallelList<Integer> list = new ParallelList<Integer>(list1, list2);
    for (List<Integer> ints : list) {
        System.out.println(String.format("%s, %s", ints.get(0), ints.get(1)));
    }
    

    哪个会打印出来:

    1, 6
    2, 7
    3, 8
    4, null
    5, null
    

    此对象支持可变长度列表,但显然它可以修改得更严格。

    不幸的是,我无法摆脱 ParallelList 构造函数上的一个编译器警告:A generic array of List&lt;Integer&gt; is created for varargs parameters,所以如果有人知道如何摆脱它,请告诉我 :)

    【讨论】:

      【解决方案4】:

      您可以在 for 循环中使用第二个约束:

          for (int i = 0; i < list1.length && i < list2.length; ++i) 
          {
            doStuff(list1[i]);
            doStuff(list2[i]);
          }//for
      

      我首选的遍历集合的方法之一是 for-each 循环,但正如 oracle 教程所提到的,在处理并行集合时使用 iterator rather than the for-each

      以下是Martin v. Löwis 在类似post 中的回答:

      it1 = list1.iterator();
      it2 = list2.iterator();
      while(it1.hasNext() && it2.hasNext()) 
      {
         value1 = it1.next();
         value2 = it2.next();
      
         doStuff(value1);
         doStuff(value2);
      }//while
      

      迭代器的优点是它是通用的,所以如果你不知道正在使用什么集合,请使用迭代器,否则如果你知道你的集合是什么,那么你就知道长度/大小函数,所以常规可以在此处使用带有附加约束的 for 循环。 (请注意,我在这篇文章中非常复数,因为一个有趣的可能性是使用的集合不同,例如,一个可能是 List,另一个可能是数组)

      希望这会有所帮助。

      【讨论】:

        【解决方案5】:

        在 Java 8 中,我使用这些以性感的方式循环:

        //parallel loop
        public static <A, B> void loop(Collection<A> a, Collection<B> b, IntPredicate intPredicate, BiConsumer<A, B> biConsumer) {
            Iterator<A> ait = a.iterator();
            Iterator<B> bit = b.iterator();
            if (ait.hasNext() && bit.hasNext()) {
                for (int i = 0; intPredicate.test(i); i++) {
                    if (!ait.hasNext()) {
                        ait = a.iterator();
                    }
                    if (!bit.hasNext()) {
                        bit = b.iterator();
                    }
                    biConsumer.accept(ait.next(), bit.next());
                }
            }
        }
        
        //nest loop
        public static <A, B> void loopNest(Collection<A> a, Collection<B> b, BiConsumer<A, B> biConsumer) {
            for (A ai : a) {
                for (B bi : b) {
                    biConsumer.accept(ai, bi);
                }
            }
        }
        

        一些例子,有这两个列表:

        List<Integer> a = Arrays.asList(1, 2, 3);
        List<String> b = Arrays.asList("a", "b", "c", "d");
        

        ab 的最小尺寸内循环:

        loop(a, b, i -> i < Math.min(a.size(), b.size()), (x, y) -> {
            System.out.println(x +  " -> " + y);
        });
        

        输出:

        1 -> a
        2 -> b
        3 -> c
        

        ab 的最大尺寸内循环(较短列表中的元素将被循环):

        loop(a, b, i -> i < Math.max(a.size(), b.size()), (x, y) -> {
            System.out.println(x +  " -> " + y);
        });
        

        输出:

        1 -> a
        2 -> b
        3 -> c
        1 -> d
        

        循环 n 次((如果 n 大于列表的大小,元素将被循环):

        loop(a, b, i -> i < 5, (x, y) -> {
            System.out.println(x +  " -> " + y);
        });
        

        输出:

        1 -> a
        2 -> b
        3 -> c
        1 -> d
        2 -> a
        

        永远循环:

        loop(a, b, i -> true, (x, y) -> {
            System.out.println(x +  " -> " + y);
        });
        

        适用于您的情况:

        loop(list1, list2, i -> i < Math.min(a.size(), b.size()), (e1, e2) -> {
            doStuff(e1);
            doStuff(e2);
        });
        

        【讨论】:

          【解决方案6】:

          简单的回答:不。

          您想要性感的迭代和 Java 字节码?查看斯卡拉: Scala for loop over two lists simultaneously

          免责声明:这确实是一个“使用另一种语言”的答案。相信我,我希望 Java 有性感的并行迭代,但是没有人因为想要性感的代码而开始使用 Java 开发。

          【讨论】:

          • 嘿乔!我们不断地相互碰撞 :) 当然,“使用不同的语言”是解决问题的方法,但不是对包含介词短语“在 Java 中”的问题的答案。
          • @TravisWebb 哦,没注意到是你!该假设的实际答案是“不”。其他语言是对性感迭代的建议。 :) 希望它们很快会成为一个不错的 Java 8 lambda 表达式答案。
          • 是的,这个问题现在已经很老了。可能需要一些更现代的答案。
          【解决方案7】:

          ArrayIterator 可以避免索引,但是如果不编写单独的类或至少是函数,就不能使用for-each 循环。正如@Alexei Blue 所说,官方建议(The Collection Interface)是:“在需要时使用Iterator 而不是for-each 构造:……并行迭代多个集合。”:

          import static com.google.common.base.Preconditions.checkArgument;
          import org.apache.commons.collections.iterators.ArrayIterator;
          
          // …
          
            checkArgument(array1.length == array2.length);
            Iterator it1 = ArrayIterator(array1);
            Iterator it2 = ArrayIterator(array2);
            while (it1.hasNext()) {
                doStuff(it1.next());
                doOtherStuff(it2.next());
            }
          

          但是:

          • 索引对于数组来说是很自然的——数组根据定义是你索引的东西,而数字 for 循环,就像在你的原始代码中一样,是非常自然和更直接的。
          • 键值对自然形成Map,正如@Isaac Truett 所说,最简洁的方法是为所有并行数组创建映射(因此此循环仅在创建映射的工厂函数中),但如果您只想遍历它们,这将是低效的。 (如果您需要支持重复,请使用Multimap。)
          • 如果你有很多这些,你可以(部分)实现ParallelArrayMap&lt;&gt;(即,由并行数组支持的映射),或者可能ParallelArrayHashMap&lt;&gt;(如果你想通过键有效查找,添加HashMap ),并使用它,它允许以原始顺序进行迭代。虽然这可能有点矫枉过正,但可以给出一个性感的答案。

          即:

          Map<T, U> map = new ParallelArrayMap<>(array1, array2);
          for (Map.Entry<T, U> entry : map.entrySet()) {
            doStuff(entry.getKey());
            doOtherStuff(entry.getValue());
          }
          

          从哲学上讲,Java 风格是具有显式的、命名的 类型,由类实现。因此,当您说“[我有] 存储键/值对的并行数组。”时,Java 会回答“编写一个实现 Map(键/值对)的 ParallelArrayMap 类,并且它有一个并行的构造函数数组,然后您可以使用 entrySet 返回一个您可以迭代的 Set,因为 Set 实现了 Collection。” – 使结构显式在一个类型中,由一个类实现。

          为了迭代两个并行集合或数组,您希望迭代 Iterable&lt;Pair&lt;T, U&gt;&gt;,不太明确的语言允许您使用 zip(@Isaac Truett 称之为 wrap)创建。然而,这不是惯用的 Java——这对的元素是什么?请参阅Java: How to write a zip function? What should be the return type?,详细讨论如何用 Java 编写它以及为什么不鼓励这样做。

          这正是 Java 做出的风格权衡:您确切地知道一切是什么类型,并且您必须指定和实现它。

          【讨论】:

            【解决方案8】:
            //Do you think I'm sexy?
            if(list1.length == list2.length){
                for (int i = 0; i < list1.length; ++i) {
                    doStuff(list1[i]);
                    doStuff(list2[i]);
                }
            }
            

            【讨论】:

            • list1.length != list2.length 时,一切都神秘地崩溃了。
            • 查看我对@QuantumMechanic 的评论
            猜你喜欢
            • 1970-01-01
            • 2016-09-19
            • 2019-11-29
            • 2011-11-27
            • 1970-01-01
            • 1970-01-01
            • 2014-11-22
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多