views:

478

answers:

7

It would be nice to use for (String item: list), but it will only iterate through one list, and you'd need an explicit iterator for the other list. Or, you could use an explicit iterator for both.

Here's an example of the problem, and a solution using an indexed for loop instead:

import java.util.*;
public class ListsToMap {
  static public void main(String[] args) {
    List<String> names = Arrays.asList("apple,orange,pear".split(","));
    List<String> things = Arrays.asList("123,456,789".split(","));
    Map<String,String> map = new LinkedHashMap<String,String>();  // ordered

    for (int i=0; i<names.size(); i++) {
      map.put(names.get(i), things.get(i));    // is there a clearer way?
    }

    System.out.println(map);
  }
}

Output:

{apple=123, orange=456, pear=789}

Is there a clearer way? Maybe in the collections API somewhere?

+7  A: 

Since the key-value relationship is implicit via the list index, I think the for-loop solution that uses the list index explicitly is actually quite clear - and short as well.

Michael Borgwardt
+2  A: 

ArrayUtils#toMap() doesn't combine two lists into a map, but does do so for a 2 dimensional array (so not quite what your looking for, but maybe of interest for future reference...)

Joel
Thanks - btw your url missed the "#" stuff: http://commons.apache.org/lang/api/org/apache/commons/lang/ArrayUtils.html#toMap(java.lang.Object[])
13ren
+5  A: 

I'd often use the following idiom. I admit it is debatable whether it is clearer.

Iterator<String> i1 = names.iterator();
Iterator<String> i2 = things.iterator();
while (i1.hasNext() && i2.hasNext()) {
    map.put(i1.next(), i2.next());
}
if (i1.hasNext() || i2.hasNext()) complainAboutSizes();

It has the advantage that it also works for Collections and similar things without random access or without a efficient random access, like LinkedList, TreeSets or SQL ResultSets. For example, if you'd use the original algorithm on LinkedLists, you've got a Shlemiel the painter algorithm.

hstoerr
This at least works on lists of inequal size without any prechecks.
BalusC
@BalusC Which may not actually be what you want. Since index -> index is supposed to match this would break silently on mismatched lists. @hstoerr In the OP's example the iterators would be using index-based random access internally so you haven't really gained anything there. When the index is the semantic relationship between two lists, I'm not sure you can get around using it.
PSpeed
@PSpeed OP here, my example code would also silently continue if the second list was longer... I'm not worried about validity-checking, but one way to do it is `while(true)`, and only break when *both* have none left (if only one has none left, then it will continue, and throw a NoSuchElementException on the next() for that list).
13ren
it's also not hard to add a check after the above code, if needed...`if (i1.hasNext() || i2.hasNext()) throw SomeException(...`
Carlos Heuberger
You could also do that by setting count = Math.max( length1, length2 ). I'm still convinced your current way is best as it says exactly what you are doing: syncing up two lists by index.
PSpeed
My last comment was to original poster and not this response specifically.
PSpeed
Thanks for the comments! I've clarified a little and added the size check.
hstoerr
+1  A: 

Use Clojure. one line is all it takes ;)

 (zipmap list1 list2)
bugspy.net
Well, I guess that *is* java, in a way; it amounts to having a function for the task (see CPerkins' answer). The equivalent java call, of `zipmap(list1, list2)` , is not all that different, if you have the function already.
13ren
Right. But Clojure is such a cool language that I couldn't resist it
bugspy.net
+1  A: 

Your solution above is correct of course, but your as question was about clarity, I'll address that.

The clearest way to combine two lists would be to put the combination into a method with a nice clear name. I've just taken your solution and extracted it to a method here:


  Map<String,String> combineListsIntoOrderedMap (List<String> keys, List<String> values) {
    if (keys.size() != values.size())
      throw new IllegalArgumentException ("Cannot combine lists with dissimilar sizes");
    Map<String,String> map = new LinkedHashMap<String,String>();
    for (int i=0; i<keys.size(); i++) {
      map.put(keys.get(i), values.get(i));
    }
    return map;
  }

And of course, your refactored main would now look like this:


  static public void main(String[] args) {
    List<String> names = Arrays.asList("apple,orange,pear".split(","));
    List<String> things = Arrays.asList("123,456,789".split(","));
    Map<String,String> map = combineListsIntoOrderedMap (names, things);
    System.out.println(map);
  }

I couldn't resist the length check.

CPerkins
... and if we put it in a class, we can genericize it over types other than String,String. Perhaps, making it a subclass of LinkedHashMap, and your method as a constructor (since that is what it is always doing). It would be nice to be able to genericize the superclass (so that the particular Map class becomes a parameter), but I'm pretty sure java doesn't allow that. It could be done in a separate class (not extending LinkedHashMap), taking three class parameters. [off the top of my head]
13ren
@13ren: yes, very nice.
CPerkins
+1  A: 

You need not even limit yourself to Strings. Modifying the code from CPerkins a little :

Map<K, V> <K, V> combineListsIntoOrderedMap (List<K> keys, List<V> values) {
      if (keys.size() != values.size())
          throw new IllegalArgumentException ("Cannot combine lists with dissimilar sizes");
Map<K, V> map = new LinkedHashMap<K, V>();
for (int i=0; i<keys.size(); i++) {
  map.put(keys.get(i), values.get(i));
}
return map;

}

fastcodejava
See my comment to CPerkins - I was thinking of also genericizing which Map class it uses? Maybe `Map<M extends Map<K, V>, K, V> map = new M<K, V>();` But it seems *type erasure* makes that impossible, because the type isn't known at runtime: http://en.wikipedia.org/wiki/Generics_in_Java#Type_erasure
13ren
Whoops, I meant the code to be `<M extends Map<K, V>, K, V>` ... `Map<K, V> map = new M<K, V>();`
13ren
A: 

As well as clarity, I think there are other things that are worth considering:

  • Correct rejection of illegal arguments, such as different sizes lists and nulls (look what happens if things is null in the question code).
  • Ability to handle lists that do not have fast random access.
  • Ability to handle concurrent and synchronised collections.

So, for library code, perhaps something like this:

@SuppressWarnings("unchecked")
public static <K,V> Map<K,V> void linkedZip(
    List<? extends K> keys, List<? extends V> values
) {
    Object[] keyArray = keys.toArray();
    Object[] valueArray = values.toArray();
    int len = keyArray.length;
    if (len != valueArray.length) {
        throwLengthMismatch(keyArray, valueArray);
    }
    Map<K,V> map = new java.util.LinkedHashMap<K,V>((int)(len/0.75f)+1);
    for (int i=0; i<len; ++i) {
        map.put((K)keyArray[i], (V)valueArray[i]);
    }
    return map;
}

(May want to check not putting multiple equal keys.)

Tom Hawtin - tackline