¿Es coherente el orden de iteración keySet () de Java HashMap?

81

Entiendo que el conjunto devuelto por el método keySet () de un mapa no garantiza ningún orden en particular.

Mi pregunta es, ¿garantiza el mismo orden en múltiples iteraciones? Por ejemplo

Map<K,V> map = getMap();

for( K k : map.keySet() )
{
}

...

for( K k : map.keySet() )
{
}

En el código anterior, asumiendo que el mapa no se modifica, la iteración sobre los conjuntos de claves estará en el mismo orden. Usando jdk15 de Sun se hace iterate en el mismo orden, pero antes de que dependen de este comportamiento, me gustaría saber si todos los JDK harán lo mismo.

EDITAR

Veo por las respuestas que no puedo depender de eso. Demasiado. Tenía la esperanza de no tener que construir una nueva colección para garantizar mi pedido. Mi código necesitaba iterar, hacer algo de lógica y luego iterar nuevamente con el mismo orden. Simplemente crearé una nueva ArrayList a partir del keySet que garantizará el orden.

karoberts
fuente
2
¿Controlas qué implementación de mapa se devuelve desde el método getMap ()?
Andrey Adamovich
2
Ciertamente, puede obtener pedidos consistentes sin crear su propia colección. Vea SortedMap como otros han mencionado. Entonces, si su método getMap () devuelve SortedMap, las personas que llaman sabrán que esperan un orden consistente.
PSpeed
Mi respuesta prueba que el orden de .keySet()y .values()es consistente. Desafortunadamente, la respuesta aceptada es incorrecta. @karoberts - ¿puedes echar un vistazo?
Harshal Parekh

Respuestas:

52

Si no se indica que está garantizado en la documentación de la API, no debería depender de él. El comportamiento puede incluso cambiar de una versión del JDK a la siguiente, incluso del JDK del mismo proveedor.

Podrías obtener fácilmente el conjunto y luego ordenarlo tú mismo, ¿verdad?

Ken Liu
fuente
3
Como mencionó alguien más, si puede controlar qué instancia de Map se devuelve desde getMap (), entonces puede devolver un SortedMap. En ese caso, probablemente desee devolver explícitamente un SortedMap de getMap () en lugar de solo un Map.
Ken Liu
4
El orden de iteración de HashMap y HashSet cambió entre Java 7 y Java 8.
user100464
@KenLiu Hola, soy muy nuevo en Java, ¿puedes darme un ejemplo sobre cómo obtener un SortedMap? Muchas gracias.
Cecilia
¿Puedes probar que es inconsistente? El hecho de que el javadoc no mencione la palabra "garantizado" no significa que sea inconsistente.
Harshal Parekh
Esta respuesta es incorrecta. Son consistentes. Lo he probado aquí .
Harshal Parekh
58

Puede usar un LinkedHashMap si desea un HashMap cuyo orden de iteración no cambie.

Además, siempre debe usarlo si recorre la colección. Iterar sobre el entrySet o keySet de HashMap es mucho más lento que sobre LinkedHashMap.

ciamej
fuente
9

Map es solo una interfaz (en lugar de una clase), lo que significa que la clase subyacente que lo implementa (y hay muchas) podría comportarse de manera diferente, y el contrato para keySet () en la API no indica que se requiera una iteración consistente.

Si está buscando una clase específica que implementa Map (HashMap, LinkedHashMap, TreeMap, etc.), entonces podría ver cómo implementa la función keySet () para determinar cuál sería el comportamiento al verificar la fuente, tendría que realmente eche un vistazo de cerca al algoritmo para ver si la propiedad que está buscando se conserva (es decir, un orden de iteración consistente cuando el mapa no ha tenido inserciones / eliminaciones entre iteraciones). La fuente de HashMap, por ejemplo, está aquí (abra JDK 6): http://www.docjar.com/html/api/java/util/HashMap.java.html

Podría variar mucho de un JDK a otro, por lo que definitivamente no confiaría en él.

Dicho esto, si un orden de iteración consistente es algo que realmente necesita, es posible que desee probar un LinkedHashMap.

mpobrien
fuente
La clase Set en sí misma no garantiza ningún orden para sus elementos, solo que es único. Entonces, cuando solicita .keySet (), devuelve una instancia de Set que no tiene un orden garantizado. Si desea ordenar, debe hacerlo por su cuenta y ordenarlos (ya sea con Collections.sort o una implementación de SortedSet)
Matt
7

La API para Map no garantiza ningún orden en absoluto, incluso entre múltiples invocaciones del método en el mismo objeto.

En la práctica, me sorprendería mucho si el orden de iteración cambiara para múltiples invocaciones posteriores (suponiendo que el mapa en sí no cambiara en el medio), pero no debería (y según la API no puede) confiar en esto.

EDITAR: si desea confiar en que el orden de iteración sea consistente, entonces desea un SortedMap que proporcione exactamente estas garantías.

Andrzej Doyle
fuente
Batirme por cinco segundos, así que solo agregaré que, incluso si pudiera confiar en él, es cuestionable si debería hacerlo. Me pregunto por qué habría que depender de esto, ya que parece muy frágil.
PSpeed
5

Solo por diversión, decidí escribir un código que puedes usar para garantizar un orden aleatorio cada vez. Esto es útil para que pueda detectar casos en los que depende del orden pero no debería hacerlo. Si desea depender del orden, como han dicho otros, debe usar un SortedMap. Si solo usa un mapa y confía en el orden, entonces el siguiente RandomIterator lo detectará. Solo lo usaría en el código de prueba, ya que utiliza más memoria que no hacerlo.

También puede ajustar el mapa (o el conjunto) para que devuelvan el RandomeIterator que luego le permitirá usar el bucle for-each.

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

public class Main
{
    private Main()
    {
    }

    public static void main(final String[] args)
    {
        final Map<String, String> items;

        items = new HashMap<String, String>();
        items.put("A", "1");
        items.put("B", "2");
        items.put("C", "3");
        items.put("D", "4");
        items.put("E", "5");
        items.put("F", "6");
        items.put("G", "7");

        display(items.keySet().iterator());
        System.out.println("---");

        display(items.keySet().iterator());
        System.out.println("---");

        display(new RandomIterator<String>(items.keySet().iterator()));
        System.out.println("---");

        display(new RandomIterator<String>(items.keySet().iterator()));
        System.out.println("---");
    }

    private static <T> void display(final Iterator<T> iterator)
    {
        while(iterator.hasNext())
        {
            final T item;

            item = iterator.next();
            System.out.println(item);
        }
    }
}

class RandomIterator<T>
    implements Iterator<T>
{
    private final Iterator<T> iterator;

    public RandomIterator(final Iterator<T> i)
    {
        final List<T> items;

        items = new ArrayList<T>();

        while(i.hasNext())
        {
            final T item;

            item = i.next();
            items.add(item);
        }

        Collections.shuffle(items);
        iterator = items.iterator();
    }

    public boolean hasNext()
    {
        return (iterator.hasNext());
    }

    public T next()
    {
        return (iterator.next());
    }

    public void remove()
    {
        iterator.remove();
    }
}
Tofu, cerveza
fuente
4

Estoy de acuerdo con LinkedHashMap. Solo pongo mis hallazgos y experiencia mientras enfrentaba el problema cuando intentaba ordenar HashMap por claves.

Mi código para crear HashMap:

HashMap<Integer, String> map;

@Before
public void initData() {
    map = new HashMap<>();

    map.put(55, "John");
    map.put(22, "Apple");
    map.put(66, "Earl");
    map.put(77, "Pearl");
    map.put(12, "George");
    map.put(6, "Rocky");

}

Tengo una función showMap que imprime las entradas del mapa:

public void showMap (Map<Integer, String> map1) {
    for (Map.Entry<Integer,  String> entry: map1.entrySet()) {
        System.out.println("[Key: "+entry.getKey()+ " , "+"Value: "+entry.getValue() +"] ");

    }

}

Ahora, cuando imprimo el mapa antes de ordenar, imprime la siguiente secuencia:

Map before sorting : 
[Key: 66 , Value: Earl] 
[Key: 22 , Value: Apple] 
[Key: 6 , Value: Rocky] 
[Key: 55 , Value: John] 
[Key: 12 , Value: George] 
[Key: 77 , Value: Pearl] 

Que es básicamente diferente al orden en el que se colocaron las claves del mapa.

Ahora, cuando lo ordeno con claves de mapa:

    List<Map.Entry<Integer, String>> entries = new ArrayList<>(map.entrySet());

    Collections.sort(entries, new Comparator<Entry<Integer, String>>() {

        @Override
        public int compare(Entry<Integer, String> o1, Entry<Integer, String> o2) {

            return o1.getKey().compareTo(o2.getKey());
        }
    });

    HashMap<Integer, String> sortedMap = new LinkedHashMap<>();

    for (Map.Entry<Integer, String> entry : entries) {
        System.out.println("Putting key:"+entry.getKey());
        sortedMap.put(entry.getKey(), entry.getValue());
    }

    System.out.println("Map after sorting:");

    showMap(sortedMap);

la salida es:

Sorting by keys : 
Putting key:6
Putting key:12
Putting key:22
Putting key:55
Putting key:66
Putting key:77
Map after sorting:
[Key: 66 , Value: Earl] 
[Key: 6 , Value: Rocky] 
[Key: 22 , Value: Apple] 
[Key: 55 , Value: John] 
[Key: 12 , Value: George] 
[Key: 77 , Value: Pearl] 

Puede ver la diferencia en el orden de las claves. El orden ordenado de las claves está bien, pero el de las claves del mapa copiado está nuevamente en el mismo orden del mapa anterior. No sé si esto es válido para decirlo, pero para dos hashmap con las mismas claves, el orden de las claves es el mismo. Esto implica para la declaración que el orden de las claves no está garantizado, pero puede ser el mismo para dos mapas con las mismas claves debido a la naturaleza inherente del algoritmo de inserción de claves si se implementa HashMap de esta versión de JVM.

Ahora, cuando uso LinkedHashMap para copiar entradas ordenadas a HashMap, obtengo el resultado deseado (que era natural, pero ese no es el punto. El punto está relacionado con el orden de las claves de HashMap)

    HashMap<Integer, String> sortedMap = new LinkedHashMap<>();

    for (Map.Entry<Integer, String> entry : entries) {
        System.out.println("Putting key:"+entry.getKey());
        sortedMap.put(entry.getKey(), entry.getValue());
    }

    System.out.println("Map after sorting:");

    showMap(sortedMap);

Salida:

Sorting by keys : 
Putting key:6
Putting key:12
Putting key:22
Putting key:55
Putting key:66
Putting key:77
Map after sorting:
[Key: 6 , Value: Rocky] 
[Key: 12 , Value: George] 
[Key: 22 , Value: Apple] 
[Key: 55 , Value: John] 
[Key: 66 , Value: Earl] 
[Key: 77 , Value: Pearl] 
Parth Joshi
fuente
3

Hashmap no garantiza que el orden del mapa se mantenga constante en el tiempo.

Amir Afghani
fuente
2

No tiene por qué serlo. La función keySet de un mapa devuelve un conjunto y el método iterador del conjunto dice esto en su documentación:

"Devuelve un iterador sobre los elementos de este conjunto. Los elementos se devuelven sin ningún orden en particular (a menos que este conjunto sea una instancia de alguna clase que proporcione una garantía)".

Entonces, a menos que esté usando una de esas clases con garantía, no hay ninguna.

Jeff Storey
fuente
2

Map es una interfaz y no define en la documentación que el orden debe ser el mismo. Eso significa que no puede confiar en el pedido. Pero si controla la implementación de Map devuelta por getMap (), entonces puede usar LinkedHashMap o TreeMap y obtener el mismo orden de claves / valores todo el tiempo que itera a través de ellos.

Andrey Adamovich
fuente
1

Lógicamente, si el contrato dice "ningún pedido en particular está garantizado", y dado que "el pedido que salió una vez" es un pedido en particular , la respuesta es no, no puede depender de que salga de la misma manera dos veces.

Jonathan Feinberg
fuente
1

tl; dr Sí.


Creo que el orden de iteración para .keySet()y .values()es consistente (Java 8).

Prueba 1 : cargamos a HashMapcon claves aleatorias y valores aleatorios. Repetimos sobre esto HashMapusando .keySet()y cargamos las claves y sus valores correspondientes a a LinkedHashMap(mantendrá el orden de las claves y valores insertados). Luego comparamos el valor .keySet()de ambos Mapas y .values()de ambos Mapas. Siempre resulta ser lo mismo, nunca falla.

public class Sample3 {

    static final String AB = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
    static SecureRandom rnd = new SecureRandom();

    // from here: https://stackoverflow.com/a/157202/8430155
    static String randomString(int len){
        StringBuilder sb = new StringBuilder(len);
        for (int i = 0; i < len; i++) {
            sb.append(AB.charAt(rnd.nextInt(AB.length())));
        }
        return sb.toString();
    }

    public static void main(String[] args) throws Exception {
        for (int j = 0; j < 10; j++) {
            Map<String, String> map = new HashMap<>();
            Map<String, String> linkedMap = new LinkedHashMap<>();

            for (int i = 0; i < 1000; i++) {
                String key = randomString(8);
                String value = randomString(8);
                map.put(key, value);
            }

            for (String k : map.keySet()) {
                linkedMap.put(k, map.get(k));
            }

            if (!(map.keySet().toString().equals(linkedMap.keySet().toString()) &&
                  map.values().toString().equals(linkedMap.values().toString()))) {
                // never fails
                System.out.println("Failed");
                break;
            }
        }
    }
}

Prueba 2 : A partir de aquí , tablees una matriz de Node<K,V>clases. Sabemos que iterar una matriz dará el mismo resultado cada vez.

/**
 * The table, initialized on first use, and resized as
 * necessary. When allocated, length is always a power of two.
 * (We also tolerate length zero in some operations to allow
 * bootstrapping mechanics that are currently not needed.)
 */
transient Node<K,V>[] table;

La clase responsable de .values():

final class Values extends AbstractCollection<V> {
    
    // more code here

    public final void forEach(Consumer<? super V> action) {
        Node<K,V>[] tab;
        if (action == null)
            throw new NullPointerException();
        if (size > 0 && (tab = table) != null) {
            int mc = modCount;
            for (int i = 0; i < tab.length; ++i) {
                for (Node<K,V> e = tab[i]; e != null; e = e.next)
                    action.accept(e.value);
            }
            if (modCount != mc)
                throw new ConcurrentModificationException();
        }
    }
}

La clase responsable de .keySet():

final class KeySet extends AbstractSet<K> {

    // more code here

    public final void forEach(Consumer<? super K> action) {
        Node<K,V>[] tab;
        if (action == null)
            throw new NullPointerException();
        if (size > 0 && (tab = table) != null) {
            int mc = modCount;
            for (int i = 0; i < tab.length; ++i) {
                for (Node<K,V> e = tab[i]; e != null; e = e.next)
                    action.accept(e.key);
            }
            if (modCount != mc)
                throw new ConcurrentModificationException();
        }
    }
}

Mire cuidadosamente ambas clases internas. Son prácticamente iguales excepto:

if (size > 0 && (tab = table) != null) {
    int mc = modCount;
    for (int i = 0; i < tab.length; ++i) {
        for (Node<K,V> e = tab[i]; e != null; e = e.next)
            action.accept(e.key);               <- from KeySet class
            // action.accept(e.value);          <- the only change from Values class
    }
    if (modCount != mc)
        throw new ConcurrentModificationException();
}

Se repiten en la misma matriz tablepara admitir .keySet()en KeySetclase y .values()en Valuesclase.


Prueba 3: esta respuesta también establece explícitamente: entonces, sí, keySet (), values ​​() y entrySet () devuelven valores en el orden que usa la lista vinculada interna.

Por lo tanto, los .keySet()y .values()son consistentes.

Harshal Parekh
fuente
0

También puede almacenar la instancia de Set devuelta por el método keySet () y puede usar esta instancia siempre que necesite el mismo pedido.

Shivang Agarwal
fuente
1
¿Está eso garantizado? ¿O cada llamada podría iterator()devolver un iterador con un orden de iteración diferente, incluso en el mismo conjunto?
Matt Leidholm