чи узгоджується порядок ітерацій Java HashMap keySet ()?


81

Я розумію, що Набір, повернений із методу keySet () Карти, не гарантує жодного конкретного замовлення.

Моє питання полягає в тому, чи гарантує він однаковий порядок протягом декількох ітерацій. Наприклад

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

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

...

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

У наведеному вище коді, припускаючи, що карта не змінена, ітерація над keySets буде в тому ж порядку. Використовуючи jdk15 Sun, він виконує ітерацію в тому ж порядку, але перш ніж я залежатиму від цієї поведінки, я хотів би знати, чи всі JDK будуть робити те саме.

РЕДАГУВАТИ

З відповідей я бачу, що не можу від цього залежати. Шкода. Я сподівався врятуватися тим, що не довелося будувати якусь нову колекцію, щоб гарантувати моє замовлення. Моєму коду потрібно було виконати ітерацію, виконати певну логіку, а потім повторити процедуру знову з тим же упорядкуванням. Я просто створю новий ArrayList із набору ключів, який гарантуватиме порядок.


2
Чи контролюєте Ви, яка реалізація Карти повертається з методу getMap ()?
Андрій Адамович

2
Ви, звичайно, можете отримати послідовне замовлення, не створюючи власної колекції. Див. SortedMap, як згадували інші. Отже, якщо ваш метод getMap () замість цього повертає SortedMap, абоненти знатимуть, що слід очікувати послідовного впорядкування.
PSшвидше

Моя відповідь доводить, що порядок .keySet()та .values()послідовність. На жаль, прийнята відповідь неправильна. @karoberts - чи можете ви подивитися?
Harshal Parekh

Відповіді:


52

Якщо це не вказано як гарантоване в документації API, тоді ви не повинні від цього залежати. Поведінка може навіть змінюватися від одного випуску JDK до наступного, навіть від JDK того самого постачальника.

Ви можете легко отримати комплект, а потім просто відсортувати його самостійно, так?


3
Як хтось інший згадував, якщо ви можете контролювати, який екземпляр Map повертається з getMap (), ви можете повернути SortedMap. У цьому випадку ви, мабуть, хочете явно повернути SortedMap з getMap () замість просто Map.
Кен Лю

4
Порядок ітерацій HashMap та HashSet змінився між Java 7 та Java 8.
user100464

@KenLiu Привіт Я дуже новачок у Java, чи можете ви навести приклад про те, як отримати SortedMap? Дуже дякую.
Cecilia

Чи можете ви довести, що це суперечливо? Те, що у javadoc не згадується слово "гарантоване", не означає, що воно суперечливе.
Harshal Parekh

Ця відповідь неправильна. Вони послідовні. Я довів це тут .
Harshal Parekh

58

Ви можете використовувати LinkedHashMap, якщо хочете HashMap, порядок ітерацій якого не змінюється.

Більше того, ви завжди повинні використовувати його, якщо переглядаєте колекцію. Ітерація над вхідним набором HashMap або keySet відбувається набагато повільніше, ніж над LinkedHashMap.


9

Map - це лише інтерфейс (а не клас), а це означає, що базовий клас, який його реалізує (а їх багато), може поводитися по-різному, а контракт на keySet () в API не вказує на необхідність послідовної ітерації.

Якщо ви переглядаєте конкретний клас, який реалізує 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)
Мет

7

API для Map не гарантує жодного замовлення, навіть між кількома викликами методу на одному і тому ж об'єкті.

На практиці я був би дуже здивований, якби порядок ітерацій змінився для кількох наступних викликів (припускаючи, що сама карта не змінювалась між ними) - але ви не повинні (і згідно з API не можете) покладатися на це.

РЕДАГУВАТИ - якщо ви хочете покластися на послідовність послідовності ітерацій, тоді вам потрібна SortedMap, яка надає саме ці гарантії.


Побий мене за п’ять секунд, і я лише додам, що навіть якщо ти міг би на це покластися, сумнівно, чи повинен ти. Мені цікаво, чому потрібно залежати від цього, оскільки це здається дуже крихким.
PS, швидкість

5

Для розваги я вирішив написати якийсь код, за допомогою якого ви зможете гарантувати випадковий порядок кожного разу. Це корисно, щоб ви могли вловлювати випадки, коли ви перебуваєте залежно від замовлення, але не повинні бути. Якщо ви хочете залежати від замовлення, ніж, як казали інші, вам слід використовувати SortedMap. Якщо ви просто використовуєте карту і випадково покладаєтесь на замовлення, то використання наступного RandomIterator це зрозуміє. Я б використовував його лише для тестування коду, оскільки він використовує більше пам'яті, ніж не робив би цього.

Ви також можете обернути карту (або набір), щоб вони повернули RandomeIterator, що дозволить вам використовувати цикл for-for.

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] 

Ви можете бачити різницю в порядку клавіш. Відсортований порядок ключів чудовий, але порядок ключів скопійованої карти знову в тому ж порядку, що і попередня карта. Я не знаю, чи правильно це казати, але для двох хеш-карт з однаковими ключами порядок ключів однаковий. Це означає твердження, що порядок ключів не гарантується, але може бути однаковим для двох карт з однаковими ключами через невід'ємну природу алгоритму вставки ключів, якщо реалізація HashMap цієї версії JVM.

Тепер, коли я використовую 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] 


2

Це не повинно бути. Функція keySet карти повертає Set, а метод ітератора набору говорить це у своїй документації:

"Повертає ітератор над елементами цього набору. Елементи повертаються у певному порядку (якщо цей набір не є екземпляром якогось класу, що забезпечує гарантію)."

Отже, якщо ви не використовуєте один із цих класів із гарантією, його немає.


2

Map - це інтерфейс, і він не визначає в документації, що порядок повинен бути однаковим. Це означає, що ви не можете покладатися на замовлення. Але якщо ви керуєте реалізацією Map, що повертається методом getMap (), ви можете використовувати LinkedHashMap або TreeMap і отримувати однаковий порядок ключів / значень весь час, коли ви їх переглядаєте.


2

tl; dr Так.


Я вважаю, що порядок повторень .keySet()і .values()послідовний (Java 8).

Доказ 1 : Ми завантажуємо a HashMapвипадковими ключами та випадковими значеннями. Ми робимо ітерацію щодо цього, 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 : Звідси , tableце масив Node<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()є послідовними.


1

Логічно, що якщо в контракті зазначено "жодне конкретне замовлення не гарантується", а оскільки "замовлення, яке вийшло один раз", є конкретним замовленням , то відповідь "ні", ви не можете залежати від того, що він вийде однаковим двічі.


0

Ви також можете зберегти екземпляр Set, повернутий методом keySet (), і можете використовувати цей екземпляр, коли вам потрібен той самий порядок.


1
Хіба що гарантовано, хоча? Або кожен виклик може iterator()повернути ітератор з іншим порядком ітерацій, навіть на тому самому наборі?
Matt Leidholm,
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.