Wednesday, March 21, 2018

14. Pemrograman Generik Bagian Dua



Perhatikan baris kode berikut:

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

Karena tipe data nilai balik dari getob() adalah Object, maka diperlukan konversi tipe data eksplisit (cast) agar nilai tersebut diauto-unbox dan disimpan ke dalam v. Jika anda menghapuskan cast tersebut (pada kasus ini (Integer)), maka program tidak bisa dikompilasi. Ini merupakan tindakan yang berpotensi menimbulkan error.

Sekarang, perhatikan runtun kode berikut di dekat akhir program:

// Ini bisa dikompilasi, tetapi secara konseptual salah!
iOb = strOb;
v = (Integer) iOb.getob(); // error run-time!

Di sini, strOb ditugaskan kepada iOb. Namun, strOb mereferensi sebuah objek yang memuat string, bukan integer. Penugasan ini secara sintaksis valid karena semua TakGen mereferensi tipe data sama (Object), dan sembarang referensi TakGen dapat mereferensi ke semua objek TakGen. Namun, statemen tersebut secara semantik salah, seperti ditunjukkan pada baris selanjutnya. Di sini, tipe data nilai balik dari getob() dikonversi paksa menjadi Integer, dan kemudian nilainya dicoba ditugaskan kepada v. Masalahnya adalah sekarang iOb mereferensi ke objek yang menyimpan sebuah String, bukan Integer. Sayangnya, tanpa menggunakan generik, Java tidak mengetahui hal ini. Pada saat program dijalankan, eksepsi run-time terjadi akibat konversi eksplisit ke Integer. Inilah mengapa pemrograman generik diperlukan.


Kelas Generik Dengan Dua Parameter Tipe
Anda bisa mendeklarasikan lebih dari satu parameter tipe pada tipe data generik. Untuk menetapkan dua atau lebih parameter tipe, Anda hanya perlu menggunakan koma sebagai pemisah antar parameter tipe. Sebagai contoh, kelas DuaGen berikut merupakan variasi dari kelas Gen yang memiliki dua parameter tipe:

// Sebuah kelas generik sederhana yang memiliki dua
// parameter tipe: T dan V.

class DuaGen<T, V> {
   T ob1;
   V ob2;

   // Melewatkan kepada konstruktor sebuah referensi ke
   // sebuah objek dengan tipe T dan objek dengan tipe V.
   DuaGen(T o1, V o2) {
      ob1 = o1;
      ob2 = o2;
   }

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

      System.out.println("Tipe data dari V adalah " +
         ob2.getClass().getName());
   }

   T getob1() {
      return ob1;
   }

   V getob2() {
      return ob2;
   }
}

public class DemoDuaGen {
   public static void main(String args[]) {
    DuaGen<Integer, String> tgObj =
         new DuaGen<Integer, String>(88, "Generik");
  
    // Menampilkan tipe data.
  tgObj.tampilTipe();
  
      // Membaca dan menampilkan nilai.
      int v = tgObj.getob1();
      System.out.println("Nilai: " + v);
      String str = tgObj.getob2();
      System.out.println("Nilai: " + str);
   }
}

Ketika dijalankan, program ini menghasilkan keluaran berikut:

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

Perhatikan bagaimana DuaGen dideklarasikan:

class DuaGen<T, V> {

Deklarasi ini menetapkan dua parameter tipe: T dan V, yang dipisahkan dengan koma. Karena ia memiliki dua parameter tipe, dua argumen tipe harus dilewatkan kepada DuaGen ketika sebuah objek diciptakan, seperti ditunjukkan berikut:

DuaGen<Integer, String> tgObj = new DuaGen<Integer, String>(88, "Generik");

Pada kasus ini, Integer menggantikan T dan String menggantikan V.

Meskipun kedua argumen tipe berbeda pada contoh ini, Anda dimungkinkan pula untuk memiliki dua argumen tipe yang sama. Misalnya, baris kode berikut adalah valid:

DuaGen<String, String> tgObj = new DuaGen<String, String>(X, "Y");


Bentuk Umum dari Kelas Generik
Sintaksis generik yang ditunjukkan pada contoh-contoh sebelumnya dapat digeneralisir. Berikut adalah sintaksis dalam mendeklarasik sebuah kelas generik:

class nama-kelas<daftar-parameter-tipe> {//…

Berikut adalah sintaksis utuh untuk mendeklarasikan sebuah referensi yang menunjuk ke sebuah kelas generik dan untuk menciptakan objek dari kelas generik:

nama-kelas<daftar-arg-tipe> nama-var = 
      new nama-kelas<daftar-arg-tipe>(daftar-arg-konstruktor);


Tipe Data Terkekang
Pada contoh-contoh sebelumnya, parameter tipe data dapat digantikan oleh sembarang tipe data kelas. Ini baik untuk banyak tujuan, tetapi adakalanya Anda perlu membatasi tipe-tipe data apa saja yang boleh dilewatkan kepada parameter tipe. Misalnya, diasumsikan bahwa Anda ingin menciptakan sebuah kelas generik yang memuat sebuah metode yang menghasilkan nilai rerata atas array nilai. Anda ingin menggunakan kelas tersebut untuk mendapatkan rerata atas sebuah array yang memuat nilai dengan tipe data apapun, termasuk integer, float, maupun double. Jadi, Anda ingin menetapkan tipe data dari elemen array secara generik menggunakan parameter tipe. Untuk menciptakan kelas semacam itu, Anda mungkin akan mencoba seperti ini:

// Statistik mencoba (tapi tidak berhasil) untuk menciptakan
// sebuah kelas generik yang dapat menghitung rerata atas
// sebuah array yang memuat nilai-nilai dengan tipe data apapun.
//
// Kelas ini memuat error!
class Statistik<T> {
   T[] arrayNilai; // arrayNilai adalah sebuah array bertipe T

   // Melewatkan kepada konstruktor sebuah referensi ke
   // sebuah array bertipe T.
   Statistik(T[] o) {
      arrayNilai = o;
   }

   // Menghasilkan tipe double pada semua kasus.
   double rerata() {
      double jum = 0.0;

      for(int i=0; i < arrayNilai.length; i++)
         jum += arrayNilai[i].doubleValue(); // Error!!!

      return jum / arrayNilai.length;
   }
}

}

Pada kelas Statistik, metode rerata() mencoba mendapatkan versi double dari tiap nilai pada array arrayNilai dengan memanggil doubleValue(). Karena semua kelas numerik, termasuk Integer dan Double, merupakan subkelas dari Number, dan Number mendefinisikan metode doubleValue(), metode ini tersedia bagi semua kelas wrapper. Masalahnya adalah bahwa kompilator tidak mengetahui bahwa Anda bermaksud untuk menciptakan objek objek Statistik hanya untuk tipe-tipe data numerik. Jadi, ketika Anda mencoba mengkompilasinya, error akan dilaporkan yang mengindikasikan bahwa metode doubleValue() tidak dikenali. Untuk menyelesaikan permasalahan ini, Anda perlu cara untuk memberitahu kompilator bahwa Anda bermaksud untuk hanya melewatkan tipe-tipe data numerik kepada T.

Untuk menangani situasi semacam itu, Java menyediakan tipe data terkekang. Ketika menetapkan sebuah parameter tipe, Anda dapat menciptakan kekangan atas yang mendeklarasikan superkelas dari mana semua argumen tipe harus diderivasi. Ini dilakukan melalui penggunaan klausa extends ketika menetapkan parameter tipe, seperti ditunjukkan di sini:

<T extends superkelas>

Ini menetapkan bahwa T hanya bisa diganti oleh superkelas, atau sub-subkelas dari superkelas. Jadi, superkelas mendefinisikan kekangan atas yang inklusif.

Anda bisa menggunakan kekangan atas untuk memperbaiki kelas Statistik sebelumnya dengan menetapkan Number sebagai kekangan atas, seperti ditunjukkan di sini:

// Pada versi dari Statistik ini, argumen tipe untuk T
// harus Number, atau kelas yang diderivasi
// dari Number.

class Statistik<T extends Number> {
   T[] arrayNilai; // arrayNilai adalah sebuah array bertipe T

   // Melewatkan kepada konstruktor sebuah referensi ke
   // sebuah array bertipe T.
   Statistik(T[] o) {
      arrayNilai = o;
   }

   // Menghasilkan tipe double pada semua kasus.
   double rerata() {
   double jum = 0.0;

   for(int i=0; i < arrayNilai.length; i++)
      jum += arrayNilai[i].doubleValue(); // Error!!!

   return jum / arrayNilai.length;
   }
}
 

public class DemoKekangan {
   public static void main(String args[]) {
      Integer iArray[] = { 1, 2, 3, 4, 5 };
      Statistik<Integer> iob = new Statistik<Integer>(iArray);
  
      double v = iob.rerata();
      System.out.println("Rerata iob = " + v);
  
      Double dArray[] = { 1.1, 2.2, 3.3, 4.4, 5.5 };
      Statistik<Double> dob = new Statistik<Double>(dArray);
  
      double w = dob.rerata();
      System.out.println("Rerata dob = " + w);
  
      // Ini tidak akan bisa dikompilasi karena String bukan
      // subkelas dari Number.
      // String strArray[] = { "1", "2", "3", "4", "5" };
      // Statistik<String> strob = new Statistik<String>(strArray);
  
      // double x = strob.rerata();
      // System.out.println("Rerata strob = " + v);
   }
}

Ketika dijalankan, program di atas menghasilkan keluaran berikut:

Rerata iob = 3.0
Rerata dob = 3.3

Perhatikan bahwa Statistik sekarang dideklarasikan oleh baris ini:

class Statistik<T extends Number> {

Karena tipe T sekarang dikekang oleh Number, kompilator Java mengetahui bahwa semua objek bertipe T dapat memanggil doubleValue() karena metode itu dideklarasikan oleh Number. Ini merupakan kekuatannya, karena kekangan terhadap T dapat mencegah objek-objek Statistik yang tidak numerik untuk diciptakan. Misalnya, jika Anda mencoba menghapus komentar pada baris-baris di akhir program, dan kemudian mengkompilasi ulang program, Anda akan menerima error kompilasi karena String bukan subkelas dari Number.

Selain menggunakan tipe kelas sebagai kekangan, Anda juga dapat menggunakan tipe antarmuka. Pada dasarnya, Anda bisa menetapkan beberapa antarmuka sebagai kekangan. Jadi, kekangan dapat mencantumka baik sebuah tipe kelas maupun satu atau lebih antarmuka. Pada kasus ini, tipe kelas harus ditetapkan lebih dahulu. Ketika sebuah kekangan mencakup sebuah tipe antarmuka, maka hanya argumen tipe yang mengimplementasikan antarmuka itu yang legal. Ketika menetapkan sebuah kekangan dengan sebuah kelas dan sebuah antarmuka, atau beberapa antarmuka, Anda menggunakan operator & untuk menghubungkannya. Sebagai contoh:

class Gen<T> extends KelasKu & AntarmukaKu> { //…

Di sini, T dikekang oleh sebuah kelas dengan nama KelasKu dan sebuah antarmuka dengan nama AntarmukaKu. Jadi, setiap argumen tipe yang dilewatkan kepada T harus merupakan subkelas dari KelasKu dan mengimplementasikan AntarmukaKu.



Menggunakan Argumen Wildcard
Untuk menjelaskannya, Anda perhatikan contoh dan penjelasannya berikut. Misalnya, diberikan kelas Statistik yang ditunjukkan sebelumnya dan diasumsikan bahwa Anda ingin menambahkan sebuah metode dengan nama rerataSama() untuk menentukan apakah dua objek Statistik memuat array yang menghasilkan rerata sama, tanpa memandang tipe data numerik yang dimuat tiap objek.

Sebagai contoh, jika satu objek memuat nilai-nilai double 1.0, 2.0, dan 3.0, dan objek lain memuat nilai-nilai integer 2, 3, dan 1, maka rerata kedua objek akan sama. Salah satu cara dalam mengimplementasikan rerataSama() adalah dengan melewatkan kepadanya sebuah argumen Statistik, dan kemudian membandingkan rerata dari argumen tersebut dengan objek pemanggil, yang menghasilkan true hanya jika kedua rerata sama. Jadi, Anda akan bisa memanggil rerataSama(), seperti ditunjukkan di sini:

Integer iArray[] = { 1, 2, 3, 4, 5 };
Double dArrray[] = { 1.1, 2.2, 3.3, 4.4, 5.5 };

Statistik<Integer> iob = new Statistik<Integer>(iArray);
Statistik<Double> dob = new Statistik<Double>(dArray);

if(iob.rerataSama(dob))
   System.out.println("Rerata keduanya sama.");
else
   System.out.println("Rerata keduanya berbeda.");

Saat pertama menciptakan rerataSama() tampak seperti mudah. Karena Statistik merupakan kelas generik dan metode rerata() dapat diterapkan pada sembarang tipe dari obje Statistik, kelihatannya mudah dalam menciptakan metode rerataSama(). Sayangnya, masalah akan muncul ketika Anda mendeklarasikan sebuah parameter dengan tipe data Statistik. Karena Statistik merupakan tipe terparameterisasi, apa yang ingin Anda tetapkan untuk parameter tipe dari Statistik ketika Anda mendeklarasikan sebuah parameter dengan tipe itu?

Mungkin Anda terpikir untuk menemukan solusi seperti ini, dimana T dipakai sebagai parameter tipe:

// Ini tidak bisa digunakan!
// Menentukan apakah dua rerata sama atau tidak.
boolean rerataSama(Statistik<T> ob) {
   if(rerata() == ob.rerata())
      return true;

   return false;
}

Masalah pada kode di atas adalah ia tidak bisa dipakai pada objek-objek Statistik dengan tipe data yang sama dengan tipe data dari objek pemanggil. Misalnya, jika objek pemanggil bertipe Statistik<Integer>, maka parameter ob harus pula bertipe Statistik<Integer>. Hal itu tidak bisa digunakan untuk membandingkan rerata dari sebuah objek bertipe Statistik<Double> dengan rerata dari sebuah objek bertipe Statistik<Short>, misalnya. Oleh karena itu, pendekatan ini tidak bisa digunakan kecuali pada konteks yang sangat sempit dan tidak menghasilkan solusi umum.

Untuk menciptakan sebuah metode generik rerataSama(), Anda perlu menggunakan fitur lain dari generik dalam Java: argumen wildcard. Argumen wildcard ditetapkan dengan ?, dan ia merepresentasikan sebuah tipe tak-diketahui. Dengan menggunakan wildcard, berikut adalah salah satu cara untuk menuliskan metode rerataSama():

// Menentukan apakah dua rerata sama atau tidak.
boolean rerataSama(Statistik<?> ob) {
   if(rerata() == ob.rerata())
      return true;

   return false;
}

Di sini, Statistik<?> akan cocok dengan semua objek Statistik, yang membolehkan dua objek Statistik dibandingkan reratanya. Program berikut mengilustrasikannya:

   T[] arrayNilai; // array yang memuat objek-objek Number atau subkelasnya

   // Melewatkan kepada konstruktor sebuah referensi ke
   // sebuah array dengan tipe Number atau subkelasnya.
   Statistik(T[] o) {
    arrayNilai = o;
   }

   // Menghasilkan tipe nilai balik double pada semua kasus.
   double rerata() {
      double jum = 0.0;

      for(int i=0; i < arrayNilai.length; i++)
         jum += arrayNilai[i].doubleValue();

      return jum / arrayNilai.length;
   }

   // Menentukan apakah kedua rerata sama atau tidak.
   // Perhatikan penggunaan wildcard.
   boolean rerataSama(Statistik<?> ob) {
      if(rerata() == ob.rerata())
         return true;

      return false;
   }
}

public class DemoWildcard {
   public static void main(String args[]) {
      Integer iArray[] = { 1, 2, 3, 4, 5 };
      Statistik<Integer> iob = new Statistik<Integer>(iArray);
  
      double v = iob.rerata();
      System.out.println("Rerata iob = " + v);
  
      Double dArray[] = { 1.1, 2.2, 3.3, 4.4, 5.5 };
      Statistik<Double> dob = new Statistik<Double>(dArray);
  
      double w = dob.rerata();
      System.out.println("Rerata dob = " + w);
  
      Float fArray[] = { 1.0F, 2.0F, 3.0F, 4.0F, 5.0F };
      Statistik<Float> fob = new Statistik<Float>(fArray);
  
      double x = fob.rerata();
      System.out.println("Rerata fob = " + x);
  
      // Melihat array mana yang memiliki rerata sama.
      System.out.print("Rerata atas iob dan dob ");
      if(iob.rerataSama(dob))
         System.out.println("adalah sama.");
      else
         System.out.println("adalah berbeda.");
  
      System.out.print("Rerata atas iob dan fob ");
      if(iob.rerataSama(fob))
         System.out.println("adalah sama.");
      else
         System.out.println("adalah berbeda.");
   }
}

Jika dijalankan, program di atas menghasilkan keluaran berikut:

Rerata iob = 3.0
Rerata dob = 3.3
Rerata fob = 3.0
Rerata atas iob dan dob adalah berbeda.
Rerata atas iob dan fob adalah sama.

Satu hal penting lain: Penting untuk memahami bahwa wildcard tidak memengaruhi apa tipe data dari objek Statistik yang diciptakan. Ini diatur oleh klausa extends pada deklarasi Statistik.


Selanjutnya  >>>




No comments: