Câu trả lời:
Về cơ bản, đó là cách mà các thế hệ được triển khai trong Java thông qua thủ thuật trình biên dịch. Mã chung được biên dịch thực sự chỉ sử dụng java.lang.Object
bất cứ nơi nào bạn nói về T
(hoặc một số tham số loại khác) - và có một số siêu dữ liệu để nói với trình biên dịch rằng nó thực sự là một loại chung.
Khi bạn biên dịch một số mã theo một kiểu hoặc phương thức chung, trình biên dịch sẽ tìm ra ý nghĩa thực sự của bạn (nghĩa là đối số kiểu T
là gì) và xác minh tại thời điểm biên dịch rằng bạn đang làm đúng, nhưng mã được phát ra lại chỉ nói chuyện về mặt java.lang.Object
- trình biên dịch tạo ra các phôi bổ sung khi cần thiết. Tại thời điểm thực hiện, a List<String>
và a List<Date>
hoàn toàn giống nhau; thông tin loại thêm đã bị xóa bởi trình biên dịch.
So sánh điều này với, giả sử, C #, trong đó thông tin được giữ lại tại thời điểm thực thi, cho phép mã chứa các biểu thức typeof(T)
tương đương với T.class
- ngoại trừ thông tin sau không hợp lệ. (Có nhiều sự khác biệt giữa khái quát .NET và khái quát Java, bạn hãy nhớ.) Loại xóa là nguồn gốc của nhiều thông báo lỗi / cảnh báo "lẻ" khi xử lý các tổng quát Java.
Các nguồn lực khác:
Object
(trong một kịch bản được gõ yếu) thực sự là một List<String>
) chẳng hạn. Trong Java, điều đó không khả thi - bạn có thể tìm ra rằng đó là một ArrayList
, nhưng không phải kiểu chung chung ban đầu là gì. Ví dụ, loại điều này có thể xuất hiện trong các tình huống tuần tự hóa / giải tuần tự hóa. Một ví dụ khác là nơi một container phải có khả năng xây dựng các thể hiện của loại chung của nó - bạn phải chuyển loại đó một cách riêng biệt trong Java (dưới dạng Class<T>
).
Class<T>
tham số vào hàm tạo (hoặc phương thức chung) đơn giản vì Java không giữ lại thông tin đó. Nhìn vào EnumSet.allOf
ví dụ - đối số kiểu chung cho phương thức là đủ; tại sao tôi cũng cần chỉ định một đối số "bình thường"? Trả lời: loại tẩy. Loại điều này gây ô nhiễm một API. Không quan tâm, bạn đã sử dụng .NET genericics nhiều chưa? (còn tiếp)
Cũng giống như một ghi chú bên lề, đây là một bài tập thú vị để thực sự thấy trình biên dịch đang làm gì khi thực hiện xóa - làm cho toàn bộ khái niệm dễ nắm bắt hơn một chút. Có một cờ đặc biệt mà bạn có thể chuyển trình biên dịch để xuất các tệp java đã xóa các tổng quát và các phôi. Một ví dụ:
javac -XD-printflat -d output_dir SomeFile.java
Các -printflat
là lá cờ đó được trao tắt để trình biên dịch mà tạo ra các tập tin. (Phần -XD
này là thứ javac
sẽ đưa nó cho jar thực thi thực sự biên dịch chứ không chỉ javac
, nhưng tôi lạc đề ...) Điều -d output_dir
này là cần thiết bởi vì trình biên dịch cần một số nơi để đặt các tệp .java mới.
Điều này, tất nhiên, không chỉ là tẩy xóa; tất cả các công cụ tự động trình biên dịch được thực hiện ở đây. Ví dụ, các hàm tạo mặc định cũng được chèn vào, các for
vòng lặp kiểu foreach mới được mở rộng thành các for
vòng lặp thông thường , v.v ... Thật tuyệt khi thấy những điều nhỏ nhặt đang diễn ra tự động.
Xóa, theo nghĩa đen có nghĩa là thông tin loại có trong mã nguồn bị xóa khỏi mã byte được biên dịch. Hãy để chúng tôi hiểu điều này với một số mã.
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class GenericsErasure {
public static void main(String args[]) {
List<String> list = new ArrayList<String>();
list.add("Hello");
Iterator<String> iter = list.iterator();
while(iter.hasNext()) {
String s = iter.next();
System.out.println(s);
}
}
}
Nếu bạn biên dịch mã này và sau đó dịch ngược mã với trình dịch ngược Java, bạn sẽ nhận được một cái gì đó như thế này. Lưu ý rằng mã dịch ngược không chứa dấu vết của thông tin loại có trong mã nguồn gốc.
import java.io.PrintStream;
import java.util.*;
public class GenericsErasure
{
public GenericsErasure()
{
}
public static void main(String args[])
{
List list = new ArrayList();
list.add("Hello");
String s;
for(Iterator iter = list.iterator(); iter.hasNext(); System.out.println(s))
s = (String)iter.next();
}
}
jigawot
nói, nó hoạt động.
Để hoàn thành câu trả lời của Jon Skeet đã rất đầy đủ, bạn phải nhận ra khái niệm về kiểu xóa xuất phát từ nhu cầu tương thích với các phiên bản Java trước đó .
Ban đầu được trình bày tại EclipseCon 2007 (không còn khả dụng), tính tương thích bao gồm các điểm đó:
Câu trả lời gốc:
Vì thế:
new ArrayList<String>() => new ArrayList()
Có những đề xuất cho một sự thống nhất lớn hơn . Reify là "coi một khái niệm trừu tượng là có thật", trong đó các cấu trúc ngôn ngữ nên là các khái niệm, không chỉ là cú pháp cú pháp.
Tôi cũng nên đề cập đến checkCollection
phương thức của Java 6, trả về một khung nhìn an toàn kiểu động của bộ sưu tập đã chỉ định. Bất kỳ nỗ lực để chèn một yếu tố của loại sai sẽ dẫn đến ngay lập tức ClassCastException
.
Cơ chế tổng quát trong ngôn ngữ cung cấp kiểm tra kiểu thời gian biên dịch (tĩnh), nhưng có thể đánh bại cơ chế này với các phôi không được kiểm tra .
Thông thường đây không phải là một vấn đề, vì trình biên dịch đưa ra cảnh báo về tất cả các hoạt động không được kiểm tra như vậy.
Tuy nhiên, có những lúc khi kiểm tra kiểu tĩnh không đủ, như:
ClassCastException
, chỉ ra rằng một phần tử được gõ không chính xác đã được đưa vào một bộ sưu tập được tham số hóa. Thật không may, ngoại lệ có thể xảy ra bất cứ lúc nào sau khi phần tử lỗi được chèn vào, do đó, nó thường cung cấp ít hoặc không có thông tin nào về nguồn gốc thực sự của vấn đề.Cập nhật tháng 7 năm 2012, gần bốn năm sau:
Bây giờ (2012) chi tiết trong " Quy tắc tương thích di chuyển API (Kiểm tra chữ ký) "
Ngôn ngữ lập trình Java thực hiện các tổng quát bằng cách sử dụng erasure, đảm bảo rằng các phiên bản kế thừa và chung thường tạo ra các tệp lớp giống hệt nhau, ngoại trừ một số thông tin phụ trợ về các loại. Khả năng tương thích nhị phân không bị phá vỡ vì có thể thay thế tệp lớp kế thừa bằng tệp lớp chung mà không thay đổi hoặc biên dịch lại bất kỳ mã máy khách nào.
Để tạo điều kiện giao tiếp với mã kế thừa không chung chung, cũng có thể sử dụng việc xóa một loại tham số hóa làm một loại. Một loại như vậy được gọi là một loại thô ( Đặc tả ngôn ngữ Java 3 / 4.8 ). Cho phép loại thô cũng đảm bảo khả năng tương thích ngược cho mã nguồn.
Theo đó, các phiên bản sau của
java.util.Iterator
lớp này đều tương thích ngược với mã nguồn và mã nguồn:
Class java.util.Iterator as it is defined in Java SE version 1.4:
public interface Iterator {
boolean hasNext();
Object next();
void remove();
}
Class java.util.Iterator as it is defined in Java SE version 5.0:
public interface Iterator<E> {
boolean hasNext();
E next();
void remove();
}
Bổ sung cho câu trả lời Jon Skeet đã được bổ sung ...
Nó đã được đề cập rằng việc thực hiện thuốc generic thông qua việc tẩy xóa dẫn đến một số hạn chế gây phiền nhiễu (ví dụ như không new T[42]
). Nó cũng đã được đề cập rằng lý do chính để thực hiện mọi thứ theo cách này là khả năng tương thích ngược trong mã byte. Điều này cũng (hầu hết) đúng. Mã byte được tạo -target 1.5 hơi khác so với chỉ đúc không có đường -target 1.4. Về mặt kỹ thuật, thậm chí có thể (thông qua mánh khóe mênh mông) để có quyền truy cập vào các kiểu khởi tạo chung chung trong thời gian chạy , chứng minh rằng thực sự có một cái gì đó trong mã byte.
Điểm thú vị hơn (chưa được nêu ra) là việc triển khai thuốc generic sử dụng tẩy xóa mang lại sự linh hoạt hơn một chút trong những gì hệ thống loại cấp cao có thể thực hiện. Một ví dụ điển hình cho việc này sẽ là triển khai JVM của Scala so với CLR. Trên JVM, có thể trực tiếp triển khai các loại cao hơn do thực tế là chính JVM không áp dụng các hạn chế đối với các loại chung (vì các "loại" này thực sự không có). Điều này trái ngược với CLR, có kiến thức về thời gian chạy tham số. Do đó, bản thân CLR phải có một số khái niệm về cách sử dụng thuốc generic, vô hiệu hóa các nỗ lực để mở rộng hệ thống với các quy tắc không dự đoán được. Kết quả là, các loại cao hơn của Scala trên CLR được triển khai bằng cách sử dụng một hình thức xóa kỳ lạ được mô phỏng trong chính trình biên dịch,
Xóa có thể bất tiện khi bạn muốn làm những việc nghịch ngợm trong thời gian chạy, nhưng nó cung cấp sự linh hoạt nhất cho các nhà văn trình biên dịch. Tôi đoán đó là một phần lý do tại sao nó sẽ không biến mất bất cứ lúc nào sớm.
Theo tôi hiểu (là một người .NET ), JVM không có khái niệm về khái quát, vì vậy trình biên dịch thay thế các tham số kiểu bằng Object và thực hiện tất cả việc truyền cho bạn.
Điều này có nghĩa là các tổng quát Java không là gì ngoài cú pháp đường và không cung cấp bất kỳ cải tiến hiệu suất nào cho các loại giá trị yêu cầu quyền anh / bỏ hộp khi được tham chiếu.
Có những lời giải thích tốt. Tôi chỉ thêm một ví dụ để chỉ ra cách thức xóa hoạt động với trình dịch ngược.
Lớp gốc,
import java.util.ArrayList;
import java.util.List;
public class S<T> {
T obj;
S(T o) {
obj = o;
}
T getob() {
return obj;
}
public static void main(String args[]) {
List<String> list = new ArrayList<>();
list.add("Hello");
// for-each
for(String s : list) {
String temp = s;
System.out.println(temp);
}
// stream
list.forEach(System.out::println);
}
}
Mã dịch ngược từ mã byte của nó,
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.Objects;
import java.util.function.Consumer;
public class S {
Object obj;
S(Object var1) {
this.obj = var1;
}
Object getob() {
return this.obj;
}
public static void main(String[] var0) {
ArrayList var1 = new ArrayList();
var1.add("Hello");
// for-each
Iterator iterator = var1.iterator();
while (iterator.hasNext()) {
String string;
String string2 = string = (String)iterator.next();
System.out.println(string2);
}
// stream
PrintStream printStream = System.out;
Objects.requireNonNull(printStream);
var1.forEach(printStream::println);
}
}
Tại sao nên sử dụng Generices
Tóm lại, generic cho phép các loại (lớp và giao diện) là tham số khi xác định các lớp, giao diện và phương thức. Giống như các tham số chính thức quen thuộc hơn được sử dụng trong khai báo phương thức, các tham số loại cung cấp một cách để bạn sử dụng lại cùng một mã với các đầu vào khác nhau. Sự khác biệt là các đầu vào cho các tham số chính thức là các giá trị, trong khi các đầu vào cho các tham số loại là các loại. ode sử dụng generic có nhiều lợi ích so với mã không chung chung:
Loại tẩy là gì
Generics đã được giới thiệu với ngôn ngữ Java để cung cấp các kiểm tra loại chặt chẽ hơn tại thời điểm biên dịch và để hỗ trợ lập trình chung. Để triển khai generic, trình biên dịch Java áp dụng kiểu xóa để:
[NB] -Phương pháp cầu là gì? Nói ngắn gọn, trong trường hợp giao diện được tham số hóa như Comparable<T>
, điều này có thể khiến các phương thức bổ sung được chèn bởi trình biên dịch; những phương pháp bổ sung này được gọi là cầu nối.
Cách thức hoạt động
Việc xóa một loại được định nghĩa như sau: loại bỏ tất cả các tham số loại từ các loại được tham số hóa và thay thế bất kỳ biến loại nào bằng cách xóa ràng buộc của nó hoặc với Object nếu nó không có ràng buộc hoặc xóa khỏi giới hạn ngoài cùng nếu nó có nhiều giới hạn. Dưới đây là một số ví dụ:
List<Integer>
, List<String>
và List<List<String>>
là List
.List<Integer>[]
là List[]
.List
là chính nó, tương tự cho bất kỳ loại thô.Integer
chính nó, tương tự cho bất kỳ loại nào không có tham số loại.T
trong định nghĩa asList
là Object
, bởi vìT
không có ràng buộc.T
trong định nghĩa max
là Comparable
, bởi vì T
đã ràng buộcComparable<? super T>
.T
trong định nghĩa cuối cùng max
là Object
, bởi vì
T
đã bị ràng buộc Object
& Comparable<T>
và chúng ta thực hiện việc xóa các giới hạn ngoài cùng bên trái.Cần cẩn thận khi sử dụng thuốc generic.
Trong Java, hai phương thức riêng biệt không thể có cùng chữ ký. Vì thuốc generic được thực hiện bằng cách xóa, nên cũng có hai phương pháp riêng biệt không thể có chữ ký có cùng độ xóa. Một lớp không thể quá tải hai phương thức có chữ ký có cùng độ xóa và một lớp không thể thực hiện hai giao diện có cùng độ xóa.
class Overloaded2 {
// compile-time error, cannot overload two methods with same erasure
public static boolean allZero(List<Integer> ints) {
for (int i : ints) if (i != 0) return false;
return true;
}
public static boolean allZero(List<String> strings) {
for (String s : strings) if (s.length() != 0) return false;
return true;
}
}
Chúng tôi dự định mã này sẽ hoạt động như sau:
assert allZero(Arrays.asList(0,0,0));
assert allZero(Arrays.asList("","",""));
Tuy nhiên, trong trường hợp này, việc xóa chữ ký của cả hai phương pháp là giống hệt nhau:
boolean allZero(List)
Do đó, một cuộc đụng độ tên được báo cáo tại thời điểm biên dịch. Không thể đặt cả hai phương thức cùng tên và cố gắng phân biệt giữa chúng bằng cách nạp chồng, vì sau khi xóa, không thể phân biệt một cuộc gọi phương thức này với phương thức khác.
Hy vọng, Reader sẽ thích :)