Dasar-dasar Java Generik

1. Perkenalan

Java Generics diperkenalkan di JDK 5.0 dengan tujuan mengurangi bug dan menambahkan lapisan abstraksi ekstra di atas jenis.

Artikel ini adalah pengenalan singkat tentang Generik di Java, tujuan dibaliknya dan bagaimana mereka dapat digunakan untuk meningkatkan kualitas kode kita.

2. Kebutuhan Generik

Bayangkan skenario di mana kita ingin membuat daftar di Java untuk menyimpan Integer ; kita bisa tergoda untuk menulis:

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

Anehnya, kompilator akan mengeluh tentang baris terakhir. Itu tidak tahu tipe data apa yang dikembalikan. Kompiler akan membutuhkan casting eksplisit:

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

Tidak ada kontrak yang dapat menjamin bahwa jenis kembalian dari daftar tersebut adalah Integer. Daftar yang ditentukan dapat menampung objek apa pun. Kami hanya tahu bahwa kami mengambil daftar dengan memeriksa konteksnya. Saat melihat tipe, itu hanya bisa menjamin bahwa itu adalah Objek , oleh karena itu membutuhkan pemeran eksplisit untuk memastikan bahwa tipe itu aman.

Pemeran ini dapat mengganggu, kita tahu bahwa tipe data dalam daftar ini adalah Integer . Pemerannya juga mengacaukan kode kita. Ini dapat menyebabkan error runtime terkait tipe jika programmer membuat kesalahan dengan casting eksplisit.

Akan jauh lebih mudah jika pemrogram dapat mengungkapkan maksud mereka menggunakan tipe tertentu dan kompiler dapat memastikan kebenaran tipe tersebut. Ini adalah ide inti di balik obat generik.

Mari kita ubah baris pertama dari potongan kode sebelumnya menjadi:

List list = new LinkedList();

Dengan menambahkan operator berlian yang berisi tipe, kami mempersempit spesialisasi daftar ini hanya ke tipe Integer yaitu kami menentukan tipe yang akan disimpan di dalam daftar. Kompilator dapat menerapkan tipe pada waktu kompilasi.

Dalam program kecil, ini mungkin tampak seperti tambahan yang sepele, namun, dalam program yang lebih besar, ini dapat menambah ketahanan yang signifikan dan membuat program lebih mudah dibaca.

3. Metode Generik

Metode umum adalah metode yang ditulis dengan deklarasi metode tunggal dan dapat dipanggil dengan argumen dari tipe yang berbeda. Kompilator akan memastikan ketepatan jenis apa pun yang digunakan. Ini adalah beberapa properti dari metode umum:

  • Metode umum memiliki parameter tipe (operator berlian melingkupi tipe) sebelum tipe kembalian dari deklarasi metode
  • Parameter jenis dapat dibatasi (batas akan dijelaskan nanti di artikel)
  • Metode umum dapat memiliki parameter tipe berbeda yang dipisahkan dengan koma di tanda tangan metode
  • Metode tubuh untuk metode umum sama seperti metode biasa

Contoh mendefinisikan metode umum untuk mengonversi larik menjadi daftar:

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

Dalam contoh sebelumnya, file dalam metode tanda tangan menyiratkan bahwa metode yang akan berurusan dengan jenis generik T . Ini diperlukan bahkan jika metode mengembalikan kekosongan.

Seperti disebutkan di atas, metode dapat menangani lebih dari satu tipe generik, di mana hal ini terjadi, semua tipe generik harus ditambahkan ke tanda tangan metode, misalnya, jika kita ingin memodifikasi metode di atas untuk menangani tipe T dan tipe G , harus ditulis seperti ini:

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

Kita meneruskan fungsi yang mengubah array dengan elemen tipe T ke daftar dengan elemen tipe G. Contohnya adalah mengonversi 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 dicatat bahwa rekomendasi Oracle adalah menggunakan huruf besar untuk mewakili tipe generik dan memilih huruf yang lebih deskriptif untuk mewakili tipe formal, misalnya di Java Collections T digunakan untuk tipe, K untuk kunci, V untuk nilai.

3.1. Generik Terikat

Seperti yang disebutkan sebelumnya, parameter tipe bisa dibatasi. Bounded artinya “ dibatasi ”, kita bisa membatasi tipe yang bisa diterima oleh suatu metode.

Sebagai contoh, kita dapat menentukan bahwa sebuah metode menerima sebuah tipe dan semua subkelasnya (batas atas) atau tipe semua kelas supernya (batas bawah).

Untuk mendeklarasikan tipe batas atas kita menggunakan kata kunci meluas setelah tipe diikuti dengan batas atas yang ingin kita gunakan. Sebagai contoh:

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

Kata kunci meluas digunakan di sini untuk mengartikan bahwa tipe T memperluas batas atas dalam kasus kelas atau mengimplementasikan batas atas dalam kasus antarmuka.

3.2. Beberapa Batas

Suatu tipe juga dapat memiliki beberapa batas atas sebagai berikut:

Jika salah satu tipe yang diperpanjang oleh T adalah kelas (yaitu Angka ), itu harus diletakkan terlebih dahulu dalam daftar batas. Jika tidak, ini akan menyebabkan kesalahan waktu kompilasi.

4. Menggunakan Wildcard Dengan Generik

Karakter pengganti diwakili oleh tanda tanya di Jawa “ ? "Dan mereka digunakan untuk merujuk pada tipe yang tidak diketahui. Karakter pengganti sangat berguna saat menggunakan obat generik dan dapat digunakan sebagai tipe parameter tetapi pertama-tama, ada catatan penting yang perlu dipertimbangkan.

Diketahui bahwa Object adalah supertipe dari semua kelas Java, namun, kumpulan Objek bukan supertipe dari koleksi apa pun.

Misalnya, List bukanlah supertipe List dan menugaskan variabel bertipe List ke variabel bertipe List akan menyebabkan kesalahan compiler. Ini untuk mencegah kemungkinan konflik yang dapat terjadi jika kita menambahkan tipe heterogen ke koleksi yang sama.

Aturan yang sama berlaku untuk kumpulan jenis dan subtipe apa pun. Pertimbangkan contoh ini:

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

Jika kita membayangkan subtipe Bangunan , misalnya Rumah , kita tidak dapat menggunakan metode ini dengan daftar Rumah , padahal Rumah adalah subtipe Bangunan . Jika kita perlu menggunakan metode ini dengan tipe Bangunan dan semua subtipe-nya, maka karakter pengganti yang dibatasi dapat melakukan keajaiban:

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

Sekarang, metode ini akan bekerja dengan tipe Bangunan dan semua subtipe-nya. Ini disebut wildcard batas atas di mana tipe Bangunan adalah batas atasnya.

Karakter pengganti juga dapat ditentukan dengan batas bawah, di mana tipe yang tidak diketahui harus supertipe dari tipe yang ditentukan. Batas bawah dapat ditentukan menggunakan kata kunci super diikuti dengan jenis tertentu, misalnya,berarti tipe tidak diketahui yang merupakan superclass dari T (= T dan semua orang tuanya).

5. Ketik Hapus

Generik ditambahkan ke Java untuk memastikan keamanan tipe dan untuk memastikan bahwa obat generik tidak akan menyebabkan overhead pada waktu proses, kompilator menerapkan proses yang disebut penghapusan tipe pada generik pada waktu kompilasi.

Penghapusan jenis menghapus semua parameter jenis dan menggantinya dengan batasnya atau dengan Objek jika parameter jenis tidak dibatasi. Jadi bytecode setelah kompilasi hanya berisi kelas normal, antarmuka dan metode sehingga memastikan bahwa tidak ada tipe baru yang diproduksi. Pengecoran yang tepat diterapkan juga pada tipe Objek pada waktu kompilasi.

Ini adalah contoh penghapusan tipe:

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 karena membuat pekerjaan programmer lebih mudah dan tidak terlalu rawan kesalahan. Generik menerapkan ketepatan tipe pada waktu kompilasi dan, yang terpenting, memungkinkan penerapan algoritme generik tanpa menyebabkan overhead tambahan pada aplikasi kita.

Kode sumber yang menyertai artikel tersedia di GitHub.