Jawaban:
Ini pada dasarnya cara generik diimplementasikan di Jawa melalui tipu daya kompiler. Kode generik yang dikompilasi sebenarnya hanya menggunakan di java.lang.Object
mana pun Anda berbicara T
(atau parameter tipe lainnya) - dan ada beberapa metadata untuk memberi tahu kompiler bahwa itu benar-benar tipe generik.
Ketika Anda mengkompilasi beberapa kode terhadap tipe atau metode umum, kompiler bekerja apa yang Anda maksud sebenarnya (yaitu apa argumen tipe T
adalah) dan memverifikasi pada waktu kompilasi bahwa Anda melakukan hal yang benar, tetapi kode yang dipancarkan lagi hanya berbicara dalam hal java.lang.Object
- kompiler menghasilkan gips tambahan jika diperlukan. Pada waktu eksekusi, a List<String>
dan a List<Date>
persis sama; informasi tipe tambahan telah dihapus oleh kompiler.
Bandingkan ini dengan, katakanlah, C #, di mana informasi disimpan pada waktu eksekusi, yang memungkinkan kode berisi ekspresi seperti typeof(T)
yang setara dengan T.class
- kecuali bahwa yang terakhir tidak valid. (Ada perbedaan lebih lanjut antara .NET generics dan Java generics, ingatkan Anda.) Tipe erasure adalah sumber dari banyak pesan peringatan / kesalahan "ganjil" ketika berhadapan dengan generik Java.
Sumber lain:
Object
(dalam skenario yang diketik dengan lemah) sebenarnya a List<String>
) misalnya. Di Jawa itu tidak layak - Anda dapat mengetahui bahwa itu adalah ArrayList
, tetapi tidak seperti apa tipe generik aslinya. Hal semacam ini dapat muncul dalam situasi serialisasi / deserialisasi, misalnya. Contoh lain adalah ketika sebuah wadah harus mampu membuat instance dari tipe generiknya - Anda harus meneruskan tipe itu secara terpisah di Java (as Class<T>
).
Class<T>
parameter ke konstruktor (atau metode umum) hanya karena Java tidak menyimpan informasi itu. Lihat EnumSet.allOf
misalnya - argumen tipe umum untuk metode harus cukup; mengapa saya perlu menentukan argumen "normal" juga? Jawab: ketik erasure. Hal semacam ini mencemari API. Karena ketertarikan, apakah Anda sudah sering menggunakan .NET generics? (lanjutan)
Sama seperti catatan tambahan, ini adalah latihan yang menarik untuk benar-benar melihat apa yang dilakukan kompiler ketika melakukan penghapusan - membuat keseluruhan konsep sedikit lebih mudah untuk dipahami. Ada flag khusus yang dapat Anda operasikan kompilator untuk menghasilkan file java yang generiknya terhapus dan gips dimasukkan. Sebuah contoh:
javac -XD-printflat -d output_dir SomeFile.java
Ini -printflat
adalah bendera yang diserahkan ke kompiler yang menghasilkan file. (Bagian -XD
inilah yang memberitahu javac
untuk menyerahkannya ke toples yang dapat dieksekusi yang sebenarnya melakukan kompilasi daripada hanya javac
, tapi saya ngelantur ...) Hal -d output_dir
ini diperlukan karena kompiler memerlukan tempat untuk meletakkan file .java baru.
Ini, tentu saja, tidak lebih dari sekadar penghapusan; semua hal otomatis yang dilakukan kompiler dilakukan di sini. Sebagai contoh, konstruktor default juga dimasukkan, for
loop gaya foreach baru diperluas ke for
loop biasa , dll. Menyenangkan untuk melihat hal-hal kecil yang terjadi secara otomatis.
Penghapusan, secara harfiah berarti bahwa informasi jenis yang ada dalam kode sumber dihapus dari bytecode yang dikompilasi. Mari kita pahami ini dengan beberapa kode.
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);
}
}
}
Jika Anda mengkompilasi kode ini dan kemudian mendekompilasi dengan Java decompiler, Anda akan mendapatkan sesuatu seperti ini. Perhatikan bahwa kode yang didekompilasi tidak mengandung jejak dari informasi jenis yang ada dalam kode sumber asli.
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
mengatakan, itu berhasil.
Untuk menyelesaikan jawaban Jon Skeet yang sudah sangat lengkap, Anda harus menyadari konsep tipe erasure berasal dari kebutuhan kompatibilitas dengan Java versi sebelumnya .
Awalnya disajikan di EclipseCon 2007 (tidak lagi tersedia), kompatibilitasnya termasuk poin-poin tersebut:
Jawaban asli:
Karenanya:
new ArrayList<String>() => new ArrayList()
Ada proposisi untuk reifikasi yang lebih besar . Tentukan kembali bahwa "Anggap konsep abstrak sebagai nyata", di mana konstruksi bahasa harus menjadi konsep, bukan hanya gula sintaksis.
Saya juga harus menyebutkan checkCollection
metode Java 6, yang mengembalikan tampilan dinamis yang aman untuk koleksi yang ditentukan. Upaya apa pun untuk memasukkan elemen dari tipe yang salah akan menghasilkan langsungClassCastException
.
Mekanisme generik dalam bahasa menyediakan pengecekan tipe waktu kompilasi (statis), tetapi dimungkinkan untuk mengalahkan mekanisme ini dengan gips yang tidak dicentang .
Biasanya ini bukan masalah, karena kompilator mengeluarkan peringatan pada semua operasi yang tidak dicentang tersebut.
Namun, ada saat-saat pengecekan tipe statis saja tidak cukup, seperti:
ClassCastException
, yang menunjukkan bahwa elemen yang diketik salah dimasukkan ke dalam koleksi parameter. Sayangnya, pengecualian dapat terjadi kapan saja setelah elemen yang salah dimasukkan, sehingga biasanya memberikan sedikit atau tidak ada informasi mengenai sumber masalah yang sebenarnya.Pembaruan Juli 2012, hampir empat tahun kemudian:
Sekarang (2012) dirinci dalam " Aturan Kompatibilitas Migrasi API (Tes Tanda Tangan) "
Bahasa pemrograman Java mengimplementasikan generik menggunakan erasure, yang memastikan bahwa versi lama dan generik biasanya menghasilkan file kelas yang identik, kecuali untuk beberapa informasi tambahan tentang tipe. Kompatibilitas biner tidak rusak karena dimungkinkan untuk mengganti file kelas lama dengan file kelas umum tanpa mengubah atau mengkompilasi ulang kode klien apa pun.
Untuk memfasilitasi interfacing dengan kode legacy non-generik, juga dimungkinkan untuk menggunakan penghapusan tipe parameter sebagai tipe. Tipe seperti ini disebut tipe mentah ( Spesifikasi Bahasa Jawa 3 / 4.8 ). Mengizinkan tipe mentah juga memastikan kompatibilitas mundur untuk kode sumber.
Menurut ini, versi berikut dari
java.util.Iterator
kelas adalah kode sumber biner dan kompatibel dengan mundur:
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();
}
Melengkapi jawaban Jon Skeet yang sudah dilengkapi ...
Telah disebutkan bahwa menerapkan obat generik melalui penghapusan menyebabkan beberapa batasan yang mengganggu (misalnya tidak new T[42]
). Juga telah disebutkan bahwa alasan utama untuk melakukan hal-hal dengan cara ini adalah kompatibilitas mundur dalam bytecode. Ini juga (kebanyakan) benar. Bytecode yang dihasilkan -target 1.5 agak berbeda dari hanya de-sugared casting -target 1.4. Secara teknis, itu bahkan mungkin (melalui tipuan besar) untuk mendapatkan akses ke instantiasi tipe generik pada saat runtime , membuktikan bahwa memang ada sesuatu dalam bytecode.
Poin yang lebih menarik (yang belum diangkat) adalah bahwa mengimplementasikan obat generik menggunakan erasure menawarkan fleksibilitas yang lebih banyak dalam apa yang dapat dicapai oleh sistem tipe tingkat tinggi. Contoh yang baik dari ini adalah implementasi JVM Scala vs CLR. Pada JVM, dimungkinkan untuk menerapkan jenis yang lebih tinggi secara langsung karena fakta bahwa JVM sendiri tidak memberlakukan pembatasan pada tipe generik (karena "tipe" ini secara efektif tidak ada). Ini kontras dengan CLR, yang memiliki pengetahuan runtime instantiations parameter. Karena itu, CLR itu sendiri harus memiliki beberapa konsep tentang bagaimana obat generik harus digunakan, membatalkan upaya untuk memperluas sistem dengan aturan yang tidak terduga. Akibatnya, Scala yang lebih tinggi pada CLR diimplementasikan menggunakan bentuk erasure aneh yang ditiru dalam kompiler itu sendiri,
Penghapusan mungkin tidak nyaman ketika Anda ingin melakukan hal-hal nakal saat runtime, tetapi memang menawarkan fleksibilitas paling besar kepada penulis kompiler. Saya menduga itu adalah bagian dari mengapa itu tidak akan pergi dalam waktu dekat.
Seperti yang saya pahami (menjadi seorang .NET guy) JVM tidak memiliki konsep generik, sehingga kompiler menggantikan parameter tipe dengan Object dan melakukan semua casting untuk Anda.
Ini berarti bahwa generik Java tidak lain adalah gula sintaksis dan tidak menawarkan peningkatan kinerja apa pun untuk tipe nilai yang memerlukan tinju / unboxing ketika disahkan oleh referensi.
Ada penjelasan bagus. Saya hanya menambahkan contoh untuk menunjukkan bagaimana tipe erasure bekerja dengan dekompiler.
Kelas asli,
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);
}
}
Kode yang didekompilasi dari bytecode-nya,
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);
}
}
Mengapa menggunakan Generices
Singkatnya, generik memungkinkan tipe (kelas dan antarmuka) menjadi parameter ketika mendefinisikan kelas, antarmuka, dan metode. Sama seperti parameter formal yang lebih akrab digunakan dalam deklarasi metode, ketik parameter memberikan cara bagi Anda untuk menggunakan kembali kode yang sama dengan input yang berbeda. Perbedaannya adalah input ke parameter formal adalah nilai, sedangkan input untuk mengetik parameter adalah tipe. ode yang menggunakan obat generik memiliki banyak manfaat dibandingkan kode non-generik:
Apa itu Penghapusan Jenis
Generik diperkenalkan ke bahasa Jawa untuk menyediakan pemeriksaan tipe yang lebih ketat pada waktu kompilasi dan untuk mendukung pemrograman generik. Untuk menerapkan obat generik, kompiler Java menerapkan tipe penghapusan untuk:
[NB] -Apa itu metode jembatan? Singkatnya, Dalam kasus antarmuka berparameter seperti Comparable<T>
, ini dapat menyebabkan metode tambahan dimasukkan oleh kompiler; metode tambahan ini disebut jembatan.
Cara Kerja Penghapusan
Penghapusan tipe didefinisikan sebagai berikut: jatuhkan semua tipe parameter dari tipe parameter, dan ganti variabel tipe apa pun dengan penghapusan batasnya, atau dengan Objek jika tidak ada batasnya, atau dengan penghapusan batas paling kiri jika ia memiliki banyak batas. Berikut ini beberapa contohnya:
List<Integer>
, List<String>
dan List<List<String>>
adalahList
.List<Integer>[]
adalahList[]
.List
itu sendiri, sama untuk semua jenis mentah.Integer
itu sendiri, sama untuk semua jenis tanpa parameter tipe.T
dalam definisi asList
adalah Object
, karenaT
tidak memiliki batas.T
dalam definisi max
adalah Comparable
, karena T
telah terikatComparable<? super T>
.T
dalam definisi akhir max
adalah Object
, karena
T
telah terikat Object
& Comparable<T>
dan kami mengambil penghapusan dari batas paling kiri.Perlu hati-hati saat menggunakan obat generik
Di Jawa, dua metode berbeda tidak dapat memiliki tanda tangan yang sama. Karena obat generik diimplementasikan oleh penghapusan, maka juga mengikuti bahwa dua metode yang berbeda tidak dapat memiliki tanda tangan dengan penghapusan yang sama. Kelas tidak dapat membebani dua metode yang tanda tangannya memiliki penghapusan yang sama, dan kelas tidak dapat mengimplementasikan dua antarmuka yang memiliki penghapusan yang sama.
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;
}
}
Kami bermaksud kode ini berfungsi sebagai berikut:
assert allZero(Arrays.asList(0,0,0));
assert allZero(Arrays.asList("","",""));
Namun, dalam hal ini penghapusan tanda tangan dari kedua metode identik:
boolean allZero(List)
Oleh karena itu, bentrokan nama dilaporkan pada waktu kompilasi. Tidak mungkin untuk memberikan kedua metode nama yang sama dan mencoba untuk membedakan antara mereka dengan overloading, karena setelah penghapusan tidak mungkin untuk membedakan satu metode panggilan dari yang lain.
Semoga Pembaca akan menikmati :)