Как наиболее элегантно перебирать параллельные коллекции?

Скажем, у меня есть две параллельные коллекции, например: список имен людей в List<String> и список их возраста в List<Int> в том же порядке (так, чтобы любой данный индекс в каждой коллекции ссылался на тот же человек).

Я хочу итерации по обеим коллекциям одновременно и вызывать имя и возраст каждого человека и что-то делать с ним. С массивами это легко сделать с помощью:

for (int i = 0; i < names.length; i++) {
   do something with names[i] ....
   do something with ages[i].....
}

Каким будет самый элегантный способ (с точки зрения удобочитаемости и скорости) сделать это с помощью коллекций?

Ответ 1

Я бы создал новый объект, который инкапсулирует два. Бросьте это в массив и повторите это.

List<Person>

Где

public class Person {
    public string name;
    public int age;
}

Ответ 2

it1 = coll1.iterator();
it2 = coll2.iterator();
while(it1.hasNext() && it2.hasNext()) {
   value1 = it1.next();
   value2 = it2.next();
   do something with it1 and it2;
}

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

Ответ 3

Вы можете создать для него интерфейс:

public interface ZipIterator<T,U> {
  boolean each(T t, U u);
}

public class ZipUtils {
  public static <T,U> boolean zip(Collection<T> ct, Collection<U> cu, ZipIterator<T,U> each) {
    Iterator<T> it = ct.iterator();
    Iterator<U> iu = cu.iterator();
    while (it.hasNext() && iu.hasNext()) {
      if (!each.each(it.next(), iu.next()) {
        return false;
      }
    }
    return !it.hasNext() && !iu.hasNext();
  }
}

И тогда у вас есть:

Collection<String> c1 = ...
Collection<Long> c2 = ...
zip(c1, c2, new ZipIterator<String, Long>() {
  public boolean each(String s, Long l) {
    ...
  }
});

Ответ 4

for (int i = 0; i < names.length; ++i) {
  name = names.get(i);
  age = ages.get(i);
  // do your stuff
}

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

Ответ 5

Как было предложено jeef3, моделирование истинного домена, а не сохранение отдельных, неявно связанных списков - это правильный путь... когда это вариант.

Существуют различные причины, по которым вы, возможно, не сможете принять этот подход. Если так...

а. Вы можете использовать подход обратного вызова, как предложено cletus.

В. Вы все же можете выбрать, чтобы открыть Iterator, который предоставляет объект объекта домена для каждого составного экземпляра. Этот подход не заставляет вас сохранять структуру параллельного списка.

private List<String> _names = ...;
private List<Integer> _ages = ...;

Iterator<Person> allPeople() {
  final Iterator<String> ni = _names.iterator();
  final Iterator<Integer> ai = _ages.iterator();
  return new Iterator() {
    public boolean hasNext() {
      return ni.hasNext();
    }
    public Person next() {
      return new Person(ni.next(), ai.next());
    }
    public void remove() {
      ni.remove();
      ai.remove();
    }
  };
}

С. Вы можете использовать этот вариант и использовать API-интерфейс курсора стиля RowSet. Пусть say IPerson - это интерфейс, который описывает Person. Тогда мы можем сделать:

public interface IPerson {
  String getName();
  void setName(String name);
  ...
}

public interface ICursor<T> {
  boolean next();
  T current();
}

private static class PersonCursor implements IPerson, ICursor<IPerson> {
  private final List<String> _names;
  ...
  private int _index = -1;

  PersonCursor(List<String> names, List<Integer> ages) {
    _names = names;
    ...
  }

  public boolean next() {
    return ++_index < _names.size();
  }

  public Person current() {
    return this;
  }

  public String getName() {
    return _names.get(_index);
  }

  public void setName(String name) {
    _names.set(0, name);
  }

  ...
}

private List<String> _names = ...;
private List<Integer> _ages = ...;

Cursor<Person> allPeople() {
  return new PersonCursor(_names, _ages);
}

Обратите внимание, что подход B также должен быть включен для поддержки обновлений для списка, введя интерфейс домена, а Iterator возвращает "живые" объекты.

Ответ 6

Я просто разместил эту функцию в этом аналогичном вопросе (который утверждает @Nils von Barth не дубликат;)), но он здесь применим и здесь:

public static <L,R,M> List<M> zipLists(
    BiFunction<L,R,M> factory, Iterable<L> left, Iterable<R> right) {
  Iterator<L> lIter = left.iterator();
  Iterator<R> rIter = right.iterator();
  ImmutableList.Builder<M> builder = ImmutableList.builder();

  while (lIter.hasNext() && rIter.hasNext()) {
    builder.add(factory.apply(lIter.next(), rIter.next()));
  }

  // Most of the existing solutions fail to enforce that the lists are the same
  // size. That is a *classic* source of bugs. Always enforce your invariants!
  checkArgument(!lIter.hasNext(),
      "Unexpected extra left elements: %s", ImmutableList.copyOf(lIter));
  checkArgument(!rIter.hasNext(),
      "Unexpected extra right elements: %s", ImmutableList.copyOf(rIter));
  return builder.build();
}

Затем вы можете предоставить операцию factory для BiFunction, например конструктор типа значения:

List<Person> people = zipLists(Person::new, names, ages);

Если вы действительно хотите перебрать их и выполнить некоторую операцию, а не создать новую коллекцию, вы можете поменять BiFunction на BiConsumer и вернуть функцию void.

Ответ 7

Я взял комментарий @cletus и улучшил его abit, и это то, что я использую:

public static <T,U> void zip(Collection<T> ct, Collection<U> cu, BiConsumer<T, U> consumer) {
    Iterator<T> it = ct.iterator();
    Iterator<U> iu = cu.iterator();
    while (it.hasNext() && iu.hasNext()) {
        consumer.accept(it.next(), iu.next());
    }
}

Использование:

zip(list1, list2, (v1, v2) -> {
    // Do stuff
});