Provide an iterator over the contents of two lists

2019-01-11 10:29发布

问题:

Suppose I have this:

public class Unit<MobileSuit, Pilot> {

    ...

    List<MobileSuit> mobileSuits;
    List<Pilot> pilots;

    ...
}

And I would like to iterate through the pair of each in the simplest way outside of that class. How should I go about doing that? I thought about doing this:

public class Unit<MobileSuit, Pilot> {

    ...
    Iterator<MobileSuit> iteratinMechas;
    Iterator<Pilot> iteratinPeople;

    class IteratorCustom<MobileSuit, Pilot> implements Iterator {

        public boolean hasNext() {
            return iteratinMechas.hasNext() && iteratinPeople.hasNext();
        }

        public void remove() {
            iteratinMechas.remove();
            iteratinPeople.remove();
        }

        public Object next() {
            // /!\
        }

    }

    public Iterator iterator() {
        return new IteratorCustom<MobileSuit, Pilot>(mobileSuits, pilots);
    }
}

Something along those lines.

Anyway, the problem is that I can't really return just a single object from next(), and I also can't have a Iterator take more than one type. So, any thoughts?

Also, I can't make a new class to combine MobileSuit and Pilot. I need to keep them separate, even though I'm iterating through both at a time. The reason is that there might be Mobile Suits that have no pilots, and I'm not sure how to fix that by keeping them at the same class. This class needs to be processed in other places, so I'd have to unify a interface around that and a lot of other stuff. Basically, assume MobileSuit and Pilot need to be separated.

回答1:

Anyway, the problem is that I can't really return just a single object from next(), and I also can't have a Iterator take more than one type. So, any thoughts?

Obviously you are going to need a light-weight "pair" class. This is roughly analogous to the Map.Entry inner class.

Here's a rough cut at a generic solution:

public class ParallelIterator <T1, T2> implements Iterator<Pair<T1, T2>> {

    public class Pair<TT1, TT2> {
        private final TT1 v1;
        private final TT2 v2;
        private Pair(TT1 v1, TT2 v2) { this.v1 = v1; this.v2 = v2; }
        ...
    }

    private final Iterator<T1> it1;
    private final Iterator<T2> it2;

    public ParallelIterator(Iterator<T1> it1, Iterator<T2> it2) { 
        this.it1 = it1; this.it2 = it2;
    }

    public boolean hasNext() { return it1.hasNext() && it2.hasNext(); }

    public Pair<T1, T2> next() {
        return new Pair<T1, T2>(it1.next(), it2.next());
    }

    ...

}

Note: this doesn't explicitly deal with cases where the lists have different lengths. What will happen is that extra elements at the end of the longer list will be silently ignored.



回答2:

This is copied+edited from Stephen C's answer. Feel free to use:

public class Pair<T1, T2> {
    private final T1 v1;
    private final T2 v2;
    Pair(T1 v1, T2 v2) {
        this.v1 = v1;
        this.v2 = v2;
    }
    public T1 first(){
        return v1;
    }
    public T2 second(){
        return v2;
    }
}

public class ParallelIterator <T1, T2> implements Iterator<Pair<T1, T2>> {

    private final Iterator<T1> it1;
    private final Iterator<T2> it2;

    public ParallelIterator(Iterator<T1> it1, Iterator<T2> it2) { 
        this.it1 = it1; this.it2 = it2;
    }

    @Override
    public boolean hasNext() { return it1.hasNext() && it2.hasNext(); }

    @Override
    public Pair<T1, T2> next() {
        return new Pair<T1, T2>(it1.next(), it2.next());
    }

    @Override
    public void remove(){
        it1.remove();
        it2.remove();
    }
}

public class IterablePair <T1, T2> implements Iterable<Pair<T1,T2>> {
    private final List<T1> first;
    private final List<T2> second;

    public IterablePair(List<T1> first, List<T2> second) { 
        this.first = first;
        this.second = second;
    }

    @Override
    public Iterator<Pair<T1, T2>> iterator(){
        return new ParallelIterator<T1,T2>( first.iterator(), second.iterator() );
    }
}

void someFunction(){
    IterablePair<X,Y> listPair = new IterablePair<X,Y>( x, y );
    for( Pair<X,Y> pair : listPair ){
        X x = pair.first();
        ...
    }
}

This stops as soon as either list is out of elements, so you might want to check lists have equal size before creating an IterablePair.



回答3:

Also, I can't make a new class to combine MobileSuit and Pilot.

That doesn't sound correct. It sounds like you can't replace MobileSuit and Pilot by a single class, but I don't see any reason why you can't have a single class that combines them - i.e. one which just has a getPilot() method and a getMobileSuit() method. You could use a generic Pair class for the same purpose, but a custom class would be easier to use.

On the other hand, if you want to do this sort of "zipping" operation in multiple places, it might be one solution. Alternatively, you could write a generic interface to represent the act of combining the two distinct items - which could return a SuitedPilot or whatever your combination class is.



回答4:

The reason is that there might be Mobile Suits that have no pilots, and I'm not sure how to fix that by keeping them at the same class.

You can use null values, right? Which is the correct way of doing it - have each suit keep track of its pilot. If it has no pilot, then indicate that with a null value there.

But, if you're dead set on not doing that for some reason...

public class SuitAndPilot
{
    public MobileSuit suit;
    public Pilot pilot;

    public SuitAndPilot(Suit s, Pilot p) {
           suit = s;
           pilot = p;
    }
}


回答5:

Why not have a class MannedMobileSuit as a subclass of MobileSuit that contains an instance of a pilot ? That would solve your problem by having a getPilot method.

Usually when you get such problems (needing to return two instances) it is because your Object model is not appropriate and should be changed. Keep your options open



回答6:

Basically, assume MobileSuit and Pilot need to be separated.

That's fine, but here you're trying to treat them as a unit, so structure your code that way. The suggestions above use a Pair class or Map.Entry, but it's much better to provide a clearly-named object that represents a MobileSuit with a Pilot, e.g.:

public class OccupiedSuit {
  private final MobileSuit suit;
  private final Pilot pilot;

  public OccupiedSuit(MobileSuit suit, Pilot pilot) {
    this.suit = checkNotNull(suit);
    this.pilot = checkNotNull(pilot);
  }

  // getters, equals, hashCode, toString
  // or just use @AutoValue: https://github.com/google/auto/tree/master/value
}

Then, rather than constructing a custom Iterator/Iterable, just write a helper function that zips up the two lists. For example:

public static List<OccupiedSuit> assignPilots(
    Iterable<MobileSuit> suits, Iterable<Pilot> pilots) {
  Iterator<MobileSuit> suitsIter = suits.iterator();
  Iterator<Pilot> pilotsIter = pilots.iterator();
  ImmutableList.Builder<OccupiedSuit> builder = ImmutableList.builder();

  while (suitsIter.hasNext() && pilotsIter.hasNext()) {
    builder.add(new OccupiedSuit(suitsIter.next(), pilotsIter.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(!suitsIter.hasNext(),
      "Unexpected extra suits: %s", ImmutableList.copyOf(suitsIter));
  checkArgument(!pilotsIter.hasNext(),
      "Unexpected extra pilots: %s", ImmutableList.copyOf(pilotsIter));
  return builder.build();
}

Now you don't need to maintain a complex custom Iterator implementation - just rely on one that already exists!


We can also generalize assignPilots() into a generic utility that works for any two inputs, like so:

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

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

Which you'd then invoke like so:

List<OccupiedSuit> occupiedSuits = zipLists(OccupiedSuit::new, suits, pilots);

Example code uses Guava's Preconditions and ImmutableList - if you don't use Guava it's easy enough to inline and swap to ArrayList, but just use Guava :)



回答7:

for(int i=0; i < mobileSuits.size(); i++) {
  MobileSuit suit = mobileSuits.get(i);
  Pilot pilot = pilots.get(i);
  ...
}


回答8:

You could just use a Map<MobileSuit, Pilot>, where a null value mapped to a MobileSuit indicates no pilot. The Iterator could just be an Iterator<Map.Entry<MobileSuit, Pilot>> retrieved by map.entrySet().iterator().



回答9:

Came across this page trying to solve this issue, and turns out that there's a library out there that's already solved it using Java 8 streams (check out the Zip function).

You can convert a list to a stream just by calling list.stream()

https://github.com/poetix/protonpack

Stream<String> streamA = Stream.of("A", "B", "C");
Stream<String> streamB  = Stream.of("Apple", "Banana", "Carrot", "Doughnut");
List<String> zipped = StreamUtils.zip(streamA,
                                      streamB,
                                      (a, b) -> a + " is for " + b)
                                 .collect(Collectors.toList());

assertThat(zipped,
           contains("A is for Apple", "B is for Banana", "C is for Carrot"));


回答10:

Isn't that enough ?

for(MobileSuit ms : MobileSuits) {
    for(Pilot p : pilots){
        //TODO
    }
}