Ist die Iterationsreihenfolge von Java HashMap keySet () konsistent?

81

Ich verstehe, dass das von der keySet () -Methode einer Map zurückgegebene Set keine bestimmte Reihenfolge garantiert.

Meine Frage ist, garantiert es die gleiche Reihenfolge über mehrere Iterationen. Zum Beispiel

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

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

...

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

Unter der Annahme, dass die Zuordnung nicht geändert wird, erfolgt im obigen Code die Iteration über die keySets in derselben Reihenfolge. Sun jdk15 Mit ihm tut Iterierte in der gleichen Reihenfolge, aber bevor ich auf dieses Verhalten abhängen, würde Ich mag wissen , ob alle JDKs das gleiche tun.

BEARBEITEN

Ich sehe aus den Antworten, dass ich mich nicht darauf verlassen kann. Schade. Ich hatte gehofft, nicht eine neue Kollektion bauen zu müssen, um meine Bestellung zu garantieren. Mein Code musste durchlaufen, logisch ausgeführt und dann mit derselben Reihenfolge erneut durchlaufen werden. Ich werde einfach eine neue ArrayList aus dem keySet erstellen, die die Reihenfolge garantiert.

Karoberts
quelle
2
Steuern Sie, welche Map-Implementierung von der getMap () -Methode zurückgegeben wird?
Andrey Adamovich
2
Sicher Sie können konsistente Bestellung erhalten , ohne Ihre eigene Sammlung zu schaffen. Siehe SortedMap, wie andere erwähnt haben. Wenn Ihre getMap () -Methode stattdessen SortedMap zurückgibt, müssen Aufrufer eine konsistente Reihenfolge erwarten.
PSpeed
Meine Antwort beweist, dass die Reihenfolge von .keySet()und .values()konsistent ist. Leider ist die akzeptierte Antwort falsch. @karoberts - kannst du bitte einen Blick darauf werfen?
Harshal Parekh

Antworten:

52

Wenn in der API-Dokumentation nicht angegeben ist, dass dies garantiert ist, sollten Sie sich nicht darauf verlassen. Das Verhalten kann sich sogar von einer Version des JDK zur nächsten ändern, selbst vom JDK desselben Anbieters.

Sie könnten das Set leicht bekommen und es dann einfach selbst sortieren, oder?

Ken Liu
quelle
3
Wie bereits erwähnt, können Sie eine SortedMap zurückgeben, wenn Sie steuern können, welche Map-Instanz von getMap () zurückgegeben wird. In diesem Fall möchten Sie wahrscheinlich explizit eine SortedMap von getMap () anstelle einer Map zurückgeben.
Ken Liu
4
Die HashMap- und HashSet-Iterationsreihenfolge wurde zwischen Java 7 und Java 8 geändert.
user100464
@ KenLiu Hallo, ich bin sehr neu in Java. Kannst du mir ein Beispiel geben, wie man eine SortedMap bekommt? Danke vielmals.
Cecilia
Können Sie beweisen, dass es inkonsistent ist? Nur weil der Javadoc das Wort "garantiert" nicht erwähnt, heißt das nicht, dass es inkonsistent ist.
Harshal Parekh
Diese Antwort ist falsch. Sie sind konsistent. Ich habe es hier bewiesen .
Harshal Parekh
58

Sie können eine LinkedHashMap verwenden, wenn Sie eine HashMap möchten, deren Iterationsreihenfolge sich nicht ändert.

Außerdem sollten Sie es immer verwenden, wenn Sie die Sammlung durchlaufen. Das Iterieren über HashMaps entrySet oder keySet ist viel langsamer als über LinkedHashMaps.

ciamej
quelle
9

Map ist nur eine Schnittstelle (und keine Klasse). Dies bedeutet, dass sich die zugrunde liegende Klasse, die sie implementiert (und es gibt viele), möglicherweise anders verhält, und der Vertrag für keySet () in der API gibt nicht an, dass eine konsistente Iteration erforderlich ist.

Wenn Sie sich eine bestimmte Klasse ansehen, die Map implementiert (HashMap, LinkedHashMap, TreeMap usw.), können Sie sehen, wie sie die Funktion keySet () implementiert, um das Verhalten durch Auschecken der Quelle zu bestimmen Schauen Sie sich den Algorithmus genau an, um festzustellen, ob die gesuchte Eigenschaft erhalten bleibt (dh eine konsistente Iterationsreihenfolge, wenn die Karte zwischen den Iterationen keine Einfügungen / Entfernungen aufweist). Die Quelle für HashMap befindet sich beispielsweise hier (öffnen Sie JDK 6): http://www.docjar.com/html/api/java/util/HashMap.java.html

Es könnte von einem JDK zum nächsten sehr unterschiedlich sein, also würde ich mich definitiv nicht darauf verlassen.

Wenn Sie jedoch eine konsistente Iterationsreihenfolge benötigen, sollten Sie eine LinkedHashMap ausprobieren.

mpobrien
quelle
Die Set-Klasse an und für sich garantiert keine Reihenfolge für ihre Elemente, nur dass sie einzigartig ist. Wenn Sie also nach .keySet () fragen, wird eine Instanz von Set zurückgegeben, für die keine garantierte Reihenfolge vorliegt. Wenn Sie bestellen möchten, müssen Sie dies selbst tun und sortieren (entweder mit Collections.sort oder einer SortedSet-Implementierung)
Matt
7

Die API für Map garantiert keinerlei Reihenfolge, auch nicht zwischen mehreren Aufrufen der Methode für dasselbe Objekt.

In der Praxis wäre ich sehr überrascht, wenn sich die Iterationsreihenfolge für mehrere nachfolgende Aufrufe ändern würde (vorausgesetzt, die Karte selbst hat sich dazwischen nicht geändert) - aber Sie sollten sich nicht darauf verlassen (und laut API nicht).

BEARBEITEN - Wenn Sie sich darauf verlassen möchten, dass die Iterationsreihenfolge konsistent ist, möchten Sie eine SortedMap, die genau diese Garantien bietet.

Andrzej Doyle
quelle
Schlagen Sie mich um fünf Sekunden, also werde ich nur hinzufügen, dass es fraglich ist, ob Sie sich darauf verlassen sollten, selbst wenn Sie sich darauf verlassen könnten. Ich muss mich fragen, warum man sich darauf verlassen muss, da es sehr fragil erscheint.
PSpeed
5

Nur zum Spaß habe ich beschlossen, einen Code zu schreiben, mit dem Sie jedes Mal eine zufällige Reihenfolge garantieren können. Dies ist nützlich, damit Sie Fälle abfangen können, in denen Sie von der Bestellung abhängig sind, dies aber nicht sein sollten. Wenn Sie von der Reihenfolge abhängig sein möchten, sollten Sie, wie bereits erwähnt, eine SortedMap verwenden. Wenn Sie nur eine Karte verwenden und sich zufällig auf die Reihenfolge verlassen, wird dies mit dem folgenden RandomIterator erfasst. Ich würde es nur zum Testen von Code verwenden, da es mehr Speicher benötigt, als dies nicht der Fall wäre.

Sie können auch die Map (oder das Set) umbrechen, damit sie den RandomeIterator zurückgeben, mit dem Sie dann die for-each-Schleife verwenden können.

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();
    }
}
TofuBeer
quelle
4

Ich stimme der Sache mit LinkedHashMap zu. Ich habe nur meine Erkenntnisse und Erfahrungen zusammengestellt, als ich vor dem Problem stand, als ich versuchte, HashMap nach Schlüsseln zu sortieren.

Mein Code zum Erstellen von 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");

}

Ich habe eine Funktion showMap, die Einträge der Karte druckt:

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

    }

}

Wenn ich jetzt die Karte vor dem Sortieren drucke, wird die folgende Reihenfolge gedruckt:

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] 

Dies unterscheidet sich grundlegend von der Reihenfolge, in der die Kartenschlüssel eingegeben wurden.

Wenn ich es jetzt mit Kartenschlüsseln sortiere:

    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);

Die Ausgabe lautet:

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] 

Sie können den Unterschied in der Reihenfolge der Schlüssel sehen. Die sortierte Reihenfolge der Schlüssel ist in Ordnung, aber die der Schlüssel der kopierten Karte liegt wieder in der gleichen Reihenfolge wie die frühere Karte. Ich weiß nicht, ob dies gültig ist, aber für zwei Hashmaps mit denselben Schlüsseln ist die Reihenfolge der Schlüssel gleich. Dies impliziert die Aussage, dass die Reihenfolge der Schlüssel nicht garantiert ist, aber für zwei Maps mit denselben Schlüsseln aufgrund der inhärenten Natur des Algorithmus zum Einfügen von Schlüsseln gleich sein kann, wenn die HashMap-Implementierung dieser JVM-Version erfolgt.

Wenn ich jetzt LinkedHashMap verwende, um sortierte Einträge in HashMap zu kopieren, erhalte ich das gewünschte Ergebnis (was natürlich war, aber das ist nicht der Punkt. Der Punkt betrifft die Reihenfolge der Schlüssel von 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);

Ausgabe:

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
quelle
3

Hashmap garantiert nicht, dass die Reihenfolge der Karte über die Zeit konstant bleibt.

Amir Afghani
quelle
2

Das muss nicht sein. Die keySet-Funktion einer Karte gibt ein Set zurück, und die Iteratormethode des Sets sagt dies in der Dokumentation aus:

"Gibt einen Iterator über die Elemente in diesem Satz zurück. Die Elemente werden in keiner bestimmten Reihenfolge zurückgegeben (es sei denn, dieser Satz ist eine Instanz einer Klasse, die eine Garantie bietet)."

Wenn Sie also keine dieser Klassen mit Garantie verwenden, gibt es keine.

Jeff Storey
quelle
2

Map ist eine Schnittstelle und definiert in der Dokumentation nicht, dass die Reihenfolge identisch sein soll. Das bedeutet, dass Sie sich nicht auf die Bestellung verlassen können. Wenn Sie jedoch die von getMap () zurückgegebene Map-Implementierung steuern, können Sie LinkedHashMap oder TreeMap verwenden und bei jeder Iteration dieselbe Reihenfolge von Schlüsseln / Werten abrufen.

Andrey Adamovich
quelle
2

tl; dr Ja.


Ich glaube, die Iterationsreihenfolge für .keySet()und .values()ist konsistent (Java 8).

Beweis 1 : Wir laden a HashMapmit zufälligen Schlüsseln und zufälligen Werten. Wir wiederholen dies HashMapmit .keySet()und laden die Schlüssel und die entsprechenden Werte zu a LinkedHashMap(die Reihenfolge der eingefügten Schlüssel und Werte bleibt erhalten). Dann vergleichen wir die .keySet()beiden Karten und .values()beide Karten. Es kommt immer gleich heraus, scheitert nie.

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;
            }
        }
    }
}

Beweis 2 : Von hier aus ist das tableein Array von Node<K,V>Klassen. Wir wissen, dass das Iterieren eines Arrays jedes Mal das gleiche Ergebnis liefert.

/**
 * 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;

Die Klasse, die verantwortlich ist für .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();
        }
    }
}

Die Klasse, die verantwortlich ist für .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();
        }
    }
}

Schauen Sie sich beide inneren Klassen genau an. Sie sind ziemlich gleich, außer:

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();
}

Sie iterieren auf demselben Array table, um es .keySet()in der KeySetKlasse und .values()in der ValuesKlasse zu unterstützen.


Beweis 3: In dieser Antwort wird auch explizit angegeben: Ja, keySet (), values ​​() und entrySet () geben Werte in der Reihenfolge zurück, in der die interne verknüpfte Liste verwendet wird.

Daher sind die .keySet()und .values()konsistent.

Harshal Parekh
quelle
1

Wenn der Vertrag besagt, dass "keine bestimmte Bestellung garantiert ist" und "die Bestellung, die einmal ausgeführt wurde" eine bestimmte Bestellung ist , lautet die Antwort logischerweise "Nein". Sie können sich nicht darauf verlassen, dass sie zweimal auf dieselbe Weise ausgeführt wird.

Jonathan Feinberg
quelle
0

Sie können auch die von der keySet () -Methode zurückgegebene Set-Instanz speichern und diese Instanz verwenden, wenn Sie dieselbe Reihenfolge benötigen.

Shivang Agarwal
quelle
1
Ist das aber garantiert? Oder könnte jeder Aufruf iterator()einen Iterator mit einer anderen Iterationsreihenfolge zurückgeben, selbst auf demselben Satz?
Matt Leidholm