Wednesday, March 21, 2018

14. Pemrograman Generik Bagian Satu



Sejak rilis pertamanya, JDK 1.0, pada tahun 1995, banyak fitur baru yang telah ditambahkan pada Java. Salah satunya adalah pemrograman generik. Yang dikenalkan sejak JDK 5, pemrograman generik mengubah Java dalam dua cara penting: Pertama, pemrograman generik menambahkan elemen sintaksis baru pada bahasa Java. Kedua, pemrograman generik merupakan bagian integral dari bahasa Java. Jadi, pemahaman atas fitur penting ini diperlukan.

Melalui penggunaan generik, Anda dimungkinkan untuk menciptakan kelas, antarmuka, dan metode yang dapat bekerja dengan cara bebas-tipe data dengan berbagai jenis data. Banyak algoritma yang secara logika sama tanpa memandang tipe data yang diproses. Sebagai contoh, mekanisme yang mendukung tumpukan sama tak peduli apakah tumpukan itu menyimpan item-item dengan tipe data Integer, String, Object, atau Thread. Dengan generik, Anda bisa mendefinisikan algoritma sekali saja, tidak terkekang tipe data tertentu, dan kemudian menerapkan algoritma tersebut untuk sejumlah tipe data tanpa usaha ekstra apapun. Kemampuan pemrograman generik yang ditambahkan pada bahasa pemrograman mampu mengubah cara kode Java dituliskan.

Mungkin salah satu fitur Java yang paling terdampak adalah Collections Framework. Fitur tersebut merupakan bagian penting dari Java API. Koleksi adalah sekelompok objek. Collection Framework mendefinisikan sejumlah kelas, seperti list dan map, yang mengelola koleksi. Kelas-kelas koleksi selalu dapat diterapkan pada sembarang jenis objek. Keuntungan yang ditambahkan oleh generik adalah bahwa kelas-kelas koleksi sekarang dapat dipakai dengan keamanan tinggi, bebas dari kekangan tipe data. Inilah mengapa pemrograman generik sangat penting bagi Java.


Apa Itu Generik?
Pada dasarnya, istilah generik berarti tipe data terparameterisasi. Tipe data terparameterisasi itu penting karena dapat Anda pakai untuk menciptakan kelas, antarmuka, dan metode dimana tipe data dari data yang akan diproses atau dimanipulasi ditetapkan sebagai parameter. Dengan menggunakan generik, Anda dimungkinkan untuk menciptakan sebuah kelas, misalnya, yang secara otomatis dapat bekerja pada pelbagai tipe data. Sebuah kelas, antarmuka, atau metode yang beroperasi pada tipe data terparameterisasi berturut-turut disebut dengan kelas generik, antarmuka generik, dan metode generik.

Adalah penting untuk memahami bahwa Java selalu memberikan Anda kemampuan untuk menciptakan kelas, antarmuka, dan metode tergeneralisasi yang bekerja pada referensi bertipe Object. Karena Object merupakan superkelas dari semua kelas, sebuah referensi Object dapat mereferensi semua tipe objek. Jadi, sebelum adanya pemrograman generik, kelas antarmuka, dan metode tergeneralisasi menggunakan referensi Object untuk beroperasi pada pelbagai tipe data objek. Masalahnya adalah tidak adanya jaminan keamanan tipe data.


Contoh Generik Sederhana
Anda akan mempelajari sebuah contoh sederhana dari kelas generik. Program berikut mendefinisikan dua kelas. Kelas pertama adalah kelas generik Gen, dan kelas kedua adalah DemoGen, yang menggunakan Gen:


// Sebuah kelas generik sederhana.
// Di sini, T merupakan sebuah parameter tipe data yang
// akan diganti oleh tipe data riil
// ketika sebuah objek bertipe Gen diciptakan.
class Gen<T> {
   T ob; // mendeklarasikan sebuah objek bertipe T

   // Melewatkan sebuah referensi yang menunjuk ke
   // sebuah objek bertipe T kepada konstruktor.
   Gen(T o) {
      ob = o;
   }

   // Menghasilkan ob.
   T getob() {
      return ob;
   }

   // Menampilkan tipe data dari T.
   void tampilTipe() {
      System.out.println("Tipe data dari T adalah " +
         ob.getClass().getName());
   }
}

public class DemoGen {
   public static void main(String args[]) {
      // Menciptakan sebuah referensi Gen untuk Integer.
      Gen<Integer> iOb;
  
      // Menciptakan sebuah objek Gen<Integer> dan menugaskan
      // referensinya kepada iOb. Perhatikan penggunaan
      // autoboxing untuk mengenkapsulasi nilai 88 
      // di dalam sebuah objek Integer.
      iOb = new Gen<Integer>(88);
  
      // Menampilkan tipe data yang dipakai oleh iOb.
      iOb.tampilTipe();
  
      // Mendapatkan nilai di dalam iOb. Perhatikan bahwa
      // tidak diperlukan cast.
      int v = iOb.getob();
      System.out.println("Nilai: " + v);
      System.out.println();
  
      // Menciptakan sebuah objek Gen untuk String.
      Gen<String> strOb = new Gen<String> ("Uji Generik");
  
      // Menampilkan tipe data yang dipakai oleh strOb.
      strOb.tampilTipe();
  
      // Mendapatkan nilai di dalam strOb. Perhatikan bahwa
      // tidak diperlukan cast.
      String str = strOb.getob();
      System.out.println("Nilai: " + str);
   }
}

Ketika dijalankan, program di atas menghasilkan keluaran berikut:

Tipe data dari T adalah java.lang.Integer
Nilai: 88

Tipe data dari T adalah java.lang.String
Nilai: Uji Generik

Mari pelajari program ini dengan lebih hati-hati. Pertama, perhatikan bagaimana Gen dideklarasikan pada baris berikut:

class Gen<T> {

Di sini, T adalah nama parameter tipe atau parameter tipe data. Nama ini dipakai sebagai pengganti untuk tipe data aktual yang akan dilewatkan kepada Gen ketika sebuah objek diciptakan. Jadi, T dipakai di dalam Gen kapanpun parameter tipe diperlukan. Perhatikan bahwa T dimuat di dalam < >. Sintaksis ini dapat digeneralisasi. Kapanpun parameter tipe dideklarasikan, ia ditempatkan di dalam kurung < >. Karena Gen menggunakan sebuah parameter tipe, Gen menjadi sebuah kelas generik, yang juga dikenal dengan tipe data terparameterisasi.

Selanjutnya, T dipakai untuk mendeklarasikan sebuah objek, dengan nama ob, seperti ditunjukkan di sini:


T ob; // mendeklarasikan sebuah objek bertipe T

Seperti dijelaskan, T adalah pengganti untuk tipe data aktual yang akan ditetapkan ketika sebuah objek Gen diciptakan. Jadi, ob akan menjadi sebuah objek dengan tipe data yang dilewatkan kepada T. Sebagai contoh, jika tipe data String dilewatkan kepada T, maka objek ob akan menjadi bertipe String.

Sekarang perhatikan konstruktor dari Gen:


Gen(T o) {
   ob = o;
}

Perhatikan bahwa parameternya, o, memiliki tipe data T. Ini berarti bahwa tipe data aktual dari o ditentukan oleh tipe data yang dilewatkan kepada T ketika sebuah objek Gen diciptakan. Selain itu, karena baik parameter o maupun variabel anggota kelas ob keduanya memiliki tipe data T, maka keduanya akan memiliki tipe data aktual yang sama ketika sebuah objek Gen diciptakan.

Parameter tipe T dapat pula dipakai untuk menetapkan tipe data nilai balik dari sebuah metode, seperti pada kasus metode getob() berikut:


T getob() {
   return ob;
}

Karena ob juga memiliki tipe data T, tipe datanya kompatibel dengan tipe nilai balik yang dihasilkan oleh getob().

Metode tampilTipe() menampilkan tipe data dari T dengan memanggil getName() pada objek Class yang dihasilkan dengan memanggil getClass() pada ob. Metode getClass() didefinisikan oleh Object dan merupakan salah satu anggota dari semua tipe kelas. Metode ini menghasilkan sebuah objek Class yang berkaitan dengan tipe data dari kelas objek yang memanggilnya. Kelas Class mendefinisikan metode getName(), yang menghasilkan representasi string dari nama kelas.

Kelas DemoGen mendemonstrasikan kelas Gen generik. Kelas ini lebih dahulu menciptakan sebuah versi dari Gen untuk nilai-nilai integer, seperti ditunjukkan di sini:


Gen<Integer> iOb;

Lihat lebih dekat pada deklarasi ini. Pertama, perhatikan bahwa tipe data Integer ditetapkan di dalam kurung < > yang ditempatkan setelah Gen. Pada kasus ini, Integer merupakan argumen tipe data yang dilewatkan kepada parameter tipe T dari Gen. Ini secara efektif menciptakan sebuah versi dari Gen dimana semua referensi ke T akan diterjemahkan menjadi referensi ke Integer. Jadi, pada deklarasi ini, ob memiliki tipe data Integer, dan tipe data nilai balik dari getob() juga adalah Integer.

Sebelum melanjutkan, penting untuk menyatakan bahwa kompilator Java tidak secara aktual menciptakan versi-versi berbeda dari Gen, atau dari sembarang kelas generik. Meskipun membantu dalam memahaminya, hal itu bukanlah apa yang terjadi. Sebenarnya kompilator menghapus semua informasi tipe data generik dan melakukan konversi tipe yang diperlukan untuk membuat kode Anda berperilaku seperti versi tertentu dari Gen. Jadi, sesungguhnya hanya ada satu versi Gen yang sebenarnya ada pada program Anda. Proses penghapusan informasi tipe data generik ini dinamakan dengan erasure.

Baris selanjutnya menugaskan kepada iOb sebuah referensi yang menunjuk ke sebuah objek Integer dari kelas Gen:


iOb = new Gen<Integer>(88);

Perhatikan bahwa ketika konstruktor Gen dipanggil, argumen tipe data Integer juga ditetapkan. Ini karena tipe data dari objek (pada kasus ini iOb) bertipe Gen<Integer>. Jadi, referensi yang dihasilkan oleh operator new juga harus bertipe Gen<Integer>. Jika tidak, error kompilasi akan terjadi. Misalnya, penugasan berikut akan menyebabkan error kompilasi:

iOb = new Gen<Integer>(88);  //Menyebabkan error kompilasi

Seperti dinyatakan oleh komentar pada program, penugasan:

iOb = new Gen<Integer>(88);

memanfaatkan autoboxing untuk mengenkapsulasi nilai 88, yang mengkonversi int menjadi Integer. Ini dapat dilakukan karena Gen<Integer> menciptakan sebuah konstruktor yang mengambil Int sebagai argumennya. Karena yang diharapkan adalah sebuah Integer, maka Java akan secara otomatis melakukan autoboxing terhadap nilai 88. Tentu, penugasan tersebut dapat pula dituliskan menjadi:

iOb = new Gen<Integer>(new Integer(88));

Tetapi, tidak ada untungnya menuliskan versi yang lebih panjang ini.

Program kemudian menampilkan tipe data dari ob yang ada di dalam iOb, yaitu tipe data Integer. Selanjutnya, program memperoleh nilai dari ob menggunakan baris kode berikut:

int v = iOb.getob();

Karena tipe data nilai balik dari getob() adalah T, yang telah digantikan oleh Integer ketika iOb dideklarasikan, tipe data nilai balik dari getob() sekarang adalah Integer, yang diunbox menjadi int ketika ditugaskan kepada v (yang bertipe data int). Jadi, tidak perlu melakukan konversi tipe data terhadap nilai balik dari getob() menjadi Integer.

Tentu, Anda tidak perlu menggunakan fitur auto-unboxing. Baris kode tersebut bisa saja dituliskan menjadi seperti ini:

int v = iOb.getob().intValue();

Namun, fitur auto-unboxing membuat kode lebih ringkas.

Selanjutnya, kelas DemoGen mendeklarasikan sebuah objek dengan tipe Gen<String>:

Gen<String> strOb = new Gen<String> ("Uji Generik");

Karena argumen tipe datanya adalah String, maka String menggantikan T di dalam Gen. Ini menciptakan sebuah versi String dari Gen.


Generik Hanya Bisa Digunakan Dengan Tipe Referensi
Ketika mendeklarasikan sebuah objek dengan tipe generik, argumen tipe data yang dilewatkan kepada parameter tipe data harus bertipe data referensi. Anda tidak bisa menggunakan tipe data primitif, seperti int atau char. Misalnya, kasus pada Gen, Anda dimungkinkan untuk melewatkan sembarang tipe data kelas kepada T, tetapi Anda tidak bisa melewatkan tipe data primitif kepada parameter tipe data. Oleh karena itu, deklarasi berikut adalah tindakan illegal:

Gen<int> intOb = new Gen<int>(78);  // Error karena tidak bisa karena tipe primitf


Tipe Data Generik Berbeda Berdasarkan Argumen Tipe Data
Kunci penting dalam memahami tipe data generik adalah bahwa versi tertentu dari tipe generik tidak kompatibel dengan tipe data versi lain dari tipe data generik yang sama. Misalnya, dengan menggunakan program sebelumnya, baris kode berikut menciptakan error kompilasi:

iOb = strOb;  // Salah!

Meskipun baik iOb dan strOb keduanya bertipe generik Gen<T>, keduanya mereferensi tipe data berbeda karena parameter tipe datanya berbeda. Ini merupakan salah satu cara generik melakukan pengamanan tipe data.


Bagaimana Generik Memperbaiki Keamanan Tipe Data
Pada titik ini, Anda mungkin bertanya seperti ini: Jika fungsionalitas sama pada kelas generik Gen dapat dicapai tanpa menggunakan generik, hanya dengan menetapkan Object sebagai tipe data dan kemudian menerapkan konversi tipe data yang tepat, maka apa untungnya menggunakan generik? Jawabannnya adalah bahwa pemrograman generik secara otomatis memastikan keamanan tipe data dari semua operasi yang melibatkan Gen. Pada prosesnya, Anda tidak lagi dipusingkan dengan konversi tipe data (cast) dan pemeriksaan kesesuaian tipe data secara manual.

Untuk memahami keuntungan dari pemrograman generik, pertama perhatikan program berikut yang menciptakan sebuah kelas tak-generik yang ekivalen dengan Gen:

// TakGen adalah sebuah fungsionalitas yang ekivalen
// dengan Gen tetapi tanpa pemrograman generik

class TakGen {
   Object ob; // ob sekarang bertipe Object

   // Melewatkan kepada konstruktor sebuah ref ke
   // sebuah objek bertipe Object
   TakGen(Object o) {
      ob = o;
   }

   // Menghasilkan nilai bali bertipe Object.
   Object getob() {
      return ob;
   }

   // Menampilkan tipe data dari ob.
   void tampilTipe() {
      System.out.println("Tipe data dari ob adalah " +
         ob.getClass().getName());
   }
}

public class DemoTakGen {
   public static void main(String args[]) {
      TakGen iOb;
  
      // Menciptakan objek TakGen dan menyimpan
      // sebuah Integer ke dalam int.
      // Autoboxing masih terjadi.
      iOb = new TakGen(88);
  
      // Menampilkan tipe data yang dipakai oleh iOb.
      iOb.tampilTipe();
  
      // Mendapatkan nilai dari iOb.
      // Kali ini, cast diperlukan.
      int v = (Integer) iOb.getob();
      System.out.println("Nilai: " + v);
      System.out.println();
  
      // Menyimpan objek TakGen lain dan
      // menyimpan sebuah String ke dalamnya.
      TakGen strOb = new TakGen("Uji Tak-Generik");
  
      // Menampilkan tipe data yang dipakai oleh iOb.
      strOb.tampilTipe();
  
      // Mendapatkan nilai dari strOb.
      // Lagim perhatikan bahwa cast diperlukan.
      String str = (String) strOb.getob();
      System.out.println("Nilai: " + str);
  
      // Ini bisa dikompilasi, tetapi secara konseptual salah!
      iOb = strOb;
      v = (Integer) iOb.getob(); // error run-time!
   }
}

Jika dijalankan, program ini akan menghasilkan keluaran:

Exception in thread "main" Tipe data dari ob adalah java.lang.Integer
Nilai: 88

Tipe data dari ob adalah java.lang.String
Nilai: Uji Tak-Generik
java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Integer
 at DemoTakGen.main(DemoTakGen.java:57)

Ada beberapa berbeda pada versi ini. Pertama, perhatikan bahwa TakGen menggantikan semua penggunaan T dengan Object. Ini membuat TakGen mampu untuk menyimpan sembarang tipe data objek, sama seperti versi generik. Namun, hal itu membuat kompilator Java tidak mengetahui secara riil tentang tipe data aktual yang disimpan di dalam TakGen, yang merupakan hal buruk karena dua hal. Pertama, konversi tipe data eksplisit (cast) harus diterapkan untuk membaca data tersimpan. Kedua, banyak error karena ketidakcocokan tipe data saat program dijalankan. Lihat lebih dekat pada tiap permasalahan tersebut.



Selanjutnya  >>>





No comments: