Cân nhắc hiệu suất cho keySet () và entrySet () của Map


76

Tất cả,

Bất cứ ai có thể vui lòng cho tôi biết chính xác các vấn đề về hiệu suất giữa cả hai là gì? Trang web: CodeRanch cung cấp tổng quan ngắn gọn về các lệnh gọi nội bộ cần thiết khi sử dụng keySet () và get (). Nhưng sẽ thật tuyệt nếu ai đó có thể cung cấp chi tiết chính xác về luồng khi các phương thức keySet () và get () được sử dụng. Điều này sẽ giúp tôi hiểu rõ hơn các vấn đề về hiệu suất.

Câu trả lời:


70

Trước hết, điều này hoàn toàn phụ thuộc vào loại Bản đồ bạn đang sử dụng. Nhưng vì chuỗi JavaRanch nói về HashMap, tôi sẽ cho rằng đó là cách triển khai mà bạn đang đề cập đến. Và hãy giả sử rằng bạn đang nói về việc triển khai API tiêu chuẩn từ Sun / Oracle.

Thứ hai, nếu bạn lo lắng về hiệu suất khi lặp lại bản đồ băm của mình, tôi khuyên bạn nên xem qua LinkedHashMap. Từ các tài liệu:

Việc lặp lại các chế độ xem bộ sưu tập của một LinkedHashMap yêu cầu thời gian tỷ lệ thuận với kích thước của bản đồ, bất kể dung lượng của nó. Việc lặp lại trên một HashMap có thể sẽ tốn kém hơn, đòi hỏi thời gian tỷ lệ thuận với dung lượng của nó.

HashMap.entrySet ()

Mã nguồn cho việc triển khai này có sẵn. Việc thực hiện về cơ bản chỉ trả về một mới HashMap.EntrySet. Một lớp trông như thế này:

private final class EntrySet extends AbstractSet<Map.Entry<K,V>> {
    public Iterator<Map.Entry<K,V>> iterator() {
        return newEntryIterator(); // returns a HashIterator...
    }
    // ...
}

HashIteratortrông giống như

private abstract class HashIterator<E> implements Iterator<E> {
    Entry<K,V> next;    // next entry to return
    int expectedModCount;   // For fast-fail
    int index;      // current slot
    Entry<K,V> current; // current entry

    HashIterator() {
        expectedModCount = modCount;
        if (size > 0) { // advance to first entry
            Entry[] t = table;
            while (index < t.length && (next = t[index++]) == null)
                ;
        }
    }

    final Entry<K,V> nextEntry() {
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
        Entry<K,V> e = next;
        if (e == null)
            throw new NoSuchElementException();

        if ((next = e.next) == null) {
            Entry[] t = table;
            while (index < t.length && (next = t[index++]) == null)
                ;
        }
    current = e;
        return e;
    }

    // ...
}

Vì vậy, bạn có nó ... Đó là mã quy định những gì sẽ xảy ra khi bạn lặp lại một entrySet. Nó đi qua toàn bộ mảng miễn là dung lượng bản đồ.

HashMap.keySet () và .get ()

Ở đây, trước tiên bạn cần nắm được bộ phím. Điều này cần thời gian tỷ lệ thuận với dung lượng của bản đồ (trái ngược với kích thước của LinkedHashMap). Sau khi hoàn tất, bạn gọi get()một lần cho mỗi phím. Chắc chắn, trong trường hợp trung bình, với việc triển khai Mã băm tốt, điều này cần thời gian liên tục. Tuy nhiên, nó chắc chắn sẽ yêu cầu rất nhiều .hashCode.equalscuộc gọi, điều này rõ ràng sẽ mất nhiều thời gian hơn là chỉ thực hiện một entry.value()cuộc gọi.


1
+1 "Việc lặp lại trên các chế độ xem bộ sưu tập của một LinkedHashMap yêu cầu thời gian tỷ lệ thuận với kích thước của bản đồ, bất kể dung lượng của nó. Lặp lại trên một HashMap có thể tốn kém hơn, đòi hỏi thời gian tỷ lệ với dung lượng của nó."
metdos

Nhưng nếu bạn chỉ cần truy cập các khóa hoặc chỉ cần truy cập các giá trị của Bản đồ thì bạn nên lặp lại các Giá trị trả về của Bộ được trả về bởi keySet () và Bộ sưu tập (). Một điểm nữa, Tập hợp được trả về bởi keySet () và Tập hợp được trả về bởi giá trị () đều được hỗ trợ bởi Bản đồ gốc. Có nghĩa là, nếu bạn thực hiện bất kỳ sửa đổi nào, chúng sẽ được phản ánh trở lại trong Bản đồ, tuy nhiên, cả hai đều không hỗ trợ phương thức add () và addAll (), tức là bạn không thể thêm khóa mới vào Set hoặc giá trị mới trong Bộ sưu tập.
sactiw

@aioobe NHƯ bạn đã viết "Đó là mã quy định điều gì sẽ xảy ra khi bạn lặp qua entrySet. Nó đi xuyên qua toàn bộ mảng miễn là dung lượng của bản đồ." Không nên là "..... dài bằng kích thước của bản đồ"?
Sumit Kumar Saha

Câu trả lời tốt tốt. Tôi luôn muốn đề cập đến mã nguồn vì nó là nguồn chân lý cuối cùng
ACV

75

Trường hợp phổ biến nhất mà việc sử dụng entrySet thích hợp hơn keySet là khi bạn đang lặp lại tất cả các cặp khóa / giá trị trong một Bản đồ.

Điều này hiệu quả hơn:

for (Map.Entry entry : map.entrySet()) {
    Object key = entry.getKey();
    Object value = entry.getValue();
}

hơn:

for (Object key : map.keySet()) {
    Object value = map.get(key);
}

Bởi vì trong trường hợp thứ hai, đối với mọi khóa trong keySet, map.get()phương thức này được gọi, mà - trong trường hợp là HashMap - yêu cầu các phương thức hashCode()equals()phương thức của đối tượng khóa được đánh giá để tìm giá trị liên quan *. Trong trường hợp đầu tiên, công việc phụ đó bị loại bỏ.

Chỉnh sửa: Điều này thậm chí còn tồi tệ hơn nếu bạn xem xét Sơ đồ cây, trong đó lệnh gọi để lấy là O (log2 (n)), tức là bộ so sánh cho ý chí có thể cần chạy log2 (n) lần (n = kích thước của Bản đồ) trước khi tìm giá trị liên quan.

* Một số triển khai Bản đồ có các tính năng tối ưu nội bộ kiểm tra danh tính của các đối tượng trước khi hashCode()equals()được gọi.


3
Ngoài ra, nếu bản đồ là Bản đồ cây chứ không phải Bản đồ HashMap, thì đó get()là một O(log(n))hoạt động.
ILMTitan

@ILMIian và Michael: Tại sao sự khác biệt giữa TreeMap và HashMap?
name_masked

TreeMap và HashMap là các cấu trúc dữ liệu khác nhau, TreeMap dựa trên cây Đỏ / Đen. HashMap là một nhóm và bảng băm danh sách. Trong cả hai trường hợp, lệnh gọi get () không miễn phí và chi phí của nó phụ thuộc vào kiểu cấu trúc dữ liệu.
Michael Barker

1
Java 8 (trở lên) có giá trị trong HashMapthực hiện như cây tìm kiếm nhị phân thay vì LinkedList. Xem openjdk.java.net/jeps/180
Người dùng mới làm quen vào

14

Dưới đây là các liên kết đến một bài viết so sánh hiệu suất entrySet(), keySet()values(), và lời khuyên liên quan đến mỗi khi sử dụng phương pháp tiếp cận.

Rõ ràng việc sử dụng keySet()nhanh hơn (ngoài ra còn tiện lợi hơn) entrySet()miễn là bạn không cần đến Map.get()các giá trị.


1
Bạn đã nói trong bài viết đó "Phương pháp này (sử dụng keySet hoặc các giá trị thay vì entrySet) mang lại lợi thế về hiệu suất hơn một chút so với phép lặp entrySet (nhanh hơn khoảng 10%) và sạch sẽ hơn." Tôi có thể biết làm thế nào bạn nhận được giá trị "10%" đó không? Bạn không hiển thị phép đo cũng như bất kỳ dữ liệu bên ngoài nào có chứa giá trị đó.
dantuch

@dantuch Tôi không phải Sergiy, nhưng bài báo có ý nghĩa IMHO. Tuy nhiên, bài viết đã cũ từ năm 2008. Bạn luôn có thể tạo một microbenchmark bằng cách sử dụng ví dụ: Caliper của Google cho JDK mới nhất nếu bạn tò mò, vui lòng đăng kết quả.
Stefan L
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.