Java HashMap keySet()の反復順序は一貫していますか?


81

MapのkeySet()メソッドから返されるSetは、特定の順序を保証するものではないことを理解しています。

私の質問は、それは複数の反復にわたって同じ順序を保証するかどうかです。例えば

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

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

...

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

上記のコードでは、マップが変更されていないと仮定すると、keySetに対する反復は同じ順序になります。Sunのjdk15を使用することはありません同じ順序で反復するが、私はこの動作に依存する前に、私はすべてのJDKが同じことをするでしょうかどうかを知りたいのです。

編集

答えから、私はそれに頼ることができないことがわかります。残念な。注文を保証するために新しいコレクションを作成する必要がないことを望んでいました。私のコードは、繰り返し処理し、いくつかのロジックを実行してから、同じ順序で再度繰り返し処理する必要がありました。keySetから新しいArrayListを作成するだけで、順序が保証されます。


2
getMap()メソッドから返されるMap実装を制御しますか?
アンドレイアダモビッチ

2
独自のコレクションを作成しなくて、一貫した順序を確実に得ることができます。他の人が言及しているように、SortedMapを参照してください。したがって、getMap()メソッドが代わりにSortedMapを返す場合、呼び出し元は一貫した順序を期待することがわかります。
pSpeed 2009

私の答えは、の順番ことを証明している.keySet()とは.values()一致しています。残念ながら、受け入れられた答えは間違っています。@ karoberts-見ていただけますか?
HarshalParekh20年

回答:


52

APIドキュメントで保証されていると記載されていない場合は、それに依存しないでください。同じベンダーのJDKであっても、JDKのリリースごとに動作が変わる可能性があります。

セットを簡単に入手して、自分で並べ替えることができますよね?


3
他の誰かが述べたように、getMap()から返されるMapインスタンスを制御できる場合は、SortedMapを返すことができます。その場合、Mapだけでなく、getMap()からSortedMapを明示的に返したいと思うでしょう。
ケンリュウ

4
ハッシュマップとJava 7とJava 8の間で変化HashSetの反復順
user100464

@KenLiuこんにちは私はJavaを初めて使用しますが、SortedMapを取得する方法の例を教えてください。どうもありがとう。
セシリア

それが矛盾していることを証明できますか?javadocが「保証された」という言葉に言及していないからといって、それが矛盾していることを意味するわけではありません。
HarshalParekh20年

この答えは正しくありません。それらは一貫しています。私はここでそれを証明しました
HarshalParekh20年

58

反復順序が変更されないHashMapが必要な場合は、LinkedHashMapを使用できます。

さらに、コレクションを反復処理する場合は、常にそれを使用する必要があります。HashMapのentrySetまたはkeySetの反復は、LinkedHashMapの反復よりもはるかに遅くなります。


9

マップは(クラスではなく)単なるインターフェースです。つまり、マップを実装する(そして多くの)基礎となるクラスは異なる動作をする可能性があり、APIのkeySet()のコントラクトは、一貫した反復が必要であることを示していません。

Mapを実装する特定のクラス(HashMap、LinkedHashMap、TreeMapなど)を調べている場合は、ソースをチェックアウトすることで、keySet()関数を実装して動作を決定する方法を確認できます。アルゴリズムを実際によく見て、探しているプロパティが保持されているかどうかを確認してください(つまり、マップに反復間に挿入/削除がない場合の一貫した反復順序)。たとえば、HashMapのソースは次のとおりです(JDK 6を開きます):http//www.docjar.com/html/api/java/util/HashMap.java.html

JDKごとに大きく異なる可能性があるため、絶対に依存しません。

そうは言っても、一貫した反復順序が本当に必要な場合は、LinkedHashMapを試してみることをお勧めします。


Setクラス自体は、その要素の順序を保証せず、一意であることのみを保証します。したがって、.keySet()を要求すると、順序が保証されていないSetのインスタンスが返されます。注文が必要な場合は、自分で注文し、並べ替える必要があります(Collections.sortまたはSortedSet実装のいずれかを使用)
Matt

7

MapのAPIは、同じオブジェクトに対するメソッドの複数の呼び出しの間であっても、順序付けを保証するものではありません。

実際には、後続の複数の呼び出しで反復順序が変更された場合(マップ自体がその間に変更されなかったと仮定)、非常に驚​​きますが、これに依存するべきではありません(APIによると)。

編集-反復順序の一貫性に依存したい場合は、これらの保証を正確に提供するSortedMapが必要です


私を5秒殴ってください。信頼できるとしても、それを追加するだけです。そうすべきかどうかは疑問です。非常に壊れやすいように見えるので、なぜこれに依存する必要があるのか​​疑問に思う必要があります。
pSpeed 2009

5

楽しみのために、毎回ランダムな順序を保証するために使用できるコードを書くことにしました。これは、順序に依存しているが、そうではない場合をキャッチできるようにするために役立ちます。他の人が言っているように、順序に依存したい場合は、SortedMapを使用する必要があります。マップを使用していて、たまたま注文に依存している場合は、次のRandomIteratorを使用するとそれをキャッチできます。コードのテストでのみ使用します。これは、使用しないよりも多くのメモリを使用するためです。

Map(またはSet)をラップして、RandomeIteratorを返すようにすることもできます。これにより、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();
    }
}

4

LinkedHashMapのことに同意します。HashMapをキーで並べ替えようとしたときに問題に直面しているときに、調査結果と経験を入力するだけです。

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

}

マップのエントリを出力する関数showMapがあります。

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

    }

}

並べ替える前に地図を印刷すると、次の順序で印刷されます。

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] 

これは基本的に、マップキーが配置された順序とは異なります。

マップキーで並べ替えると、次のようになります。

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

出力は次のとおりです。

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] 

キーの順序の違いを確認できます。キーの並べ替え順序は問題ありませんが、コピーされたマップのキーの順序は、以前のマップと同じ順序になります。これが正しいかどうかはわかりませんが、同じキーを持つ2つのハッシュマップの場合、キーの順序は同じです。これは、キーの順序は保証されていませんが、このJVMバージョンのHashMap実装の場合、キー挿入アルゴリズムの固有の性質により、同じキーを持つ2つのマップで同じになる可能性があることを意味します。

LinkedHashMapを使用してソートされたエントリをHashMapにコピーすると、目的の結果が得られます(これは自然なことですが、それは重要ではありません。重要なのは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);

出力:

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] 

3

ハッシュマップは、マップの順序が時間の経過とともに一定に保たれることを保証するものではありません。


3

tl; drはい。


の反復順序は一貫している.keySet()と思います.values()(Java8)。

証明1HashMapランダムなキーとランダムな値をaにロードします。キーとそれに対応する値をHashMap使用してこれを繰り返し、.keySet()aにロードしますLinkedHashMap(挿入されたキーと値の順序は保持されます)。次に.keySet()、両方のマップと両方のマップを比較し.values()ます。それは常に同じであり、失敗することはありません。

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

証明2ここから、tableNode<K,V>クラスの配列です。配列を反復すると、毎回同じ結果が得られることがわかっています。

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

担当するクラス.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();
        }
    }
}

担当するクラス.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();
        }
    }
}

両方の内部クラスを注意深く見てください。それらは、次の点を除いてほとんど同じです。

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

彼らは、同じアレイ上の反復処理tableをサポートするため.keySet()KeySetクラスと.values()Valuesクラス。


証明3:この回答も明示的に述べています-したがって、はい、keySet()、values()、およびentrySet()は、内部リンクリストが使用する順序で値を返します。

したがって、.keySet().values()は一貫しています。


2

そうである必要はありません。マップのkeySet関数はSetを返し、セットのイテレータメソッドはそのドキュメントでこれを示しています。

「このセットの要素に対してイテレータを返します。要素は特定の順序で返されません(このセットが保証を提供するクラスのインスタンスである場合を除く)。」

したがって、これらのクラスの1つを保証付きで使用していない限り、何もありません。


2

マップはインターフェースであり、順序が同じである必要があることをドキュメントで定義していません。つまり、注文に頼ることはできません。ただし、getMap()によって返されるMap実装を制御する場合は、LinkedHashMapまたはTreeMapを使用して、それらを反復処理するたびに同じ順序のキー/値を取得できます。


1

論理的には、契約書に「特定の注文は保証されない」と記載されており、「1回の注文」は特定の注文であるため、答えはノーです。同じ方法で2回発行されることに依存することはできません。


0

keySet()メソッドによって返されたSetインスタンスを保存することもでき、同じ順序が必要なときはいつでもこのインスタンスを使用できます。


1
であることが、保証?またはiterator()、同じセットであっても、各呼び出しで異なる反復順序のイテレータを返すことができますか?
Matt Leidholm 2017
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.