Подтвердить что ты не робот

Nifty способ перебора параллельных массивов в Java с использованием foreach

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

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

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

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

4b9b3361

Ответ 1

Я бы сам использовал Map. Но беря вас за ваше слово, что в вашем случае есть пара массивов, как насчет метода утилиты, который берет ваши два массива и возвращает обертку Iterable?

Концептуально:

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

Оболочка Iterable<Pair<K,V>> скроет проверку границ.

Ответ 2

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

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

В принципе, вам лучше использовать обычный цикл.

Если вы используете эти пары массивов для имитации карты, вы всегда можете написать класс, который реализует интерфейс 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<Integer> 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, но, как упоминается в руководстве по оракулу, при работе с параллельными коллекциями использовать iterator, а не для каждого.

Ниже был ответ Martin v. Löwis в аналогичном сообщение:

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

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

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

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

Ответ 5

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

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

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

Ответ 6

С 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

Петля 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);
});

Ответ 7

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, поэтому самым чистым было бы создание карт для всех ваших параллельных массивов (так что этот цикл будет только в функции factory, которая создает карты), хотя это было бы неэффективно, если вы просто захотите перебирать их. (Используйте 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: вы точно знаете, какой тип все есть, и вы должны указать и реализовать его.

Ответ 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]);
    }
}