Datang, lihat, digeneralisasi: direndam dalam Java Generics

Java Generics adalah salah satu perubahan paling signifikan dalam sejarah bahasa Java. Generik yang tersedia dengan Java 5 telah menjadikan penggunaan Java Collection Framework lebih mudah, lebih nyaman, dan lebih aman. Kesalahan yang terkait dengan penggunaan tipe yang salah sekarang terdeteksi pada tahap kompilasi. Ya, dan bahasa Jawa sendiri menjadi lebih aman. Terlepas dari kesederhanaan jenis generik yang tampak, banyak pengembang mengalami kesulitan menggunakannya. Dalam posting ini saya akan berbicara tentang fitur-fitur bekerja dengan Java Generics, sehingga Anda memiliki lebih sedikit kesulitan ini. Berguna jika Anda bukan guru umum, dan akan membantu menghindari banyak kesulitan saat membahas topik tersebut.



Bekerja dengan koleksi


Misalkan bank perlu menghitung jumlah tabungan di rekening pelanggan. Sebelum munculnya "obat generik," metode penghitungan jumlah tampak seperti ini:

public long getSum(List accounts) {   long sum = 0;   for (int i = 0, n = accounts.size(); i < n; i++) {       Object account = accounts.get(i);       if (account instanceof Account) {           sum += ((Account) account).getAmount();       }   }   return sum; } 

Kami mengulang, menelusuri daftar akun, dan memeriksa apakah elemen dari daftar ini benar-benar turunan dari kelas Account - yaitu, akun pengguna. Jenis objek kami dari kelas Account dan metode getAmount , yang mengembalikan jumlah dalam akun ini. Kemudian mereka menyimpulkan semuanya dan mengembalikan jumlah total. Diperlukan dua langkah:
 if (account instanceof Account) { // (1) 

 sum += ((Account) account).getAmount(); // (2) 

Jika Anda tidak memeriksa ( instanceof ) dari milik kelas Account , maka pada tahap kedua ClassCastException dimungkinkan - yaitu, crash program. Karena itu, cek semacam itu wajib.

Dengan munculnya Generics, kebutuhan untuk pengecekan dan pengecoran tipe telah menghilang:
 public long getSum2(List<Account> accounts) {  long sum = 0;  for (Account account : accounts) {      sum += account.getAmount();  }  return sum; } 

Sekarang metode
 getSum2(List<Account> accounts) 
menerima sebagai argumen hanya daftar objek Account kelas. Pembatasan ini ditunjukkan dalam metode itu sendiri, dalam tanda tangannya, programmer tidak dapat mentransfer daftar lain - hanya daftar akun klien.

Kita tidak perlu memeriksa tipe elemen dari daftar ini: itu tersirat oleh deskripsi tipe parameter metode
 List<Account> accounts 
(dapat dibaca sebagai Account ). Dan kompiler akan melempar kesalahan jika terjadi kesalahan - yaitu, jika seseorang mencoba meneruskan daftar objek selain kelas Account ke metode ini.

Pada baris kedua cek, kebutuhan juga hilang. Jika perlu, casting akan dilakukan pada tahap kompilasi.

Prinsip substitusi


Prinsip substitusi dari Barbara Liskov adalah definisi spesifik dari subtipe dalam pemrograman berorientasi objek. Gagasan Liskov tentang β€œsubtipe” mendefinisikan konsep substitusi: jika S adalah subtipe dari T , maka objek tipe T dalam suatu program dapat digantikan oleh objek tipe S tanpa ada perubahan pada sifat yang diinginkan dari program ini.

Jenis
Subtipe
Nomor
Integer
Daftar <E>
ArrayList <E>
Koleksi <E>
Daftar <E>
Berterima kasih <E>
Koleksi <E>

Contoh / Jenis Hubungan Contoh

Berikut adalah contoh penggunaan prinsip substitusi di Jawa:
 Number n = Integer.valueOf(42); List<Number> aList = new ArrayList<>(); Collection<Number> aCollection = aList; Iterable<Number> iterable = aCollection; 

Integer adalah subtipe dari Number , oleh karena itu, variabel n type Number dapat diberi nilai yang dikembalikan oleh metode Integer.valueOf(42) .

Kovarian, contravariance dan invarian


Pertama, sedikit teori. Kovarian adalah pelestarian hierarki pewarisan tipe sumber dalam tipe turunan dalam urutan yang sama. Misalnya, jika Kucing adalah subtipe Hewan , maka Perangkat <Cats> adalah subtipe dari Perangkat <Hewan> . Oleh karena itu, dengan mempertimbangkan prinsip substitusi, seseorang dapat melakukan penugasan berikut:

Many <Hewan> = Banyak <Cats>

Contravariance adalah pembalikan hirarki tipe sumber dalam tipe turunan. Misalnya, jika Kucing adalah subtipe , maka Set <Hewan> adalah subtipe dari Set <Cat> . Oleh karena itu, dengan mempertimbangkan prinsip substitusi, seseorang dapat melakukan penugasan berikut:

Many <Cats> = Many <Animals>

Invariance - kurangnya pewarisan antara tipe turunan. Jika Kucing adalah subtipe Hewan , maka Perangkat <Cats> bukan subtipe dari Perangkat <Hewan> dan Perangkat <Hewan> bukan subtipe dari Perangkat <Cat> .

Array di Jawa adalah kovarian . Tipe S[] adalah subtipe dari T[] jika S adalah subtipe dari T Contoh Penugasan:
 String[] strings = new String[] {"a", "b", "c"}; Object[] arr = strings; 

Kami menetapkan tautan ke array string ke arr variabel, jenisnya adalah Β« Β» . Jika array bukan kovarian, kami tidak akan dapat melakukan ini. Java memungkinkan Anda untuk melakukan ini, program mengkompilasi dan berjalan tanpa kesalahan.

 arr[0] = 42; // ArrayStoreException.       

Tetapi jika kita mencoba mengubah isi array melalui variabel arr dan menulis angka 42 di sana, kita akan mendapatkan ArrayStoreException pada tahap eksekusi program, karena 42 bukan string, tetapi angka. Ini adalah kelemahan dari kovarians array Java: kita tidak dapat melakukan pemeriksaan pada tahap kompilasi, dan sesuatu mungkin sudah rusak saat runtime.

"Generik" tidak berubah. Berikut ini sebuah contoh:
 List<Integer> ints = Arrays.asList(1,2,3); List<Number> nums = ints; // compile-time error.      nums.set(2, 3.14); assert ints.toString().equals("[1, 2, 3.14]"); 

Jika Anda mengambil daftar bilangan bulat, maka itu bukan subtipe dari Number , atau subtipe lainnya. Dia hanya subtipe dari dirinya sendiri. Yaitu, List <Integer> adalah List<Integer> dan tidak ada yang lain. Compiler akan memastikan bahwa variabel ints dideklarasikan sebagai daftar objek kelas Integer yang hanya berisi objek kelas Integer dan tidak ada yang lain. Pada tahap kompilasi, pemeriksaan dilakukan, dan tidak ada yang akan jatuh pada runtime kami.

Kartu liar


Apakah Generik selalu invarian? Tidak. Saya akan memberikan contoh:
 List<Integer> ints = new ArrayList<Integer>(); List<? extends Number> nums = ints; 

Ini adalah kovarians. List<Integer> - subtipe List<? extends Number> List<? extends Number>

 List<Number> nums = new ArrayList<Number>(); List<? super Integer> ints = nums; 

Ini adalah penghinaan. List<Number> adalah subtipe List<? super Integer> List<? super Integer> .

Catatan seperti "? extends ..." atau "? super ..." disebut wildcard atau wildcard, dengan batas atas ( extends ) atau batas bawah ( super ). List<? extends Number> List<? extends Number> dapat berisi objek yang kelasnya Number atau diwarisi dari Number . List<? super Number> List<? super Number> dapat berisi objek yang kelasnya Number atau yang Number adalah pewarisnya (supertype from Number ).


meluas B - wildcard dengan batas atas
super B - wildcard dengan batas bawah
di mana B - mewakili perbatasan

Catatan bentuk T2 <= T1 berarti bahwa himpunan tipe yang dijelaskan oleh T2 adalah himpunan bagian dari himpunan tipe yang dijelaskan oleh T 1

yaitu
Angka <=? memperpanjang Object
? memperpanjang Number <=? memperpanjang Object
dan
? super Object <=? nomor super


Lebih banyak interpretasi matematis dari topik

Sepasang tugas untuk menguji pengetahuan:

1. Mengapa kesalahan waktu kompilasi dalam contoh di bawah ini? Nilai apa yang bisa saya tambahkan ke daftar nums ?
 List<Integer> ints = new ArrayList<Integer>(); ints.add(1); ints.add(2); List<? extends Number> nums = ints; nums.add(3.14); // compile-time error 

Jawabannya
Haruskah wadah dideklarasikan dengan wildcard ? extends ? extends , Anda hanya bisa membaca nilai. Tidak ada yang bisa ditambahkan ke daftar kecuali null . Untuk menambahkan objek ke daftar, kita perlu jenis wildcard lain - ? super ? super


2. Mengapa saya tidak bisa mendapatkan item dari daftar di bawah ini?
 public static <T> T getFirst(List<? super T> list) {  return list.get(0); // compile-time error } 

Jawabannya
Tidak dapat membaca item dari wadah dengan wildcard ? super ? super , kecuali objek Object kelas

 public static <T> Object getFirst(List<? super T> list) {  return list.get(0); } 



Prinsip Get and Put atau PECS (Produser Memperpanjang Super Konsumen)


Fitur wildcard dengan batas atas dan bawah memberikan fitur tambahan yang terkait dengan penggunaan jenis yang aman. Anda hanya bisa membaca dari satu jenis variabel, hanya menulis ke yang lain (pengecualiannya adalah kemampuan untuk menulis null untuk extends dan membaca Object untuk super ). Untuk membuatnya lebih mudah diingat ketika menggunakan wildcard yang mana, ada prinsip PECS - Produser Memperluas Konsumen Super.

  • Jika kami mendeklarasikan wildcard dengan extends , maka ini adalah produser . Dia hanya "menghasilkan", menyediakan elemen dari wadah, dan dia tidak menerima apa pun.
  • Jika kami mengumumkan wildcard dengan super , maka ini adalah konsumen . Dia hanya menerima, tetapi tidak bisa memberikan apa-apa.

Pertimbangkan menggunakan Wildcard dan prinsip PECS menggunakan metode salin di kelas java.util.Collections sebagai contoh.

 public static <T> void copy(List<? super T> dest, List<? extends T> src) { … } 

Metode ini menyalin elemen dari daftar src asli ke daftar tujuan. src - dideklarasikan dengan wildcard ? extends ? extends dan merupakan produser, dan dest dinyatakan dengan wildcard ? super ? super dan merupakan konsumen. Mengingat kovarians dan contravariance wildcard, Anda dapat menyalin elemen dari daftar ints ke daftar nums :
 List<Number> nums = Arrays.<Number>asList(4.1F, 0.2F); List<Integer> ints = Arrays.asList(1,2); Collections.copy(nums, ints); 


Jika kita keliru parameter metode salin karena kesalahan dan mencoba untuk menyalin dari daftar nums ke daftar ints , kompiler tidak akan mengizinkan kita untuk melakukan ini:
 Collections.copy(ints, nums); // Compile-time error 


<?> dan Jenis mentah


Di bawah ini adalah wildcard dengan wildcard tak terbatas. Kami hanya menempatkan <?> , Tanpa kata kunci super atau extends :
 static void printCollection(Collection<?> c) {  // a wildcard collection  for (Object o : c) {      System.out.println(o);  } } 


Bahkan, wildcard "tak terbatas" semacam itu masih terbatas, dari atas. Collection<?> Apakah juga wildcard, seperti " ? extends Object ". Catatan bentuk Collection<?> setara dengan Collection<? extends Object> Collection<? extends Object> , yang berarti bahwa koleksi dapat berisi objek dari kelas apa saja, karena semua kelas di Jawa mewarisi dari Object - sehingga substitusi disebut tidak terbatas.

Jika kita menghilangkan indikasi jenis, misalnya, seperti di sini:
 ArrayList arrayList = new ArrayList(); 

kemudian mereka mengatakan bahwa ArrayList adalah tipe Raw dari ArrayList yang diparameterisasi <T> . Menggunakan tipe Raw, kami kembali ke era generik dan secara sadar meninggalkan semua fitur yang melekat pada tipe parameter.

Jika kita mencoba memanggil metode parameterisasi pada tipe Raw, kompiler akan memberi kita peringatan "Panggilan tidak dicentang". Jika kita mencoba untuk menetapkan referensi ke tipe Raw yang diparameterisasi menjadi suatu tipe, kompiler akan memberikan peringatan "Penandaan yang tidak dicentang". Mengabaikan peringatan ini, seperti yang akan kita lihat nanti, dapat menyebabkan kesalahan selama eksekusi aplikasi kita.
 ArrayList<String> strings = new ArrayList<>(); ArrayList arrayList = new ArrayList(); arrayList = strings; // Ok strings = arrayList; // Unchecked assignment arrayList.add(1); //unchecked call 


Pengambilan wildcard


Sekarang mari kita coba menerapkan metode yang memungkinkan elemen daftar dalam urutan terbalik.

 public static void reverse(List<?> list); // ! public static void reverse(List<?> list) { List<Object> tmp = new ArrayList<Object>(list); for (int i = 0; i < list.size(); i++) {   list.set(i, tmp.get(list.size()-i-1)); // compile-time error } } 

Kesalahan kompilasi terjadi karena metode reverse mengambil daftar dengan karakter wildcard tak terbatas <?> Sebagai argumen.
<?> Berarti sama dengan <? extends Object> <? extends Object> . Oleh karena itu, sesuai dengan prinsip PECS, list adalah producer . Dan producer hanya menghasilkan elemen. Dan kita dalam for loop memanggil metode set() , mis. mencoba menulis ke list . Jadi kami bersandar pada perlindungan Java, yang tidak memungkinkan kami untuk menetapkan beberapa nilai berdasarkan indeks.

Apa yang harus dilakukan Pola Pengambilan Wildcard Capture akan membantu kami. Di sini kita membuat metode rev generik. Ini dideklarasikan dengan variabel tipe T Metode ini menerima daftar tipe T , dan kita dapat membuat satu set.
 public static void reverse(List<?> list) { rev(list); } private static <T> void rev(List<T> list) { List<T> tmp = new ArrayList<T>(list); for (int i = 0; i < list.size(); i++) {   list.set(i, tmp.get(list.size()-i-1)); } } 

Sekarang semuanya akan dikompilasi bersama kami. Penangkapan wildcard ditangkap di sini. Ketika metode reverse(List<?> list) disebut reverse(List<?> list) , daftar beberapa objek (misalnya, string atau bilangan bulat) dilewatkan sebagai argumen. Jika kita dapat menangkap tipe objek-objek ini dan menetapkannya ke variabel tipe X , maka kita dapat menyimpulkan bahwa T adalah X

Anda dapat membaca lebih lanjut tentang Wildcard Capture sini dan di sini .

Kesimpulan


Jika Anda perlu membaca dari wadah, maka gunakan wildcard dengan batas atas " ? extends ". Jika Anda perlu menulis ke wadah, maka gunakan wildcard dengan batas bawah " ? super ". Jangan gunakan wildcard jika Anda perlu merekam dan membaca.

Jangan gunakan jenis Raw ! Jika argumen tipe tidak didefinisikan, maka gunakan wildcard <?> .

Ketik variabel


Ketika kami menuliskan pengidentifikasi dalam kurung sudut, misalnya, <T> atau <E> saat mendeklarasikan kelas atau metode, kami membuat variabel tipe . Variabel tipe adalah pengidentifikasi tanpa kualifikasi yang dapat digunakan sebagai tipe di tubuh kelas atau metode. Variabel tipe dapat dibatasi di atas.
 public static <T extends Comparable<T>> T max(Collection<T> coll) { T candidate = coll.iterator().next(); for (T elt : coll) {   if (candidate.compareTo(elt) < 0) candidate = elt; } return candidate; } 

Dalam contoh ini, ekspresi T extends Comparable<T> mendefinisikan T (variabel tipe) yang dibatasi di atas oleh tipe Comparable<T> . Tidak seperti wildcard, variabel tipe hanya dapat dibatasi di atas (hanya extends ). Tidak bisa menulis super . Selain itu, dalam contoh ini, T tergantung pada dirinya sendiri, itu disebut recursive bound - perbatasan rekursif.

Ini adalah contoh lain dari kelas Enum:
 public abstract class Enum<E extends Enum<E>>implements Comparable<E>, Serializable 

Di sini, kelas Enum diparameterisasi berdasarkan tipe E, yang merupakan subtipe dari Enum<E> .

Banyak batas


Multiple Bounds - beberapa batasan. Ini ditulis melalui karakter " & ", yaitu, kita mengatakan bahwa tipe yang diwakili oleh variabel tipe T harus dibatasi dari atas oleh kelas Object dan antarmuka yang Comparable .

 <T extends Object & Comparable<? super T>> T max(Collection<? extends T> coll) 

Rekam Object & Comparable<? super T> Object & Comparable<? super T> membentuk tipe persimpangan Multiple Bounds . Keterbatasan pertama - dalam hal ini, Object - digunakan untuk erasure , proses jenis penulisan ulang. Ini dilakukan oleh kompiler pada tahap kompilasi.

Kesimpulan


Variabel tipe hanya dapat dibatasi di atas satu atau beberapa tipe. Dalam kasus beberapa kendala, batas kiri (kendala pertama) digunakan dalam proses penulisan ulang (Penghapusan Jenis).

Ketik penghapusan


Penghapusan Tipe adalah pemetaan tipe (mungkin termasuk tipe parameter dan variabel tipe) ke tipe yang tidak pernah tipe parameter atau tipe variabel. Kami menulis tipe T mashing sebagai |T| .

Tampilan mashing didefinisikan sebagai berikut:
  • Menghancurkan parameterisasi tipe G < T1 , ..., Tn > adalah | G |
  • Menghancurkan tipe bersarang TC adalah | T |. C
  • Menghancurkan array tipe T [] adalah | T | []
  • Menghancurkan variabel tipe sedang menumbuk batas kirinya
  • Menghancurkan jenis lainnya adalah jenis ini sendiri


Selama pelaksanaan Penghapusan Jenis (type mashing), kompiler melakukan tindakan berikut:
  • menambahkan casting tipe untuk memberikan keamanan tipe jika perlu
  • menghasilkan metode Bridge untuk mempertahankan polimorfisme


T (Jenis)
| T | (Tipe tumbuk)
Daftar <Integer>, Daftar <String>, Daftar <Daftar <String >>
Daftar
Daftar <Integer> []
Daftar []
Daftar
Daftar
int
int
Integer
Integer
<T extends Sebanding <T>>
Sebanding
<T memperluas Objek & Sebanding <? super T >>
Obyek
LinkedCollection <E> .Node
LinkedCollection.Node

Tabel ini menunjukkan apa tipe yang berbeda berubah selama proses menumbuk, Type Erasure.

Pada tangkapan layar di bawah ini adalah dua contoh program:


Perbedaan antara keduanya adalah bahwa kesalahan waktu kompilasi terjadi di sebelah kiri, dan di sebelah kanan semuanya dikompilasi tanpa kesalahan. Mengapa

Jawabannya
Di Jawa, dua metode berbeda tidak dapat memiliki tanda tangan yang sama. Dalam proses Penghapusan Jenis, kompiler akan menambahkan metode jembatan public int compareTo(Object o) . Tetapi kelas sudah berisi metode dengan tanda tangan sedemikian rupa sehingga akan menyebabkan kesalahan saat kompilasi.

Kompilasi kelas Name dengan menghapus metode compareTo(Object o) dan lihat bytecode yang dihasilkan menggunakan javap:
 # javap Name.class Compiled from "Name.java" public class ru.sberbank.training.generics.Name implements java.lang.Comparable<ru.sberbank.training.generics.Name> { public ru.sberbank.training.generics.Name(java.lang.String); public java.lang.String toString(); public int compareTo(ru.sberbank.training.generics.Name); public int compareTo(java.lang.Object); } 

Kami melihat bahwa kelas tersebut berisi metode int compareTo(java.lang.Object) , meskipun kami menghapusnya dari kode sumber. Ini adalah metode jembatan yang ditambahkan oleh kompiler.


Jenis yang dapat diverifikasi


Di Jawa, kami mengatakan bahwa suatu jenis dapat reifiable jika informasinya sepenuhnya dapat diakses pada saat dijalankan. Jenis yang dapat diverifikasi meliputi:
  • Jenis primitif ( int , panjang , boolean )
  • Tipe-tipe yang tidakparameter (non-generik) ( String , Integer )
  • Tipe parameter yang parameternya direpresentasikan sebagai wildcard tak terbatas (karakter wildcard tak terbatas) ( Daftar <?> , Koleksi <?> )
  • Jenis mentah (tidak berbentuk) ( Daftar , ArrayList )
  • Array yang komponennya adalah jenis yang dapat diverifikasi ( int [] , Nomor [] , Daftar <?> [] , Daftar [ )


Mengapa informasi tentang beberapa jenis tersedia tetapi tidak tentang yang lain? Faktanya adalah bahwa karena proses menimpa jenis oleh kompiler, informasi tentang beberapa jenis mungkin hilang. Jika hilang, maka jenis ini tidak lagi dapat diverifikasi. Artinya, itu tidak tersedia saat runtime. Jika tersedia - masing-masing, dapat diverifikasi.

Keputusan untuk tidak menyediakan semua tipe generik pada saat run time adalah salah satu keputusan desain yang paling penting dan saling bertentangan dalam sistem tipe Java. Ini dilakukan, pertama-tama, untuk kompatibilitas dengan kode yang ada. Saya harus membayar untuk kompatibilitas migrasi - aksesibilitas penuh dari sistem tipe generik pada saat run time tidak dimungkinkan.

Jenis apa yang tidak dapat diverifikasi:
  • Jenis variabel ( T )
  • Jenis parameter dengan tipe parameter yang ditentukan ( Daftar <Number> ArrayList <String> , Daftar <List <String>> )
  • Tipe parameter dengan batas atas atau bawah yang ditentukan ( Daftar <? Perluas Nomor>, Sebandingi <? Super String> ). Tapi di sini ada reservasi: Daftar <? extends Object> - tidak dapat diverifikasi, tetapi List <?> - reifiable


Dan satu tugas lagi. Mengapa dalam contoh di bawah ini tidak dapat membuat Pengecualian berparameter?

 class MyException<T> extends Exception {  T t; } 

Jawabannya
Setiap ekspresi catch dalam try-catch memeriksa tipe pengecualian yang diterima selama eksekusi program (yang setara dengan instanceof), masing-masing, tipe tersebut harus dapat diverifikasi. Oleh karena itu, Throwable dan subtipe-nya tidak dapat diparameterisasi.

 class MyException<T> extends Exception {// Generic class may not extend 'java.lang.Throwable'  T t; } 



Peringatan yang tidak dicentang


Mengkompilasi aplikasi kita dapat menghasilkan apa yang disebut Unchecked Warning - peringatan bahwa kompiler tidak dapat dengan benar menentukan tingkat keamanan penggunaan tipe kita. Ini bukan kesalahan, tapi peringatan, jadi Anda bisa melewatinya. Tetapi disarankan untuk memperbaiki semuanya agar terhindar dari masalah di masa depan.

Menumpuk polusi


Seperti yang kami sebutkan sebelumnya, menetapkan referensi ke tipe Raw ke variabel dari tipe parameter menyebabkan peringatan "Tidak ditandai tugas". Jika kita mengabaikannya, sebuah situasi yang disebut " Heap Pollution " (heap polusi) mungkin terjadi. Berikut ini sebuah contoh:
 static List<String> t() {  List l = new ArrayList<Number>();  l.add(1);  List<String> ls = l; // (1)  ls.add("");  return ls; } 

Pada baris (1), kompiler memperingatkan "Tugas tidak dicentang."

Kita perlu memberikan contoh lain dari "tumpukan polusi" - ketika kita menggunakan objek parameterisasi. Cuplikan kode di bawah ini dengan jelas menunjukkan bahwa tidak diperbolehkan menggunakan tipe parameter sebagai argumen untuk metode yang menggunakan Varargs . Dalam kasus ini, parameter metode m adalah List<String>… , mis. sebenarnya, sebuah array elemen dari tipe List<String> . Diberi aturan menampilkan tipe selama mashing, tipe stringLists berubah menjadi array daftar mentah ( List[] ), mis. tugas dapat dilakukan Object[] array = stringLists; dan kemudian menulis ke array objek selain daftar string (1), yang akan ClassCastException dalam string (2).

 static void m(List<String>... stringLists) {  Object[] array = stringLists;  List<Integer> tmpList = Arrays.asList(42);  array[0] = tmpList; // (1)  String s = stringLists[0].get(0); // (2) } 


Pertimbangkan contoh lain:
 ArrayList<String> strings = new ArrayList<>(); ArrayList arrayList = new ArrayList(); arrayList = strings; // (1) Ok arrayList.add(1); // (2) unchecked call 

Java mengizinkan penugasan pada baris (1). Ini diperlukan untuk kompatibilitas ke belakang. Tetapi jika kita mencoba menjalankan metode add pada baris (2), kita mendapatkan peringatan Unchecked call - kompiler memperingatkan kita akan kemungkinan kesalahan. Faktanya, kami mencoba menambahkan integer ke daftar string.

Refleksi


Meskipun, selama kompilasi, tipe parameterisasi menjalani prosedur tipe erasure, kita bisa mendapatkan beberapa informasi menggunakan Reflection.

  • Semua dapat diverifikasi tersedia melalui mekanisme Refleksi.
  • Informasi tentang jenis bidang kelas, parameter metode, dan nilai yang dikembalikan oleh mereka tersedia melalui Refleksi.

Jika kami ingin mendapatkan informasi tentang jenis objek melalui Refleksi dan jenis ini tidak Reifiable, maka kami tidak akan berhasil. Tetapi, jika, misalnya, objek ini dikembalikan kepada kami dengan beberapa metode, maka kami bisa mendapatkan jenis nilai yang dikembalikan oleh metode ini:
 java.lang.reflect.Method.getGenericReturnType() 

Dengan munculnya Generics, kelas telah java.lang.Classmenjadi parameter. Pertimbangkan kode ini:
 List<Integer> ints = new ArrayList<Integer>(); Class<? extends List> k = ints.getClass(); assert k == ArrayList.class; 


Variabelnya intsadalah tipe List<Integer>dan berisi referensi ke objek tipe ArrayList< Integer>. Maka itu ints.getClass()akan mengembalikan objek bertipe Class<ArrayLis>, karena List<Integer>ditimpa dalam List. Class<ArrayList>Bisakah objek tipe ditugaskan ke variabel ktipe Class<? extends List>sesuai dengan kovarian karakter wildcard? extends. A ArrayList.classmengembalikan objek bertipe Class<ArrayList>.

Kesimpulan


, Reifiable. Reifiable : , , , Raw , reifiable.

Unchecked Warnings Β« Β» .

Reflection , Reifiable. Reflection , .

Type Inference


Β« Β». () . :
 List<Integer> list = new ArrayList<Integer>(); 

- Java 7 ArrayList :
 List<Integer> list = new ArrayList<>(); 

ArrayList – List<Integer> . type inference .

Java 8 JEP 101.
Type Inference. :
  • (reduction)
  • (incorporation)
  • (resolution)

: , , β€” .
, . JEP 101 .

, :
 class List<E> {  static <Z> List<Z> nil() { ... };  static <Z> List<Z> cons(Z head, List<Z> tail) { ... };  E head() { ... } } 

List.nil() :
 List<String> ls = List.nil(); 

, List.nil() String β€” JDK 7, .

, , , :
 List.cons(42, List.nil()); //error: expected List<Integer>, found List<Object> 

JDK 7 compile-time error. JDK 8 . JEP-101, β€” . JDK 8 β€” :
 List.cons(42, List.<Integer>nil()); 


JEP-101 , , :
 String s = List.nil().head(); //error: expected String, found Object 

, . , JDK , :
 String s = List.<String>nil().head(); 


JEP 101 StackOverflow . , , 7- , 8- – ? :
 class Test {  static void m(Object o) {      System.out.println("one");  }  static void m(String[] o) {      System.out.println("two");  }  static <T> T g() {      return null;  }  public static void main(String[] args) {      m(g());  } } 


- JDK1.8:
   public static void main(java.lang.String[]);   descriptor: ([Ljava/lang/String;)V   flags: ACC_PUBLIC, ACC_STATIC   Code:     stack=1, locals=1, args_size=1        0: invokestatic  #6   // Method g:()Ljava/lang/Object;        3: checkcast     #7   // class "[Ljava/lang/String;"        6: invokestatic  #8   // Method m:([Ljava/lang/String;)V        9: return     LineNumberTable:       line 15: 0       line 16: 9 


0 g:()Ljava/lang/Object; java.lang.Object . , 3 («») , java.lang.String , 6 m:([Ljava/lang/String;) , «two».

- JDK1.7 – Java 7:
   public static void main(java.lang.String[]);   flags: ACC_PUBLIC, ACC_STATIC   Code:     stack=1, locals=1, args_size=1        0: invokestatic  #6   // Method g:()Ljava/lang/Object;        3: invokestatic  #7   // Method m:(Ljava/lang/Object;)V        6: return            LineNumberTable:       line 15: 0       line 16: 6 


, checkcast , Java 8, m:(Ljava/lang/Object;) , Β«oneΒ». Checkcast – , Java 8.

, Oracle JDK1.7 JDK 1.8 , Java, , .

, Java 8 , Java 7, :

 public static void main(String[] args) { m((Object)g()); } 


Kesimpulan


Java Generics . , :


  • Bloch, Joshua. Effective Java. Third Edition. Addison-Wesley. ISBN-13: 978-0-13-468599-1

, Java Generics.

Source: https://habr.com/ru/post/id416413/


All Articles