Saturday, 19 April 2008

Java 7 - For-each loops for Maps

Have you ever been fustrated by the new Java 5 for each loop because it didn't operate directly on maps?

For-each loop for Maps

I have documented a proposal to change Java to allow for each loops on maps. I have also used the Kijaro project to implement the enhanced for-each loops!

  Map<String, Integer> map = new HashMap<String, Integer>();
 
  for (String str, Integer val : map) {
    System.out.println("Entry" + str + "=" + val);
  }

The altered version of javac can be downloaded, with the normal caveats of 'no warranty' and 'not intended for production'.

Closures

The real question here is whether we should use closures to obtain this functionality, or just code a specific language feature. Since it is far from certain that closures will appear in Java 7 due to timescale and resourcing questions, maybe we should be considering the alternatives?

Extending for-each loops to cover maps is a simple extension to the Java language that introduces no radically new concepts. Existing developers should be able to pick up the feature without any difficulty. In addition, developers that have never been exposed to the feature (but have seen a Java 5 for-each loop) should be able to read the code and grasp the meaning without tuition.

The truth is that sometimes the simple solution is the right one. Perhaps, closures are overkill for many of the uses in Java? Hopefully this document and prototype will allow people to kick the tyres on implementing this concept as a language feature allowing a fair comparison with closures.

Summary

I've released a document and prototype of For-each loop for Maps, a language change to build on the Java 5 for-each loop. All feedback welcomed!

23 comments:

  1. Should the for loop be using entrySet instead of keySet? I always thought it was more performant to retrieve the entries from a map instead of keys and then looking up the values from that.

    ReplyDelete
  2. The dichotomy is really between having few low-level orthogonal features or many high-level features. The former is like classic Unix command-line tools: a few, powerful, simple tools you can combine in arbitrary ways. The latter is like a modern GUI: many buttons that do what you want in a single click. The for-each idea is a high-level button that does exactly what you want, using the syntax you'd want. You're approaching language design like a UI designer: figure out the common use cases and make those directly possible without forcing the user to navigate a large search space of primitive operations. For Java 7 this comes down to whether Java programmers are more like average Windows users or wizard Unix hackers.

    ReplyDelete
  3. I personally like it -- after coding a version as it stands right now i saw the problem: it's wayyy too verbose and if you wanna use foreach you can only use it for either keys or values -- not both. If you want both you have to use an iterator! Your solution is nice -- though groovy's solution is nicer:

    def map = ['1024MB':'1GB']
    map.each {k,v -> // do something }

    Nonetheless i like your proposal :)

    ReplyDelete
  4. I take that back you _CAN_ get the value from a keySet() -- just not as simplistic as using a foreach... but you can't get it from the loop itself is what i meant.

    ReplyDelete
  5. Your proposal breaks compatibility with existing Map implementations.

    ReplyDelete
  6. Why can't you iterate through the pairs of key and value of a map now? This is barely even syntactic sugaring on what we have. Perhaps that's why it was so easy to modify javac to do it.

    To use the example above, the non-novel way would be:

    Map map = new HashMap();

    for(Map.Entry entry : map.entrySet())
    {
    entry.getKey(); entry.getValue(); // do whatever
    }

    I'm confused about why people say above you can only do keys or values but not both

    ReplyDelete
  7. Neal: Yeh after looking around -- i saw that...I feel like an idiot. :/

    ReplyDelete
  8. Stephen Colebourne19 April 2008 11:03

    Matt, keySet() vs entrySet() is never documented as to which is faster and varies by map implementation. The document proposes a MapIterator to solve the problem.

    Lawrence, Exactly the point - and maybe Java really is a Windows language.

    Neal, "All map implementations would implement MapIterable, although the Map interface itself cannot be changed due to compatibility". I don't see any compatibility problem as the Map interface itself doesn't change, just the implementations. The syntax would work off Map or MapIterable (I've clarified the document to make this clear).

    Martin, Yes it is simple syntax sugar - but that is the point. The Java 5 foreach loop itself is just minor syntax sugar too. And I certainly appreciate the reduction in typing and clarity gained.

    ReplyDelete
  9. Hm, why making it more complicate than necessary? I'd define some interface like follows:
    public interface EntryIterable {
    ´ ´ Set> entrySet();
    }
    And match the expression:
    Map map = ...;
    for (Integer i, String s : map) {
    ´ ´ ...
    }
    to:
    for (Map.Entry entry : map.entrySet()) {
    ´ ´ Integer i = entry.getKey();
    ´ ´ String s = entry.getValue();
    ´ ´ ...
    }
    Even Map could extend EntryIterable this way.
    Or am I missing something?

    ReplyDelete
  10. Before having foreach for maps (where I don't mind iterating over Map.Entries too much), I'd love to see it work for iterators. For example:
    Iterator iter = myList.iterator();
    for(String elem : iter) {
    if (elem.equals("java")) iter.remove();
    }
    As opposed to:
    for(Iterator iter = myList.iterator(); iter.hasNext();} {
    if (iter.next().equals("java")) iter.remove();
    }

    This becomes really useful if
    - there is more than one access of the current element (then you have to store the current element in a local variable, as Iterator does not have a peek() method).
    - you have to close iterators (e.g. in database applications)

    ReplyDelete
  11. To all of the people naysaying, especially the uncalled for "windows user" slap, maybe you owe an apology to the author. When 5.0 introduced foreach syntax, the first steps were taken towards the "windows user" rather than "unix wizard". Anyone remember all the old UNIX scripting languages that have slowly died? Would you like to see Java suffer the same fate?
    Personally, I still prefer to manually iterate over collections, but that's just because I'm old and set in my ways (I'm specifying what I'm doing explicitly instead of hiding behind the foreach facade). I'm slowly switching to foreach just because I'm annoyed by the warning messages.
    How does implementing Iterable break existing map implementations? Noone has to remove the existing map functionality.

    ReplyDelete
  12. Well, I'm all for saving us more typing, but I honestly would prefer bigger, more radical changes in Java. I'd like to see the type system reified (is that how you spell that?), and I'd like to see some of the older APIs cleaned up, an attempt at closures, and I'd like to see some sort of language support for properties w/ beans.

    I realize Sun's position about their handcuffs of backward compatibility, but I think Java has much bigger problems still to solve than adding some foreach bling. I love Java but I worry I'll end up switching to some other language on the JVM that improves upon Java just because Sun can't break old libraries.

    ReplyDelete
  13. I prefer non-syntactic ways of doing this, because syntax enforces the current world-view after it has passed.

    For example, with today's collection implementations on today's hardware it may be faster to use keySet() and then map.get(key) on each iteration, in the implementation of the foreach loop, but later or on some collections it may be faster to use entrySet(). That's only one consideration, there might be 500 that we haven't yet thought of.

    Make it a library function, then I can choose whether to use it or use my own without paying a syntax penalty.

    hashMap.foreach(String key, String value) {
    . . stuff
    }

    I also have to wonder why we're still worrying about loops instead of folds.

    ReplyDelete
  14. Stephen Colebourne19 April 2008 19:34

    Stefan, Yes your code will work, but it assumes that entrySet() is the best way to iterate over a map. That isn't necessarily true - sometimes its a keySet(). Adding MapIterable allows the map implementation to decide its best iteration style.

    Alex, I don't disagree that is a useful enhancement too. If you'd like to code it in Kijaro, feel free :-)

    Martin, The problem as I understand it is partly about resources within Sun. A simple change could potentially be achieved where a complex one like closures might not. (Reification and properties are definitely bigger changes)

    Ricky, I've address keySet() vs entrySet(). This is really a proposal that asks what Java is - and I'm not sure it is a fold language.

    ReplyDelete
  15. Stephen, I doubt it's easier to implement to a new interface than optimizing the entrySet's iterator being returned. As both would require a Map implementation adopting to its new purpose, it should be the same.

    Ricky, I agree that Map-for-each does not gain much. I'd rather replace current language-for-each with an API-for-each, but that's JCA.

    ReplyDelete
  16. I think that this is a good suggestion. Some thoughts:

    1. The debate around whether a new interface should be defined or whether entrySet can be used applies equally to internal iterators, map.each, and closures; but in some of the comments above you might read into the comment that if closures were used, for example, then this problem goes away, this isn't true.

    3. I would favour making an interface EntryIterable that contains keySet and values methods and make the description of these methods say that it is OK to modify the map whilst using iterators on the returned objects from these methods. This way Map could extend this definition of EntryIterable, but the existing map implementations would need to be rewritten to make the iteration safe. There is a chance that an old Map implementation (not updated to the new definition) might be used with the new for-each construct and the map modified, this would result in a runtime error (so I think that is OK).

    3. It would be nice to have map literal syntax, that made a MapEntry, and to allow [] to be used on maps in three forms: map[key]=value, value=map[key], and map[entrySet]. Similarly allow [] on RandomAccess plus List classes including an entry set with an Integer key (also arrays for Integer-key EntrySets).

    4. It would be nice if array and RandomAccess plus List classes could also be used with this new syntax with an int index for the key.

    ReplyDelete
  17. Even if closures with a for-each Map api make it into java 7 I would still like this feature. One addition would be to have a loop counter variable like in jstl. It is now often the case that you have to switch from short for-each syntax to the old counter syntax just because you need something special on the first or last element of the collection.

    ReplyDelete
  18. Stephen, you wrote :
    "Map interface itself cannot be changed" so it's useless :)

    void f(Map map) {
    // how to iterate
    }

    Rémi

    ReplyDelete
  19. Dobby, I'm with you. I have previously proposed for.index, for.last and for.first. These simple additions could make a lot of the write-and-rewrite scenarios go away. They are darn annoying.

    Cheers,
    Mikael Grev

    ReplyDelete
  20. Mikael Grev, yes the syntax for for.index, for.last is nice. Could this easily be implemented in the kijaro version of the for-each?

    ReplyDelete
  21. Stephen Colebourne22 April 2008 17:51

    Howard, Access via map[key] is being implemented on Kijaro as we speak (not by me...).

    Dobby/Mikael, If I had a little more time, I might implement the ability to access iteration level data.

    Remi, I agree that this is a problem. Three options: (1) define and use a MapEntrySet interface, (2) have a bytecode instanceof check for MapIterable, or (3) hard code a cast to MapIterable and thus block non-compliant old implementation (anyone that didn't extend AbstractMap). Its a tough call, but maybe (3) isn't that bad.

    ReplyDelete
  22. This looks amazingly clean and cool. I hope Java take this suggestion. I am just thinking they could have done this while introducing new for loop in java 5.

    Javin
    How to traverse hashmap in Java 1.5

    ReplyDelete