Apa konsep penghapusan dalam obat generik di Jawa?

141

Apa konsep penghapusan dalam obat generik di Jawa?

Tushu
sumber

Jawaban:

200

Ini pada dasarnya cara generik diimplementasikan di Jawa melalui tipu daya kompiler. Kode generik yang dikompilasi sebenarnya hanya menggunakan di java.lang.Objectmana 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 Tadalah) 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:

Jon Skeet
sumber
6
@Ogerio: Tidak, objek tidak akan memiliki tipe generik yang berbeda. The bidang mengetahui jenis, tetapi benda tidak.
Jon Skeet
8
@Ogerio: Tentu saja - sangat mudah untuk mengetahui pada waktu eksekusi apakah sesuatu yang hanya disediakan 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>).
Jon Skeet
6
Saya tidak pernah mengklaim bahwa itu selalu atau hampir selalu merupakan masalah - tapi itu setidaknya cukup sering menjadi masalah dalam pengalaman saya. Ada berbagai tempat di mana saya terpaksa menambahkan Class<T>parameter ke konstruktor (atau metode umum) hanya karena Java tidak menyimpan informasi itu. Lihat EnumSet.allOfmisalnya - 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)
Jon Skeet
5
Sebelum saya menggunakan .NET generics, saya menemukan Java generics canggung dalam berbagai cara (dan wildcarding masih sakit kepala, meskipun bentuk varians "yang ditentukan oleh penelepon" jelas memiliki kelebihan) - tetapi itu hanya terjadi setelah saya menggunakan .NET generics untuk sementara itu saya melihat berapa banyak pola menjadi canggung atau tidak mungkin dengan generik Java. Ini paradoks Blub lagi. Saya tidak mengatakan bahwa .NET generics tidak memiliki kelemahan, btw - ada berbagai jenis hubungan yang tidak dapat diekspresikan, sayangnya - tapi saya jauh lebih suka ke generik Java.
Jon Skeet
5
@Rogerio: Ada banyak hal yang dapat Anda lakukan dengan refleksi - tetapi saya tidak cenderung menemukan saya ingin melakukan hal-hal itu hampir sesering hal-hal yang tidak dapat saya lakukan dengan Java generics. Saya tidak ingin mengetahui argumen jenis untuk lapangan hampir sesering yang saya ingin mengetahui argumen jenis objek yang sebenarnya.
Jon Skeet
41

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 -printflatadalah bendera yang diserahkan ke kompiler yang menghasilkan file. (Bagian -XDinilah yang memberitahu javacuntuk menyerahkannya ke toples yang dapat dieksekusi yang sebenarnya melakukan kompilasi daripada hanya javac, tapi saya ngelantur ...) Hal -d output_dirini 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, forloop gaya foreach baru diperluas ke forloop biasa , dll. Menyenangkan untuk melihat hal-hal kecil yang terjadi secara otomatis.

jigawot
sumber
29

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

    }
} 
Parag
sumber
Saya mencoba menggunakan java decompiler untuk melihat kode setelah tipe erasure dari file .class, tetapi file .class masih memiliki tipe informasi. Saya mencoba jigawotmengatakan, itu berhasil.
frank
25

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:

  • Kompatibilitas sumber (Senang memiliki ...)
  • Kompatibilitas biner (Harus ada!)
  • Kompatibilitas migrasi
    • Program yang ada harus terus bekerja
    • Perpustakaan yang ada harus dapat menggunakan tipe generik
    • Harus punya!

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 checkCollectionmetode 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:

  • ketika koleksi diteruskan ke perpustakaan pihak ketiga dan sangat penting bahwa kode perpustakaan tidak merusak koleksi dengan memasukkan elemen dari tipe yang salah.
  • sebuah program gagal dengan 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.Iteratorkelas 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();
}
VONC
sumber
2
Perhatikan bahwa kompatibilitas ke belakang dapat dicapai tanpa penghapusan tipe, tetapi tidak tanpa programmer Java yang belajar kumpulan koleksi baru. Itulah rute yang ditempuh NET. Dengan kata lain, ini peluru ketiga yang penting. (Lanjutan.)
Jon Skeet
15
Secara pribadi saya pikir ini adalah kesalahan rabun - itu memberi keuntungan jangka pendek dan kerugian jangka panjang.
Jon Skeet
8

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.

Daniel Spiewak
sumber
6
Ketidaknyamanan ini bukan saat Anda ingin melakukan hal-hal "nakal" pada waktu eksekusi. Itu ketika Anda ingin melakukan hal-hal yang masuk akal pada waktu eksekusi. Bahkan, ketik penghapusan memungkinkan Anda untuk melakukan hal-hal yang jauh lebih nakal - seperti melemparkan Daftar <String> ke Daftar dan kemudian ke Daftar <Tanggal> dengan hanya peringatan.
Jon Skeet
5

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.

Andrew Kennan
sumber
3
Java generics tidak dapat mewakili tipe nilai - tidak ada yang namanya Daftar <int>. Namun, tidak ada pass-by-reference di Jawa sama sekali - itu benar-benar melewati nilai (di mana nilai itu mungkin menjadi referensi.)
Jon Skeet
2

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


   }
}
snr
sumber
2

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:

  • Pemeriksaan tipe yang lebih kuat pada waktu kompilasi.
  • Eliminasi gips.
  • Memungkinkan pemrogram untuk mengimplementasikan algoritma umum.

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:

  • Ganti semua parameter tipe dalam tipe generik dengan batas atau Objek mereka jika parameter tipe tidak terikat. Bytecode yang dihasilkan, oleh karena itu, hanya berisi kelas, antarmuka, dan metode biasa.
  • Masukkan gips tipe jika perlu untuk menjaga keamanan tipe.
  • Hasilkan metode jembatan untuk melestarikan polimorfisme dalam tipe generik yang diperluas.

[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:

  • The penghapusan List<Integer>, List<String>dan List<List<String>>adalahList .
  • Penghapusan List<Integer>[]adalahList[] .
  • Penghapusan List itu sendiri, sama untuk semua jenis mentah.
  • Penghapusan int itu sendiri, sama untuk semua tipe primitif.
  • Penghapusan Integer itu sendiri, sama untuk semua jenis tanpa parameter tipe.
  • Penghapusan Tdalam definisi asListadalah Object, karenaT tidak memiliki batas.
  • Penghapusan Tdalam definisi maxadalah Comparable, karena T telah terikatComparable<? super T> .
  • Penghapusan Tdalam definisi akhir maxadalah Object, karena Ttelah 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 :)

Atif
sumber