Penghapusan tipe generik Java: kapan dan apa yang terjadi?

238

Saya membaca tentang penghapusan tipe Java di situs web Oracle .

Kapan penghapusan tipe terjadi? Pada waktu kompilasi atau runtime? Kapan kelas dimuat? Kapan kelas dipakai?

Banyak situs (termasuk tutorial resmi yang disebutkan di atas) mengatakan penghapusan tipe terjadi pada waktu kompilasi. Jika informasi jenis dihapus sepenuhnya pada waktu kompilasi, bagaimana JDK memeriksa kompatibilitas jenis ketika metode menggunakan obat-obatan dipanggil tanpa informasi jenis atau informasi jenis yang salah?

Pertimbangkan contoh berikut: Katakanlah kelas Amemiliki metode empty(Box<? extends Number> b),. Kami mengkompilasi A.javadan mendapatkan file kelas A.class.

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

Sekarang kita buat kelas lain Byang memanggil metode emptydengan argumen non-parameter (tipe mentah): empty(new Box()). Jika kita kompilasi B.javadengan A.classdi classpath, javac cukup pintar untuk membangkitkan peringatan. Jadi A.class ada beberapa jenis informasi yang tersimpan di dalamnya.

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());
    }
}

Dugaan saya adalah bahwa penghapusan tipe terjadi ketika kelas dimuat, tetapi itu hanya dugaan. Jadi kapan itu terjadi?

Radiodef
sumber
2
Versi yang lebih "umum" dari pertanyaan ini: stackoverflow.com/questions/313584/…
Ciro Santilli 郝海东 冠状 病 六四 六四 事件 事件
@afryingpan: Artikel yang disebutkan dalam jawaban saya menjelaskan secara terperinci bagaimana dan kapan penghapusan tipe terjadi. Ini juga menjelaskan kapan informasi jenis disimpan. Dengan kata lain: generik reified tersedia di Jawa, bertentangan dengan kepercayaan yang tersebar luas. Lihat: rgomes.info/using-typetokens-to-retrieve-generic-parameters
Richard Gomes

Jawaban:

240

Penghapusan tipe berlaku untuk penggunaan obat generik. Jelas ada metadata di file kelas untuk mengatakan apakah metode / tipe adalah generik, dan apa batasannya, dll. Tetapi ketika generik digunakan , mereka dikonversi menjadi pemeriksaan waktu kompilasi dan waktu eksekusi. Jadi kode ini:

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

dikompilasi menjadi

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

Pada waktu eksekusi tidak ada cara untuk mengetahui bahwa T=Stringuntuk objek daftar - informasi itu hilang.

... tetapi List<T>antarmuka itu sendiri masih mengiklankan dirinya sebagai generik.

EDIT: Hanya untuk memperjelas, kompiler tidak menyimpan informasi tentang variabel menjadi List<String>- tetapi Anda masih tidak bisa mengetahui bahwa T=Stringuntuk objek daftar itu sendiri.

Jon Skeet
sumber
6
Tidak, bahkan dalam penggunaan tipe generik mungkin ada metadata yang tersedia saat runtime. Variabel lokal tidak dapat diakses melalui Refleksi, tetapi untuk parameter metode yang dinyatakan sebagai "Daftar <String> l", akan ada ketik metadata saat runtime, tersedia melalui API Refleksi. Yep, "penghapusan jenis" tidak sesederhana banyak orang berpikir ...
Rogério
4
@Rogerio: Saat saya membalas komentar Anda, saya yakin Anda bingung antara bisa mendapatkan jenis variabel dan bisa mendapatkan jenis objek . Objek itu sendiri tidak tahu argumen tipenya, meskipun bidangnya.
Jon Skeet
Tentu saja, hanya dengan melihat objek itu sendiri Anda tidak dapat mengetahui bahwa itu adalah Daftar <String>. Tapi objek tidak muncul begitu saja. Mereka dibuat secara lokal, diteruskan sebagai argumen pemanggilan metode, dikembalikan sebagai nilai pengembalian dari pemanggilan metode, atau membaca dari bidang beberapa objek ... Dalam semua kasus ini Anda BISA tahu pada saat runtime apa jenis generiknya, baik secara implisit atau dengan menggunakan Java Reflection API.
Rogério
13
@Rogerio: Bagaimana Anda tahu dari mana benda itu berasal? Jika Anda memiliki parameter tipe, List<? extends InputStream>bagaimana Anda bisa tahu tipe apa itu ketika dibuat? Bahkan jika Anda dapat mengetahui jenis bidang referensi yang telah disimpan, mengapa Anda harus melakukannya? Mengapa Anda bisa mendapatkan semua informasi tentang objek pada waktu eksekusi, tetapi tidak pada argumen tipe generiknya? Anda tampaknya mencoba untuk membuat penghapusan tipe menjadi hal kecil ini yang tidak mempengaruhi pengembang sebenarnya - sedangkan saya menemukan itu menjadi masalah yang sangat signifikan dalam beberapa kasus.
Jon Skeet
Tapi tipe erasure adalah hal kecil yang tidak terlalu memengaruhi pengembang! Tentu saja, saya tidak bisa berbicara untuk orang lain, tetapi dalam pengalaman SAYA itu tidak pernah menjadi masalah besar. Saya benar-benar memanfaatkan informasi tipe runtime dalam desain Java mocking API (JMockit); Ironisnya, .NET ejek API tampaknya kurang memanfaatkan sistem tipe generik yang tersedia di C #.
Rogério
99

Compiler bertanggung jawab untuk memahami Generics pada waktu kompilasi. Compiler juga bertanggung jawab untuk membuang "pemahaman" kelas generik ini, dalam proses yang kita sebut penghapusan tipe . Semua terjadi pada waktu kompilasi.

Catatan: Bertentangan dengan kepercayaan mayoritas pengembang Java, dimungkinkan untuk menyimpan informasi tipe waktu kompilasi dan mengambil informasi ini pada saat runtime, meskipun dengan cara yang sangat terbatas. Dengan kata lain: Java memang menyediakan reified generics dengan cara yang sangat terbatas .

Mengenai penghapusan jenis

Perhatikan bahwa, pada waktu kompilasi, kompiler memiliki informasi tipe lengkap yang tersedia tetapi informasi ini sengaja dihapus secara umum ketika kode byte dihasilkan, dalam proses yang dikenal sebagai tipe erasure . Ini dilakukan dengan cara ini karena masalah kompatibilitas: Tujuan dari perancang bahasa adalah menyediakan kompatibilitas kode sumber penuh dan kompatibilitas kode byte penuh antara versi platform. Jika itu diterapkan secara berbeda, Anda harus mengkompilasi ulang aplikasi lawas Anda ketika bermigrasi ke versi platform yang lebih baru. Cara itu dilakukan, semua tanda tangan metode dipertahankan (kompatibilitas kode sumber) dan Anda tidak perlu mengkompilasi ulang apa pun (kompatibilitas biner).

Mengenai reified generics di Java

Jika Anda perlu menyimpan informasi tipe waktu kompilasi, Anda harus menggunakan kelas anonim. Intinya adalah: dalam kasus khusus kelas anonim, dimungkinkan untuk mengambil informasi tipe waktu kompilasi penuh pada saat runtime yang, dengan kata lain berarti: reified generics. Ini berarti bahwa kompiler tidak membuang informasi jenis ketika kelas anonim terlibat; informasi ini disimpan dalam kode biner yang dihasilkan dan sistem runtime memungkinkan Anda untuk mengambil informasi ini.

Saya telah menulis artikel tentang subjek ini:

https://rgomes.info/using-typetokens-to-retrieve-generic-parameters/

Catatan tentang teknik yang dijelaskan dalam artikel di atas adalah bahwa teknik ini tidak jelas untuk sebagian besar pengembang. Meskipun bekerja dan bekerja dengan baik, sebagian besar pengembang merasa bingung atau tidak nyaman dengan teknik ini. Jika Anda memiliki basis kode bersama atau berencana untuk merilis kode Anda kepada publik, saya tidak merekomendasikan teknik di atas. Di sisi lain, jika Anda adalah satu-satunya pengguna kode Anda, Anda dapat memanfaatkan kekuatan yang diberikan teknik ini kepada Anda.

Kode sampel

Artikel di atas memiliki tautan ke kode sampel.

Richard Gomes
sumber
1
@ will824: Saya telah meningkatkan jawaban secara besar-besaran dan saya telah menambahkan tautan ke beberapa kasus uji. Cheers :)
Richard Gomes
1
Sebenarnya, mereka tidak memelihara kompatibilitas biner dan sumber: oracle.com/technetwork/java/javase/compatibility-137462.html Di mana saya dapat membaca lebih lanjut tentang niat mereka? Documents mengatakan bahwa ia menggunakan tipe erasure, tetapi tidak mengatakan mengapa.
Dzmitry Lazerka
@ Richard Memang, artikel yang bagus! Anda bisa menambahkan bahwa kelas lokal juga berfungsi dan bahwa, dalam kedua kasus (kelas anonim dan lokal), informasi tentang argumen tipe yang diinginkan disimpan hanya dalam kasus akses langsung new Box<String>() {};bukan dalam kasus akses tidak langsung void foo(T) {...new Box<T>() {};...}karena kompiler tidak menyimpan informasi tipe untuk deklarasi metode terlampir.
Yann-Gaël Guéhéneuc
Saya telah memperbaiki tautan yang rusak ke artikel saya. Saya perlahan-lahan menghapus googling hidup saya dan mendapatkan kembali data saya. :-)
Richard Gomes
33

Jika Anda memiliki bidang yang merupakan tipe generik, parameter tipenya dikompilasi ke dalam kelas.

Jika Anda memiliki metode yang mengambil atau mengembalikan tipe generik, parameter tipe tersebut dikompilasi ke dalam kelas.

Informasi ini adalah apa yang menggunakan compiler untuk memberitahu Anda bahwa Anda tidak dapat lulus Box<String>dengan empty(Box<T extends Number>)metode.

API yang rumit, tetapi Anda dapat memeriksa informasi jenis ini melalui refleksi API dengan metode seperti getGenericParameterTypes, getGenericReturnType, dan, untuk bidang, getGenericType.

Jika Anda memiliki kode yang menggunakan tipe generik, kompiler menyisipkan gips yang diperlukan (di pemanggil) untuk memeriksa jenis. Objek generik itu sendiri hanyalah tipe mentah; tipe yang diparameterisasi "dihapus". Jadi, saat Anda membuat new Box<Integer>(), tidak ada informasi tentang Integerkelas di Boxobjek.

FAQ Angelika Langer adalah referensi terbaik yang pernah saya lihat untuk Java Generics.

erickson
sumber
2
Sebenarnya, ini adalah tipe generik formal bidang dan metode yang dikompilasi ke dalam kelas, yaitu, ketik "T". Untuk mendapatkan tipe nyata dari tipe generik, Anda harus menggunakan "trik kelas anonim" .
Yann-Gaël Guéhéneuc
13

Generik dalam Bahasa Jawa adalah panduan yang sangat bagus tentang topik ini.

Generik diimplementasikan oleh kompiler Java sebagai konversi front-end yang disebut erasure. Anda dapat (hampir) menganggapnya sebagai terjemahan sumber-ke-sumber, di mana versi generik loophole()dikonversi ke versi non-generik.

Jadi, ini pada waktu kompilasi. JVM tidak akan pernah tahu yang ArrayListAnda gunakan.

Saya juga merekomendasikan jawaban Tn. Skeet tentang Apa konsep penghapusan dalam obat generik di Jawa?

Eugene Yokota
sumber
6

Penghapusan tipe terjadi pada waktu kompilasi. Apa yang dimaksud dengan penghapusan tipe adalah bahwa ia akan melupakan jenis generik, bukan tentang setiap jenis. Selain itu, masih akan ada metadata tentang jenis yang generik. Sebagai contoh

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

dikonversi menjadi

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

pada waktu kompilasi. Anda mungkin mendapatkan peringatan bukan karena kompiler tahu tentang jenis apa yang merupakan generik, tetapi sebaliknya, karena tidak cukup tahu sehingga tidak dapat menjamin keamanan jenis.

Selain itu, kompiler mempertahankan informasi tipe tentang parameter pada pemanggilan metode, yang dapat Anda ambil melalui refleksi.

Panduan ini adalah yang terbaik yang saya temukan pada subjek.

Vinko Vrsalovic
sumber
6

Istilah "type erasure" sebenarnya bukan deskripsi yang benar tentang masalah Java dengan obat generik. Penghapusan tipe bukan per hal yang buruk, memang sangat diperlukan untuk kinerja dan sering digunakan dalam beberapa bahasa seperti C ++, Haskell, D.

Sebelum Anda jijik, harap ingat definisi penghapusan tipe yang benar dari Wiki

Apa itu tipe erasure?

type erasure mengacu pada proses load-time dimana anotasi tipe eksplisit dihapus dari suatu program, sebelum dieksekusi pada saat run-time

Penghapusan tipe berarti membuang tag tipe yang dibuat pada waktu desain atau tag tipe yang disimpulkan pada waktu kompilasi sehingga program yang dikompilasi dalam kode biner tidak mengandung tag tipe apa pun. Dan ini adalah kasus untuk setiap bahasa pemrograman yang dikompilasi ke kode biner kecuali dalam beberapa kasus di mana Anda membutuhkan tag runtime. Pengecualian ini termasuk misalnya semua tipe eksistensial (Tipe Referensi Java yang subtipe, Semua Jenis dalam banyak bahasa, Tipe Union). Alasan penghapusan jenis adalah bahwa program ditransformasikan ke bahasa yang dalam beberapa jenis uni-diketik (bahasa biner hanya memungkinkan bit) karena jenis adalah abstraksi saja dan menyatakan struktur untuk nilai-nilainya dan semantik yang tepat untuk menanganinya.

Jadi ini balasannya, hal yang wajar.

Masalah Java berbeda dan disebabkan oleh bagaimana ia direvisi.

Pernyataan yang sering dibuat tentang Java tidak memiliki reified generics juga salah.

Java memang memverifikasi, tetapi dengan cara yang salah karena kompatibilitas ke belakang.

Apa itu reifikasi?

Dari Wiki kami

Reifikasi adalah proses dimana ide abstrak tentang program komputer diubah menjadi model data eksplisit atau objek lain yang dibuat dalam bahasa pemrograman.

Reifikasi berarti mengubah sesuatu yang abstrak (Tipe Parametrik) menjadi sesuatu yang konkret (Jenis Beton) dengan spesialisasi.

Kami menggambarkan ini dengan contoh sederhana:

Daftar Array dengan definisi:

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

adalah abstraksi, secara terperinci sebuah konstruktor tipe, yang akan "diverifikasi" ketika dikhususkan dengan tipe beton, katakan Integer:

ArrayList<Integer>
{
    Integer[] elems;
}

di mana ArrayList<Integer>sebenarnya tipe.

Tapi ini persis hal yang bukan Java !!! , sebagai gantinya mereka merevisi tipe yang abstrak secara konstan dengan batasannya, yaitu memproduksi tipe beton yang sama tanpa parameter yang diteruskan untuk spesialisasi:

ArrayList
{
    Object[] elems;
}

yang di sini diverifikasi dengan Objek terikat implisit ( ArrayList<T extends Object>== ArrayList<T>).

Meskipun demikian itu membuat array generik tidak dapat digunakan dan menyebabkan beberapa kesalahan aneh untuk tipe mentah:

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

itu menyebabkan banyak ambiguitas

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

lihat fungsi yang sama:

void function(ArrayList list)

dan karena itu overloading metode generik tidak dapat digunakan di Jawa.

iconfly
sumber
2

Saya sudah menemukan tipe erasure di Android. Dalam produksi kami menggunakan gradle dengan opsi minify. Setelah minifikasi, saya mendapat pengecualian fatal. Saya telah membuat fungsi sederhana untuk menunjukkan rantai pewarisan objek saya:

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()));
    }
}

Dan ada dua hasil fungsi ini:

Bukan kode yang diperkecil:

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

Kode yang diperkecil:

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

Jadi, dalam kode minified, kelas parametrized aktual diganti dengan tipe kelas mentah tanpa informasi tipe apa pun. Sebagai solusi untuk proyek saya, saya menghapus semua panggilan refleksi dan menggantinya dengan tipe params eksplisit yang diteruskan dalam argumen fungsi.

porfirion
sumber