Các lý do tại sao Map.get (Khóa đối tượng) không chung chung (đầy đủ)


405

Các lý do đằng sau quyết định không có một phương thức get hoàn toàn chung chung trong giao diện của java.util.Map<K, V>.

Để làm rõ câu hỏi, chữ ký của phương pháp là

V get(Object key)

thay vì

V get(K key)

và tôi tự hỏi tại sao (điều tương tự cho remove, containsKey, containsValue).


3
Câu hỏi tương tự liên quan đến Bộ sưu tập: stackoverflow.com/questions/104799/
Mạnh


1
Kinh ngạc. Tôi đang sử dụng Java từ hơn 20 năm nay và hôm nay tôi nhận ra vấn đề này.
GhostCat

Câu trả lời:


260

Như đã đề cập bởi những người khác, lý do tại sao get(), v.v. không chung chung vì khóa của mục bạn đang truy xuất không phải cùng loại với đối tượng mà bạn chuyển đến get(); đặc tả của phương thức chỉ yêu cầu chúng bằng nhau. Điều này xuất phát từ cách equals()phương thức lấy một đối tượng làm tham số, không chỉ cùng loại với đối tượng.

Mặc dù có thể đúng là nhiều lớp đã equals()định nghĩa sao cho các đối tượng của nó chỉ có thể bằng với các đối tượng của lớp của chính nó, có nhiều nơi trong Java không phải là trường hợp này. Ví dụ, đặc tả cho List.equals()biết hai đối tượng List bằng nhau nếu cả hai đều là List và có cùng nội dung, ngay cả khi chúng là các cách triển khai khác nhau List. Vì vậy, quay trở lại ví dụ trong câu hỏi này, theo đặc điểm kỹ thuật của phương thức có thể có một Map<ArrayList, Something>và để tôi gọi get()với một LinkedListđối số là, và nó sẽ lấy ra khóa là một danh sách có cùng nội dung. Điều này sẽ không thể xảy ra nếu get()chung chung và hạn chế loại đối số của nó.


28
Vậy thì tại sao lại V Get(K k)ở C #?

134
Câu hỏi là, nếu bạn muốn gọi m.get(linkedList), tại sao bạn không xác định mloại là Map<List,Something>? Tôi không thể nghĩ ra một usecase khi gọi m.get(HappensToBeEqual)mà không thay đổi Maploại để có giao diện hợp lý.
Elazar Leibovich

58
Wow, lỗ hổng thiết kế nghiêm trọng. Bạn không nhận được cảnh báo trình biên dịch, vặn vẹo. Tôi đồng ý với Elazar. Nếu điều này thực sự hữu ích, điều mà tôi nghi ngờ xảy ra thường xuyên, thì getByEquals (Khóa đối tượng) nghe có vẻ hợp lý hơn ...
mmm

37
Quyết định này có vẻ như được đưa ra trên cơ sở thuần túy về mặt lý thuyết hơn là thực tiễn. Đối với phần lớn các cách sử dụng, các nhà phát triển muốn thấy đối số bị giới hạn bởi loại mẫu hơn là không giới hạn để hỗ trợ các trường hợp cạnh như câu hỏi được đề cập bởi newacct trong câu trả lời của anh ta. Để lại các chữ ký không templated tạo ra nhiều vấn đề hơn nó giải quyết.
Sam Goldberg

14
@newacct: Loại hoàn toàn an toàn là một yêu cầu mạnh mẽ đối với một cấu trúc có thể thất bại không thể đoán trước khi chạy. Đừng thu hẹp tầm nhìn của bạn vào các bản đồ băm xảy ra với điều đó. TreeMapcó thể thất bại khi bạn chuyển các đối tượng sai loại cho getphương thức nhưng đôi khi có thể vượt qua, ví dụ khi bản đồ xảy ra trống. Và thậm chí tệ hơn, trong trường hợp của một cung cấp Comparatorcác comparephương pháp (trong đó có một chữ ký chung!) Có thể được gọi với đối số của các loại sai mà không cần bất kỳ cảnh báo không được kiểm soát. Đây hành vi bị hỏng.
Holger

105

Một lập trình viên Java tuyệt vời tại Google, Kevin Bourrillion, đã viết về chính xác vấn đề này trong một bài đăng trên blog cách đây một thời gian (phải thừa nhận trong bối cảnh Setthay vì Map). Câu liên quan nhất:

Đồng nhất, các phương thức của Khung bộ sưu tập Java (và Thư viện bộ sưu tập của Google cũng vậy) không bao giờ hạn chế các loại tham số của chúng trừ khi cần thiết để ngăn bộ sưu tập bị hỏng.

Tôi không hoàn toàn chắc chắn rằng tôi đồng ý với nguyên tắc này - .NET dường như vẫn ổn khi yêu cầu đúng loại khóa, chẳng hạn - nhưng nó đáng để tuân theo lý luận trong bài đăng trên blog. (Đã đề cập đến .NET, đáng để giải thích rằng một phần lý do tại sao nó không phải là vấn đề trong .NET là có vấn đề lớn hơn trong .NET về phương sai hạn chế hơn ...)


4
Apocalisp: điều đó không đúng, tình hình vẫn như vậy.
Kevin Bourrillion

9
@ user102008 Không, bài viết không sai. Mặc dù một Integervà một Doublekhông bao giờ có thể bằng nhau, nhưng vẫn là một câu hỏi công bằng để hỏi liệu một Set<? extends Number>có chứa giá trị hay không new Integer(5).
Kevin Bourrillion

33
Tôi chưa bao giờ muốn kiểm tra tư cách thành viên trong a Set<? extends Foo>. Tôi đã rất thường xuyên thay đổi loại khóa của bản đồ và sau đó thất vọng vì trình biên dịch không thể tìm thấy tất cả những nơi mà mã cần cập nhật. Tôi thực sự không tin rằng đây là sự đánh đổi chính xác.
porculus

4
@EarthEngine: Nó luôn bị hỏng. Đó là toàn bộ vấn đề - mã bị hỏng, nhưng trình biên dịch không thể bắt được nó.
Jon Skeet

1
Và nó vẫn bị hỏng, và chỉ gây cho chúng tôi một lỗi ... câu trả lời tuyệt vời.
GhostCat

28

Hợp đồng được thể hiện như vậy:

Chính thức hơn, nếu bản đồ này chứa ánh xạ từ khóa k đến giá trị v sao cho (key == null? K == null: key.equals (k) ), thì phương thức này trả về v; nếu không nó trả về null. (Có thể có nhiều nhất một ánh xạ như vậy.)

(nhấn mạnh của tôi)

và như vậy, việc tra cứu khóa thành công phụ thuộc vào việc triển khai phương thức bình đẳng của khóa đầu vào. Điều đó không nhất thiết phụ thuộc vào lớp k.


4
Nó cũng phụ thuộc vào hashCode(). Nếu không có cách triển khai hashCode () thích hợp, thì việc triển khai độc đáo equals()sẽ khá vô dụng trong trường hợp này.
rudolfson

5
Tôi đoán, về nguyên tắc, điều này sẽ cho phép bạn sử dụng proxy nhẹ cho một khóa, nếu việc tạo lại toàn bộ khóa là không thực tế - miễn là bằng () và hashCode () được triển khai chính xác.
Bill Michell

5
@rudolfson: Theo như tôi biết, chỉ có một HashMap dựa vào mã băm để tìm đúng nhóm. Ví dụ, TreeMap sử dụng cây tìm kiếm nhị phân và không quan tâm đến hashCode ().
Cướp

4
Nói đúng ra, get()không cần phải tranh luận kiểu Objectđể thỏa mãn liên hệ. Hãy tưởng tượng phương thức get được giới hạn ở loại khóa K- hợp đồng vẫn sẽ có hiệu lực. Tất nhiên, việc sử dụng trong đó loại thời gian biên dịch không phải là một lớp con Kbây giờ sẽ không thể biên dịch, nhưng điều đó không làm mất hiệu lực hợp đồng, vì các hợp đồng ngầm thảo luận về những gì xảy ra nếu mã biên dịch.
BeeOnRope

16

Đó là một ứng dụng của Luật Postel, "hãy thận trọng trong những gì bạn làm, hãy tự do trong những gì bạn chấp nhận từ người khác."

Kiểm tra bình đẳng có thể được thực hiện bất kể loại; các equalsphương pháp được định nghĩa trên Objectlớp và chấp nhận bất kỳ Objectnhư một tham số. Vì vậy, nó có ý nghĩa đối với sự tương đương chính và các hoạt động dựa trên sự tương đương chính, để chấp nhận bất kỳ Objectloại nào .

Khi bản đồ trả về các giá trị chính, nó sẽ bảo tồn càng nhiều thông tin loại càng tốt, bằng cách sử dụng tham số loại.


4
Vậy thì tại sao lại V Get(K k)ở C #?

1
V Get(K k)ở C # vì nó cũng có ý nghĩa. Sự khác biệt giữa các cách tiếp cận Java và .NET thực sự chỉ là những người ngăn chặn những thứ không phù hợp. Trong C #, nó là trình biên dịch, trong Java là bộ sưu tập. Tôi giận dữ về các lớp học tập không phù hợp NET của một lần trong một thời gian, nhưng Get()Remove()chỉ chấp nhận một loại phù hợp chắc chắn ngăn cản bạn vô tình đi qua một giá trị sai trong.
Wormbo

26
Đó là một ứng dụng sai của Luật Postel. Hãy tự do trong những gì bạn chấp nhận từ người khác, nhưng đừng quá tự do. API ngu ngốc này có nghĩa là bạn không thể biết sự khác biệt giữa "không có trong bộ sưu tập" và "bạn đã mắc lỗi gõ tĩnh". Nhiều ngàn giờ lập trình viên bị mất có thể đã được ngăn chặn bằng get: K -> boolean.
Thẩm phán tâm thần

1
Tất nhiên điều đó nên có contains : K -> boolean.
Thẩm phán tâm thần


13

Tôi nghĩ phần này của Generics Tutorial giải thích tình huống (sự nhấn mạnh của tôi):

"Bạn cần chắc chắn rằng API chung không bị hạn chế quá mức, nó phải tiếp tục hỗ trợ hợp đồng ban đầu của API. Hãy xem xét lại một số ví dụ từ java.util.Collection. API tiền chung giống như:

interface Collection { 
  public boolean containsAll(Collection c);
  ...
}

Một nỗ lực ngây thơ để khái quát nó là:

interface Collection<E> { 
  public boolean containsAll(Collection<E> c);
  ...
}

Mặc dù điều này chắc chắn là an toàn, nhưng nó không tuân theo hợp đồng ban đầu của API. Phương thức chứa All () hoạt động với bất kỳ loại bộ sưu tập nào đến. Nó sẽ chỉ thành công nếu bộ sưu tập đến thực sự chỉ chứa các phiên bản của E, nhưng:

  • Kiểu tĩnh của bộ sưu tập đến có thể khác nhau, có lẽ vì người gọi không biết loại chính xác của bộ sưu tập được truyền vào hoặc có thể vì đó là Bộ sưu tập <S>, trong đó S là một kiểu con của E.
  • Việc gọi hàm ALL () với một bộ sưu tập thuộc loại khác là hoàn toàn hợp pháp. Các thói quen nên làm việc, trả lại sai. "

2
tại sao không containsAll( Collection< ? extends E > c ), sau đó?
Thẩm phán tâm thần

1
@JudgeMental, mặc dù không được đưa ra làm ví dụ ở trên nó cũng là cần thiết để cho phép containsAllvới một Collection<S>nơi Slà một siêu kiểu của E. Điều này sẽ không được phép nếu nó được containsAll( Collection< ? extends E > c ). Hơn nữa, như được nêu rõ trong ví dụ, việc thông qua một bộ sưu tập thuộc loại khác (với giá trị trả về là hợp pháp false).
davmac

Không cần thiết phải cho phép chứa All với bộ sưu tập siêu kiểu E. Tôi cho rằng cần phải không cho phép cuộc gọi đó bằng kiểm tra kiểu tĩnh để ngăn ngừa lỗi. Đó là một hợp đồng ngớ ngẩn, mà tôi nghĩ là điểm của câu hỏi ban đầu.
Thẩm phán tâm thần

6

Lý do là ngăn chặn được xác định bởi equalshashCodeđó là các phương thức trên Objectvà cả hai đều lấy Objecttham số. Đây là một lỗ hổng thiết kế ban đầu trong các thư viện tiêu chuẩn của Java. Cùng với các hạn chế trong hệ thống loại của Java, nó buộc mọi thứ liên quan đến bằng và hashCode phải thực hiện Object.

Cách duy nhất để có bảng băm kiểu an toàn và bình đẳng trong Java là để tránh né Object.equalsObject.hashCodevà sử dụng để thay thế chung. Java chức năng đi kèm với các lớp loại cho mục đích này: Hash<A>Equal<A>. Một trình bao bọc HashMap<K, V>được cung cấp có Hash<K>Equal<K>trong hàm tạo của nó. Do đó, các phương thức getvà lớp này containscó một đối số chung về kiểu K.

Thí dụ:

HashMap<String, Integer> h =
  new HashMap<String, Integer>(Equal.stringEqual, Hash.stringHash);

h.add("one", 1);

h.get("one"); // All good

h.get(Integer.valueOf(1)); // Compiler error

4
Bản thân điều này không ngăn loại 'get' được khai báo là "V get (khóa K)", vì 'Object' luôn là tổ tiên của K, vì vậy "key.hashCode ()" vẫn hợp lệ.
finnw

1
Trong khi nó không ngăn chặn nó, tôi nghĩ nó giải thích nó. Nếu họ chuyển phương thức đẳng thức thành lực lượng đẳng thức, chắc chắn họ không thể nói với mọi người rằng cơ chế cơ bản để định vị đối tượng trong bản đồ sử dụng hàm bằng () và hàm băm () khi các nguyên mẫu phương thức cho các phương thức đó không tương thích.
cgp

5

Khả năng tương thích.

Trước khi thuốc generic có sẵn, chỉ có get (Object o).

Nếu họ thay đổi phương thức này để có được (<K> o) thì nó sẽ có khả năng buộc phải bảo trì mã lớn cho người dùng java chỉ để làm cho mã hoạt động được biên dịch lại.

Họ có thể đã giới thiệu một phương thức bổ sung , giả sử get_checked (<K> o) và không dùng phương thức get () cũ để có đường dẫn chuyển tiếp nhẹ nhàng hơn. Nhưng vì một số lý do, điều này đã không được thực hiện. (Tình huống hiện tại của chúng tôi là bạn cần cài đặt các công cụ như findBugs để kiểm tra tính tương thích của loại giữa đối số get () và loại khóa được khai báo <K> của bản đồ.)

Các đối số liên quan đến ngữ nghĩa của .equals () là không có thật, tôi nghĩ vậy. .


4

Có một lý do nặng nề hơn, nó không thể được thực hiện về mặt kỹ thuật, bởi vì nó làm hỏng Bản đồ.

Java có cấu trúc chung đa hình như thế nào <? extends SomeClass>. Đánh dấu tham chiếu như vậy có thể trỏ đến loại đã ký với <AnySubclassOfSomeClass>. Nhưng chung đa hình làm cho tham chiếu đó chỉ đọc . Trình biên dịch cho phép bạn chỉ sử dụng các kiểu chung như kiểu trả về của phương thức (như các getters đơn giản), nhưng các khối sử dụng các phương thức trong đó kiểu chung là đối số (như setters thông thường). Nó có nghĩa là nếu bạn viết Map<? extends KeyType, ValueType>, trình biên dịch không cho phép bạn gọi phương thức get(<? extends KeyType>)và bản đồ sẽ vô dụng. Giải pháp duy nhất là làm cho phương thức này không chung chung : get(Object).


Tại sao phương thức thiết lập được gõ mạnh sau đó?
Sentenza

nếu bạn có nghĩa là 'put': Phương thức put () thay đổi bản đồ và nó sẽ không khả dụng với các tổng quát như <? mở rộng một sốClass>. Nếu bạn gọi nó, bạn có ngoại lệ biên dịch. Bản đồ như vậy sẽ là "chỉ đọc"
Owheee

1

Tương thích ngược, tôi đoán. Map(hoặc HashMap) vẫn cần hỗ trợ get(Object).


13
Nhưng lập luận tương tự có thể được đưa ra put(điều này hạn chế các kiểu chung). Bạn có được khả năng tương thích ngược bằng cách sử dụng các loại thô. Generics là "opt-in".
Thilo

Cá nhân, tôi nghĩ lý do rất có thể cho quyết định thiết kế này là khả năng tương thích ngược.
geekdenz

1

Tôi đã nhìn vào điều này và nghĩ tại sao họ làm theo cách này. Tôi không nghĩ bất kỳ câu trả lời hiện có nào giải thích lý do tại sao họ không thể làm cho giao diện chung mới chỉ chấp nhận loại thích hợp cho khóa. Lý do thực tế là mặc dù họ đã giới thiệu thuốc generic nhưng họ KHÔNG tạo ra giao diện mới. Giao diện Bản đồ là bản đồ không chung chung cũ, nó chỉ đóng vai trò là cả phiên bản chung và không chung. Bằng cách này nếu bạn có một phương thức chấp nhận Bản đồ không chung chung, bạn có thể vượt qua nó Map<String, Customer>và nó vẫn hoạt động. Đồng thời hợp đồng nhận được đối tượng nên giao diện mới cũng hỗ trợ hợp đồng này.

Theo tôi, họ nên đã thêm một giao diện mới và triển khai cả trên bộ sưu tập hiện có nhưng họ đã quyết định ủng hộ các giao diện tương thích ngay cả khi nó có nghĩa là thiết kế tồi hơn cho phương thức get. Lưu ý rằng bản thân các bộ sưu tập sẽ chỉ tương thích với các phương thức hiện có.


0

Chúng tôi đang thực hiện tái cấu trúc lớn ngay bây giờ và chúng tôi đã bỏ lỡ get () được gõ mạnh này để kiểm tra xem chúng tôi đã không bỏ lỡ một số get () với loại cũ.

Nhưng tôi đã tìm thấy một cách giải quyết / mẹo xấu để kiểm tra thời gian biên dịch: tạo giao diện Bản đồ với kiểu gõ mạnh get, chứaKey, xóa ... và đặt nó vào gói java.util của dự án của bạn.

Bạn sẽ nhận được các lỗi biên dịch chỉ khi gọi get (), ... với các loại sai, mọi thứ khác có vẻ ổn đối với trình biên dịch (ít nhất là bên trong kepler nhật thực).

Đừng quên xóa giao diện này sau khi kiểm tra bản dựng của bạn vì đây không phải là thứ bạn muốn trong thời gian chạy.

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.