4

I'm having some strange behaviour.

[UPDATE: Full runnable example given:]

package finaltestwithenummapentry;

import java.util.ArrayList;
import java.util.EnumMap;
import java.util.Map.Entry;

public class FinalTestWithEnumMapEntry {

    enum SomeEnum{
        ONE, TWO, THREE;
    }

    public static void main(String[] args) {
        EnumMap<SomeEnum, Integer> map = new EnumMap<SomeEnum, Integer>(SomeEnum.class);
        map.put(SomeEnum.ONE, 1);
        map.put(SomeEnum.TWO, 2);
        map.put(SomeEnum.THREE, 3);

        ArrayList<Entry<SomeEnum, Integer>> entryList = new ArrayList<Entry<SomeEnum, Integer>>();  

        for(final Entry<SomeEnum, Integer> entry : map.entrySet()){      
            System.out.println("Key is " + entry.getKey() + ", value is " + entry.getValue());     
            //This prints the correct keys and values      

            entryList.add(entry); 
        }  

        System.out.println("");

        for(Entry<SomeEnum, Integer> entry:entryList){     
            System.out.println("Key is " + entry.getKey() + ", value is " + entry.getValue());     
            //This prints only the last entry each time 
        }
    }
}

Output (JavaSE 1.6):

Key is ONE, value is 1
Key is TWO, value is 2
Key is THREE, value is 3

Key is THREE, value is 3
Key is THREE, value is 3
Key is THREE, value is 3

My entry, which I'm presuming is final, is seemingly overwritten by the next one each time. I need to be able to capture the correct entry each time, as I am passing each item to an anonymous inner class instance inside the for loop.

[UPDATE: This problem doesn't exist in Java 7, only Java 6 (and maybe before) ]

UPDATE: I might have to make my code work whether it's compiled against Java 6 or 7, so what's the most efficient workaround for making it work in either case?

Navigateur
  • 1,852
  • 3
  • 23
  • 39
  • I am not sure why but the (only the last entry) issue remains even if you remove that `final` keyword. – Bhesh Gurung Dec 25 '11 at 16:26
  • 1
    On my machine it prints the three lines for the first loop and exactly the same number of entries for the second loop. So I do not see the problem here. – Jagger Dec 25 '11 at 16:47
  • The number of entries is correct, but I'm getting the only the last entry each time. Is your version printing the entries separately and correctly? If so, what are you doing differently than the above code? – Navigateur Dec 25 '11 at 16:51
  • Could you please also provide the console output you are getting? – Jagger Dec 25 '11 at 16:54
  • The thing is I am not doing anything differently. I just copied and pasted your code. – Jagger Dec 25 '11 at 17:00
  • Really? And are you getting 1,2,3 in the second loop as expected? What version of Java are you using? – Navigateur Dec 25 '11 at 17:05
  • @Navigateur: I wasn't expecting it but I am seeing the same problem. May be a bug with the version we are using. – Bhesh Gurung Dec 25 '11 at 17:10
  • @MДΓΓБДLL: That's exactly what I am seeing. It happens even if you remove that final keyword. – Bhesh Gurung Dec 25 '11 at 17:15
  • @Navigateur: I am using Java 7. Here is exactly what I get after `java -version`. `java version "1.7.0_02" Java(TM) SE Runtime Environment (build 1.7.0_02-b13) Java HotSpot(TM) Client VM (build 22.0-b10, mixed mode, sharing)` – Jagger Dec 25 '11 at 17:17
  • 1
    Not surprisingly, SO has already answered this :) http://stackoverflow.com/questions/6198188/iterating-over-enummapentryset – AHungerArtist Dec 25 '11 at 17:19
  • http://code.google.com/p/guava-libraries/issues/detail?id=448 – Bhesh Gurung Dec 25 '11 at 17:20
  • Yes! The problem exists in Java 6 but not in Java 7! What's your recommended workaround for this? Because I might have to make my code work whether it's compiled against Java 6 or 7. – Navigateur Dec 25 '11 at 17:20
  • How about using a hash map or linked hash map? – AHungerArtist Dec 25 '11 at 17:24
  • That's a really ugly bug in Java 6. I changed slightly my example and ran it on `ideone.com` under Java 6 and I get the same wrong behaviour! – Jagger Dec 25 '11 at 17:28
  • @Jagger I don't think it's a bug (it complies with the spec). It's just unexpected behavior. – Matt Ball Dec 25 '11 at 17:34
  • @AHungerArtist Unfortunately it must be an EnumMap. I'm trying to refactor existing code, so don't really want to change it unless absolutely necessary. – Navigateur Dec 25 '11 at 17:35
  • Does it have to be a list that you're saving the entries into? – AHungerArtist Dec 25 '11 at 17:40
  • No I'm not saving into a list. That was just for example. I'm actually passing them into a new anonymous inner class instance (within the for loop each time) to be executed with later. But the principle is exactly the same. – Navigateur Dec 25 '11 at 17:43
  • 1
    See http://stackoverflow.com/questions/3110547/java-how-to-create-new-entry-key-value for a possible solution. You can use either of the first two answers and that should be compatible. Just make sure to create the new entry using the key/value pair and not by using the map entry object directly (ie, passing it in or cloning or something else). – AHungerArtist Dec 25 '11 at 17:44
  • @MДΓΓБДLL I think it is (was) a bug as in Java 7 everything works fine. – Jagger Dec 25 '11 at 18:06

6 Answers6

3

I think I've figured out what's going on here, and it has nothing to do with the final keyword. If you remove the final modifier from this line:

for(final Entry<SomeEnum, Integer> entry : map.entrySet()){      

the exact same behavior occurs.

But what's this? From the Map.Entry JavaDocs (emphasis added):

A map entry (key-value pair). The Map.entrySet method returns a collection-view of the map, whose elements are of this class. The only way to obtain a reference to a map entry is from the iterator of this collection-view. These Map.Entry objects are valid only for the duration of the iteration.

...which I read as "don't try to use Map.Entry objects outside of an iteration on the collection-view returned by Map.entrySet" — this invokes undefined behavior.


This answer explains it in more detail. The problem the OP sees is related specifically to EnumMap's implementation of Map.Entry.

Community
  • 1
  • 1
Matt Ball
  • 354,903
  • 100
  • 647
  • 710
  • 1
    Yep that was mentioned in a google talk by Bloch and Gafter. An optimization of a bygone age when creating short lived objects was expensive so they cached the entry object. Actually there's at least one other collection which has the same behavior, so user beware. Maybe I can find the recording - there were some pretty nifty puzzlers there. – Voo Dec 25 '11 at 17:57
  • 1
    Found it, but oh dear me better replace "gafter" with "manson" above. Still a really fun talk: [see here](http://www.youtube.com/watch?feature=player_embedded&v=wbp-3BJWsU8) – Voo Dec 25 '11 at 18:04
  • @Voo, by the way the behaviour no longer exists in Java 7. – Navigateur Dec 25 '11 at 23:54
  • @Navigateur Yeah read your edit and certainly a really good change since this behavior really violates good API design (principle of least astonishment). Really overdue that. – Voo Dec 26 '11 at 00:47
2

This is a behaviour in Java 6 and earlier for EnumMap and IdentityHashMap. It was done for the sake of performance (source - Josh Bloch, 13:56, "Java Puzzlers", May 2011 (original video link given by Voo in comment)). It no longer happens as of Java 7, where you can now rely on collected Map.Entry objects, regardless of subsequent iterations.

The best workaround, if you intend to use an entry after the next iteration occurs, is to clone it via

new AbstractMap.SimpleImmutableEntry(entry)

and use that instead.

Navigateur
  • 1,852
  • 3
  • 23
  • 39
1

OK, I went spelunking into the source code - EnumMap does a very weird thing when it comes to sending out the keySet, valueSet and entrySet. From the JDK Source code:

/**
 * Since we don't use Entry objects, we use the Iterator itself as entry.
 */
private class EntryIterator extends EnumMapIterator<Map.Entry<K,V>>
        implements Map.Entry<K,V>
{
    public Map.Entry<K,V> next() {
        if (!hasNext())
            throw new NoSuchElementException();
        lastReturnedIndex = index++;
        return this;
    }

They have a bunch of custom classes in EnumMap that return the value during the iteration, thus the reason they don't want you to use it outside that iterator.

That's why it behaves differently than other Map.Entry. The solution for you is to keep/create a separate map/list and fill it with the keys/values, instead of keeping the Entry objects.

(old answer below)

'final' in this case defines that behavior for a single iteration. If you're passing it to an inner class, define final at the class entry point:

for(final Entry<K, V> entry : map.entrySet()){ 
  System.out.println("Key is " + entry.getKey() + ", value is " + entry.getValue());
  //This prints the correct keys and values
  doSomething(entry);
}

private void doSomething(final Entry<K,V> entry){}

Like that.

Kylar
  • 8,876
  • 8
  • 41
  • 75
  • 1
    correct, `entry` is a variable within the loop's scope and it is 'final' for its scope. try reassigning it a value inside the loop :-) if you need to send the whole entry set to the inner class, that's what you should do :-) – aishwarya Dec 25 '11 at 15:54
  • I don't think this is his problem. He's saying the list has only the last value from the map. – AHungerArtist Dec 25 '11 at 15:55
  • 1
    then its not this code that causes problem, and certainly has nothing to do with 'final' keyword. – aishwarya Dec 25 '11 at 16:03
  • @aishwarya you're right. It does not have anything to do with the `final` keyword. – Matt Ball Dec 25 '11 at 17:26
1

What do you mean by this?

//This prints only the last entry each time

Using this piece of code (Java 7):

import java.util.ArrayList;
import java.util.EnumMap;
import java.util.List;
import java.util.Map;

public class ForeachLoopWithFinal {

enum MyEnum {
    ONE("one"),
    TWO("two"),
    THREE("three");

    private String name;

    private MyEnum(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return name;
    }

}

public static void main(String[] args) {
    List<Map.Entry<MyEnum, Integer>> entryList = new ArrayList<>();
    Map<MyEnum, Integer> map = new EnumMap<>(MyEnum.class);

    map.put(MyEnum.ONE, 1);
    map.put(MyEnum.TWO, 2);
    map.put(MyEnum.THREE, 3);

    for (final Map.Entry<MyEnum, Integer> entry : map.entrySet()) {
        System.out.printf("Key is %s, value is %s%n", entry.getKey(), entry.getValue());
        entryList.add(entry);
    }

    for (Map.Entry<MyEnum, Integer> entry : entryList) {
        System.out.printf("Key is %s, value is %s%n", entry.getKey(), entry.getValue());
    }
}

}

I get exactly this output. So the exact same number of entries is printed the first and the second time.

Key is one, value is 1
Key is two, value is 2
Key is three, value is 3
Key is one, value is 1
Key is two, value is 2
Key is three, value is 3

Using your example I get the same.

Key is ONE, value is 1
Key is TWO, value is 2
Key is THREE, value is 3

Key is ONE, value is 1
Key is TWO, value is 2
Key is THREE, value is 3
Jagger
  • 10,350
  • 9
  • 51
  • 93
0

I believe it's because your 'entry' is getting created each time a loop happens so it's actually a new variable each time. If you tried to set entry to something else inside the loop, I believe you'd get a compiler error.

Edit, based on feedback: There must be something you're not showing us.

Map<String, String> map = new HashMap<String, String>();
        map.put("barf", "turd");
        map.put("car", "bar");
        ArrayList<Entry<String, String>> entryList = new ArrayList<Entry<String, String>>();

        //Assuming I have a Map<K,V> called "map":
        for(final Entry<String, String> entry : map.entrySet()){ 
            System.out.println("Key is " + entry.getKey() + ", value is " + entry.getValue());
            //This prints the correct keys and values

            entryList.add(entry);
        }

        for(Entry<String,String> entry:entryList){
            System.out.println("Key is " + entry.getKey() + ", value is " + entry.getValue());
            //This prints only the last entry each time
        }

prints out:

    Key is car, value is bar
    Key is barf, value is turd
    Key is car, value is bar
Key is barf, value is turd

See this post: Iterating over EnumMap#entrySet It will explain things.

Community
  • 1
  • 1
AHungerArtist
  • 9,332
  • 17
  • 73
  • 109
  • Yes I _want_ it to be a new variable each time... the only thing is it doesn't "capture" that unique variable as unique when I try to add it to a list or an anonymous inner class instance, which I want it to. Any ideas? – Navigateur Dec 25 '11 at 15:47
  • Ah, I see. I misread, my apologies. Are you sure the map has the entries you expect? This is actually strange behavior according to my understanding of how it should be working. – AHungerArtist Dec 25 '11 at 15:49
  • Yes, the map has all the correct entries, and is printed out correctly in the first loop. – Navigateur Dec 25 '11 at 15:50
  • Can you try it with an `EnumMap`? (Question updated). – Navigateur Dec 25 '11 at 16:13
  • 1
    It works the same. Why don't you show us the actual code that exhibits your problem instead of having us guess what your code is doing? – AHungerArtist Dec 25 '11 at 16:20
  • How did you get your version to work? I've edited the question to show a fully runnable example. – Navigateur Dec 25 '11 at 16:45
  • I realize now before I had just used a regular Map. You're right, I get the same problem you're seeing and I truly have no idea why. – AHungerArtist Dec 25 '11 at 17:06
0

According to the Java Language Specification, the enhanced for loop when using an iterator that looks like this:

for ( VariableModifiersopt Type Identifier: Expression) Statement

is exactly equivalent to:

for (I #i = Expression.iterator(); #i.hasNext(); ) {
        VariableModifiersopt Type Identifier = #i.next();
   Statement
}

(where I is the type of Expression.iterator()). In your case, VariableModifiersopt is final. As is clear from the equivalent form, the final applies only to the use of the variable within Statement.

Nandkumar Tekale
  • 16,024
  • 8
  • 58
  • 85
Ted Hopp
  • 232,168
  • 48
  • 399
  • 521