thứ tự lặp lại Java HashMap keySet () có nhất quán không?


81

Tôi hiểu rằng Tập hợp được trả về từ phương thức keySet () của Bản đồ không đảm bảo bất kỳ thứ tự cụ thể nào.

Câu hỏi của tôi là, nó có đảm bảo cùng một thứ tự qua nhiều lần lặp lại không. Ví dụ

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

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

...

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

Trong đoạn mã trên, giả sử rằng bản đồ không được sửa đổi, thì việc lặp lại các Bộ khóa có theo cùng một thứ tự không. Sử dụng jdk15 của Sun nó không lặp theo thứ tự, nhưng trước khi tôi phụ thuộc vào hành vi này, tôi muốn biết nếu tất cả JDK sẽ làm như vậy.

BIÊN TẬP

Tôi thấy từ những câu trả lời mà tôi không thể phụ thuộc vào nó. Quá tệ. Tôi đã hy vọng thoát khỏi việc không phải xây dựng một Bộ sưu tập mới nào đó để đảm bảo việc đặt hàng của tôi. Mã của tôi cần phải lặp lại, thực hiện một số logic, và sau đó lặp lại với cùng một thứ tự. Tôi sẽ chỉ tạo một ArrayList mới từ keySet mà sẽ đảm bảo thứ tự.


2
Bạn có kiểm soát việc triển khai Bản đồ nào được trả về từ phương thức getMap () không?
Andrey Adamovich

2
Bạn chắc chắn có thể đặt hàng nhất quán mà không cần tạo bộ sưu tập của riêng mình. Xem Bản đồ sắp xếp như những người khác đã đề cập. Vì vậy, nếu phương thức getMap () của bạn trả về SortedMap thay vào đó, người gọi sẽ biết mong đợi thứ tự nhất quán.
PSpeed

Câu trả lời của tôi chứng minh rằng thứ tự của .keySet().values()là nhất quán. Thật không may, câu trả lời được chấp nhận là sai. @karoberts - bạn có thể vui lòng xem qua được không?
Harshal Parekh

Câu trả lời:


52

Nếu nó không được đảm bảo trong tài liệu API, thì bạn không nên phụ thuộc vào nó. Hành vi thậm chí có thể thay đổi từ một bản phát hành JDK sang bản tiếp theo, thậm chí từ JDK của cùng một nhà cung cấp.

Bạn có thể dễ dàng lấy bộ và sau đó chỉ cần tự mình sắp xếp, phải không?


3
Như ai đó đã đề cập, nếu bạn có thể kiểm soát cá thể Bản đồ nào được trả về từ getMap (), thì bạn có thể trả về Bản đồ sắp xếp. Trong trường hợp đó, bạn có thể muốn trả về một cách rõ ràng Bản đồ sắp xếp từ getMap () thay vì chỉ một Bản đồ.
Ken Liu

4
HashMap và HashSet trật tự lặp thay đổi giữa Java 7 và Java 8.
user100464

@KenLiu Xin chào, tôi là người mới sử dụng Java, bạn có thể cho tôi một ví dụ về cách tải Bản đồ sắp xếp không? Cảm ơn nhiều.
Cecilia

Bạn có thể chứng minh rằng nó là không nhất quán? Chỉ vì javadoc không đề cập đến từ "đảm bảo" không có nghĩa là nó không nhất quán.
Harshal Parekh

Câu trả lời này không chính xác. Chúng nhất quán. Tôi đã chứng minh điều đó ở đây .
Harshal Parekh

58

Bạn có thể sử dụng LinkedHashMap nếu bạn muốn một HashMap có thứ tự lặp lại không thay đổi.

Hơn nữa, bạn nên luôn sử dụng nó nếu bạn lặp lại qua bộ sưu tập. Lặp lại entrySet hoặc keySet của HashMap chậm hơn nhiều so với LinkedHashMap.


9

Bản đồ chỉ là một giao diện (thay vì một lớp), có nghĩa là lớp bên dưới triển khai nó (và có nhiều) có thể hoạt động khác nhau và hợp đồng cho keySet () trong API không chỉ ra rằng cần phải lặp lại nhất quán.

Nếu bạn đang xem một lớp cụ thể triển khai Bản đồ (HashMap, LinkedHashMap, TreeMap, v.v.) thì bạn có thể thấy cách nó triển khai hàm keySet () để xác định hành vi sẽ như thế nào bằng cách kiểm tra nguồn, bạn phải thực sự xem xét kỹ thuật toán để xem liệu thuộc tính bạn đang tìm có được bảo toàn hay không (nghĩa là thứ tự lặp nhất quán khi bản đồ không có bất kỳ lần chèn / xóa nào giữa các lần lặp lại). Nguồn cho HashMap, ví dụ, ở đây (mở JDK 6): http://www.docjar.com/html/api/java/util/HashMap.java.html

Nó có thể thay đổi rất nhiều từ JDK này sang JDK tiếp theo, vì vậy tôi chắc chắn sẽ không dựa vào nó.

Điều đó đang được nói, nếu thứ tự lặp lại nhất quán là thứ bạn thực sự cần, bạn có thể muốn thử một Bản đồ liên kết.


Lớp Set trong và của chính nó đảm bảo không có thứ tự nào cho các phần tử của nó, chỉ là nó là duy nhất. Vì vậy, khi bạn yêu cầu .keySet (), nó trả về một thể hiện của Set không có thứ tự đảm bảo. Nếu bạn muốn có thứ tự, bạn phải tự làm và sắp xếp chúng (có thể là với Collections.sort hoặc triển khai SortedSet)
Matt

7

API cho Bản đồ không đảm bảo bất kỳ thứ tự nào, ngay cả giữa nhiều lần gọi phương thức trên cùng một đối tượng.

Trong thực tế, tôi sẽ rất ngạc nhiên nếu thứ tự lặp lại thay đổi cho nhiều lần gọi tiếp theo (giả sử bản thân bản đồ không thay đổi ở giữa) - nhưng bạn không nên (và theo API thì không thể) dựa vào điều này.

CHỈNH SỬA - nếu bạn muốn dựa vào thứ tự lặp lại nhất quán, thì bạn muốn có Bản đồ sắp xếp cung cấp chính xác những đảm bảo này.


Đánh bại tôi năm giây vì vậy tôi sẽ chỉ thêm điều đó ngay cả khi bạn có thể dựa vào nó, bạn có nên nghi ngờ không. Tôi phải tự hỏi tại sao người ta cần phải phụ thuộc vào điều này vì nó có vẻ rất mong manh.
PSpeed

5

Chỉ cho vui thôi, tôi quyết định viết một số mã mà bạn có thể sử dụng để đảm bảo một đơn đặt hàng ngẫu nhiên mỗi lần. Điều này rất hữu ích để bạn có thể nắm bắt được các trường hợp tùy theo đơn hàng nhưng không nên. Nếu bạn muốn phụ thuộc vào thứ tự, hơn như những người khác đã nói, bạn nên sử dụng Bản đồ sắp xếp. Nếu bạn chỉ sử dụng Bản đồ và tình cờ dựa vào thứ tự thì việc sử dụng RandomIterator sau đây sẽ nắm bắt được điều đó. Tôi chỉ sử dụng nó trong mã thử nghiệm vì nó sử dụng nhiều bộ nhớ hơn thì sẽ không làm được.

Bạn cũng có thể bọc Bản đồ (hoặc Tập hợp) để yêu cầu chúng trả về RandomeIterator, sau đó sẽ cho phép bạn sử dụng vòng lặp 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

Tôi đồng ý với điều LinkedHashMap. Chỉ đưa những phát hiện và kinh nghiệm của tôi trong khi tôi đang đối mặt với vấn đề khi tôi đang cố gắng sắp xếp HashMap theo các khóa.

Mã của tôi để tạo 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");

}

Tôi có một hàm showMap in các mục của bản đồ:

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

    }

}

Bây giờ khi tôi in bản đồ trước khi sắp xếp, nó sẽ in chuỗi sau:

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] 

Về cơ bản khác với thứ tự đặt các phím bản đồ.

Bây giờ Khi tôi sắp xếp nó bằng các phím bản đồ:

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

đặt ra là:

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] 

Bạn có thể thấy sự khác biệt về thứ tự của các phím. Thứ tự các phím được sắp xếp là ổn nhưng các phím của bản đồ được sao chép lại theo cùng thứ tự của bản đồ trước đó. Tôi không biết liệu điều này có hợp lệ để nói hay không, nhưng đối với hai bản đồ băm có các khóa giống nhau, thứ tự các khóa giống nhau. Điều này ngụ ý tuyên bố rằng thứ tự các khóa không được đảm bảo nhưng có thể giống nhau đối với hai bản đồ có cùng khóa vì bản chất vốn có của thuật toán chèn khóa nếu HashMap triển khai phiên bản JVM này.

Bây giờ khi tôi sử dụng LinkedHashMap để sao chép các Mục nhập được sắp xếp vào HashMap, tôi nhận được kết quả mong muốn (điều này là tự nhiên, nhưng đó không phải là vấn đề. Điểm liên quan đến thứ tự các khóa của 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);

Đầu ra:

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

Hashmap không đảm bảo rằng thứ tự của bản đồ sẽ không đổi theo thời gian.


2

Nó không cần phải được. Hàm keySet của bản đồ trả về một Tập hợp và phương thức trình lặp của tập hợp cho biết điều này trong tài liệu của nó:

"Trả về một trình lặp trên các phần tử trong tập hợp này. Các phần tử được trả về không theo thứ tự cụ thể (trừ khi tập hợp này là một phiên bản của một số lớp cung cấp bảo đảm)."

Vì vậy, trừ khi bạn đang sử dụng một trong những lớp đó có bảo đảm, còn không thì không có.


2

Bản đồ là một giao diện và nó không xác định trong tài liệu rằng thứ tự phải giống nhau. Điều đó có nghĩa là bạn không thể dựa vào đơn đặt hàng. Nhưng nếu bạn kiểm soát việc triển khai Bản đồ được trả về bởi getMap (), thì bạn có thể sử dụng LinkedHashMap hoặc TreeMap và nhận cùng một thứ tự các khóa / giá trị mọi lúc bạn lặp lại chúng.


1

Về mặt logic, nếu hợp đồng nói rằng "không có đơn đặt hàng cụ thể nào được đảm bảo" và vì "đơn đặt hàng xuất hiện một lần" là một đơn đặt hàng cụ thể , thì câu trả lời là không, bạn không thể phụ thuộc vào việc nó ra cùng một cách hai lần.


1

tl; dr Có.


Tôi tin rằng thứ tự lặp lại cho .keySet().values()nhất quán (Java 8).

Chứng minh 1 : Chúng tôi tải a HashMapvới các khóa ngẫu nhiên và các giá trị ngẫu nhiên. Chúng tôi lặp lại điều này HashMapbằng cách sử dụng .keySet()và tải các khóa và nó có các giá trị tương ứng với a LinkedHashMap(nó sẽ bảo toàn thứ tự của các khóa và giá trị được chèn vào). Sau đó, chúng tôi so sánh .keySet()của cả Bản đồ và .values()của cả Bản đồ. Nó luôn luôn giống nhau, không bao giờ thất bại.

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

Chứng minh 2 : Từ đây , tablemảng là một Node<K,V>lớp. Chúng ta biết rằng việc lặp lại một mảng sẽ cho kết quả giống nhau mọi lúc.

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

Lớp chịu trách nhiệm .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();
        }
    }
}

Lớp chịu trách nhiệm .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();
        }
    }
}

Cẩn thận xem xét cả hai lớp bên trong. Chúng khá giống nhau ngoại trừ:

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

Chúng lặp lại trên cùng một mảng tableđể hỗ trợ .keySet()trong KeySetlớp và .values()trong Valueslớp.


Bằng chứng 3: câu trả lời này cũng nói rõ - Vì vậy, có, keySet (), giá trị () và entrySet () trả về giá trị theo thứ tự mà danh sách liên kết nội bộ sử dụng.

Do đó, .keySet().values()là nhất quán.


0

Bạn cũng có thể lưu trữ thể hiện Set được trả về bởi phương thức keySet () và có thể sử dụng thể hiện này bất cứ khi nào bạn cần cùng một thứ tự.


1
Điều đó có được đảm bảo không? Hoặc mỗi cuộc gọi có thể iterator()trả về một trình lặp với một thứ tự lặp khác nhau, thậm chí trên cùng một tập hợp?
Matt Leidholm
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.