Làm thế nào để chúng ta quyết định thực hiện tốt nhất hashCode()
phương thức cho một bộ sưu tập (giả sử rằng phương thức bằng đã được ghi đè chính xác)?
collection.hashCode()
( hg.openjdk.java.net/jdk7/jdk7/jdk/file/9b8c96f96a0f/src/share/... )
Làm thế nào để chúng ta quyết định thực hiện tốt nhất hashCode()
phương thức cho một bộ sưu tập (giả sử rằng phương thức bằng đã được ghi đè chính xác)?
collection.hashCode()
( hg.openjdk.java.net/jdk7/jdk7/jdk/file/9b8c96f96a0f/src/share/... )
Câu trả lời:
Việc thực hiện tốt nhất? Đó là một câu hỏi khó vì nó phụ thuộc vào mô hình sử dụng.
Một gần như cho tất cả các trường hợp triển khai tốt hợp lý đã được đề xuất trong Java hiệu quả của Josh Bloch trong Mục 8 (phiên bản thứ hai). Điều tốt nhất là tìm nó ở đó bởi vì tác giả giải thích tại sao cách tiếp cận là tốt.
Tạo một int result
và gán một giá trị khác không .
Đối với mọi trường f
được kiểm tra trong equals()
phương thức, hãy tính mã băm c
bằng cách:
boolean
: tính toán (f ? 0 : 1)
;byte
, char
, short
hay int
: tính toán (int)f
;long
: tính toán (int)(f ^ (f >>> 32))
;float
: tính toán Float.floatToIntBits(f)
;double
: tính toán Double.doubleToLongBits(f)
và xử lý giá trị trả về như mọi giá trị dài;hashCode()
phương thức hoặc 0 if f == null
;Kết hợp giá trị băm c
với result
:
result = 37 * result + c
Trở về result
Điều này sẽ dẫn đến một phân phối giá trị băm thích hợp cho hầu hết các tình huống sử dụng.
Nếu bạn hài lòng với triển khai Java hiệu quả được đề xuất bởi dmeister, bạn có thể sử dụng lệnh gọi thư viện thay vì tự thực hiện:
@Override
public int hashCode() {
return Objects.hashCode(this.firstName, this.lastName);
}
Điều này đòi hỏi Guava ( com.google.common.base.Objects.hashCode
) hoặc thư viện chuẩn trong Java 7 ( java.util.Objects.hash
) nhưng hoạt động theo cùng một cách.
hashCode
là nếu bạn có một tùy chỉnh equals
và đó chính xác là những gì các phương thức thư viện này được thiết kế cho. Các tài liệu khá rõ ràng về hành vi của họ liên quan đến equals
. Việc triển khai thư viện không yêu cầu bạn miễn trừ việc biết các đặc điểm của hashCode
việc triển khai chính xác là gì - những thư viện này giúp bạn dễ dàng thực hiện việc triển khai tuân thủ như vậy đối với phần lớn các trường hợp equals
bị chồng chéo.
java.util.Objects.hash(...)
phương thức JDK7 hơn là com.google.common.base.Objects.hashCode(...)
phương pháp ổi . Tôi nghĩ rằng hầu hết mọi người sẽ chọn thư viện tiêu chuẩn thay vì phụ thuộc thêm.
hashCode()
đối với một mảng chỉ là của nó java.lang.System.identityHashCode(...)
.
Tốt hơn là sử dụng chức năng do Eclipse cung cấp, một công việc khá tốt và bạn có thể nỗ lực và năng lượng của mình để phát triển logic kinh doanh.
Mặc dù điều này được liên kết với Android
tài liệu (Wayback Machine) và mã riêng của tôi trên Github , nhưng nó sẽ hoạt động cho Java nói chung. Câu trả lời của tôi là phần mở rộng của Câu trả lời của dmeister chỉ với mã dễ đọc và dễ hiểu hơn nhiều.
@Override
public int hashCode() {
// Start with a non-zero constant. Prime is preferred
int result = 17;
// Include a hash for each field.
// Primatives
result = 31 * result + (booleanField ? 1 : 0); // 1 bit » 32-bit
result = 31 * result + byteField; // 8 bits » 32-bit
result = 31 * result + charField; // 16 bits » 32-bit
result = 31 * result + shortField; // 16 bits » 32-bit
result = 31 * result + intField; // 32 bits » 32-bit
result = 31 * result + (int)(longField ^ (longField >>> 32)); // 64 bits » 32-bit
result = 31 * result + Float.floatToIntBits(floatField); // 32 bits » 32-bit
long doubleFieldBits = Double.doubleToLongBits(doubleField); // 64 bits (double) » 64-bit (long) » 32-bit (int)
result = 31 * result + (int)(doubleFieldBits ^ (doubleFieldBits >>> 32));
// Objects
result = 31 * result + Arrays.hashCode(arrayField); // var bits » 32-bit
result = 31 * result + referenceField.hashCode(); // var bits » 32-bit (non-nullable)
result = 31 * result + // var bits » 32-bit (nullable)
(nullableReferenceField == null
? 0
: nullableReferenceField.hashCode());
return result;
}
BIÊN TẬP
Thông thường, khi bạn ghi đè hashcode(...)
, bạn cũng muốn ghi đè equals(...)
. Vì vậy, đối với những người sẽ hoặc đã thực hiện equals
, đây là một tài liệu tham khảo tốt từ Github của tôi ...
@Override
public boolean equals(Object o) {
// Optimization (not required).
if (this == o) {
return true;
}
// Return false if the other object has the wrong type, interface, or is null.
if (!(o instanceof MyType)) {
return false;
}
MyType lhs = (MyType) o; // lhs means "left hand side"
// Primitive fields
return booleanField == lhs.booleanField
&& byteField == lhs.byteField
&& charField == lhs.charField
&& shortField == lhs.shortField
&& intField == lhs.intField
&& longField == lhs.longField
&& floatField == lhs.floatField
&& doubleField == lhs.doubleField
// Arrays
&& Arrays.equals(arrayField, lhs.arrayField)
// Objects
&& referenceField.equals(lhs.referenceField)
&& (nullableReferenceField == null
? lhs.nullableReferenceField == null
: nullableReferenceField.equals(lhs.nullableReferenceField));
}
Trước tiên hãy chắc chắn rằng bằng được thực hiện chính xác. Từ một bài viết IBM DeveloperWorks :
- Đối xứng: Cho hai tham chiếu, a và b, a.equals (b) khi và chỉ khi b.equals (a)
- Tính phản xạ: Đối với tất cả các tham chiếu không null, a.equals (a)
- Độ biến đổi: Nếu a.equals (b) và b.equals (c), thì a.equals (c)
Sau đó, đảm bảo rằng mối quan hệ của họ với hashCode tôn trọng liên hệ (từ cùng một bài viết):
- Tính nhất quán với hashCode (): Hai đối tượng bằng nhau phải có cùng giá trị hashCode ()
Cuối cùng, một hàm băm tốt nên cố gắng tiếp cận hàm băm lý tưởng .
about8.blogspot.com, bạn đã nói
if equals () trả về true cho hai đối tượng, thì hashCode () sẽ trả về cùng một giá trị. Nếu bằng () trả về false, thì hashCode () sẽ trả về các giá trị khác nhau
Tôi không thể đồng ý với bạn. Nếu hai đối tượng có cùng mã băm thì không có nghĩa là chúng bằng nhau.
Nếu A bằng B thì A.hashcode phải bằng B.hascode
nhưng
nếu A.hashcode bằng B.hascode thì không có nghĩa là A phải bằng B
(A != B) and (A.hashcode() == B.hashcode())
, đó là những gì chúng ta gọi là va chạm hàm băm. Đó là vì tên miền của hàm băm luôn hữu hạn, trong khi tên miền thường thì không. Tên miền càng lớn thì càng ít xảy ra va chạm. Hàm băm tốt sẽ trả về các giá trị băm khác nhau cho các đối tượng khác nhau với khả năng lớn nhất có thể đạt được với kích thước tên miền cụ thể. Nó hiếm khi có thể được đảm bảo đầy đủ mặc dù.
Nếu bạn sử dụng nhật thực, bạn có thể tạo equals()
và hashCode()
sử dụng:
Nguồn -> Tạo hashCode () và bằng ().
Sử dụng hàm này, bạn có thể quyết định trường nào bạn muốn sử dụng để tính toán bằng mã và hàm băm và Eclipse tạo ra các phương thức tương ứng.
Có một thực hiện tốt các hiệu quả Java 's hashcode()
và equals()
logic trong Apache Commons Lang . Thanh toán HashCodeBuilder và EqualsBuilder .
Objects
lớp cung cấp hash(Object ..args)
& equals()
phương thức từ Java7 trở đi. Chúng được khuyến nghị cho mọi ứng dụng sử dụng jdk 1.7+
IdentityHashMap
). FWIW Tôi sử dụng mã băm dựa trên id và bằng với tất cả các thực thể.
Chỉ cần một lưu ý nhanh để hoàn thành câu trả lời chi tiết khác (về mã):
Nếu tôi xem xét câu hỏi how-do-i- created -a-hash-table-in-java và đặc biệt là mục FAQ của jGuru , tôi tin rằng một số tiêu chí khác mà mã băm có thể được đánh giá là:
Nếu tôi hiểu chính xác câu hỏi của bạn, bạn có một lớp bộ sưu tập tùy chỉnh (nghĩa là một lớp mới mở rộng từ giao diện Bộ sưu tập) và bạn muốn thực hiện phương thức hashCode ().
Nếu lớp bộ sưu tập của bạn mở rộng AbstractList, thì bạn không phải lo lắng về nó, đã có một triển khai của Equals () và hashCode () hoạt động bằng cách lặp qua tất cả các đối tượng và thêm hashCodes () của chúng với nhau.
public int hashCode() {
int hashCode = 1;
Iterator i = iterator();
while (i.hasNext()) {
Object obj = i.next();
hashCode = 31*hashCode + (obj==null ? 0 : obj.hashCode());
}
return hashCode;
}
Bây giờ nếu những gì bạn muốn là cách tốt nhất để tính mã băm cho một lớp cụ thể, tôi thường sử dụng toán tử ^ (bitwise độc quyền hoặc) để xử lý tất cả các trường mà tôi sử dụng trong phương thức bằng:
public int hashCode(){
return intMember ^ (stringField != null ? stringField.hashCode() : 0);
}
@ about8: có một lỗi khá nghiêm trọng đấy.
Zam obj1 = new Zam("foo", "bar", "baz");
Zam obj2 = new Zam("fo", "obar", "baz");
cùng mã băm
bạn có thể muốn một cái gì đó như
public int hashCode() {
return (getFoo().hashCode() + getBar().hashCode()).toString().hashCode();
.
foo
và bar
dẫn đến cùng hashCode
. toString
AFAIK của bạn không biên dịch, và nếu có, thì nó không hiệu quả. Một cái gì đó như 109 * getFoo().hashCode() + 57 * getBar().hashCode()
là nhanh hơn, đơn giản hơn và không tạo ra va chạm không cần thiết.
Sử dụng các phương thức phản chiếu trên Apache Commons EqualsBuilder và HashCodeBuilder .
Tôi sử dụng một trình bao bọc nhỏ xung quanh Arrays.deepHashCode(...)
vì nó xử lý các mảng được cung cấp dưới dạng tham số chính xác
public static int hash(final Object... objects) {
return Arrays.deepHashCode(objects);
}
bất kỳ phương pháp băm nào phân phối đồng đều giá trị băm trong phạm vi có thể là một triển khai tốt. Xem java hiệu quả ( http://books.google.com.au/books?id=ZZOiqZQIbRMC&dq=effective+java&pg=PP1&ots=UZMZ2siN25&sig=kR0n73DHJOn-D77qGj0wOxAxiZw&hl=en&sa=X&oi=book_result&resnum=1&ct=result ), có một mẹo tốt trong đó để thực hiện mã băm (mục 9 tôi nghĩ ...).
Dưới đây là một trình diễn tiếp cận JDK 1.7+ khác với các logic siêu lớp được hạch toán. Tôi thấy nó khá thuận tiện với tài khoản hashCode () của lớp Object, phụ thuộc JDK thuần túy và không có công việc thủ công thêm. Xin lưu ýObjects.hash()
là không khoan dung.
Tôi không bao gồm bất kỳ equals()
thực hiện nhưng trong thực tế, bạn tất nhiên sẽ cần nó.
import java.util.Objects;
public class Demo {
public static class A {
private final String param1;
public A(final String param1) {
this.param1 = param1;
}
@Override
public int hashCode() {
return Objects.hash(
super.hashCode(),
this.param1);
}
}
public static class B extends A {
private final String param2;
private final String param3;
public B(
final String param1,
final String param2,
final String param3) {
super(param1);
this.param2 = param2;
this.param3 = param3;
}
@Override
public final int hashCode() {
return Objects.hash(
super.hashCode(),
this.param2,
this.param3);
}
}
public static void main(String [] args) {
A a = new A("A");
B b = new B("A", "B", "C");
System.out.println("A: " + a.hashCode());
System.out.println("B: " + b.hashCode());
}
}
Việc thực hiện tiêu chuẩn là yếu và sử dụng nó dẫn đến va chạm không cần thiết. Hãy tưởng tượng một
class ListPair {
List<Integer> first;
List<Integer> second;
ListPair(List<Integer> first, List<Integer> second) {
this.first = first;
this.second = second;
}
public int hashCode() {
return Objects.hashCode(first, second);
}
...
}
Hiện nay,
new ListPair(List.of(a), List.of(b, c))
và
new ListPair(List.of(b), List.of(a, c))
có cùng hashCode
, cụ thể 31*(a+b) + c
là số nhân được sử dụng choList.hashCode
được tái sử dụng ở đây. Rõ ràng, va chạm là không thể tránh khỏi, nhưng tạo ra va chạm không cần thiết chỉ là ... không cần thiết.
Không có gì đáng kể thông minh về việc sử dụng 31
. Số nhân phải là số lẻ để tránh mất thông tin (bất kỳ số nhân chẵn nào cũng mất ít nhất là bit đáng kể nhất, bội số của bốn mất hai, v.v.). Bất kỳ số nhân lẻ đều có thể sử dụng. Số nhân nhỏ có thể dẫn đến tính toán nhanh hơn (JIT có thể sử dụng dịch chuyển và bổ sung), nhưng với điều kiện nhân có độ trễ chỉ ba chu kỳ trên Intel / AMD hiện đại, điều này hầu như không thành vấn đề. Số nhân nhỏ cũng dẫn đến xung đột nhiều hơn cho đầu vào nhỏ, đôi khi có thể là một vấn đề.
Sử dụng một số nguyên tố là vô nghĩa vì các số nguyên tố không có ý nghĩa trong vòng Z / (2 ** 32).
Vì vậy, tôi khuyên bạn nên sử dụng một số lẻ lớn được chọn ngẫu nhiên (thoải mái lấy số nguyên tố). Vì CPU i86 / amd64 có thể sử dụng một lệnh ngắn hơn cho các toán hạng phù hợp trong một byte đã ký, có một lợi thế tốc độ nhỏ cho các số nhân như 109. Để giảm thiểu va chạm, hãy lấy thứ gì đó như 0x58a54cf5.
Sử dụng các số nhân khác nhau ở những nơi khác nhau là hữu ích, nhưng có lẽ không đủ để biện minh cho công việc bổ sung.
Khi kết hợp các giá trị băm, tôi thường sử dụng phương thức kết hợp được sử dụng trong thư viện boost c ++, cụ thể là:
seed ^= hasher(v) + 0x9e3779b9 + (seed<<6) + (seed>>2);
Điều này làm một công việc khá tốt để đảm bảo phân phối đồng đều. Để biết một số thảo luận về cách thức hoạt động của công thức này, hãy xem bài đăng StackOverflow: Số ma thuật trong boost :: hash_combine
Có một cuộc thảo luận tốt về các hàm băm khác nhau tại: http://burtleburtle.net/bob/hash/doobs.html
Đối với một lớp đơn giản, thường dễ thực hiện hàm hashCode () dựa trên các trường lớp được kiểm tra bằng cách thực hiện bằng ().
public class Zam {
private String foo;
private String bar;
private String somethingElse;
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
Zam otherObj = (Zam)obj;
if ((getFoo() == null && otherObj.getFoo() == null) || (getFoo() != null && getFoo().equals(otherObj.getFoo()))) {
if ((getBar() == null && otherObj. getBar() == null) || (getBar() != null && getBar().equals(otherObj. getBar()))) {
return true;
}
}
return false;
}
public int hashCode() {
return (getFoo() + getBar()).hashCode();
}
public String getFoo() {
return foo;
}
public String getBar() {
return bar;
}
}
Điều quan trọng nhất là giữ cho hashCode () và bằng () nhất quán: nếu bằng () trả về true cho hai đối tượng, thì hashCode () sẽ trả về cùng một giá trị. Nếu bằng () trả về false, thì hashCode () sẽ trả về các giá trị khác nhau.
("abc"+""=="ab"+"c"=="a"+"bc"==""+"abc")
. Đó là lỗ hổng nghiêm trọng. Sẽ tốt hơn khi đánh giá mã băm cho cả hai trường và sau đó tính toán kết hợp tuyến tính của chúng (tốt nhất là sử dụng các số nguyên tố làm hệ số).
foo
và bar
tạo ra một sự va chạm không cần thiết, quá.
Objects.hashCode(collection)
nên là một giải pháp hoàn hảo!