Class dan method sudah kita parametrisasi; kini giliran kontrak antar class. Interface generik memungkinkan kita mendefinisikan perjanjian perilaku yang tipe datanya baru ditentukan oleh class pengimplementasi — pustaka standar Java memakai pola ini di mana-mana, dari Comparable<T> hingga seluruh kerabat Collection<E>. Ketika sebuah class mengimplementasikan interface generik, compiler javac menawarkan dua pilihan strategi: mengunci type parameter menjadi tipe konkret, atau meneruskannya sebagai type parameter class itu sendiri. Pada Chapter ini kita bangun interface Penyimpan<T> buatan sendiri, mencoba kedua strategi implementasi, lalu menutup dengan dua interface generik paling berpengaruh dari pustaka standar: Comparable dan Comparator.
Interface Generik Buatan Sendiri #
Deklarasi interface generik identik dengan class generik: type parameter ditulis setelah nama interface. Seluruh method di dalamnya boleh memakai type parameter tersebut sebagai tipe parameter method maupun tipe kembalian. Berikut kontrak sederhana untuk wadah yang menyimpan nilai satu per satu:
public interface Penyimpan<T> {
void simpan(T nilai);
T ambil();
int jumlah();
}Interface di atas menjanjikan tiga kemampuan: menyimpan nilai bertipe T (simpan), mengambil nilai terakhir bertipe T (ambil), dan menghitung banyaknya isi (jumlah). Tipe T tidak diikat oleh interface itu sendiri — setiap pengimplementasi yang menandatangani kontrak ini yang menentukan tipe konkret yang dipakai.
Implementasi Konkret vs Implementasi Generik Lanjut #
Strategi pertama meneruskan type parameter: PenyimpanMemori<T> implements Penyimpan<T>. Class hasil tetap generik, sehingga PenyimpanMemori<String> dan PenyimpanMemori<Integer> sama-sama sah:
import java.util.ArrayDeque;
import java.util.Deque;
public class PenyimpanMemori<T> implements Penyimpan<T> {
private final Deque<T> isi = new ArrayDeque<>();
@Override
public void simpan(T nilai) {
isi.addLast(nilai);
}
@Override
public T ambil() {
return isi.pollLast();
}
@Override
public int jumlah() {
return isi.size();
}
}Strategi kedua mengunci type parameter menjadi tipe konkret: PenyimpanRiwayat implements Penyimpan<String>. Class hasil bukan lagi generik, dan seluruh T pada kontrak terikat ke String. Program berikut memakai kedua strategi sekaligus:
public class AplikasiPenyimpan {
public static void main(String[] args) {
// Implementasi generik lanjut: T diteruskan, tidak dikunci
PenyimpanMemori<String> memo = new PenyimpanMemori<>();
memo.simpan("Budi");
memo.simpan("Siti");
System.out.println("Memo : jumlah " + memo.jumlah());
System.out.println("Ambil : " + memo.ambil() + ", sisa " + memo.jumlah());
PenyimpanMemori<Integer> stok = new PenyimpanMemori<>();
stok.simpan(12);
stok.simpan(7);
System.out.println("Stok : jumlah " + stok.jumlah());
System.out.println("Ambil : " + stok.ambil() + ", sisa " + stok.jumlah());
// Implementasi konkret: type parameter dikunci menjadi String
PenyimpanRiwayat riwayat = new PenyimpanRiwayat(2);
riwayat.simpan("login");
riwayat.simpan("unggah");
riwayat.simpan("logout");
System.out.println("Riwayat : " + riwayat.tampilkan());
// Polymorphism lewat variabel bertipe interface generik
Penyimpan<String> sebagai = riwayat;
System.out.println("Interface: jumlah " + sebagai.jumlah());
}
}
class PenyimpanRiwayat implements Penyimpan<String> {
private final java.util.ArrayDeque<String> tumpukan = new java.util.ArrayDeque<>();
private final int kapasitas;
PenyimpanRiwayat(int kapasitas) {
this.kapasitas = kapasitas;
}
@Override
public void simpan(String nilai) {
if (tumpukan.size() == kapasitas) {
tumpukan.pollFirst(); // buang peristiwa paling lama
}
tumpukan.addLast(nilai);
}
@Override
public String ambil() {
return tumpukan.peekLast();
}
@Override
public int jumlah() {
return tumpukan.size();
}
public String tampilkan() {
return tumpukan.toString();
}
}Output yang Dihasilkan:
Memo : jumlah 2
Ambil : Siti, sisa 1
Stok : jumlah 2
Ambil : 7, sisa 1
Riwayat : [unggah, logout]
Interface: jumlah 2Mari kita bedah pemakaian kedua strategi pada program di atas:
- Baris 4-14 (
PenyimpanMemori<String>laluPenyimpanMemori<Integer>): satu class implementasi melayani dua tipe. Compiler memastikanmemo.simpan("Budi")sah danstok.simpan(12)sah — masing-masing sesuai type argument-nya. - Baris 29 (
class PenyimpanRiwayat implements Penyimpan<String>): type parameter terkunci. Signature method menjadisimpan(String)danambil()yang mengembalikanString— persis seperti mengganti setiapTdenganStringdi kontraknya. - Baris 39-41 (
if (tumpukan.size() == kapasitas)): implementasi konkret menambah aturan bisnis yang tidak diminta interface — membatasi kapasitas dengan membuang peristiwa paling lama. Interface mendefinisikan kontrak; implementasi bebas menambah detail. - Baris 24-25 (
Penyimpan<String> sebagai = riwayat): variabel bertipe interface generik menampung objek implementasi apa pun yangT-nya cocok — polymorphism yang tetap type-safe tanpa cast.
Pilihan strategi mengikuti kebutuhan: teruskan type parameter bila implementasi netral terhadap tipe (seperti ArrayList<E>), dan kunci tipe bila logika implementasi memang spesifik untuk satu tipe (seperti class pengurai konfigurasi yang hanya bekerja pada String).
Comparable dan Comparator #
Pustaka standar membangun urutan data di atas dua interface generik. Comparable<T> melekat pada class data dan mendefinisikan natural ordering lewat method compareTo(T); Comparator<T> berdiri di luar data dan menyediakan urutan alternatif. Keduanya bekerja sama pada program pengurutan berikut:
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
public class UrutMahasiswa {
static class Mahasiswa implements Comparable<Mahasiswa> {
private final String nama;
private final int nilai;
Mahasiswa(String nama, int nilai) {
this.nama = nama;
this.nilai = nilai;
}
public String nama() {
return nama;
}
@Override
public String toString() {
return nama + "(" + nilai + ")";
}
@Override
public int compareTo(Mahasiswa lain) {
return Integer.compare(lain.nilai, this.nilai); // nilai tertinggi duluan
}
}
public static void main(String[] args) {
List<Mahasiswa> kelas = new ArrayList<>(List.of(
new Mahasiswa("Budi", 78),
new Mahasiswa("Siti", 92),
new Mahasiswa("Dewi", 85)));
Collections.sort(kelas); // pakai compareTo
System.out.println("Per nilai : " + kelas);
kelas.sort(Comparator.comparing(Mahasiswa::nama)); // pakai Comparator
System.out.println("Per nama : " + kelas);
}
}Output yang Dihasilkan:
Per nilai : [Siti(92), Dewi(85), Budi(78)]
Per nama : [Budi(78), Dewi(85), Siti(92)]Dua baris penting layak dicermati: implements Comparable<Mahasiswa> pada baris 7 mengikat class terhadap dirinya sendiri — idiom baku yang membuat method Collections.sort(kelas) langsung bekerja. Adapun Comparator.comparing(Mahasiswa::nama) di baris 40 menghasilkan objek Comparator<Mahasiswa> dari method reference, lalu method sort memakai pembanding tersebut tanpa menyentuh compareTo. Satu class data, dua strategi urutan, keduanya diperiksa compiler.
Interface Penyimpan<T> kita bebas memakai nilai T apa pun — compiler hanya mengizinkan operasi yang dimiliki semua objek. Bagaimana bila kontrak menuntut lebih, misalnya method doubleValue() yang hanya dimiliki angka? Java menyediakan pembatasan bounded type parameter untuk kebutuhan tersebut, dan itulah topik Chapter berikutnya.
Referensi #
- Java API: java.lang.Comparable
- Java API: java.util.Comparator
- JLS SE25 §9.1.2: Generic Interfaces and Type Parameters
- dev.java: Generics
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
AplikasiPenyimpan.java
Unduh berkaspublic class AplikasiPenyimpan {
public static void main(String[] args) {
// Implementasi generik lanjut: T diteruskan, tidak dikunci
PenyimpanMemori<String> memo = new PenyimpanMemori<>();
memo.simpan("Budi");
memo.simpan("Siti");
System.out.println("Memo : jumlah " + memo.jumlah());
System.out.println("Ambil : " + memo.ambil() + ", sisa " + memo.jumlah());
PenyimpanMemori<Integer> stok = new PenyimpanMemori<>();
stok.simpan(12);
stok.simpan(7);
System.out.println("Stok : jumlah " + stok.jumlah());
System.out.println("Ambil : " + stok.ambil() + ", sisa " + stok.jumlah());
// Implementasi konkret: type parameter dikunci menjadi String
PenyimpanRiwayat riwayat = new PenyimpanRiwayat(2);
riwayat.simpan("login");
riwayat.simpan("unggah");
riwayat.simpan("logout");
System.out.println("Riwayat : " + riwayat.tampilkan());
// Polymorphism lewat variabel bertipe interface generik
Penyimpan<String> sebagai = riwayat;
System.out.println("Interface: jumlah " + sebagai.jumlah());
}
}
class PenyimpanRiwayat implements Penyimpan<String> {
private final java.util.ArrayDeque<String> tumpukan = new java.util.ArrayDeque<>();
private final int kapasitas;
PenyimpanRiwayat(int kapasitas) {
this.kapasitas = kapasitas;
}
@Override
public void simpan(String nilai) {
if (tumpukan.size() == kapasitas) {
tumpukan.pollFirst(); // buang peristiwa paling lama
}
tumpukan.addLast(nilai);
}
@Override
public String ambil() {
return tumpukan.peekLast();
}
@Override
public int jumlah() {
return tumpukan.size();
}
public String tampilkan() {
return tumpukan.toString();
}
}
Penyimpan.java
Unduh berkaspublic interface Penyimpan<T> {
void simpan(T nilai);
T ambil();
int jumlah();
}
PenyimpanMemori.java
Unduh berkasimport java.util.ArrayDeque;
import java.util.Deque;
public class PenyimpanMemori<T> implements Penyimpan<T> {
private final Deque<T> isi = new ArrayDeque<>();
@Override
public void simpan(T nilai) {
isi.addLast(nilai);
}
@Override
public T ambil() {
return isi.pollLast();
}
@Override
public int jumlah() {
return isi.size();
}
}
UrutMahasiswa.java
Unduh berkasimport java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
public class UrutMahasiswa {
static class Mahasiswa implements Comparable<Mahasiswa> {
private final String nama;
private final int nilai;
Mahasiswa(String nama, int nilai) {
this.nama = nama;
this.nilai = nilai;
}
public String nama() {
return nama;
}
@Override
public String toString() {
return nama + "(" + nilai + ")";
}
@Override
public int compareTo(Mahasiswa lain) {
return Integer.compare(lain.nilai, this.nilai); // nilai tertinggi duluan
}
}
public static void main(String[] args) {
List<Mahasiswa> kelas = new ArrayList<>(List.of(
new Mahasiswa("Budi", 78),
new Mahasiswa("Siti", 92),
new Mahasiswa("Dewi", 85)));
Collections.sort(kelas); // pakai compareTo
System.out.println("Per nilai : " + kelas);
kelas.sort(Comparator.comparing(Mahasiswa::nama)); // pakai Comparator
System.out.println("Per nama : " + kelas);
}
}