Отличный способ перебирать параллельные массивы в Java с помощью foreach - PullRequest
31 голосов
/ 05 апреля 2011

Я унаследовал набор кода, который широко использует параллельные массивы для хранения пар ключ / значение.Это действительно имело смысл делать таким образом, но было бы неудобно писать циклы, которые перебирают эти значения.Мне действительно нравится новая конструкция Java foreach, но, похоже, нет способа перебирать параллельные списки с помощью этого.

С помощью обычного цикла for я могу легко это сделать:

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

Но, на мой взгляд, это не семантически чисто, так как мы не проверяем границы list2 во время итерации.Есть ли какой-нибудь умный синтаксис, похожий на for-each, который я могу использовать с параллельными списками?

Ответы [ 8 ]

22 голосов
/ 05 апреля 2011

Я бы использовал Map сам.Но, клянусь вам, что пара массивов имеет смысл в вашем случае, как насчет вспомогательного метода, который берет ваши два массива и возвращает Iterable упаковщик?1007 * Оболочка Iterable<Pair<K,V>> скрывает проверку границ.

11 голосов
/ 05 апреля 2011

На официальной странице Oracle в расширенном цикле for:

Наконец, он не может использоваться для циклов, которые должны выполнять итерацию в нескольких коллекциях параллельно.Эти недостатки были известны дизайнерам, которые приняли сознательное решение использовать чистую, простую конструкцию, которая бы охватывала подавляющее большинство случаев.loop.

Если вы используете эти пары массивов для имитации карты, вы всегда можете написать класс, реализующий интерфейс Map с двумя массивами;это может позволить вам абстрагироваться от большей части циклов.

Не глядя на ваш код, я не могу сказать вам, является ли этот вариант лучшим способом продвижения вперед, но это то, что вы могли бы рассмотреть.

9 голосов
/ 05 апреля 2011

Это было забавное упражнение. Я создал объект с именем 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<Integer> is created for varargs parameters, поэтому, если кто-нибудь знает, как от него избавиться, дайте мне знать:)

6 голосов
/ 29 ноября 2011

Вы можете использовать второе ограничение в цикле for:

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

Один из моих предпочтительных методов обхода коллекций - цикл for-each, но, как говорится в руководстве по оракулу, при работе с параллельными коллекциямииспользовать итератор, а не для каждого .

Ниже был дан ответ Мартин против Лёвиса в аналогичной записи :

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

   doStuff(value1);
   doStuff(value2);
}//while

Преимущество итератора заключается в том, что он является универсальным, поэтому, если вы не знаете, какие коллекции используются, используйте итератор, в противном случае, если вы знаете, какие у вас коллекции, вы знаете длину /функции размера и, таким образом, обычный цикл for с дополнительным ограничением может быть использован здесь.(Обратите внимание, что в этом посте я нахожусь во множественном числе, поскольку интересная возможность может быть связана с тем, что используемые коллекции отличаются, например, один может быть списком, а другой, например, массивом)

Надеюсь, это помогло.

1 голос
/ 13 декабря 2016

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

Петля в минимальном размере a и b :

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

Выход:

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

Цикл в пределах максимального размера a и b (элементы в более коротком списке будут циклически):

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

Loop 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);
});
1 голос
/ 11 ноября 2014

Простой ответ: Нет.

Вы хотите сексуальную итерацию и байт-код Java? Проверьте Scala: Scala для цикла по двум спискам одновременно

Отказ от ответственности: Это действительно ответ "используйте другой язык". Поверьте мне, я бы хотел, чтобы у Java была сексуальная параллельная итерация, но никто не начал разрабатывать на Java, потому что им нужен сексуальный код.

0 голосов
/ 16 июня 2015

ArrayIterator позволяет избежать индексирования, но вы не можете использовать цикл for-each без написания отдельного класса или хотя бы функции. Как отмечает @Alexei Blue, официальная рекомендация (на Интерфейс коллекции ): «Используйте 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());
  }

Тем не менее:

  • Индексирование является естественным для массивов - массив по определению что-то, что вы индексируете, и числовой цикл, как в вашем исходном коде, совершенно естественны и более прямолинейны.
  • Пары ключ-значение естественно образуют Map, как отмечает @Isaac Truett, поэтому наиболее чистым было бы создание карт для всех ваших параллельных массивов (так что этот цикл был бы только в фабричной функции, которая создает карты), хотя это было бы неэффективно, если вы просто хотите перебирать их. (Используйте Multimap , если вам нужно поддерживать дубликаты.)
  • Если у вас их много, вы можете (частично) реализовать ParallelArrayMap<> (т. Е. Карту с параллельными массивами) или, возможно, ParallelArrayHashMap<> (добавить 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 отвечает «Напишите класс ParallelArrayMap, который реализует Map (пары ключ / значение) и в котором есть конструктор, который принимает параллель массивы, а затем вы можете использовать entrySet для возврата Set, который вы можете перебрать, поскольку Set реализует Collection. ”- сделать структуру явной в типе, реализованном класс.

Для итерации по двум параллельным коллекциям или массивам вы хотите итерировать по Iterable<Pair<T, U>>, которые менее явные языки позволяют создавать с помощью zip (который @Isaac Truett называет wrap) , Однако это не идиоматическая Java - каковы элементы пары? См. Java: Как написать функцию zip? Каким должен быть тип возвращаемого значения? для подробного обсуждения того, как написать это на Java и почему его не рекомендуется.

Это именно тот стилистический компромисс, который делает Java: вы точно знаете, к какому типу относится все, и у вас есть для его указания и реализации.

0 голосов
/ 05 апреля 2011
//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]);
    }
}
...