Tại sao tôi phải quan tâm rằng Java không có các generic được sửa đổi?


102

Đây là một câu hỏi mà tôi đã hỏi trong một cuộc phỏng vấn gần đây như một thứ mà ứng viên muốn thấy được thêm vào ngôn ngữ Java. Nó thường được xác định là một nỗi đau mà Java không có các generic được sửa đổi lại, nhưng khi được thúc đẩy, ứng viên không thể thực sự cho tôi biết loại thứ mà anh ta có thể đạt được khi họ ở đó.

Rõ ràng là vì các kiểu thô được cho phép trong Java (và các kiểm tra không an toàn), nên có thể lật ngược các loại chung và kết thúc bằng một loại List<Integer>(ví dụ) thực sự chứaString s. Điều này rõ ràng có thể được thực hiện không thể được sửa đổi thông tin loại; nhưng phải có nhiều hơn thế này !

Mọi người có thể đăng ví dụ về những điều mà họ thực sự muốn làm , có sẵn các chỉ số chung được sửa đổi không? Ý tôi là, rõ ràng bạn có thể nhận được loại a Listtrong thời gian chạy - nhưng bạn sẽ làm gì với nó?

public <T> void foo(List<T> l) {
   if (l.getGenericType() == Integer.class) {
       //yeah baby! err, what now?

CHỈNH SỬA : Cập nhật nhanh điều này vì các câu trả lời dường như chủ yếu quan tâm đến nhu cầu chuyển vào một Classdưới dạng tham số (ví dụ EnumSet.noneOf(TimeUnit.class)). Tôi đã tìm kiếm nhiều hơn cho một cái gì đó dọc theo ranh giới mà điều này không thể thực hiện được . Ví dụ:

List<?> l1 = api.gimmeAList();
List<?> l2 = api.gimmeAnotherList();

if (l1.getGenericType().isAssignableFrom(l2.getGenericType())) {
    l1.addAll(l2); //why on earth would I be doing this anyway?

Điều này có nghĩa là bạn có thể nhận được lớp của một loại chung trong thời gian chạy? (nếu vậy, tôi có một ví dụ!)
James B

Tôi nghĩ rằng hầu hết mong muốn với các generic có thể xác minh lại là từ những người sử dụng generic chủ yếu với các tập hợp và muốn các tập hợp đó hoạt động giống mảng hơn.
kdgregory

2
Câu hỏi thú vị hơn (đối với tôi): sẽ cần những gì để triển khai chung kiểu C ++ trong Java? Nó chắc chắn có vẻ khả thi trong thời gian chạy, nhưng sẽ phá vỡ tất cả các trình nạp lớp hiện có (vì findClass()sẽ phải bỏ qua tham số hóa, nhưng defineClass()không thể). Và như chúng ta đã biết, The Powers That Be có khả năng tương thích ngược là điều tối quan trọng.
kdgregory

1
Trên thực tế, Java cung cấp các generic được sửa đổi theo một cách rất hạn chế . Tôi cung cấp thêm chi tiết trong chủ đề SO này: stackoverflow.com/questions/879855/…
Richard Gomes,

Các JavaOne Keynote chỉ ra rằng Java 9 sẽ hỗ trợ reification.
Rangi Keen

Câu trả lời:


81

Từ một vài lần mà tôi bắt gặp "nhu cầu" này, cuối cùng nó kết thúc với cấu trúc này:

public class Foo<T> {

    private T t;

    public Foo() {
        this.t = new T(); // Help?
    }

}

Điều này hoạt động trong C # giả sử rằng nó Tcó một hàm tạo mặc định . Bạn thậm chí có thể lấy kiểu thời gian chạy typeof(T)và lấy các hàm tạo bằng Type.GetConstructor().

Giải pháp Java phổ biến sẽ là chuyển Class<T>đối số dưới dạng.

public class Foo<T> {

    private T t;

    public Foo(Class<T> cls) throws Exception {
        this.t = cls.newInstance();
    }

}

(nó không nhất thiết phải được truyền dưới dạng đối số hàm tạo, vì đối số phương thức cũng tốt, ở trên chỉ là một ví dụ, cũng try-catchđược bỏ qua cho ngắn gọn)

Đối với tất cả các cấu trúc kiểu chung khác, kiểu thực tế có thể dễ dàng được giải quyết với một chút trợ giúp của sự phản ánh. Phần Hỏi & Đáp dưới đây minh họa các trường hợp sử dụng và khả năng:


5
Điều này hoạt động (ví dụ) trong C #? Làm thế nào để bạn biết rằng T có một hàm tạo mặc định?
Thomas Jung

4
Có, nó có. Và đây chỉ là một ví dụ cơ bản (giả sử rằng chúng tôi làm việc với javabeans). Toàn bộ vấn đề là với Java, bạn không thể lấy lớp trong thời gian chạy bằng T.classhoặc T.getClass(), để bạn có thể truy cập vào tất cả các trường, hàm tạo và phương thức của nó. Nó làm cho việc xây dựng cũng không thể.
BalusC

3
Điều này có vẻ thực sự yếu đối với tôi là "vấn đề lớn", đặc biệt vì đây là chỉ có khả năng là hữu ích trong việc kết hợp với một số phản ánh rất giòn xung quanh nhà xây dựng / thông số, vv
oxbow_lakes

33
Nó biên dịch trong C # với điều kiện là bạn khai báo kiểu là: public class Foo <T> trong đó T: new (). Điều này sẽ giới hạn các kiểu hợp lệ của T đối với những kiểu chứa hàm tạo không tham số.
Martin Harris

3
@Colin Bạn thực sự có thể nói với java rằng một kiểu chung nhất định được yêu cầu để mở rộng nhiều hơn một lớp hoặc giao diện. Xem phần "Nhiều vòng" của docs.oracle.com/javase/tutorial/java/generics/bounds.html . (Tất nhiên, bạn không thể nói rằng đối tượng sẽ là một trong một tập hợp cụ thể không chia sẻ các phương thức có thể được trừu tượng hóa thành một giao diện, điều này có thể được triển khai phần nào trong C ++ bằng cách sử dụng chuyên biệt mẫu.)
JAB

99

Điều mà tôi thường gặp nhất là không có khả năng tận dụng nhiều công văn trên nhiều loại chung chung. Những điều sau đây là không thể và có nhiều trường hợp nó sẽ là giải pháp tốt nhất:

public void my_method(List<String> input) { ... }
public void my_method(List<Integer> input) { ... }

5
Hoàn toàn không cần cải tiến để có thể làm được điều đó. Lựa chọn phương pháp được thực hiện tại thời điểm biên dịch khi thông tin về loại thời gian biên dịch có sẵn.
Tom Hawtin - tackline

26
@Tom: cái này thậm chí không biên dịch được vì xóa kiểu. Cả hai đều được biên dịch thànhpublic void my_method(List input) {} . Tuy nhiên, tôi chưa bao giờ gặp phải nhu cầu này, đơn giản vì chúng không trùng tên. Nếu họ có cùng tên, tôi sẽ đặt câu hỏi nếu public <T extends Object> void my_method(List<T> input) {}không phải là một ý tưởng tốt hơn.
BalusC

1
Hm, tôi sẽ có xu hướng tránh hoàn toàn quá tải với số lượng tham số giống hệt nhau, và thích một cái gì đó giống như myStringsMethod(List<String> input)myIntegersMethod(List<Integer> input)ngay cả khi việc quá tải cho một trường hợp như vậy là có thể trong Java.
Fabianamondsg

4
@Fabian: Có nghĩa là bạn phải có mã riêng biệt và ngăn chặn loại lợi thế bạn nhận được từ <algorithm>C ++.
David Thornley

Tôi bối rối, điều này về cơ bản giống với điểm thứ hai của tôi, nhưng tôi đã nhận được 2 phiếu phản đối trên đó? Có ai quan tâm để khai sáng cho tôi?
rsp

34

Loại an toàn được nghĩ đến. Việc hạ cấp xuống một loại tham số sẽ luôn không an toàn nếu không có các chỉ số chung được sửa đổi:

List<String> myFriends = new ArrayList();
myFriends.add("Alice");
getSession().put("friends", myFriends);
// later, elsewhere
List<Friend> myFriends = (List<Friend>) getSession().get("friends");
myFriends.add(new Friend("Bob")); // works like a charm!
// and so...
List<String> myFriends = (List<String>) getSession().get("friends");
for (String friend : myFriends) print(friend); // ClassCastException, wtf!? 

Cũng thế, tóm tắt sẽ ít bị rò rỉ hơn - ít nhất là những phần có thể quan tâm đến thông tin thời gian chạy về các tham số kiểu của chúng. Ngày nay, nếu bạn cần bất kỳ loại thông tin thời gian chạy nào về loại của một trong các tham số chung, bạn cũng phải chuyển nó Classtheo. Bằng cách đó, giao diện bên ngoài của bạn phụ thuộc vào việc triển khai của bạn (cho dù bạn có sử dụng RTTI về các tham số của mình hay không).


1
Có - Tôi có một cách giải quyết vấn đề này trong đó tôi tạo một ParametrizedListbản sao dữ liệu trong các loại kiểm tra thu thập nguồn. Nó hơi giống Collections.checkedListnhưng có thể được gieo bằng một bộ sưu tập để bắt đầu.
oxbow_lakes

@tackline - tốt, một vài thông tin tóm tắt sẽ ít bị rò rỉ hơn. Nếu bạn cần quyền truy cập để nhập siêu dữ liệu trong quá trình triển khai của mình, giao diện bên ngoài sẽ thông báo về bạn vì các máy khách cần gửi cho bạn một đối tượng lớp.
gustafc

... nghĩa là với các loại chung được sửa đổi, bạn có thể thêm những thứ như T.class.getAnnotation(MyAnnotation.class)(đâu Tlà loại chung) mà không cần thay đổi giao diện bên ngoài.
gustafc

@gustafc: nếu bạn nghĩ rằng C ++ mẫu cung cấp cho bạn an toàn loại đầy đủ, đọc: kdgregory.com/index.php?page=java.generics.cpp
kdgregory

@kdgregory: Tôi chưa bao giờ nói rằng C ++ là loại an toàn 100% - chỉ là xóa các thiệt hại về loại an toàn. Như bạn tự nói, "Hóa ra, C ++ có một dạng xóa kiểu riêng của nó, được gọi là ép kiểu con trỏ kiểu C." Nhưng Java chỉ thực hiện các phôi động (không diễn giải lại), vì vậy việc sửa đổi sẽ gắn toàn bộ điều này vào hệ thống kiểu.
gustafc

26

Bạn có thể tạo các mảng chung trong mã của mình.

public <T> static void DoStuff() {
    T[] myArray = new T[42]; // No can do
}

có gì sai với Object? Mảng đối tượng dù sao cũng là mảng tham chiếu. Nó không giống như dữ liệu đối tượng đang nằm trên ngăn xếp - tất cả đều nằm trong đống.
Ran Biron

19
Loại an toàn. Tôi có thể đặt bất cứ thứ gì tôi muốn trong Đối tượng [], nhưng chỉ Chuỗi trong Chuỗi [].
Turnor

3
Ran: Không mỉa mai: Bạn có thể thích sử dụng ngôn ngữ kịch bản thay vì Java, sau đó bạn có thể linh hoạt với các biến không định kiểu ở bất cứ đâu!
cừu bay

2
Mảng là hiệp phương sai (và do đó không an toàn về kiểu chữ) trong cả hai ngôn ngữ. String[] strings = new String[1]; Object[] objects = strings; objects[0] = new Object();Biên dịch tốt ở cả hai ngôn ngữ. Chạy không ổn định.
Martijn

16

Đây là một câu hỏi cũ, có rất nhiều câu trả lời, nhưng tôi nghĩ rằng các câu trả lời hiện có là không đúng.

"reified" chỉ có nghĩa là thực và thường chỉ có nghĩa ngược lại với loại tẩy.

Vấn đề lớn liên quan đến Java Generics:

  • Yêu cầu quyền anh khủng khiếp này và sự ngắt kết nối giữa các loại nguyên thủy và các loại tham chiếu. Điều này không liên quan trực tiếp đến việc sửa đổi hoặc tẩy xóa loại. C # / Scala sửa lỗi này.
  • Không có tự loại. JavaFX 8 đã phải loại bỏ "người xây dựng" vì lý do này. Hoàn toàn không liên quan đến tẩy xóa kiểu chữ. Scala sửa lỗi này, không chắc chắn về C #.
  • Không có phương sai loại bên khai báo. C # 4.0 / Scala có cái này. Hoàn toàn không liên quan đến tẩy xóa kiểu chữ.
  • Không thể quá tải void method(List<A> l)method(List<B> l). Đây là do tẩy xóa kiểu nhưng cực kỳ nhỏ.
  • Không hỗ trợ phản chiếu kiểu thời gian chạy. Đây là trọng tâm của loại tẩy. Nếu bạn thích trình biên dịch siêu nâng cao xác minh và chứng minh càng nhiều logic chương trình của bạn tại thời điểm biên dịch, bạn nên sử dụng phản chiếu càng ít càng tốt và kiểu xóa kiểu này sẽ không làm phiền bạn. Nếu bạn thích lập trình kiểu động, chắp vá, phức tạp hơn và không quan tâm nhiều đến việc trình biên dịch chứng minh càng nhiều logic của bạn càng chính xác càng tốt, thì bạn muốn phản ánh tốt hơn và việc sửa lỗi xóa là quan trọng.

1
Phần lớn tôi thấy khó với các trường hợp tuần tự hóa. Bạn thường muốn có thể phát hiện ra các loại lớp của những thứ chung chung đang được tuần tự hóa nhưng bạn bị dừng lại vì bị xóa kiểu. Nó làm cho nó khó có thể làm một cái gì đó như thế nàydeserialize(thingy, List<Integer>.class)
Cogman

3
Tôi nghĩ đây là câu trả lời tốt nhất. Đặc biệt là các phần mô tả những vấn đề thực sự là về mặt quỹ do xóa kiểu và những gì chỉ là các vấn đề thiết kế ngôn ngữ Java. Điều đầu tiên tôi nói với những người bắt đầu tẩy xóa là Scala và Haskell cũng đang làm việc bằng cách xóa kiểu.
aemxdp

13

Serialization sẽ đơn giản hơn với việc sửa đổi. Những gì chúng tôi muốn là

deserialize(thingy, List<Integer>.class);

Những gì chúng ta phải làm là

deserialize(thing, new TypeReference<List<Integer>>(){});

trông xấu xí và hoạt động vui nhộn.

Cũng có những trường hợp sẽ thực sự hữu ích nếu nói những điều như

public <T> void doThings(List<T> thingy) {
    if (T instanceof Q)
      doCrazyness();
  }

Những thứ này không cắn thường xuyên, nhưng chúng sẽ cắn khi chúng xảy ra.


Điều này. Một ngàn lần điều này. Mỗi khi tôi cố gắng viết mã deserialization bằng Java, tôi dành cả ngày để than thở rằng tôi không làm việc trong một thứ khác
Basic

11

Khả năng tiếp xúc của tôi với Java Geneircs là khá hạn chế, và ngoài những điểm mà các câu trả lời khác đã đề cập, có một kịch bản được giải thích trong cuốn sách Java Generics and Collections , bởi Maurice Naftalin và Philip Walder, nơi mà những generic được sửa lại rất hữu ích.

Vì không thể tái định danh các loại, nên không thể có ngoại lệ Tham số hóa.

Ví dụ, khai báo của biểu mẫu dưới đây không hợp lệ.

class ParametericException<T> extends Exception // compile error

Điều này là do mệnh đề bắt kiểm tra xem ngoại lệ được ném có khớp với một kiểu nhất định hay không. Kiểm tra này cũng giống như kiểm tra được thực hiện bởi kiểm tra mẫu và vì kiểu này không thể tái lập nên biểu mẫu trên không hợp lệ.

Nếu mã trên là hợp lệ thì việc xử lý ngoại lệ theo cách dưới đây sẽ có thể thực hiện được:

try {
     throw new ParametericException<Integer>(42);
} catch (ParametericException<Integer> e) { // compile error
  ...
}

Cuốn sách cũng đề cập rằng nếu các tổng thể Java được định nghĩa tương tự như cách các mẫu C ++ được định nghĩa (mở rộng), nó có thể dẫn đến việc triển khai hiệu quả hơn vì điều này mang lại nhiều cơ hội hơn để tối ưu hóa. Nhưng không cung cấp bất kỳ lời giải thích nào hơn thế này, vì vậy bất kỳ lời giải thích nào (gợi ý) từ những người hiểu biết sẽ hữu ích.


1
Đó là một điểm hợp lệ nhưng tôi không chắc tại sao một lớp ngoại lệ được tham số hóa như vậy lại hữu ích. Bạn có thể sửa đổi câu trả lời của mình để chứa một ví dụ ngắn gọn về thời điểm điều này có thể hữu ích không?
oxbow_lakes

@oxbow_lakes: Xin lỗi Kiến thức của tôi về Java Generics còn khá hạn chế và tôi đang cố gắng cải thiện nó. Vì vậy, bây giờ tôi không thể nghĩ ra bất kỳ ví dụ nào mà ngoại lệ được tham số hóa có thể hữu ích. Sẽ cố gắng suy nghĩ về nó. Cám ơn.
sateesh

Nó có thể hoạt động như một sự thay thế cho đa kế thừa của các kiểu ngoại lệ.
Meriton

Khả năng ứng biến hiệu suất là hiện tại, một tham số kiểu phải kế thừa từ Đối tượng, yêu cầu quyền của các kiểu nguyên thủy, áp đặt chi phí thực thi và bộ nhớ.
Meriton

8

Mảng có thể sẽ hoạt động tốt hơn nhiều với generic nếu chúng được sửa đổi.


2
Chắc chắn, nhưng họ vẫn sẽ gặp vấn đề. List<String>không phải là a List<Object>.
Tom Hawtin - tackline

Đồng ý - nhưng chỉ dành cho các số nguyên (Số nguyên, Dài, v.v.). Đối với Object "thông thường", điều này cũng tương tự. Vì các nguyên mẫu không thể là một kiểu tham số hóa (một vấn đề nghiêm trọng hơn nhiều, ít nhất là IMHO), tôi không thấy đây là một vấn đề thực sự.
Ran Biron

3
Vấn đề với mảng là hiệp phương sai của chúng, không liên quan gì đến việc sửa đổi.
Tái diễn vào

5

Tôi có một trình bao bọc trình bày một tập kết quả jdbc như một trình lặp, (điều đó có nghĩa là tôi có thể kiểm tra đơn vị các hoạt động bắt nguồn từ cơ sở dữ liệu dễ dàng hơn rất nhiều thông qua chèn phụ thuộc).

API trông giống như Iterator<T> trong đó T là một số kiểu có thể được xây dựng chỉ bằng cách sử dụng các chuỗi trong hàm tạo. Sau đó, Iterator xem xét các chuỗi được trả về từ truy vấn sql và sau đó cố gắng so khớp nó với một phương thức khởi tạo kiểu T.

Theo cách hiện tại mà các generic được triển khai, tôi cũng phải chuyển vào lớp các đối tượng mà tôi sẽ tạo từ tập kết quả của mình. Nếu tôi hiểu đúng, nếu generic được sửa đổi, tôi chỉ có thể gọi T.getClass () lấy các hàm tạo của nó và sau đó không phải ép kiểu kết quả của Class.newInstance (), sẽ gọn gàng hơn nhiều.

Về cơ bản, tôi nghĩ nó làm cho việc viết API (thay vì chỉ viết một ứng dụng) dễ dàng hơn, vì bạn có thể suy luận nhiều hơn từ các đối tượng và do đó sẽ cần ít cấu hình hơn ... Tôi không đánh giá cao ý nghĩa của chú thích cho đến khi tôi đã thấy chúng được sử dụng trong những thứ như spring hoặc xstream thay vì hàng loạt cấu hình.


Nhưng việc vượt qua lớp có vẻ an toàn hơn đối với tôi. Trong mọi trường hợp, việc tạo các phiên bản từ các truy vấn cơ sở dữ liệu một cách phản ánh là cực kỳ dễ xảy ra đối với những thay đổi chẳng hạn như cấu trúc lại (trong cả mã của bạn và cơ sở dữ liệu). Tôi đoán tôi đang tìm kiếm điều đó nó chỉ là không thể thực hiện để cung cấp các lớp
oxbow_lakes

5

Một điều tốt đẹp sẽ là tránh quyền anh cho các loại (giá trị) nguyên thủy. Điều này phần nào liên quan đến khiếu nại về mảng mà những người khác đã nêu ra và trong trường hợp việc sử dụng bộ nhớ bị hạn chế, nó thực sự có thể tạo ra sự khác biệt đáng kể.

Cũng có một số loại vấn đề khi viết một khung công tác trong đó khả năng phản ánh qua kiểu tham số hóa là rất quan trọng. Tất nhiên điều này có thể được giải quyết bằng cách truyền một đối tượng lớp xung quanh trong thời gian chạy, nhưng điều này che khuất API và tạo thêm gánh nặng cho người dùng của khung.


2
Về cơ bản, điều này hoàn thiện để có thể thực hiện được new T[]khi T thuộc loại nguyên thủy!
oxbow_lakes

2

Không phải là bạn sẽ đạt được bất cứ điều gì phi thường. Nó sẽ đơn giản hơn để hiểu. Xóa kiểu có vẻ như là một thời gian khó khăn cho người mới bắt đầu và cuối cùng nó đòi hỏi sự hiểu biết của một người về cách thức hoạt động của trình biên dịch.

Ý kiến ​​của tôi là, generic chỉ đơn giản là một phần bổ sung giúp tiết kiệm rất nhiều việc đúc dư thừa.


Đây chắc chắn là những gì chung trong Java . Trong C # và các ngôn ngữ khác, chúng là một công cụ mạnh mẽ
Basic

1

Một cái gì đó mà tất cả các câu trả lời ở đây đã bỏ lỡ khiến tôi liên tục đau đầu là vì các loại bị xóa, bạn không thể kế thừa một giao diện chung hai lần. Đây có thể là một vấn đề khi bạn muốn tạo giao diện chi tiết.

    public interface Service<KEY,VALUE> {
           VALUE get(KEY key);
    }

    public class PersonService implements Service<Long, Person>,
        Service<String, Person> //Can not do!!

0

Đây là một trong những điều khiến tôi chú ý hôm nay: mà không cần sửa đổi, nếu bạn viết một phương thức chấp nhận một danh sách kỳ lạ của các mục chung chung ... người gọi có thể NGHĨ chúng là loại an toàn, nhưng vô tình vượt qua bất kỳ thứ gì cũ kỹ và làm hỏng phương pháp của bạn.

Có vẻ như điều đó sẽ không xảy ra? ... Chắc chắn rồi, cho đến khi ... bạn sử dụng Lớp làm kiểu dữ liệu của mình. Tại thời điểm này, người gọi của bạn sẽ vui vẻ gửi cho bạn nhiều đối tượng Lớp, nhưng một lỗi đánh máy đơn giản sẽ gửi cho bạn các đối tượng Lớp không tuân theo T và thảm họa sẽ xảy ra.

(NB: Tôi có thể đã nhầm lẫn ở đây, nhưng tìm kiếm xung quanh "generics varargs", những điều ở trên dường như chỉ là những gì bạn mong đợi. Điều làm cho vấn đề này trở thành một vấn đề thực tế là việc sử dụng Class, tôi nghĩ - những người gọi dường như để bớt cẩn thận :()


Ví dụ, tôi đang sử dụng một mô hình sử dụng các đối tượng Lớp làm khóa trong bản đồ (nó phức tạp hơn một bản đồ đơn giản - nhưng về mặt khái niệm thì đó là những gì đang xảy ra).

ví dụ: điều này hoạt động tốt trong Java Generics (ví dụ nhỏ):

public <T extends Component> Set<UUID> getEntitiesPossessingComponent( Class<T> componentType)
    {
        // find the entities that are mapped (somehow) from that class. Very type-safe
    }

ví dụ như không sửa đổi trong Java Generics, cái này chấp nhận BẤT KỲ đối tượng "Lớp" nào. Và nó chỉ là một phần mở rộng nhỏ của mã trước đó:

public <T extends Component> Set<UUID> getEntitiesPossessingComponents( Class<T>... componentType )
    {
        // find the entities that are mapped (somehow) to ALL of those classes
    }

Các phương pháp trên phải được viết ra hàng nghìn lần trong một dự án riêng lẻ - do đó, khả năng xảy ra lỗi của con người là rất cao. Gỡ lỗi cho những sai lầm được chứng minh là "không vui". Tôi hiện đang cố gắng tìm một giải pháp thay thế, nhưng không có nhiều hy vọng.

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.