Triển khai HashMap Java 8


92

Theo tài liệu liên kết sau: Triển khai Java HashMap

Tôi bối rối với việc triển khai HashMap(hay đúng hơn là một cải tiến trong HashMap). Truy vấn của tôi là:

Đầu tiên

static final int TREEIFY_THRESHOLD = 8;
static final int UNTREEIFY_THRESHOLD = 6;
static final int MIN_TREEIFY_CAPACITY = 64;

Tại sao và làm thế nào những hằng số này được sử dụng? Tôi muốn một số ví dụ rõ ràng cho điều này. Làm thế nào họ đạt được hiệu suất tăng với điều này?

Thứ hai

Nếu bạn thấy mã nguồn của HashMaptrong JDK, bạn sẽ tìm thấy lớp bên trong tĩnh sau:

static final class TreeNode<K, V> extends java.util.LinkedHashMap.Entry<K, V> {
    HashMap.TreeNode<K, V> parent;
    HashMap.TreeNode<K, V> left;
    HashMap.TreeNode<K, V> right;
    HashMap.TreeNode<K, V> prev;
    boolean red;

    TreeNode(int arg0, K arg1, V arg2, HashMap.Node<K, V> arg3) {
        super(arg0, arg1, arg2, arg3);
    }

    final HashMap.TreeNode<K, V> root() {
        HashMap.TreeNode arg0 = this;

        while (true) {
            HashMap.TreeNode arg1 = arg0.parent;
            if (arg0.parent == null) {
                return arg0;
            }

            arg0 = arg1;
        }
    }
    //...
}

Nó được sử dụng như thế nào? Tôi chỉ muốn giải thích về thuật toán .

Câu trả lời:


225

HashMapchứa một số nhóm nhất định. Nó sử dụng hashCodeđể xác định thùng nào để đưa những thứ này vào. Vì đơn giản, hãy tưởng tượng nó như một mô đun.

Nếu mã băm của chúng ta là 123456 và chúng ta có 4 nhóm, 123456 % 4 = 0thì mặt hàng sẽ nằm trong nhóm đầu tiên, Nhóm 1.

Bản đồ băm

Nếu hàm băm của chúng ta tốt, nó sẽ cung cấp một phân phối đồng đều để tất cả các nhóm sẽ được sử dụng như nhau. Trong trường hợp này, nhóm sử dụng danh sách được liên kết để lưu trữ các giá trị.

Nhóm được liên kết

Nhưng bạn không thể dựa vào mọi người để triển khai các hàm băm tốt. Mọi người thường sẽ viết các hàm băm kém, dẫn đến phân phối không đồng đều. Cũng có thể chúng tôi gặp xui xẻo với những đầu vào của mình.

Bản đồ băm kém

Phân phối này càng ít đồng đều, chúng ta càng tiến xa hơn từ các phép toán O (1) và chúng ta càng tiến gần đến các phép toán O (n).

Việc triển khai Hashmap cố gắng giảm thiểu điều này bằng cách tổ chức một số nhóm thành cây thay vì danh sách được liên kết nếu các nhóm trở nên quá lớn. Đây là những gì TREEIFY_THRESHOLD = 8dành cho. Nếu một thùng chứa nhiều hơn tám vật phẩm, nó sẽ trở thành một cái cây.

Thùng cây

Cây này là cây Đỏ-Đen. Đầu tiên nó được sắp xếp theo mã băm. Nếu các mã băm giống nhau, nó sử dụng compareTophương pháp Comparablenếu các đối tượng triển khai giao diện đó, nếu không mã băm nhận dạng.

Nếu các mục nhập bị xóa khỏi bản đồ, số mục nhập trong nhóm có thể giảm đến mức cấu trúc cây này không còn cần thiết nữa. Đó là những gì UNTREEIFY_THRESHOLD = 6là cho. Nếu số phần tử trong một nhóm giảm xuống dưới sáu, chúng tôi cũng có thể quay lại sử dụng danh sách được liên kết.

Cuối cùng, có MIN_TREEIFY_CAPACITY = 64.

Khi một bản đồ băm tăng kích thước, nó sẽ tự động thay đổi kích thước để có nhiều nhóm hơn. Nếu chúng ta có một bản đồ băm nhỏ, khả năng chúng ta nhận được các nhóm rất đầy là khá cao, bởi vì chúng tôi không có nhiều nhóm khác nhau để đưa các thứ vào. Sẽ tốt hơn nhiều nếu có một bản đồ băm lớn hơn, với nhiều nhóm hơn mà ít đầy hơn. Hằng số này về cơ bản nói rằng không bắt đầu tạo thùng thành cây nếu bản đồ băm của chúng ta rất nhỏ - thay vào đó, nó nên thay đổi kích thước để lớn hơn.


Để trả lời câu hỏi của bạn về mức tăng hiệu suất, những tính năng tối ưu này đã được thêm vào để cải thiện trường hợp xấu nhất . Tôi chỉ suy đoán nhưng bạn có thể sẽ chỉ thấy một sự cải thiện hiệu suất đáng chú ý vì những tối ưu hóa này nếu hashCodechức năng của bạn không tốt lắm.


3
Phân phối không đồng đều không phải lúc nào cũng là dấu hiệu của các hàm băm kém. Một số kiểu dữ liệu, ví dụ String, có không gian giá trị lớn hơn nhiều so với mã intbăm, do đó, xung đột là không thể tránh khỏi. Bây giờ nó phụ thuộc vào các giá trị thực tế, chẳng hạn như các giá trị thực tế String, bạn đưa vào bản đồ, cho dù bạn có được phân phối đồng đều hay không. Một phân phối không tốt có thể là kết quả của vận rủi.
Holger

3
+1, Tôi muốn thêm rằng một kịch bản cụ thể mà cách tiếp cận cây này giảm nhẹ là một cuộc tấn công DOS xung đột băm . java.lang.Stringcó tính xác định, không phải là mật mã hashCode, vì vậy những kẻ tấn công có thể tạo ra các Chuỗi khác biệt một cách dễ dàng với các mã băm xung đột. Trước khi tối ưu hóa này, điều này có thể làm suy giảm các hoạt động HashMap thành O (n) -time, bây giờ nó chỉ làm suy giảm chúng thành O (log (n)).
MikeFHay

1
+1, if the objects implement that interface, else the identity hash code.tôi đang tìm kiếm phần khác này.
Con số 945

1
@NateGlenn mã băm mặc định nếu bạn không ghi đè nó
Michael

Tôi không hiểu "Hằng số này về cơ bản nói rằng không bắt đầu tạo thùng thành cây nếu bản đồ băm của chúng ta rất nhỏ - thay vào đó, nó nên thay đổi kích thước để lớn hơn." cho MIN_TREEIFY_CAPACITY. Có nghĩa là "Sau khi chúng tôi chèn một khóa sẽ được băm vào nhóm đã chứa 8 ( TREEIFY_THRESHOLD) khóa và nếu đã có 64 MIN_TREEIFY_CAPACITYkhóa ( ) HashMap, danh sách liên kết của nhóm đó sẽ được chuyển thành cây cân bằng."
anir

16

Nói một cách đơn giản hơn (tôi càng đơn giản càng tốt) + một số chi tiết khác.

Những thuộc tính này phụ thuộc vào rất nhiều thứ bên trong sẽ rất tuyệt để hiểu - trước khi chuyển trực tiếp đến chúng.

TREEIFY_THRESHOLD -> khi một nhóm duy nhất đạt đến con số này (và tổng số vượt quá MIN_TREEIFY_CAPACITY), nó được chuyển thành một nút cây màu đỏ / đen cân bằng hoàn hảo . Tại sao? Vì tốc độ tìm kiếm. Hãy nghĩ về nó theo một cách khác:

sẽ mất nhiều nhất 32 bước để tìm kiếm Mục nhập trong một thùng / thùng có mục nhập Integer.MAX_VALUE .

Một số giới thiệu cho chủ đề tiếp theo. Tại sao số thùng / thùng luôn là lũy thừa của hai ? Ít nhất hai lý do: nhanh hơn hoạt động của modulo và modulo trên số âm sẽ là số âm. Và bạn không thể đặt Mục nhập vào nhóm "phủ định":

 int arrayIndex = hashCode % buckets; // will be negative

 buckets[arrayIndex] = Entry; // obviously will fail

Thay vào đó, có một thủ thuật hay được sử dụng thay vì modulo:

 (n - 1) & hash // n is the number of bins, hash - is the hash function of the key

Điều đó về mặt ngữ nghĩa cũng giống như hoạt động modulo. Nó sẽ giữ các bit thấp hơn. Điều này có một hệ quả thú vị khi bạn làm:

Map<String, String> map = new HashMap<>();

Trong trường hợp trên, quyết định về vị trí của một mục nhập chỉ dựa trên 4 bit cuối cùng của mã băm của bạn.

Đây là lúc mà việc nhân lên các nhóm phát huy tác dụng. Trong những điều kiện nhất định (sẽ mất rất nhiều thời gian để giải thích chi tiết chính xác ), các thùng có kích thước gấp đôi. Tại sao? Khi các thùng được tăng kích thước lên gấp đôi, sẽ có một chút nữa phát huy tác dụng .

Vì vậy, bạn có 16 nhóm - 4 bit cuối cùng của mã băm quyết định vị trí của mục nhập. Bạn nhân đôi các nhóm: 32 nhóm - 5 bit cuối cùng quyết định mục nhập sẽ đi đến đâu.

Vì vậy, quá trình này được gọi là băm lại. Điều này có thể trở nên chậm chạp. Đó là (đối với những người quan tâm) như HashMap được "nói đùa" là: nhanh, nhanh, nhanh, slooow . Có các cách triển khai khác - bản đồ băm không tạm dừng tìm kiếm ...

Giờ đây, UNTREEIFY_THRESHOLD sẽ hoạt động sau khi băm lại. Tại thời điểm đó, một số mục nhập có thể di chuyển từ thùng này sang thùng khác (chúng thêm một bit nữa vào (n-1)&hashtính toán - và như vậy có thể chuyển sang các thùng khác ) và nó có thể đạt được điều này UNTREEIFY_THRESHOLD. Tại thời điểm này, việc giữ nguyên thùng rác không có lợi red-black tree node, mà LinkedListthay vào đó, như

 entry.next.next....

MIN_TREEIFY_CAPACITY là số lượng nhóm tối thiểu trước khi một nhóm nhất định được chuyển đổi thành Cây.


10

TreeNodelà một cách thay thế để lưu trữ các mục nhập thuộc một thùng duy nhất của HashMap. Trong các triển khai cũ hơn, các mục nhập của thùng được lưu trữ trong danh sách liên kết. Trong Java 8, nếu số mục nhập trong thùng vượt qua ngưỡng ( TREEIFY_THRESHOLD), chúng được lưu trữ trong cấu trúc cây thay vì danh sách liên kết ban đầu. Đây là một sự tối ưu hóa.

Từ việc thực hiện:

/*
 * Implementation notes.
 *
 * This map usually acts as a binned (bucketed) hash table, but
 * when bins get too large, they are transformed into bins of
 * TreeNodes, each structured similarly to those in
 * java.util.TreeMap. Most methods try to use normal bins, but
 * relay to TreeNode methods when applicable (simply by checking
 * instanceof a node).  Bins of TreeNodes may be traversed and
 * used like any others, but additionally support faster lookup
 * when overpopulated. However, since the vast majority of bins in
 * normal use are not overpopulated, checking for existence of
 * tree bins may be delayed in the course of table methods.

không hoàn toàn đúng. Nếu chúng vượt qua TREEIFY_THRESHOLD thì tổng số thùng là ít nhất MIN_TREEIFY_CAPACITY. Tôi đã cố gắng che điều đó trong câu trả lời của mình ...
Eugene

3

Bạn sẽ cần phải hình dung nó: giả sử có một Khóa lớp chỉ có hàm hashCode () được ghi đè để luôn trả về cùng một giá trị

public class Key implements Comparable<Key>{

  private String name;

  public Key (String name){
    this.name = name;
  }

  @Override
  public int hashCode(){
    return 1;
  }

  public String keyName(){
    return this.name;
  }

  public int compareTo(Key key){
    //returns a +ve or -ve integer 
  }

}

và sau đó ở một nơi khác, tôi đang chèn 9 mục nhập vào HashMap với tất cả các khóa là các phiên bản của lớp này. ví dụ

Map<Key, String> map = new HashMap<>();

    Key key1 = new Key("key1");
    map.put(key1, "one");

    Key key2 = new Key("key2");
    map.put(key2, "two");
    Key key3 = new Key("key3");
    map.put(key3, "three");
    Key key4 = new Key("key4");
    map.put(key4, "four");
    Key key5 = new Key("key5");
    map.put(key5, "five");
    Key key6 = new Key("key6");
    map.put(key6, "six");
    Key key7 = new Key("key7");
    map.put(key7, "seven");
    Key key8 = new Key("key8");
    map.put(key8, "eight");

//Since hascode is same, all entries will land into same bucket, lets call it bucket 1. upto here all entries in bucket 1 will be arranged in LinkedList structure e.g. key1 -> key2-> key3 -> ...so on. but when I insert one more entry 

    Key key9 = new Key("key9");
    map.put(key9, "nine");

  threshold value of 8 will be reached and it will rearrange bucket1 entires into Tree (red-black) structure, replacing old linked list. e.g.

                  key1
                 /    \
               key2   key3
              /   \   /  \

Duyệt cây nhanh hơn {O (log n)} so với LinkedList {O (n)} và khi n lớn lên, sự khác biệt càng trở nên đáng kể.


Nó không thể xây dựng một cây hiệu quả vì nó không có cách nào để so sánh các khóa khác với mã băm của chúng, tất cả đều giống nhau và phương thức bằng của chúng, không giúp ích gì cho việc sắp xếp.
user253751

@immibis Mã băm của họ không nhất thiết phải giống nhau. Chúng rất có thể khác nhau. Nếu các lớp triển khai nó, nó sẽ sử dụng thêm compareTotừ Comparable. identityHashCodelà một cơ chế khác mà nó sử dụng.
Michael

@Michael Trong ví dụ này, tất cả các mã băm nhất thiết phải giống nhau và lớp không triển khai So sánh. IdentityHashCode sẽ không có giá trị trong việc tìm ra nút chính xác.
user253751

@immibis À vâng, tôi chỉ lướt qua thôi nhưng bạn nói đúng. Vì vậy, như Keykhông triển khai Comparable, identityHashCodesẽ được sử dụng :)
Michael

@EmonMishra thật không may, chỉ đơn giản là hình ảnh sẽ không đủ, tôi đã cố gắng đề cập đến điều đó trong câu trả lời của mình.
Eugene

2

Thay đổi trong triển khai HashMap đã được thêm vào với JEP-180 . Mục đích là:

Cải thiện hiệu suất của java.util.HashMap trong các điều kiện xung đột băm cao bằng cách sử dụng cây cân bằng thay vì danh sách được liên kết để lưu trữ các mục bản đồ. Thực hiện cùng một cải tiến trong lớp LinkedHashMap

Tuy nhiên, hiệu suất thuần túy không phải là lợi ích duy nhất. Nó cũng sẽ ngăn chặn tấn công HashDoS , trong trường hợp một bản đồ băm được sử dụng để lưu trữ thông tin đầu vào của người dùng, vì cây đỏ đen được sử dụng để lưu trữ dữ liệu trong thùng có độ phức tạp chèn trong trường hợp xấu nhất là O (log n). Cây được sử dụng sau khi đáp ứng một số tiêu chí nhất định - xem câu trả lời của Eugene .


-1

Để hiểu cách triển khai nội bộ của hashmap, bạn cần phải hiểu về băm. Hashing ở dạng đơn giản nhất, là một cách để gán một mã duy nhất cho bất kỳ biến / đối tượng nào sau khi áp dụng bất kỳ công thức / thuật toán nào trên các thuộc tính của nó.

Một hàm băm thực sự phải tuân theo quy tắc này:

“Hàm băm nên trả về cùng một mã băm mỗi lần khi hàm được áp dụng trên các đối tượng giống nhau hoặc bằng nhau. Nói cách khác, hai đối tượng bằng nhau phải tạo ra cùng một mã băm một cách nhất quán ”.


Điều này không trả lời câu hỏi.
Stephen C
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.