Tính nhất quán của hashCode () trên chuỗi Java


134

Giá trị hashCode của Chuỗi Java được tính là ( String.hashCode () ):

s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]

Có bất kỳ trường hợp nào (giả sử phiên bản JVM, nhà cung cấp, v.v.) theo đó biểu thức sau sẽ đánh giá là sai?

boolean expression = "This is a Java string".hashCode() == 586653468

Cập nhật # 1: Nếu bạn cho rằng câu trả lời là "có, có những trường hợp như vậy" - thì vui lòng cho một ví dụ cụ thể khi "Đây là một chuỗi Java" .hashCode ()! = 586653468. Hãy cố gắng cụ thể / cụ thể càng tốt

Cập nhật # 2: Chúng ta đều biết rằng việc dựa vào các chi tiết triển khai của hashCode () nói chung là không tốt. Tuy nhiên, tôi đang nói cụ thể về String.hashCode () - vì vậy vui lòng giữ câu trả lời tập trung vào String.hashCode (). Object.hashCode () hoàn toàn không liên quan trong bối cảnh của câu hỏi này.


2
Bạn có thực sự cần chức năng này? Tại sao bạn cần giá trị chính xác?
Brian Agnew

26
@Brian: Tôi đang cố gắng tìm hiểu hợp đồng của String.hashCode ().
knorv

3
@Knorv Không cần thiết phải hiểu chính xác cách thức hoạt động - điều quan trọng hơn là phải hiểu hợp đồng và ý nghĩa thầm kín của nó.
mP.

45
@mP: Cảm ơn bạn đã đóng góp, nhưng tôi đoán đó là do tôi quyết định.
knorv

Tại sao họ lại cho nhân vật đầu tiên sức mạnh lớn nhất? Khi bạn muốn tối ưu hóa tốc độ để duy trì các phép tính bổ sung, bạn sẽ lưu trữ sức mạnh của lần trước, nhưng lần trước sẽ là từ ký tự cuối cùng đến ký tự đầu tiên. điều này có nghĩa là cũng sẽ có những lỗi nhớ cache. không hiệu quả hơn khi có thuật toán: s [0] + s [1] * 31 + s [2] * 31 ^ 2 + ... + s [n-1] * 31 ^ [n-1 ]?
nhà phát triển Android

Câu trả lời:


101

Tôi có thể thấy tài liệu đó xa như Java 1.2.

Mặc dù nói chung là bạn không nên dựa vào việc triển khai mã băm như cũ, nhưng giờ đây nó đã được ghi nhận hành vi java.lang.String, vì vậy việc thay đổi nó sẽ được tính là phá vỡ các hợp đồng hiện tại.

Bất cứ nơi nào có thể, bạn không nên dựa vào mã băm giữ nguyên các phiên bản, v.v. - nhưng trong suy nghĩ của tôi java.lang.Stringlà một trường hợp đặc biệt đơn giản vì thuật toán đã được chỉ định ... miễn là bạn sẵn sàng từ bỏ khả năng tương thích với các bản phát hành trước thuật toán đã được chỉ định, tất nhiên.


7
Hành vi được ghi lại của Chuỗi đã được chỉ định kể từ Java 1.2 Trong v1.1 của API, tính toán mã băm không được chỉ định cho lớp Chuỗi.
Martin OConnor

Trong trường hợp này, chúng ta tốt hơn nên viết mã băm của riêng mình 'ight matey?
Felype

@Felype: Tôi thực sự không biết bạn đang cố nói gì ở đây, tôi sợ.
Jon Skeet

@JonSkeet Ý tôi là, trong trường hợp này có lẽ chúng ta có thể viết mã của riêng mình để tạo ra hàm băm của riêng mình, để cấp tính di động. Là nó?
Felype

@Felype: Không rõ bạn đang nói về loại tính di động nào, cũng không thực sự bạn muốn nói gì trong "trong trường hợp này" - trong trường hợp cụ thể nào? Tôi nghi ngờ bạn nên hỏi một câu hỏi mới.
Jon Skeet

18

Tôi đã tìm thấy vài thứ về JDK 1.0 và 1.1 và> = 1.2:

Trong JDK 1.0.x và 1.1.x, hàm hashCode cho các chuỗi dài hoạt động bằng cách lấy mẫu mọi ký tự thứ n. Điều này khá đảm bảo bạn sẽ có nhiều chuỗi băm có cùng giá trị, do đó làm chậm quá trình tra cứu Hashtable. Trong JDK 1.2, chức năng đã được cải thiện để nhân kết quả cho đến nay sau đó thêm 31 ký tự tiếp theo. Điều này chậm hơn một chút, nhưng tốt hơn nhiều trong việc tránh va chạm. Nguồn: http://mindprod.com/jgloss/hashcode.html

Một cái gì đó khác biệt, bởi vì bạn dường như cần một số: Làm thế nào về việc sử dụng CRC32 hoặc MD5 thay vì mã băm và bạn rất tốt để đi - không cần thảo luận và không phải lo lắng gì cả ...


8

Bạn không nên dựa vào mã băm bằng với một giá trị cụ thể. Chỉ là nó sẽ trả về kết quả nhất quán trong cùng một thực thi. Các tài liệu API nói như sau:

Hợp đồng chung của hashCode là:

  • Bất cứ khi nào nó được gọi trên cùng một đối tượng nhiều lần trong khi thực thi ứng dụng Java, phương thức hashCode phải luôn trả về cùng một số nguyên, miễn là không có thông tin nào được sử dụng trong các phép so sánh trên đối tượng được sửa đổi. Số nguyên này không cần nhất quán từ một thực thi của một ứng dụng đến một thực thi khác của cùng một ứng dụng.

EDIT Vì javadoc cho String.hashCode () chỉ định cách tính mã băm của Chuỗi, mọi vi phạm này sẽ vi phạm đặc tả API công khai.


1
Câu trả lời của bạn là hợp lệ, nhưng không giải quyết câu hỏi cụ thể được hỏi.
knorv

6
Đó là hợp đồng mã băm chung - nhưng hợp đồng cụ thể cho String cung cấp chi tiết về thuật toán và ghi đè hiệu quả IMO hợp đồng chung này.
Jon Skeet

4

Như đã nói ở trên, nói chung bạn không nên dựa vào mã băm của một lớp còn lại. Lưu ý rằng ngay cả các lần chạy tiếp theo của cùng một ứng dụng trên cùng một VM có thể tạo ra các giá trị băm khác nhau. Hàm băm của AFAIK the Sun JVM tính toán cùng hàm băm trên mỗi lần chạy, nhưng điều đó không được đảm bảo.

Lưu ý rằng đây không phải là lý thuyết. Hàm băm cho java.lang.String đã được thay đổi trong JDK1.2 (hàm băm cũ có vấn đề với các chuỗi phân cấp như URL hoặc tên tệp, vì nó có xu hướng tạo ra cùng một hàm băm cho các chuỗi chỉ khác nhau ở cuối).

java.lang.String là một trường hợp đặc biệt, vì thuật toán của hàm hashCode () của nó là (bây giờ) được ghi lại, vì vậy bạn có thể có thể dựa vào đó. Tôi vẫn coi đó là thực hành xấu. Nếu bạn cần một thuật toán băm với các thuộc tính đặc biệt, được ghi lại, chỉ cần viết một :-).


4
Nhưng thuật toán được chỉ định trong các tài liệu trước JDK 1.2? Nếu không, đó là một tình huống khác. Thuật toán hiện được đặt trong các tài liệu, vì vậy việc thay đổi nó sẽ là một thay đổi đột phá đối với hợp đồng công khai.
Jon Skeet

(Tôi nhớ nó là 1.1.) Thuật toán gốc (kém hơn) đã được ghi lại. Không chính xác. Thuật toán được ghi lại trên thực tế đã tạo ra một ArrayIndexOutOfBoundException.
Tom Hawtin - tackline

@Jon Skeet: Ah, không biết rằng thuật toán của String.hashCode () được ghi lại. Tất nhiên điều đó thay đổi mọi thứ. Cập nhật bình luận của tôi.
sleske

3

Một vấn đề khác (!) Đáng lo ngại là sự thay đổi có thể thực hiện giữa các phiên bản sớm / muộn của Java. Tôi không tin rằng các chi tiết triển khai được đặt trong đá và do đó có khả năng nâng cấp cho tương lai phiên bản Java có thể gây ra sự cố.

Điểm mấu chốt là, tôi sẽ không dựa vào việc thực hiện hashCode() .

Có lẽ bạn có thể làm nổi bật vấn đề mà bạn thực sự đang cố gắng giải quyết bằng cách sử dụng cơ chế này và điều đó sẽ làm nổi bật một cách tiếp cận phù hợp hơn.


1
Cảm ơn câu trả lời của bạn. Bạn có thể đưa ra một ví dụ cụ thể về thời điểm "Đây là một chuỗi Java" .hashCode ()! = 586653468 không?
knorv

1
Không xin lỗi. Quan điểm của tôi là mọi thứ bạn kiểm tra có thể hoạt động theo cách bạn muốn. Nhưng điều đó vẫn không đảm bảo. Vì vậy, nếu bạn đang làm việc trong một dự án ngắn hạn (nói) mà bạn có quyền kiểm soát VM, v.v., thì những điều trên có thể phù hợp với bạn. Nhưng bạn không thể dựa vào nó trong thế giới rộng lớn hơn.
Brian Agnew

2
"Nâng cấp lên phiên bản Java trong tương lai có thể gây ra sự cố". Việc nâng cấp lên phiên bản Java trong tương lai có thể loại bỏ hoàn toàn phương thức hashCode. Hoặc làm cho nó luôn trả về 0 cho chuỗi. Đó là những thay đổi không tương thích cho bạn. Câu hỏi đặt ra là liệu Sun ^ HOracle ^ HThe JCP sẽ coi đó là một thay đổi đột phá và do đó đáng để tránh. Vì thuật toán có trong hợp đồng, người ta hy vọng họ sẽ làm được.
Steve Jessop

@SteveJessop tốt, kể từ khi switchtuyên bố trên dây biên dịch mã dựa trên một mã băm cố định đặc biệt, thay đổi để String's thuật toán mã băm chắc chắn sẽ phá vỡ mã hiện ...
Holger

3

Chỉ cần trả lời câu hỏi của bạn và không tiếp tục bất kỳ cuộc thảo luận nào. Việc triển khai JDK của Apache Harmony dường như sử dụng một thuật toán khác, ít nhất là nó trông hoàn toàn khác:

CN JDK

public int hashCode() {
    int h = hash;
    if (h == 0) {
        int off = offset;
        char val[] = value;
        int len = count;

        for (int i = 0; i < len; i++) {
            h = 31*h + val[off++];
        }
        hash = h;
    }
    return h;
}

Hài hòa Apache

public int hashCode() {
    if (hashCode == 0) {
        int hash = 0, multiplier = 1;
        for (int i = offset + count - 1; i >= offset; i--) {
            hash += value[i] * multiplier;
            int shifted = multiplier << 5;
            multiplier = shifted - multiplier;
        }
        hashCode = hash;
    }
    return hashCode;
}

Hãy tự mình kiểm tra ...


23
Tôi nghĩ rằng họ chỉ đang mát mẻ và tối ưu hóa nó. :) "(số nhân << 5) - số nhân" chỉ là số nhân 31 *, sau tất cả ...
thư giãn

Ok, quá lười để kiểm tra điều đó. Cảm ơn!
ReneS

1
Nhưng để làm cho nó rõ ràng từ phía tôi ... Đừng bao giờ dựa vào mã băm vì mã băm là một cái gì đó bên trong.
ReneS

1
các biến của "offset", "Count" và "hashCode" nghĩa là gì? Tôi giả sử "hashcode" được sử dụng làm giá trị được lưu trong bộ nhớ cache, để tránh các phép tính trong tương lai và "số đếm" đó là số lượng ký tự, nhưng "offset" là gì? giả sử tôi muốn sử dụng mã này để nó phù hợp, được cung cấp một chuỗi, tôi nên làm gì với nó?
nhà phát triển Android

1
@androiddeveloper Bây giờ, đó là một câu hỏi thú vị - mặc dù tôi nên đoán nó, dựa trên tên người dùng của bạn. Từ các tài liệu Android, có vẻ như hợp đồng giống nhau: s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]Trừ khi tôi nhầm, điều này là do Android sử dụng triển khai đối tượng String của Sun mà không có thay đổi.
Kartik Chugh

2

Nếu bạn lo lắng về các thay đổi và có thể là VM không thể thay đổi, chỉ cần sao chép triển khai mã băm hiện có vào lớp tiện ích của riêng bạn và sử dụng mã đó để tạo mã băm.


Tôi sẽ nói điều này. Trong khi các câu trả lời khác trả lời câu hỏi, viết một hàm hashCode riêng có lẽ là giải pháp thích hợp cho vấn đề của knorv.
Nick

1

Mã băm sẽ được tính dựa trên các giá trị ASCII của các ký tự trong Chuỗi.

Đây là cách triển khai trong Lớp Chuỗi như sau

public int hashCode() {
    int h = hash;
    if (h == 0 && value.length > 0) {
        hash = h = isLatin1() ? StringLatin1.hashCode(value)
                              : StringUTF16.hashCode(value);
    }
    return h;
}

Va chạm trong hashcode là không thể tránh khỏi. Ví dụ: các chuỗi "Ea" và "FB" cho cùng mã băm là 2236

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.