Kiểu khái quát Java xóa: khi nào và điều gì xảy ra?


237

Tôi đọc về loại tẩy của Java trên trang web của Oracle .

Khi nào loại tẩy xảy ra? Tại thời gian biên dịch hoặc thời gian chạy? Khi lớp được tải? Khi lớp được khởi tạo?

Rất nhiều trang web (bao gồm cả hướng dẫn chính thức được đề cập ở trên) cho biết loại xóa xảy ra tại thời điểm biên dịch. Nếu thông tin loại được loại bỏ hoàn toàn tại thời điểm biên dịch, làm thế nào để kiểm tra tính tương thích của loại JDK khi một phương thức sử dụng tổng quát được gọi mà không có thông tin loại hoặc thông tin loại sai?

Xem xét ví dụ sau: Say class Acó một phương thức , empty(Box<? extends Number> b). Chúng tôi biên dịch A.javavà lấy tệp lớp A.class.

public class A {
    public static void empty(Box<? extends Number> b) {}
}
public class Box<T> {}

Bây giờ chúng ta tạo một lớp khác Bgọi phương thức emptyvới một đối số không tham số (kiểu thô) : empty(new Box()). Nếu chúng ta biên dịch B.javavới A.classtrong classpath, javac là đủ thông minh để nâng cao một cảnh báo. Vì vậy, A.class một số loại thông tin được lưu trữ trong đó.

public class B {
    public static void invoke() {
        // java: unchecked method invocation:
        //  method empty in class A is applied to given types
        //  required: Box<? extends java.lang.Number>
        //  found:    Box
        // java: unchecked conversion
        //  required: Box<? extends java.lang.Number>
        //  found:    Box
        A.empty(new Box());
    }
}

Tôi đoán là kiểu xóa đó xảy ra khi lớp được tải, nhưng nó chỉ là phỏng đoán. Vậy khi nào nó xảy ra?


2
Một phiên bản "chung chung" hơn của câu hỏi này: stackoverflow.com/questions/313584/iêu
Ciro Santilli 冠状 病 六四 法轮功

@afryingpan: Bài báo được đề cập trong câu trả lời của tôi giải thích chi tiết về cách thức và thời điểm xảy ra xóa. Nó cũng giải thích khi thông tin loại được lưu giữ. Nói cách khác: khái quát thống nhất có sẵn trong Java, trái với niềm tin rộng rãi. Xem: rgomes.info/USE-typetokens-to-retrieve-generic-parameter
Richard Gomes

Câu trả lời:


240

Loại tẩy được áp dụng cho việc sử dụng thuốc generic. Chắc chắn có siêu dữ liệu trong tệp lớp để nói liệu một phương thức / loại chung chung hay không và các ràng buộc là gì, nhưng khi sử dụng chung , chúng được chuyển đổi thành kiểm tra thời gian biên dịch và thời gian thực hiện. Mã này:

List<String> list = new ArrayList<String>();
list.add("Hi");
String x = list.get(0);

được biên dịch thành

List list = new ArrayList();
list.add("Hi");
String x = (String) list.get(0);

Tại thời điểm thực hiện, không có cách nào để tìm ra điều đó T=Stringcho đối tượng danh sách - thông tin đó đã biến mất.

... nhưng List<T>bản thân giao diện vẫn tự quảng cáo là chung chung.

EDIT: Chỉ cần làm rõ, trình biên dịch sẽ giữ lại thông tin về biến là một List<String>- nhưng bạn vẫn không thể tìm ra điều đó T=Stringcho chính đối tượng danh sách.


6
Không, ngay cả trong việc sử dụng một loại chung có thể có siêu dữ liệu có sẵn trong thời gian chạy. Không thể truy cập biến cục bộ thông qua Reflection, nhưng đối với tham số phương thức được khai báo là "Danh sách <String> l", sẽ có loại siêu dữ liệu khi chạy, có sẵn thông qua API Reflection. Đúng, "loại tẩy" không đơn giản như nhiều người nghĩ ...
Rogério

4
@Rogerio: Khi tôi trả lời nhận xét của bạn, tôi tin rằng bạn đang bị nhầm lẫn giữa việc có thể lấy loại biến và có thể lấy loại đối tượng . Bản thân đối tượng không biết đối số kiểu của nó, mặc dù trường này có.
Jon Skeet

Tất nhiên, chỉ cần nhìn vào chính đối tượng bạn không thể biết đó là Danh sách <Chuỗi>. Nhưng các vật thể không xuất hiện từ đâu cả. Chúng được tạo cục bộ, được truyền vào dưới dạng đối số gọi phương thức, được trả về làm giá trị trả về từ lệnh gọi phương thức hoặc đọc từ trường của một đối tượng nào đó ... Trong tất cả các trường hợp này, bạn có thể biết trong thời gian chạy loại chung là gì ngầm hoặc bằng cách sử dụng API phản chiếu Java.
Rogério

13
@Rogerio: Làm thế nào để bạn biết đối tượng đã đến từ đâu? Nếu bạn có một tham số của loại, List<? extends InputStream>làm thế nào bạn có thể biết nó là loại gì khi nó được tạo ra? Ngay cả khi bạn có thể tìm ra loại trường mà tài liệu tham khảo đã được lưu trữ, tại sao bạn phải làm vậy? Tại sao bạn có thể nhận được tất cả phần còn lại của thông tin về đối tượng tại thời điểm thực hiện, nhưng không phải là đối số loại chung của nó? Có vẻ như bạn đang cố gắng biến loại hình này thành một thứ nhỏ bé không thực sự ảnh hưởng đến các nhà phát triển - trong khi tôi thấy nó là một vấn đề rất quan trọng trong một số trường hợp.
Jon Skeet

Nhưng loại tẩy xóa là một điều nhỏ bé không thực sự ảnh hưởng đến các nhà phát triển! Tất nhiên, tôi không thể nói cho người khác, nhưng theo kinh nghiệm của tôi, nó không bao giờ là vấn đề lớn. Tôi thực sự tận dụng thông tin loại thời gian chạy trong thiết kế API giả lập Java (JMockit) của tôi; trớ trêu thay, các API chế nhạo .NET dường như ít tận dụng lợi thế của hệ thống loại chung có sẵn trong C #.
Rogério

99

Trình biên dịch có trách nhiệm hiểu Generics tại thời gian biên dịch. Trình biên dịch cũng chịu trách nhiệm loại bỏ "sự hiểu biết" này về các lớp chung, trong một quá trình chúng ta gọi là xóa kiểu . Tất cả xảy ra vào thời gian biên dịch.

Lưu ý: Trái với niềm tin của đa số các nhà phát triển Java, có thể giữ thông tin loại thời gian biên dịch và truy xuất thông tin này trong thời gian chạy, mặc dù theo cách rất hạn chế. Nói cách khác: Java không cung cấp các tổng quát thống nhất theo một cách rất hạn chế .

Về loại tẩy

Chú ý rằng, tại thời gian biên dịch, trình biên dịch có thông tin đầy đủ loại có sẵn nhưng thông tin này được cố ý giảm nói chung khi mã byte được tạo ra, trong một quá trình được gọi là loại tẩy xoá . Điều này được thực hiện theo cách này do các vấn đề tương thích: Ý định của các nhà thiết kế ngôn ngữ là cung cấp khả năng tương thích mã nguồn đầy đủ và khả năng tương thích mã byte đầy đủ giữa các phiên bản của nền tảng. Nếu nó được triển khai khác đi, bạn sẽ phải biên dịch lại các ứng dụng cũ của mình khi chuyển sang các phiên bản mới hơn của nền tảng. Cách nó được thực hiện, tất cả các chữ ký phương thức đều được giữ nguyên (khả năng tương thích mã nguồn) và bạn không cần phải biên dịch lại bất cứ thứ gì (tương thích nhị phân).

Về khái quát thống nhất trong Java

Nếu bạn cần giữ thông tin loại thời gian biên dịch, bạn cần sử dụng các lớp ẩn danh. Vấn đề là: trong trường hợp rất đặc biệt của các lớp ẩn danh, có thể truy xuất thông tin loại thời gian biên dịch đầy đủ trong thời gian chạy, nói cách khác có nghĩa là: tổng quát thống nhất. Điều này có nghĩa là trình biên dịch không vứt bỏ thông tin loại khi các lớp ẩn danh có liên quan; thông tin này được giữ trong mã nhị phân được tạo và hệ thống thời gian chạy cho phép bạn truy xuất thông tin này.

Tôi đã viết một bài viết về chủ đề này:

https://rgomes.info/USE-typetokens-to-retrieve-generic-parameter/

Một lưu ý về kỹ thuật được mô tả trong bài viết trên là kỹ thuật này không rõ ràng đối với phần lớn các nhà phát triển. Mặc dù nó hoạt động và hoạt động tốt, hầu hết các nhà phát triển cảm thấy bối rối hoặc không thoải mái với kỹ thuật này. Nếu bạn có cơ sở mã được chia sẻ hoặc dự định phát hành mã của mình ra công chúng, tôi không khuyến nghị kỹ thuật trên. Mặt khác, nếu bạn là người sử dụng mã duy nhất của mình, bạn có thể tận dụng sức mạnh mà kỹ thuật này mang lại cho bạn.

Mã mẫu

Bài viết trên có liên kết đến mã mẫu.


1
@ will824: Tôi đã cải thiện ồ ạt câu trả lời và tôi đã thêm một liên kết đến một số trường hợp thử nghiệm. Chúc mừng :)
Richard Gomes

1
Trên thực tế, họ đã không duy trì cả khả năng tương thích nhị phân và nguồn: oracle.com/technetwork/java/javase/compabilities-137462.html Tôi có thể đọc thêm về ý định của họ ở đâu? Tài liệu nói rằng nó sử dụng loại tẩy, nhưng không nói lý do tại sao.
Dzmitry Lazerka

@Richard Thật vậy, bài viết tuyệt vời! Bạn có thể thêm rằng các lớp cục bộ cũng hoạt động và trong cả hai trường hợp (lớp ẩn danh và lớp cục bộ), thông tin về đối số kiểu mong muốn chỉ được lưu trong trường hợp truy cập trực tiếp new Box<String>() {};không phải trong trường hợp truy cập gián tiếp void foo(T) {...new Box<T>() {};...}vì trình biên dịch không giữ thông tin kiểu cho khai báo phương thức kèm theo.
Yann-Gaël Guéhéneuc

Tôi đã sửa một liên kết bị hỏng đến bài viết của tôi. Tôi đang dần dần làm xáo trộn cuộc sống của mình và lấy lại dữ liệu của mình. :-)
Richard Gomes

33

Nếu bạn có một trường là một loại chung, các tham số loại của nó được biên dịch vào lớp.

Nếu bạn có một phương thức lấy hoặc trả về một kiểu chung, các tham số kiểu đó sẽ được biên dịch vào lớp.

Thông tin này là những gì trình biên dịch sử dụng để nói với bạn rằng bạn không thể vượt qua Box<String> cho empty(Box<T extends Number>)phương thức.

API là rất phức tạp, nhưng bạn có thể kiểm tra thông tin loại này thông qua API phản chiếu với các phương pháp như getGenericParameterTypes, getGenericReturnTypevà, đối với các lĩnh vực,getGenericType .

Nếu bạn có mã sử dụng một loại chung, trình biên dịch sẽ chèn các phôi nếu cần (trong trình gọi) để kiểm tra các loại. Các đối tượng chung chỉ là loại thô; loại tham số hóa là "bị xóa". Vì vậy, khi bạn tạo mộtnew Box<Integer>() , không có thông tin về Integerlớp trong Boxđối tượng.

Câu hỏi thường gặp của Angelika Langer là tài liệu tham khảo tốt nhất tôi từng thấy cho Java Generics.


2
Trên thực tế, đây là loại chung chung chính thức của các trường và phương thức được biên dịch vào lớp, nghĩa là kiểu chữ "T". Để có được loại thực của loại chung, bạn phải sử dụng "thủ thuật lớp ẩn danh" .
Yann-Gaël Guéhéneuc

13

Generics trong Java Language là một hướng dẫn thực sự tốt về chủ đề này.

Generics được trình biên dịch Java triển khai như một chuyển đổi front-end được gọi là erasure. Bạn có thể (gần như) nghĩ về nó như một bản dịch từ nguồn sang nguồn, theo đó phiên bản chung của loophole()được chuyển đổi thành phiên bản không chung chung.

Vì vậy, đó là lúc biên dịch. JVM sẽ không bao giờ biết ArrayListbạn đã sử dụng cái gì.

Tôi cũng muốn giới thiệu câu trả lời của ông Skeet về khái niệm tẩy xóa trong thuốc generic trong Java là gì?


6

Loại tẩy xảy ra tại thời gian biên dịch. Loại tẩy có nghĩa là nó sẽ quên về loại chung chung, không phải về mọi loại. Bên cạnh đó, vẫn sẽ có siêu dữ liệu về các loại chung chung. Ví dụ

Box<String> b = new Box<String>();
String x = b.getDefault();

được chuyển đổi thành

Box b = new Box();
String x = (String) b.getDefault();

tại thời gian biên dịch. Bạn có thể nhận được cảnh báo không phải vì trình biên dịch biết loại chung là gì, mà ngược lại, vì nó không biết đủ nên không thể đảm bảo an toàn cho loại.

Ngoài ra, trình biên dịch sẽ giữ lại thông tin loại về các tham số trong lệnh gọi phương thức mà bạn có thể truy xuất thông qua sự phản chiếu.

Đây hướng dẫn là tốt nhất mà tôi đã tìm thấy về đề tài này.


6

Thuật ngữ "xóa kiểu" không thực sự là mô tả chính xác về vấn đề của Java với khái quát. Loại xóa không phải là một điều xấu, thực sự nó rất cần thiết cho hiệu suất và thường được sử dụng trong một số ngôn ngữ như C ++, Haskell, D.

Trước khi bạn ghê tởm, xin vui lòng nhớ lại định nghĩa chính xác về loại tẩy từ Wiki

Loại tẩy là gì?

loại xóa liên quan đến quá trình thời gian tải mà theo đó các chú thích loại rõ ràng được xóa khỏi chương trình, trước khi nó được thực thi vào thời gian chạy

Loại xóa có nghĩa là loại bỏ các thẻ loại được tạo tại thời điểm thiết kế hoặc các loại thẻ được suy ra tại thời điểm biên dịch sao cho chương trình được biên dịch trong mã nhị phân không chứa bất kỳ thẻ loại nào. Và đây là trường hợp cho mọi ngôn ngữ lập trình biên dịch thành mã nhị phân trừ một số trường hợp bạn cần thẻ thời gian chạy. Ví dụ, các ngoại lệ này bao gồm tất cả các loại tồn tại (Các loại tham chiếu Java có thể phân loại được, Loại bất kỳ trong nhiều ngôn ngữ, Loại liên minh). Lý do cho việc xóa kiểu là vì các chương trình được chuyển đổi sang một ngôn ngữ trong một loại không được gõ (ngôn ngữ nhị phân chỉ cho phép các bit) vì các loại chỉ là trừu tượng và khẳng định cấu trúc cho các giá trị của nó và ngữ nghĩa phù hợp để xử lý chúng.

Vì vậy, đây là trở lại, một điều tự nhiên bình thường.

Vấn đề của Java là khác nhau và do cách nó thống nhất.

Các tuyên bố thường được thực hiện về Java không có khái quát thống nhất cũng là sai.

Java đã thống nhất, nhưng theo một cách sai lầm do khả năng tương thích ngược.

Thống nhất là gì?

Từ Wiki của chúng tôi

Reization là quá trình mà một ý tưởng trừu tượng về một chương trình máy tính được biến thành một mô hình dữ liệu rõ ràng hoặc đối tượng khác được tạo bằng ngôn ngữ lập trình.

Reization có nghĩa là chuyển đổi một cái gì đó trừu tượng (Loại tham số) thành một cái gì đó cụ thể (Loại bê tông) bằng cách chuyên môn hóa.

Chúng tôi minh họa điều này bằng một ví dụ đơn giản:

Một ArrayList với định nghĩa:

ArrayList<T>
{
    T[] elems;
    ...//methods
}

là một sự trừu tượng, cụ thể là một hàm tạo kiểu, được "thống nhất" khi chuyên với một kiểu cụ thể, nói Integer:

ArrayList<Integer>
{
    Integer[] elems;
}

nơi ArrayList<Integer>thực sự là một loại.

Nhưng đây chính xác là điều mà Java không có !!! , thay vào đó, chúng thống nhất các loại trừu tượng liên tục với giới hạn của chúng, tức là tạo ra cùng một loại cụ thể độc lập với các tham số được truyền vào để chuyên môn hóa:

ArrayList
{
    Object[] elems;
}

ở đây được thống nhất với Đối tượng ràng buộc ngầm ( ArrayList<T extends Object>== ArrayList<T>).

Mặc dù điều đó làm cho các mảng chung không thể sử dụng được và gây ra một số lỗi lạ cho các kiểu thô:

List<String> l= List.<String>of("h","s");
List lRaw=l
l.add(new Object())
String s=l.get(2) //Cast Exception

nó gây ra nhiều sự mơ hồ như

void function(ArrayList<Integer> list){}
void function(ArrayList<Float> list){}
void function(ArrayList<String> list){}

tham khảo cùng chức năng:

void function(ArrayList list)

và do đó, quá tải phương thức chung không thể được sử dụng trong Java.


2

Tôi đã gặp phải loại xóa trong Android. Trong sản xuất, chúng tôi sử dụng gradle với tùy chọn minify. Sau khi giảm thiểu, tôi đã có ngoại lệ gây tử vong. Tôi đã thực hiện chức năng đơn giản để hiển thị chuỗi thừa kế của đối tượng của mình:

public static void printSuperclasses(Class clazz) {
    Type superClass = clazz.getGenericSuperclass();

    Log.d("Reflection", "this class: " + (clazz == null ? "null" : clazz.getName()));
    Log.d("Reflection", "superClass: " + (superClass == null ? "null" : superClass.toString()));

    while (superClass != null && clazz != null) {
        clazz = clazz.getSuperclass();
        superClass = clazz.getGenericSuperclass();

        Log.d("Reflection", "this class: " + (clazz == null ? "null" : clazz.getName()));
        Log.d("Reflection", "superClass: " + (superClass == null ? "null" : superClass.toString()));
    }
}

Và có hai kết quả của chức năng này:

Mã không được rút gọn:

D/Reflection: this class: com.example.App.UsersList
D/Reflection: superClass: com.example.App.SortedListWrapper<com.example.App.Models.User>

D/Reflection: this class: com.example.App.SortedListWrapper
D/Reflection: superClass: android.support.v7.util.SortedList$Callback<T>

D/Reflection: this class: android.support.v7.util.SortedList$Callback
D/Reflection: superClass: class java.lang.Object

D/Reflection: this class: java.lang.Object
D/Reflection: superClass: null

Mã rút gọn:

D/Reflection: this class: com.example.App.UsersList
D/Reflection: superClass: class com.example.App.SortedListWrapper

D/Reflection: this class: com.example.App.SortedListWrapper
D/Reflection: superClass: class android.support.v7.g.e

D/Reflection: this class: android.support.v7.g.e
D/Reflection: superClass: class java.lang.Object

D/Reflection: this class: java.lang.Object
D/Reflection: superClass: null

Vì vậy, trong mã rút gọn, các lớp tham số thực tế được thay thế bằng các loại lớp thô mà không có bất kỳ thông tin loại nào. Là một giải pháp cho dự án của tôi, tôi đã loại bỏ tất cả các cuộc gọi phản ánh và thay thế chúng bằng các loại tham số rõ ràng được truyền trong các đối số hàm.

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.