Java: tại sao các bộ sưu tập chấp nhận Bộ so sánh nhưng không (giả thuyết) Hasher và Equator?


25

Vấn đề này là rõ ràng nhất khi bạn có các triển khai giao diện khác nhau và cho các mục đích của một bộ sưu tập cụ thể, bạn chỉ quan tâm đến chế độ xem giao diện của các đối tượng. Ví dụ: giả sử bạn có một giao diện như thế này:

public interface Person {
    int getId();
}

Cách thông thường để thực hiện hashcode()equals()trong các lớp triển khai sẽ có mã như thế này trong equalsphương thức:

if (getClass() != other.getClass()) {
    return false;
}

Điều này gây ra vấn đề khi bạn kết hợp thực hiện Persontrong a HashMap. Nếu HashMapchỉ quan tâm đến chế độ xem cấp độ giao diện Person, thì nó có thể kết thúc với các bản sao chỉ khác nhau trong các lớp triển khai của chúng.

Bạn có thể làm cho trường hợp này hoạt động bằng cách sử dụng cùng một equals()phương thức tự do cho tất cả các triển khai, nhưng sau đó bạn có nguy cơ equals()làm sai trong một ngữ cảnh khác (chẳng hạn như so sánh hai Persons được hỗ trợ bởi các bản ghi cơ sở dữ liệu với số phiên bản).

Trực giác của tôi nói với tôi rằng sự bình đẳng nên được xác định trên mỗi bộ sưu tập thay vì mỗi lớp. Khi sử dụng các bộ sưu tập dựa trên thứ tự, bạn có thể sử dụng một tùy chỉnh Comparatorđể chọn đúng thứ tự trong từng bối cảnh. Không có tương tự cho các bộ sưu tập dựa trên băm. Tại sao lại thế này?

Chỉ cần làm rõ, câu hỏi này khác với " Tại sao .compareTo () trong một giao diện trong khi .equals () nằm trong một lớp trong Java? " Bởi vì nó liên quan đến việc triển khai các bộ sưu tập. compareTo()equals()/ hashcode()cả hai đều gặp phải vấn đề về tính phổ quát khi sử dụng các bộ sưu tập: bạn không thể chọn các chức năng so sánh khác nhau cho các bộ sưu tập khác nhau. Vì vậy, đối với mục đích của câu hỏi này, hệ thống phân cấp thừa kế của một đối tượng hoàn toàn không thành vấn đề; tất cả vấn đề là liệu hàm so sánh được xác định theo từng đối tượng hay mỗi bộ sưu tập.


5
Bạn luôn có thể giới thiệu các đối tượng trình bao bọc để Personthực hiện dự kiến equalshashCodehành vi. Sau đó bạn sẽ có một HashMap<PersonWrapper, V>. Đây là một ví dụ trong đó cách tiếp cận OOP thuần túy không thanh lịch: không phải mọi thao tác trên một đối tượng đều có ý nghĩa như một phương thức của đối tượng đó. Toàn bộ Java Objectloại là một hỗn hợp của các trách nhiệm khác nhau - chỉ getClass, finalizetoStringphương pháp dường như từ xa chính đáng bằng cách thực hành tốt nhất hiện nay.
amon

1
1) Trong C #, bạn có thể chuyển một IEqualityComparer<T>bộ sưu tập dựa trên hàm băm. Nếu bạn không chỉ định một, nó sẽ sử dụng triển khai mặc định dựa trên Object.EqualsObject.GetHashCode(). 2) IMO ghi đè Equalslên một loại tham chiếu có thể thay đổi hiếm khi là một ý tưởng tốt. Bằng cách đó, sự bình đẳng mặc định là khá nghiêm ngặt, nhưng bạn có thể sử dụng quy tắc bình đẳng thoải mái hơn khi bạn cần nó thông qua một tùy chỉnh IEqualityComparer<T>.
CodeInChaos

Câu trả lời:


23

Thiết kế này đôi khi được gọi là "Bình đẳng phổ quát", người ta tin rằng hai thứ có bằng nhau hay không là một tài sản chung.

Hơn nữa, đẳng thức là một thuộc tính của hai đối tượng, nhưng trong OO, bạn luôn gọi một phương thức trên một đối tượng và đối tượng đó chỉ quyết định cách xử lý cuộc gọi phương thức đó. Vì vậy, trong một thiết kế như Java, nơi bình đẳng là một tài sản của một trong hai đối tượng được so sánh, nó không phải là thậm chí có thể đảm bảo một số tính chất cơ bản của bình đẳng như đối xứng ( a == bb == a), bởi vì trong trường hợp đầu tiên, phương pháp này đang được kêu gọi avà trong trường hợp thứ hai, nó đang được gọi bvà do các nguyên tắc cơ bản của OO, đó là aquyết định của riêng họ (trong trường hợp đầu tiên) hoặcbQuyết định của bạn (trong trường hợp thứ hai) liệu nó có tự coi mình bằng với người khác hay không. Cách duy nhất để đạt được sự đối xứng là khiến hai đối tượng hợp tác, nhưng nếu họ không gặp may mắn.

Một giải pháp sẽ là làm cho sự bình đẳng không phải là một thuộc tính của một đối tượng, mà là một thuộc tính của hai đối tượng hoặc một thuộc tính của đối tượng thứ ba. Tùy chọn thứ hai đó cũng giải quyết vấn đề về đẳng thức phổ quát, bởi vì nếu bạn biến sự bình đẳng thành một thuộc tính của một đối tượng "bối cảnh" thứ ba, thì bạn có thể tưởng tượng có các EqualityComparerđối tượng khác nhau cho các bối cảnh khác nhau.

Đây thiết kế được chọn cho Haskell, ví dụ, với kiểu chữ Eq. Đây cũng là thiết kế được lựa chọn bởi một số thư viện Scala của bên thứ ba (ví dụ ScalaZ), nhưng không phải là lõi Scala hoặc thư viện chuẩn, sử dụng tính công bằng phổ quát để tương thích với nền tảng máy chủ bên dưới.

Thật thú vị, đây cũng là thiết kế được chọn với các giao diện Comparable/ Java Comparator. Các nhà thiết kế của Java rõ ràng đã nhận thức được vấn đề, nhưng vì một số lý do chỉ giải quyết nó để đặt hàng chứ không phải cho sự bình đẳng (hoặc băm).

Vì vậy, như câu hỏi

Tại sao có một Comparatorgiao diện nhưng không HasherEquator?

câu trả lời là "Tôi không biết". Rõ ràng, các nhà thiết kế của Java đã nhận thức được vấn đề, bằng chứng là sự tồn tại của nó Comparator, nhưng rõ ràng họ không nghĩ đó là vấn đề cho sự bình đẳng và băm. Các ngôn ngữ và thư viện khác đưa ra các lựa chọn khác nhau.


7
+1, nhưng lưu ý rằng có các ngôn ngữ OO tồn tại nhiều công văn (Smalltalk, Common Lisp). Vì vậy, luôn luôn quá mạnh trong câu sau: "trong OO, bạn luôn gọi một phương thức trên một đối tượng duy nhất".
coredump

Tôi đã tìm thấy trích dẫn mà tôi đang tìm kiếm; theo JLS 1.0, The methods equals and hashCode are declared for the benefit of hashtables such as java.util.Hashtabletức là cả hai equalshashCodeđược giới thiệu là Objectphương thức của các nhà phát triển Java chỉ vì mục đích Hashtable- không có khái niệm về UE hay bất cứ thứ gì silimar ở bất cứ đâu trong thông số kỹ thuật và trích dẫn là đủ rõ ràng đối với tôi; nếu không phải vì Hashtable, equalscó lẽ đã ở trong một giao diện như thế nào Comparable. Như vậy, trong khi trước đây tôi tin rằng câu trả lời của bạn là chính xác, thì bây giờ tôi cho rằng nó không có căn cứ.
vaxquis

@ JörgWMittag đó là một lỗi đánh máy, IFTFY. BTW, nói về clone- ban đầu nó là một toán tử , không phải là một phương thức (xem Đặc tả ngôn ngữ Oak), trích dẫn: The unary operator clone is applied to an object. (...) The clone operator is normally used inside new to clone the prototype of some class, before applying the initializers (constructors)- ba toán tử giống như từ khóa là instanceof new clone(phần 8.1, toán tử). Tôi cho rằng đó là lý do thực sự (lịch sử) của clone/ Cloneablemess - Cloneablechỉ đơn giản là một phát minh sau này và clonemã hiện tại được trang bị thêm với nó.
vaxquis

2
"Đây là thiết kế được chọn cho Haskell, ví dụ, với kiểu chữ Eq" Đây là loại đúng, nhưng đáng chú ý là Haskell tuyên bố rõ ràng rằng hai đối tượng thuộc các loại khác nhau không bao giờ bằng nhau trong khi cách tiếp cận của Java không bao giờ bằng nhau. Do đó, hoạt động bình đẳng là một phần của loại , (do đó "typeclass") không phải là một phần của giá trị ngữ cảnh thứ ba.
Jack

19

Câu trả lời thực sự cho

Tại sao có một Comparatorgiao diện nhưng không HasherEquator?

là, trích dẫn lịch sự của Josh Bloch :

Các API Java ban đầu được thực hiện rất nhanh theo thời hạn chặt chẽ để đáp ứng một cửa sổ thị trường đóng cửa. Nhóm Java ban đầu đã làm một công việc đáng kinh ngạc, nhưng không phải tất cả các API đều hoàn hảo.

Vấn đề chỉ nằm trong lịch sử của Java, như với các vấn đề tương tự khác, ví dụ .clone()so với Cloneable.

tl; dr

đó là vì lý do lịch sử là chủ yếu; hành vi / trừu tượng hiện tại đã được giới thiệu trong JDK 1.0 và sau đó không được sửa chữa vì hầu như không thể làm như vậy với việc duy trì khả năng tương thích mã ngược.


Đầu tiên, hãy tổng hợp một vài sự kiện Java nổi tiếng:

  1. Java, từ khi bắt đầu cho đến ngày nay, đã tự hào tương thích ngược, yêu cầu các API kế thừa vẫn được hỗ trợ trong các phiên bản mới hơn,
  2. như vậy, gần như mọi cấu trúc ngôn ngữ được giới thiệu với JDK 1.0 tồn tại cho đến ngày nay,
  3. Hashtable, .hashCode()& .equals()đã được triển khai trong JDK 1.0, ( Hashtable )
  4. Comparable/ Comparatorđã được giới thiệu trong JDK 1.2 ( Có thể so sánh ),

Bây giờ, nó theo sau:

  1. Hầu như không thể và vô nghĩa khi trang bị thêm .hashCode().equals()cho các giao diện riêng biệt trong khi vẫn duy trì khả năng tương thích ngược sau khi mọi người nhận ra có sự trừu tượng hóa tốt hơn so với việc đưa chúng vào superobject, bởi vì, ví dụ như mọi người lập trình Java đều biết rằng mọi người đều Objectcó chúng và họ đã có để duy trì ở đó một cách vật lý để cung cấp khả năng tương thích mã được biên dịch (JVM) - và thêm một giao diện rõ ràng cho mọi Objectlớp con thực sự triển khai chúng sẽ làm cho mớ hỗn độn này (sic!) thành Clonablemột ( Bloch thảo luận về lý do tại sao Clonizable , cũng được thảo luận trong ví dụ EJ 2nd và nhiều nơi khác, bao gồm SO),
  2. họ chỉ để chúng ở đó cho thế hệ tương lai có nguồn WTF liên tục.

Bây giờ, bạn có thể hỏi "những gì Hashtablecó với tất cả điều này"?

Câu trả lời là: hashCode()/ equals()hợp đồng và các kỹ năng thiết kế ngôn ngữ không tốt của các nhà phát triển Java cốt lõi vào năm 1995/1996.

Trích dẫn từ Spec 1.0 Language Spec, ngày 1996 - 4.3.2 Lớp Object, tr.41:

Các phương thức equalshashCodeđược khai báo vì lợi ích của hashtables, chẳng hạn như java.util.Hashtable(§21.7). Phương thức bằng xác định một khái niệm về đẳng thức đối tượng, dựa trên giá trị, không tham chiếu, so sánh.

(lưu ý tuyên bố này chính xác đã được thay đổi trong các phiên bản sau này, để nói, trích dẫn: The method hashCode is very useful, together with the method equals, in hashtables such as java.util.HashMap., làm cho nó không thể thực hiện trực tiếp Hashtable- hashCode- equalskết nối mà không đọc JLS lịch sử!)

Nhóm Java đã quyết định họ muốn có một bộ sưu tập kiểu từ điển tốt và họ đã tạo ra Hashtable(ý tưởng tốt cho đến nay), nhưng họ muốn lập trình viên có thể sử dụng nó với càng ít mã / đường cong học tập càng tốt (rất tiếc! và, vì vẫn chưa có chung chung [đó là JDK 1.0], điều đó có nghĩa là mọi người Object được đưa vào Hashtablesẽ phải thực hiện một cách rõ ràng một số giao diện (và các giao diện vẫn chỉ mới bắt đầu từ lúc đó ... Comparablethậm chí còn chưa!) , làm cho điều này trở thành một biện pháp ngăn chặn để sử dụng nó cho nhiều người - hoặc Objectsẽ phải ngầm thực hiện một số phương pháp băm.

Rõ ràng, họ đã đi với giải pháp 2, vì những lý do đã nêu ở trên. Yup, bây giờ chúng tôi biết họ đã sai. ... thật dễ dàng để trở nên thông minh trong nhận thức muộn màng. cười thầm

Bây giờ, hashCode() yêu cầu mọi đối tượng có nó phải có một equals()phương thức riêng biệt - vì vậy nó cũng khá rõ ràng equals()phải được đưa vào Object.

Kể từ khi mặc định triển khai những phương pháp trên có hiệu lực a& b Objects cơ bản là vô dụng bằng cách dự phòng (làm a.equals(b) bằng đến a==ba.hashCode() == b.hashCode() xấp xỉ bằng để a==bcũng có, trừ khi hashCodevà / hoặc equalsđược overriden, hoặc bạn GC hàng trăm ngàn Objects trong vòng đời của ứng dụng của bạn 1 ) , thật an toàn khi nói rằng chúng được cung cấp chủ yếu như một biện pháp dự phòng và để thuận tiện cho việc sử dụng. Đây chính xác là cách chúng ta có được một thực tế nổi tiếng là luôn ghi đè cả hai .equals()& .hashCode()nếu bạn có ý định thực sự so sánh các đối tượng hoặc lưu trữ băm chúng. Chỉ ghi đè một trong số chúng mà không có cái kia là một cách tốt để vặn mã của bạn (bằng cách so sánh kết quả xấu hoặc giá trị va chạm xô cực kỳ cao) - và nhận được xung quanh nó là một nguồn gây nhầm lẫn & lỗi liên tục cho người mới bắt đầu (tìm kiếm SO để xem nó cho chính mình) và phiền toái thường xuyên cho những người dày dạn hơn.

Ngoài ra, lưu ý rằng mặc dù C # xử lý bằng và mã băm theo cách tốt hơn một chút, bản thân Eric Lippert nói rằng họ đã mắc lỗi gần như tương tự với C # mà Sun đã làm với Java nhiều năm trước khi thành lập C # :

Nhưng tại sao mọi đối tượng nên có thể tự băm để chèn vào bảng băm? Có vẻ như một điều kỳ lạ để yêu cầu mọi đối tượng có thể làm. Tôi nghĩ rằng nếu chúng ta thiết kế lại hệ thống loại từ đầu ngày hôm nay, việc băm có thể được thực hiện khác đi, có lẽ với một IHashablegiao diện. Nhưng khi hệ thống loại CLR được thiết kế, không có loại chung và do đó cần có bảng băm mục đích chung để có thể lưu trữ bất kỳ đối tượng nào.

Tất nhiên, 1Object#hashCode vẫn có thể va chạm, nhưng phải mất một chút nỗ lực để làm điều đó, hãy xem: http://bugs.java.com/bugdatabase/view_orms.do?orms_id=6809470 và báo cáo lỗi được liên kết để biết chi tiết; /programming/1381060/hashcode-uniquety/1381114#1381114 bao quát chủ đề này sâu hơn.


Tuy nhiên, đó không chỉ là Java. Nhiều người cùng thời (Ruby, Python, Mạnh) và người tiền nhiệm của nó (Smalltalk, HP) và một số người kế nhiệm của nó cũng có tính bình đẳng phổ quát và tính băm phổ quát (đó có phải là một từ không?).
Jörg W Mittag

@ JörgWMittag xem lập trình viên.stackexchange.com/questions/283194/iêu - Tôi không đồng ý về "UE" trong Java; UE trong lịch sử không bao giờ là một mối quan tâm thực sự trong Objectthiết kế của; khả năng băm là.
vaxquis

@vaxquis Tôi không muốn harp về điều này, nhưng nhận xét trước đây của tôi cho thấy hai đối tượng có thể truy cập đồng thời có thể có cùng mã băm (mặc định).
Phục hồi lại

1
@vaxquis OK. Tôi mua cái đó Mối quan tâm của tôi là ai đó đang học sẽ thấy điều này và nghĩ rằng họ thông minh bằng cách sử dụng mã băm Hệ thống thay vì bằng, v.v. Nếu họ làm điều đó, nó có thể sẽ hoạt động đủ tốt trừ những lần hiếm hoi không có và sẽ có không có cách nào để tái tạo vấn đề một cách đáng tin cậy
JimmyJames

1
Đây phải là câu trả lời được chấp nhận, vì kết luận của câu trả lời được chấp nhận là "tôi không biết"
Phoenix
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.