5

I apologize if this question is a duplicate, searching was difficult as I was unsure of the proper name for what I'm trying to accomplish. The simplest explanation would be

List<A>, List<B> into Map<Key, Tuple<A,B>> where A.Key matched B.Key

To clarify: I have a list of A object and B object that share a key. I'd like to then correlate these two lists into a map where the key matches into a map of key, and tuple A,B.

I've played around with many ideas on how to do this in my head, but most of them end with me feeling like I've misused the library (such as Maps.uniqueIndex, and Iterables.transform). Can anyone point me in the right direction?

gerges
  • 264
  • 4
  • 14
  • This is almost _identical_ to http://stackoverflow.com/questions/8354200/java-how-to-transform-from-listt-to-mapf1t-listf2t-without-iterating, FYI. – Louis Wasserman Jan 14 '12 at 23:26

3 Answers3

6

There are no tuple (pair etc.) implementations in Guava. (It's another discussion if it's good idea to implementation tuples in Java at all.) The natural mapping I would suggest is to use a Multimap:

List<A> as = Lists.newArrayList(new A(1, "a"), new A(3, "c"), new A(2, "b"));
List<B> bs = Lists.newArrayList(new B(1, 2), new B(3, 6), new B(5, 10));

Function<WithKey, Object> toKey = new Function<WithKey, Object>() {
    @Override public Object apply(WithKey input) { return input.key(); }
};
ImmutableListMultimap<Object, AbstractWithKey> index = 
    Multimaps.index(Iterables.concat(as, bs), toKey);

or

Multimap<Object, WithKey> m = ArrayListMultimap.create();
for (WithKey w : Iterables.concat(as, bs)) m.put(w.key(), w);

You have to check your invariants before using the multimap (or while your iterating over the multimap entries) for example there could be keys with only a A or B instance. (This shouldn't be a performance issue as it can be done lazily with Iterables.filter.)

Duplicates of one type is another issue. You could check them or use a HashMultimap to ignore them. You could even build a multimap with a constrainted set for values that checks that a value is unique (see Multimaps.newSetMultimap(Map> map, Supplier> factory) and Constraints.constrainedSet(Set set, Constraint constraint)). This has the advantage that it fails fast.

With these A and B implementations:

interface WithKey {
    Object key();
}
abstract class AbstractWithKey implements WithKey {
    Object key;
    Object v;
    @Override public Object key() { return key; }
    @Override public String toString() { 
        return MoreObjects.toStringHelper(this).add("k", key).add("v", v).toString(); 
    }
}
class A extends AbstractWithKey {
    public A(int i, String v) { 
        key = i;
        this.v = v;
    } 
}
class B extends AbstractWithKey {
    public B(int i, int v) { 
        key = i;
        this.v = v;
    }
}

the output is:

{1=[A{k=1, v=a}, B{k=1, v=2}], 2=[A{k=2, v=b}], 3=[A{k=3, v=c}, B{k=3, v=6}], 5=[B{k=5, v=10}]}

Update:

If you have to end up with your tuple instances, you can transform the Multimap.

Multimap<Object, WithKey> m = ArrayListMultimap.create(); 
for (WithKey w : Iterables.concat(as, bs)) m.put(w.key(), w);

Function<Collection<WithKey>, Tuple> f = 
    new Function<Collection<WithKey>, Tuple>(){
    @Override public Tuple apply(Collection<WithKey> input) {
        Iterator<WithKey> iterator = input.iterator();
        return new Tuple(iterator.next(), iterator.next());
    } };
Map<Object, Tuple> result = Maps.transformValues(m.asMap(), f);

Output ((a,b) is the tuple syntax):

{1=(A{k=1, v=a},B{k=1, v=2}), 3=(A{k=3, v=c},B{k=3, v=6})}
facundofarias
  • 2,973
  • 28
  • 27
Thomas Jung
  • 32,428
  • 9
  • 84
  • 114
  • I love the answer, but my hope was to end with an object that I could iterate over in JSTL. If I'm understanding this correctly, the Multimap is holding a list of values. If I used this solution and called asMap before passing to the view, would there be any easy way to distinguish what object is an A, and what is a B? – gerges Jan 12 '12 at 13:53
  • 1
    I have to think about it. If the values in the map are stored in a list you know from the insertion order that the first value is of type A and the second value if of type B. But that breaks too fast and is not explicit enough for my taste. You could also create a Multiset that is backed by a "tuple" (I wouldn't be a real tuple just an collection implementation that let's you retrieve the A and B instances). There should be a better way. – Thomas Jung Jan 12 '12 at 14:50
1

Are you guaranteed that keys are unique? (That is, that no two A's have the same key?)

If so, I'd write something like the following:

Map<Key, A> aMap = Maps.uniqueIndex(theAs, aKeyFunction); // Guava!
Map<Key, B> bMap = Maps.uniqueIndex(theBs, bKeyFunction);

Map<Key, AWithMatchingB> joinedMap = Maps.newHashMap();
for(Map.Entry<Key, A> aEntry : aMap.entrySet()) {
  joinedMap.put(aEntry.getKey(), AWithMatchingB.match(
     aEntry.getValue(), bMap.get(aEntry.getKey())));
}

If you're not guaranteed that aMap.keySet().equals(bMap.keySet()), then you'd modify this appropriately: check whether or not there's a matching B or not, etc.

Louis Wasserman
  • 191,574
  • 25
  • 345
  • 413
  • For reference, I see that you feel like this solution might "misuse the library" -- speaking as a Guava developer, this is exactly what I'd do. – Louis Wasserman Jan 14 '12 at 20:32
0

Sorting the lists by key and transforming the two lists to tuples without much help from Guava is quite readable:

Comparator<WithKey>c = new Comparator<WithKey>(){
    @Override public int compare(WithKey o1, WithKey o2) {
        return o1.key().compareTo(o2.key());
    }
};
Collections.sort(as, c);
Collections.sort(bs, c);

Preconditions.checkArgument(as.size() == bs.size());

Iterator<A> aIt = as.iterator();
Iterator<B> bIt = bs.iterator();
Map<Integer, Tuple> ts = Maps.newHashMap();
while(aIt.hasNext()) {
    A a = aIt.next();
    B b = bIt.next();
    Preconditions.checkArgument(a.key().equals(b.key()));
    ts.put(a.key(), new Tuple(a, b));
}

Output ((a,b) is the tuple syntax):

{1=(A{k=1, v=a},B{k=1, v=2}), 3=(A{k=3, v=c},B{k=3, v=6})}

This can be implemented nicer when Guava supports zip similar to Python:

sa = [(1, "a"), (3, "c")]
sb = [(1, 2), (3, 6)]

sa.sort()
sb.sort()

vs = [(a[0], (a,b)) for (a, b) in zip(sa, sb)]
Thomas Jung
  • 32,428
  • 9
  • 84
  • 114