Asas Generik Java

1. Pengenalan

Java Generics diperkenalkan pada JDK 5.0 dengan tujuan untuk mengurangkan bug dan menambahkan lapisan tambahan abstraksi terhadap jenis.

Artikel ini adalah pengenalan ringkas kepada Generics di Java, tujuan di belakangnya dan bagaimana ia dapat digunakan untuk meningkatkan kualiti kod kami.

2. Keperluan Generik

Mari bayangkan senario di mana kita ingin membuat senarai di Java untuk menyimpan Integer ; kita boleh tergoda untuk menulis:

List list = new LinkedList(); list.add(new Integer(1)); Integer i = list.iterator().next(); 

Anehnya, penyusun akan mengadu tentang baris terakhir. Ia tidak tahu apa jenis data yang dikembalikan. Penyusun akan memerlukan pemutaran eksplisit:

Integer i = (Integer) list.iterator.next();

Tidak ada kontrak yang dapat menjamin bahawa jenis pengembalian senarai adalah Integer. Senarai yang ditentukan dapat menyimpan objek apa pun. Kami hanya tahu bahawa kami mendapatkan senarai dengan memeriksa konteksnya. Ketika melihat jenis, hanya dapat menjamin bahawa itu adalah Objek , sehingga memerlukan pemeran eksplisit untuk memastikan jenisnya selamat.

Pelakon ini boleh menjengkelkan, kita tahu bahawa jenis data dalam senarai ini adalah Integer . Pelakon juga mengacaukan kod kami. Ia boleh menyebabkan ralat runtime yang berkaitan dengan jenis jika pengaturcara melakukan kesalahan dengan transmisi eksplisit.

Akan lebih mudah jika pengaturcara dapat menyatakan niat mereka menggunakan jenis tertentu dan penyusun dapat memastikan kebenaran jenis tersebut. Inilah idea utama di sebalik generik.

Mari ubah baris pertama coretan kod sebelumnya ke:

List list = new LinkedList();

Dengan menambahkan operator berlian yang mengandungi jenisnya, kami mempersempit pengkhususan senarai ini hanya untuk jenis Integer iaitu kami menentukan jenis yang akan disimpan di dalam senarai. Penyusun dapat menguatkan jenis pada masa kompilasi.

Dalam program kecil, ini mungkin kelihatan seperti penambahan sepele, namun, dalam program yang lebih besar, ini dapat menambah ketahanan yang signifikan dan menjadikan program lebih mudah dibaca.

3. Kaedah Generik

Kaedah generik adalah kaedah yang ditulis dengan satu deklarasi kaedah dan boleh dipanggil dengan argumen dari pelbagai jenis. Penyusun akan memastikan kebenaran jenis mana yang digunakan. Ini adalah beberapa sifat kaedah generik:

  • Kaedah generik mempunyai parameter jenis (operator berlian yang merangkumi jenis) sebelum jenis pengembalian kaedah pengisytiharan
  • Parameter jenis dapat dibatasi (batas dijelaskan kemudian dalam artikel)
  • Kaedah generik boleh mempunyai parameter jenis yang berbeza dipisahkan dengan koma dalam tandatangan kaedah
  • Kaedah kaedah untuk kaedah generik sama seperti kaedah biasa

Contoh mendefinisikan kaedah generik untuk menukar array ke senarai:

public  List fromArrayToList(T[] a) { return Arrays.stream(a).collect(Collectors.toList()); }

Dalam contoh sebelumnya, dalam kaedah tandatangan menunjukkan bahawa kaedah yang akan berurusan dengan jenis generik T . Ini diperlukan walaupun kaedah ini tidak sah.

Seperti yang disebutkan di atas, kaedah ini dapat menangani lebih dari satu jenis generik, jika demikian, semua jenis generik mesti ditambahkan pada tanda tangan kaedah, misalnya, jika kita ingin mengubah kaedah di atas untuk menangani jenis T dan jenis G , ia harus ditulis seperti ini:

public static  List fromArrayToList(T[] a, Function mapperFunction) { return Arrays.stream(a) .map(mapperFunction) .collect(Collectors.toList()); }

Kami meneruskan fungsi yang menukar array dengan unsur-unsur jenis T untuk disenaraikan dengan elemen jenis G. Contohnya ialah menukar Integer ke representasi Stringnya :

@Test public void givenArrayOfIntegers_thanListOfStringReturnedOK() { Integer[] intArray = {1, 2, 3, 4, 5}; List stringList = Generics.fromArrayToList(intArray, Object::toString); assertThat(stringList, hasItems("1", "2", "3", "4", "5")); }

Perlu diperhatikan bahawa cadangan Oracle adalah menggunakan huruf besar untuk mewakili jenis generik dan memilih huruf yang lebih deskriptif untuk mewakili jenis formal, misalnya dalam Java Collections T digunakan untuk jenis, K untuk kunci, V untuk nilai.

3.1. Generik Terikat

Seperti yang disebutkan sebelumnya, parameter jenis dapat dibatasi. Terikat bermaksud " dibatasi ", kita dapat membatasi jenis yang dapat diterima dengan kaedah.

Sebagai contoh, kita dapat menentukan bahawa kaedah menerima jenis dan semua subkelasnya (batas atas) atau jenis semua cermin mata hitamnya (batas bawah).

Untuk menyatakan jenis batas atas kita menggunakan kata kunci meluas setelah jenis diikuti oleh batas atas yang ingin kita gunakan. Sebagai contoh:

public  List fromArrayToList(T[] a) { ... } 

Kata kunci meluas digunakan di sini untuk bermaksud bahawa jenis T memanjangkan batas atas sekiranya kelas atau menerapkan batas atas sekiranya terdapat antara muka.

3.2. Pelbagai Batas

Jenis juga boleh mempunyai beberapa batas atas seperti berikut:

Sekiranya salah satu jenis yang diperpanjang oleh T adalah kelas (iaitu Nombor ), ia mesti didahulukan dalam senarai batas. Jika tidak, ia akan menyebabkan ralat waktu kompilasi.

4. Menggunakan Kad Liar Dengan Generik

Wildcard diwakili oleh tanda tanya di Java “ ? "Dan mereka digunakan untuk merujuk kepada jenis yang tidak diketahui. Kad liar sangat berguna ketika menggunakan generik dan boleh digunakan sebagai jenis parameter tetapi pertama, ada nota penting yang perlu dipertimbangkan.

Telah diketahui bahawa Object adalah supertype dari semua kelas Java, namun, kumpulan Object bukan supertype dari koleksi mana pun.

Contohnya, List bukan supertype List dan menetapkan pemboleh ubah jenis List ke pemboleh ubah jenis List akan menyebabkan kesalahan penyusun. Ini untuk mengelakkan konflik yang mungkin berlaku jika kita menambahkan jenis yang heterogen pada koleksi yang sama.

Peraturan yang sama berlaku untuk koleksi jenis dan subjenisnya. Pertimbangkan contoh ini:

public static void paintAllBuildings(List buildings) { buildings.forEach(Building::paint); }

if we imagine a subtype of Building, for example, a House, we can't use this method with a list of House, even though House is a subtype of Building. If we need to use this method with type Building and all its subtypes, then the bounded wildcard can do the magic:

public static void paintAllBuildings(List buildings) { ... } 

Now, this method will work with type Building and all its subtypes. This is called an upper bounded wildcard where type Building is the upper bound.

Wildcards can also be specified with a lower bound, where the unknown type has to be a supertype of the specified type. Lower bounds can be specified using the super keyword followed by the specific type, for example, means unknown type that is a superclass of T (= T and all its parents).

5. Type Erasure

Generics were added to Java to ensure type safety and to ensure that generics wouldn't cause overhead at runtime, the compiler applies a process called type erasure on generics at compile time.

Type erasure removes all type parameters and replaces it with their bounds or with Object if the type parameter is unbounded. Thus the bytecode after compilation contains only normal classes, interfaces and methods thus ensuring that no new types are produced. Proper casting is applied as well to the Object type at compile time.

This is an example of type erasure:

public  List genericMethod(List list) { return list.stream().collect(Collectors.toList()); } 

With type erasure, the unbounded type T is replaced with Object as follows:

// for illustration public List withErasure(List list) { return list.stream().collect(Collectors.toList()); } // which in practice results in public List withErasure(List list) { return list.stream().collect(Collectors.toList()); } 

If the type is bounded, then the type will be replaced by the bound at compile time:

public  void genericMethod(T t) { ... } 

would change after compilation:

public void genericMethod(Building t) { ... }

6. Generics and Primitive Data Types

A restriction of generics in Java is that the type parameter cannot be a primitive type.

For example, the following doesn't compile:

List list = new ArrayList(); list.add(17);

To understand why primitive data types don't work, let's remember that generics are a compile-time feature, meaning the type parameter is erased and all generic types are implemented as type Object.

As an example, let's look at the add method of a list:

List list = new ArrayList(); list.add(17);

The signature of the add method is:

boolean add(E e);

And will be compiled to:

boolean add(Object e);

Therefore, type parameters must be convertible to Object. Since primitive types don't extend Object, we can't use them as type parameters.

However, Java provides boxed types for primitives, along with autoboxing and unboxing to unwrap them:

Integer a = 17; int b = a; 

So, if we want to create a list which can hold integers, we can use the wrapper:

List list = new ArrayList(); list.add(17); int first = list.get(0); 

The compiled code will be the equivalent of:

List list = new ArrayList(); list.add(Integer.valueOf(17)); int first = ((Integer) list.get(0)).intValue(); 

Future versions of Java might allow primitive data types for generics. Project Valhalla aims at improving the way generics are handled. The idea is to implement generics specialization as described in JEP 218.

7. Conclusion

Java Generics adalah tambahan yang kuat untuk bahasa Java kerana ia menjadikan tugas pengaturcara lebih mudah dan kurang ralat. Generik menegakkan ketepatan jenis pada waktu penyusunan dan, yang paling penting, memungkinkan pelaksanaan algoritma generik tanpa menyebabkan overhead tambahan pada aplikasi kami.

Kod sumber yang menyertai artikel ini terdapat di GitHub.