Wednesday, March 21, 2018

14. Pemrograman Generik Bagian Empat



Sekarang, perhatikan bagaimana apaDiDalama() dipanggil di dalam main() menggunakan sintaksis normal, tanpa perlu menetapkan argumen tipe. Ini karena tipe dari argumen secara otomatis dipahami, dan tipe data dari T dan V disesuaikan oleh kompilator. Sebagai contoh, pada pemanggilan pertama:

apaDiDalam(2, arrayNilai)

tipe data dari argumen pertama adalah Integer (akibat autoboxing), yang menyebabkan Integer menggantikan T. Tipe data basis untuk argumen kedua juga adalah Integer, yang membuat Integer menggantikan V. Pada pemanggilan kedua, tipe data String digunakan, dan tipe data dari T dan V digantikan dengan String.

Meskipun inferensi (penyimpulan) tipe data cukup untuk kebanyakan pemanggilan metode, Anda dapat secara eksplisit menetapkan argumen tipe jika diperlukan. Sebagai contoh, berikut adalah bagaimana pemanggilan pertama atas apaDiDalam() ketika argumen-argumen tipe ditetapkan:

DemoMetodeGenerik.<Integer, Integer>apaDiDalam(2, arrayNilai)

Tentu, pada kasus ini, tidak ada yang keuntungan yang didapatkan. Selain itu, JDK 8 telah memperbaiki penyimpulan tipe data ketika hal itu berkaitan dengan metode. Hasilnya, hanya ada beberapa kasus dimana argumen tipe eksplisit perlu dilakukan.

Sekarang, perhatikan kode yang dijadikan komentar, yang ditunjukkan di sini:

// if(apaDiDalam("dua", arrayNilai))
// System.out.println("dua ada di dalam arrayString");

Jika Anda menghapus simbol komentar pada dua baris tersebut dan kemudian mencoba mengkompilasi program, Anda akan menerima error kompilasi. Alasannya adalah bahwa parameter tipe V dikekang oleh T pada klausa extends pada deklarasi V. Ini berarti bahwa V harus bertipe sama dengan T, atau subkelas dari T. Pada kasus ini, argumen pertama bertipe String, yang membuat T menjadi bertipe String, tetapi argumen kedua bertipe Integer, yang bukan subkelas dari String. Ini akan menyebabkan error kompilasi karena ketidakcocokan tipe data. Kemampuan untuk menegaskan keamanan tipe data merupakan salah satu keuntungan utama penggunaan metode generik.

Sintaksis yang dipakai untuk menciptakan apaDiDalam() dapat digeneralisasi. Berikut adalah sintaksis untuk metode generik:

<daftar-parameter-tipe> tipe-nilai-balik nama-metode(daftar-parameter) { //…


Konstruktor Generik
Kontruktor bisa dijadikan generik, meskipun kelasnya tidak generik. Sebagai contoh, perhatikan program pendek berikut:

// Menggunakan sebuah konstruktor generik.
class KonsGen {
   private double nil;

   <T extends Number> KonsGen(T arg) {
    nil = arg.doubleValue();
   }

   void tampilNil() {
      System.out.println("Nilai: " + nil);
   }
}

public class DemoKonstruktorGenerik {
   public static void main(String args[]) {
      KonsGen uji = new KonsGen(100);
      KonsGen uji2 = new KonsGen(123.5F);
  
      uji.tampilNil();
      uji2.tampilNil();
   }
}

Keluaran program ditunjukkan di sini:

Nilai: 100.0
Nilai: 123.5

Karena KonsGen() menetapkan sebuah parameter dengan tipe generik, yang harus merupakan sebuah subkelas dari Number, konstruktor KonsGen() dapat dipanggil dengan tipe numerik apapun, termasuk Integer, Float, atau Double. Oleh karena itu, meskipun KonsGen bukan kelas generik, konstruktornya bisa dijadikan generik.


Antarmuka Generik
Selain kelas dan metode generik, Anda dapat pula memiliki antarmuka generik. Antarmuka generik dibuat seperti kelas generik. Berikut diberikan satu contoh sederhana, yang menciptakan sebuah antarmuka generik dengan nama MinMaks yang mendeklarasikan metode min() dan maks(), yang diharapkan untuk menghasilkan nilai minimum dan nilai maksimum dari sejumlah objek.

// Sebuah contoh antarmuka generik.

// Antarmuka MinMaks.
interface MinMaks<T extends Comparable<T>> {
   T min();
   T maks();
}

// Sekarang mengimplementasikan MinMaks
class KelasKu<T extends Comparable<T>> implements MinMaks<T> {
   T[] arrayNilai;

   KelasKu(T[] o) { arrayNilai = o; }
   
   // Menghasilkan nilai minimum di dalam arrayNilai.
   public T min() {
      T v = arrayNilai[0];

      for(int i=1; i < arrayNilai.length; i++)
         if(arrayNilai[i].compareTo(v) < 0) v = arrayNilai[i];

      return v;
   }

   // Menghasilkan nilai maksimum di dalam arrayNilai.
   public T maks() {
      T v = arrayNilai[0];

      for(int i=1; i < arrayNilai.length; i++)
         if(arrayNilai[i].compareTo(v) > 0) v = arrayNilai[i];

      return v;
   }
}

public class DemoAntarmukaGenerik {
   public static void main(String args[]) {
      Integer iArray[] = {3, 6, 2, 8, 6 };
      Character chArray[] = {'b', 'r', 'p', 'w' };
  
      KelasKu<Integer> iob = new KelasKu<Integer>(iArray);
      KelasKu<Character> cob = new KelasKu<Character>(chArray);
  
      System.out.println("Nilai maksimum di dalam iArray: " + iob.maks());
      System.out.println("Nilai minimum di dalam chArray: " + iob.min());
      System.out.println("Nilai maksimum di dalam iArray: " + cob.maks());
      System.out.println("Nilai minimum di dalam chArray: " + cob.min());
   }
}

Keluaran program ditampilkan berikut:

Nilai maksimum di dalam iArray: 8
Nilai minimum di dalam chArray: 2
Nilai maksimum di dalam iArray: w
Nilai minimum di dalam chArray: b

Meskipun sebagian besar aspek dari program ini mudah dipahami, beberapa hal kunci bisa dijelaskan. Pertama, perhatikan bahwa antarmuka MinMaks dideklarasikan seperti ini:

interface MinMaks<T extends Comparable<T>> {

Secara umum, sebuah antarmuka generik dideklarasikan dengan cara sama seperti kelas generik. Pada kasus ini, parameter tipe T, dan kekangan atasnya adalah Comparable. Seperti dijelaskan sebelumnya, Comparable adalah sebuah antarmuka yang didefinisikan oleh java.lang yang menetapkan bagaimana objek-objek dibandingkan. Parameter tipenya menetapkan tipe data objek yang sedang dibandingkan.

Selanjutnya, MinMaks diimplementasikan oleh KelasKu. Perhatikan deklarasi dari KelasKu, ditunjukkan di sini:

class KelasKu<T extends Comparable<T>> implements MinMaks<T> {

Perhatikan khusus pada cara bagaimana parameter tipe T dideklarasikan oleh KelasKu dan kemudian dilewatkan kepada MinMaks. Karena antarmuka MinMaks mensyaratkan sebuah tipe data yang mengimplementasikan Comparable, kelas pengimplementasi (pada kasus ini KelasKu) harus menetapkan kekangan atas. Selain itu, setelah kekangan ini dibuat, tidak diperlukan lagi untuk menetapkannya pada klausa implements. Pada dasarnya, hal itu salah untuk dilakukan. Sebagai contoh, baris konde ini tidak akan bisa dikompilasi:

// Ini salah!
class KelasKu<T extends Comparable<T>> implements MinMaks<T extends Comparable<T>> {

Setelah parameter tipe ditetatpkan, ia hanya dilewatkan kepada antarmuka tanpa modifikasi lebih lanjut.

Secara umum, jika sebuah kelas mengimplementasikan sebuah antarmuka generik, maka kelas itu harus pula generik, setidaknya kelas itu mengambil parameter tipe yang dilewatkan kepada antarmuka. Sebagai contoh, kode berikut yang mendeklarasikan KelasKu akan menghasilkan error:

class KelasKu implements MinMaks<T> { // Salah!

Karena KelasKu tidak mendeklarasikan tipe parameter, jadi tidak ada cara lain untuk melewatkannya kepada MinMaks. Pada kasus ini, pengenal T tidak diketaui, dan kompilator akan melaporkan error. Tentu, jika sebuah kelas mengimplementasikan tipe spesifik dari antarmuka generik, seperti ditunjukkan di sini:

class KelasKu implements MinMaks<Integer> { // OK!

maka kelas pengimplementasi tidak perlu menjadi generik.


Tipe Data Mentah dan Kode Warisan
Karena dukungan generik belum ada sebelum JDK 5, Anda perlu mengetahui beberapa hal tentang transisi kode dari kode lama, pra-generik. Pada saat penulisan buku ini, masih banyak kode warisan pra-generik yang fungsional dan kompatibel dengan generik. Kode pra-generik harus bisa dimanfaatkan dengan kode generik, dan kode generik harus bisa bekerja dengan kode pra-generik.

Untuk menangani transisi ke generik, Java mengijinkan kelas generik untuk digunakan tanpa argumen tipe data. Ini akan menciptakan tipe data mentah untuk kelas tersebut. Tipe data mentah ini kompatibel dengan kode warisan, yang tidak mengenal pemrograman generik. Kekurangan utama dari penggunaan tipe data mentah ini adalah tidak adanya keamanan tipe data dari generik.

Berikut adalah sebuah contoh yang menunjukkan bagaimana tipe data mentah dimanfaatkan:

// Mendemonstrasikan sebuah tipe data mentah.
class Gen<T> {
   T ob; // Mendeklarasikan sebuah objek bertipe T

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

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

public class DemoTipeDataMentah {
   public static void main(String args[]) {
      // Menciptakan sebuah objek Gen untuk Integer-Integer.
      Gen<Integer> iOb = new Gen<Integer>(88);
  
      // Menciptakan sebuah objek Gen untuk String-String.
      Gen<String> strOb = new Gen<String>("Uji Generik");
  
      // Menciptakan sebuah objek Gen tipe data mentah dan
      // memberikannya sebuah nilai Double.
      Gen mentah = new Gen(new Double(98.6));
  
      // Cast di sini diperlukan karena tipe data tidak diketahui.
      double d = (Double) mentah.getob();
      System.out.println("Nilai: " + d);
  
      // Penggunaan tipe data mentah dapat menyebabkan eksepsi
      // run-time. Berikut adalah beberapa contohnya.
      // Cast berikut menyebabkan error run-time!
      // int i = (Integer) mentah.getob(); // error run-time
  
      // Penugasan ini membatalkan keamanan tipe data.
      strOb = mentah; // OK, tetapi berpotensi salah
      // String str = strOb.getob(); // error run-time 
 
      // Penugasan ini juga membatalkan keamanan tipe data.
      mentah = iOb; // OK, tetapi berpotensi salah
      // d = (Double) raw.getob(); // run-time error
   }
}

Program ini memuat beberapa hal penting. Pertama, sebuah tipe data mentah dari kelas Gen generik diciptakan dengan deklarasi berikut:

Gen mentah = new Gen(new Double(98.6));

Perhatikan bahwa tidak ada argumen tipe yang ditetapkan. Ini menciptakan sebuah objek Gen dengan tipe T yang diganti dengan Object.

Tipe data mentah tidak memiliki keamanan tipe data. Jadi, sebuah varibel dengan tipe data mentah dapat ditugasi sebuah referensi yang menunjuk ke sembarang tipe dari objek Gen. Kebalikannya juga berlaku; sebuah variabel bertipe Gen spesifik dapat ditugasi sebuah referensi yang menunjuk ke sebuah objek Gen bertipe data mentah. Namun, kedua operasi tersebut berpotensi tidak aman karena mekanisme pemeriksaan tipe data dari generik tidak berlaku.

Tidak adanya keamanan tipe data ini diilustrasikan oleh kode yang dijadikan komentar di akhir program. Anda akan memeriksanya satu per satu. Pertama, perhatikan situasi berikut:

int i = (Integer) mentah.getob(); // error run-time

Pada statemen ini, nilai dari ob di dalam mentah diperoleh, dan nilai ini dikonversi paksa (cast) menjadi Integer. Permasalahannya adalah bahwa mentah memuat sebuah nilai Double, bukan nilai integer. Namun, ini tidak bisa dideteksi pada waktu kompilasi karena tipe data dari mentah belum diketahui. Jadi, statemen ini akan menghasilkan error ketika program dijalankan.

Runtun kode berikut menugaskan kepada strOb (sebuah referensi bertipe Gen<String>) sebuah referensi ke sebuah objek Gen mentah:

strOb = mentah; // OK, tetapi berpotensi salah
// String str = strOb.getob(); // error run-time

Penugasan itu sendiri secara sintaksis valid, tetapi dipertanyakan. Karena strOb bertipe Gen<String>, ia diasumsikan memuat sebuah String. Namun, setelah penugasan, objek yang ditunjuk oleh strOb memuat sebuah Double. Jadi, pada saat program dijalankan, ketika dicoba untuk menugaskan isi dari strOb kepada str, error run-time akan dihasilkan karena strOb sekarang memuat sebuah Double. Jadi, penugasan dari sebuah referensi mentah kepada sebuah referensi generik melompati mekanisme pengamanan tipe data.

Runtun kode berikut kebalikan dari kasus sebelumnya:

mentah = iOb; // OK, tetapi berpotensi salah
// d = (Double) raw.getob(); // run-time error

Di sini, sebuah referensi generik ditugaskan kepada sebuah variabel referensi mentah. Meskipun ini secara sintaksis valid, hal ini bisa menyebabkan masalah, seperti diilustrasikan pada baris kedua. Pada kasus ini, mentah sekarang mereferensi ke sebuah objek yang memuat sebuah objek Integer, tetapi cast mengasumsikan bahwa ia memuat sebuah Double. Error ini tidak dapat dicegah saat program dijalankan.

Karena potensi error pada tipe data mentah, javac menampilkan peringatan unchecked warnings ketika sebuah tipe data mentah dipakai dengan membahayakan keamanan tipe data. Pada program tersebut, dua baris kode berikut menghasilkan peringatan keamanan data:

Gen mentah = new Gen(new Double(98.6));
strOb = mentah; // OK, tetapi berpotensi salah

Pada baris pertama, pemanggilan kepada konstruktor Gen tanpa argumen tipe data menyebabkan peringatan keamanan data. Pada baris kedua, penugasan dari sebuah referensi mentah kepada sebuah variabel generik menyebabkan peringatan keamanan data.

Anda mungkin berpikir bahwa baris kode berikut juga menghasikan peringatan keamanan data, tetapi sebenarnya tidak:

mentah = iOb; // OK, tetapi berpotensi salah

Tidak ada peringatan dari kompilator karena penugasan tidak menyebabkan hilangnya keamanan tipe data.


Hierarki Pewarisan Kelas Generik
Kelas generik dapat menjadi bagian dari sebuah hierarki pewarisan kelas dengan cara yang sama seperti kelas tak-generik. Jadi, sebuah kelas generik dapat berperan sebagai superkelas atau subkelas. Perbedaan kunci antara hierarki pewarisan generik dan hierarki pewarisan tak-generik adalah bahwa pada hierarki pewarisan generik, argumen tipe data yang diperlukan oleh superkelas generik harus dilewatkan oleh semua subkelasnya dalam hierarki pewarisan kelas.



Selanjutnya  >>>



No comments: