2011-06-01 11 views
9

Enumerar sobre Map#entrySet no funciona como se espera para todas las implementaciones Mapa, especialmente para EnumMap, IdentityHashMap y aquí está el código de ejemplo de Josh Bloch puzzler presentation (Puzzle 5) -interactuando sobre EnumMap # entrySet

public class Size { 

    private enum Sex { MALE, FEMALE } 

    public static void main(String[] args) { 
     printSize(new HashMap<Sex, Sex>()); 
     printSize(new EnumMap<Sex, Sex>(Sex.class)); 
    } 

    private static void printSize(Map<Sex, Sex> map) { 
     map.put(Sex.MALE, Sex.FEMALE); 
     map.put(Sex.FEMALE, Sex.MALE); 
     map.put(Sex.MALE, Sex.MALE); 
     map.put(Sex.FEMALE, Sex.FEMALE); 
     Set<Map.Entry<Sex, Sex>> set = 
      new HashSet<Map.Entry<Sex, Sex>>(map.entrySet()); 
     System.out.println(set.size()); 
    } 
} 

y sí que produce el resultado erróneo -

supone que es

2 
2 

pero produce

2 
1 

pero si lo intento con el código de abajo - se produce el resultado correcto

ACTUALIZACIÓN
Aunque el tamaño del conjunto resultante es 2, pero las entradas son iguales.

public class Test{ 

private enum Sex { MALE, FEMALE } 

    public static void main(String... args){ 
     printSize(new HashMap<Sex, String>()); 
     printSize(new EnumMap<Sex, String>(Sex.class)); 
    } 


    private static void printSize(Map<Sex, String> map) { 
     map.put(Sex.MALE, "1"); 
     map.put(Sex.FEMALE, "2"); 
     map.put(Sex.MALE, "3"); 
     map.put(Sex.FEMALE, "4"); 
     Set<Map.Entry<Sex, String>> set = 
      new HashSet<Map.Entry<Sex, String>>(map.entrySet()); 
     System.out.println(set.size()); 
    } 
} 

Incluso probé el código anterior con los dos tipos de enum diferentes como clave y valor.

Esto parece ser un problema solo si EnumMap tiene la misma enumeración que una clave y un valor.

Me gustaría saber por qué es esto? o me falta algo. ¿Por qué no se solucionó cuando ConcurrentHashMap se arregló hace mucho tiempo?

+0

que estoy recibiendo en IntelliJ: 2, 2 –

Respuesta

9

Eche un vistazo a la implementación EnumMap.EntryIterator.next(). Esto debería ser suficiente para resolver el problema.

Una pista es que el conjunto resultante es:

[FEMALE=2, FEMALE=2] 

que no es el resultado correcto.

El efecto que ve se debe a la implementación EnumMap.EntryIterator.hashCode() (que es el Map.Entry aquí). Es

h = key^value 

Esto da como resultado el mismo valor hash para las entradas producidas por

map.put(Sex.MALE, Sex.MALE); 
map.put(Sex.FEMALE, Sex.FEMALE); 

un establo 0.

o

map.put(Sex.MALE, Sex.FEMALE); 
map.put(Sex.FEMALE, Sex.MALE); 

aquí se trata de una inestable (por múltiple ejecuciones) int value. Siempre verá el efecto si key y value hashs tienen el mismo valor porque: a^b == b^a. Esto da como resultado el mismo valor hash para la Entrada.

Si las entradas tienen el mismo valor hash, terminan en el mismo cubo de la tabla hash y los iguales siempre funcionarán, ya que son el mismo objeto de todos modos.

Con este conocimiento ahora también podemos producir el mismo efecto con otros tipos como entero (donde sabemos que la aplicación hashCode):

map.put(Sex.MALE, Integer.valueOf(Sex.MALE.hashCode())); 
map.put(Sex.FEMALE, Integer.valueOf(Sex.MALE.hashCode())); 

[FEMALE=1671711, FEMALE=1671711] 

bonificación: La aplicación EnumMap rompe los iguales() Contrato:

EnumMap<Sex, Object> enumMap = new EnumMap<Sex, Object>(Sex.class); 
enumMap.put(Sex.MALE, "1"); 
enumMap.entrySet().iterator().next().equals(enumMap.entrySet().iterator()); 

Lanza:

Exception in thread "main" java.lang.IllegalStateException: Entry was removed 
    at java.util.EnumMap$EntryIterator.checkLastReturnedIndexForEntryUse(EnumMap.java:601) 
    at java.util.EnumMap$EntryIterator.getValue(EnumMap.java:557) 
    at java.util.EnumMap$EntryIterator.equals(EnumMap.java:576) 
    at com.Test.main(Test.java:13) 
+1

¡Eso es realmente feo! Me pregunto si se ajusta a las especificaciones. (Por cierto: [aquí está la fuente (comenzando en la línea 561)] (http://hg.openjdk.java.net/jdk6/jdk6-gate/jdk/file/tip/src/share/classes/java/ util/EnumMap.java)). –

+0

Estoy de acuerdo .. me gustaría saber por qué no se solucionó cuando elHashMap concurrente se arregló hace mucho tiempo atrás? – Premraj

+0

Creo que después de la próxima debe mirar para igualar la implementación. –

0

El problema no está en el mapa sino en la implementación de EntryIterator y la especificación HashSet que acepta solo elementos que no son iguales.

En el caso 1 y 2 mapas deben tener dos elementos, se puede verificar que llamar

map.entrySet().size(); 

El 'problema' es en la implementación de EntryIterator por clase EnumMap, ya que este es un rompecabezas tratar de averiguar sí mismo por qué.

ps. usar depurador

Editar:

Esto es lo que está haciendo en realidad es:

Set<Map.Entry<Sex, Sex>> set = new HashSet<Map.Entry<Sex, Sex>>(); 



    Iterator<Map.Entry<Sex, Sex>> e = entrySet.iterator(); 
    while (e.hasNext()) { 
     set.add(e.next()); 
    } 

Recuerde que HashSet se implementa sobre HashMap, los valores añadidos a Hashmap basado en código hash y la igualdad.

BTW Todo lo explicado en OP enlace al rompecabezas. El error está en el método igual que después de la segunda invocación del método next(), cambie la forma de trabajar y compare el tipo de clase que los valores return o == this;.

+2

Estás engañosa Falcon. El problema no está en el HashSet. –

+1

Es cierto que el problema es la implementación de EntryIterator en Enum. –

2

EnumMap.EntryIterator.next() devuelve this referencia. Puede comprobarlo de la siguiente manera:

Iterator<? extends Map.Entry<Sex, Sex>> e = map.entrySet().iterator(); 
while (e.hasNext()) { 
    Map.Entry<Sex, Sex> x = e.next(); 
    System.out.println(System.identityHashCode(x)); 
} 
Cuestiones relacionadas