Deklarasi List<String> catatan = new ArrayList<>() memuat kebiasaan kecil yang dampaknya panjang, yaitu memilih tipe variabel berupa kontrak, bukan kelas implementasinya. Praktik ini dikenal sebagai program to an interface, not an implementation, yakni kode pemakai hanya bergantung pada janji yang ditetapkan interface, sedangkan pilihan kelas konkret ditunda sampai saat objek dibuat. Topik ini mempraktikkan kebiasaan tersebut pada variabel, parameter, dan tipe kembalian, sekaligus menandai tempat-tempat yang memang masih menuntut tipe konkret.
Dari ArrayList ke List#
ArrayList adalah kelas koleksi bawaan java.util yang menyimpan daftar objek dengan ukuran yang tumbuh otomatis, sedangkan List adalah interface dalam java.util yang mewakili daftar berurutan, lengkap dengan janji metode add, get, dan size. Keduanya tersedia sejak baris import pertama, dan tipe elemennya ditulis di antara tanda kurung sudut sesuai aturan generics. Program berikut menyimpan catatan belanja dengan deklarasi yang memisahkan keduanya.
import java.util.ArrayList;
import java.util.List;
public class DemoCatatan {
public static void main(String[] args) {
List<String> catatan = new ArrayList<>();
catatan.add("Beli beras");
catatan.add("Bayar tagihan air");
catatan.add("Kembalikan buku perpustakaan");
for (String item : catatan) {
System.out.println("- " + item);
}
System.out.println("Jumlah catatan: " + catatan.size());
}
}- Beli beras
- Bayar tagihan air
- Kembalikan buku perpustakaan
Jumlah catatan: 3Tipe di kiri tanda sama dengan dan tipe di kanannya memegang peran yang berbeda. List<String> menyatakan kontrak yang dipatuhi variabel, yaitu daftar berurutan berisi teks, sedangkan new ArrayList<>() memilih wujud nyatanya saat program berjalan. Seluruh kode berikutnya hanya melihat wajah List, sehingga keputusan tentang wujud itu terkunci di satu baris.
Pergantian implementasi di satu baris#
Manfaat kebiasaan ini muncul ketika kebutuhan berubah. Menulis ulang deklarasi menjadi List<String> catatan = new LinkedList<>() cukup menyentuh satu baris, karena LinkedList adalah kelas koleksi lain yang juga menepati kontrak List, sementara sisa program memanggil metode yang dijanjikan List, bukan metode milik ArrayList saja.
// tidak-dijalankan
List<String> catatan = new ArrayList<>(); // versi semula
List<String> catatan = new LinkedList<>(); // versi pengganti, baris lain tetapKebiasaan yang sama wajib diterapkan pada parameter metode, karena parameter bertipe konkret memaksa setiap pemanggil menggunakan kelas itu. Metode jumlahPendek pada program berikut menerima List<String> apa pun, sehingga daftar dari sumber mana pun dapat diproses tanpa penyesuaian.
import java.util.ArrayList;
import java.util.List;
public class RekapKata {
public static void main(String[] args) {
List<String> kata = new ArrayList<>();
kata.add("api");
kata.add("ombak");
kata.add("air");
kata.add("langit");
System.out.println("Kata berukuran 4 huruf atau kurang: " + jumlahPendek(kata, 4));
}
static int jumlahPendek(List<String> daftar, int batas) {
int jumlah = 0;
for (String item : daftar) {
if (item.length() <= batas) {
jumlah = jumlah + 1;
}
}
return jumlah;
}
}Kata berukuran 4 huruf atau kurang: 2Diagram berikut merangkum hubungan yang membuat pergantian di atas bekerja, yaitu List sebagai kontrak dan kelas-kelas yang menepatinya sebagai pilihan yang dapat dipertukarkan. Kebiasaan ini berlaku juga pada tipe kembalian metode, karena metode yang mengembalikan List<String> bebas mengganti isi return-nya antarversi, sedangkan yang mengembalikan ArrayList<String> mengikat setiap pemanggilnya pada kelas itu untuk selamanya. Tipe kembalian yang longgar memberi kebebasan yang sama kepada metode itu sendiri saat implementasinya berganti.
Pola yang sama berlaku pada kontrak-kontrak koleksi lain di pustaka standar, dan tabel berikut menunjukkan padanan yang paling sering dipakai.
| Kontrak | Implementasi yang lazim dipakai |
|---|---|
List | ArrayList |
Set | HashSet |
Map | HashMap |
Deque | ArrayDeque |
List menjanjikan urutan, Set menjanjikan keunikan elemen, Map menjanjikan pasangan kunci-nilai, dan Deque menjanjikan antrean yang bisa diisi dan dikosongkan dari dua ujung. Kontrak-kontrak itu dipakai di banyak tempat pada jalur ini, dan di sini cukup dipatok kebiasaannya, yaitu kontrak di kiri, konkret di kanan.
Kapan tipe konkret masih diperlukan#
Kebiasaan ini bukan larangan mutlak, karena sebagian kemampuan memang hanya dimiliki kelas konkret. Metode ensureCapacity dan trimToSize hanya ada di ArrayList, sehingga kode yang benar-benar membutuhkannya boleh menampung objeknya dalam variabel ArrayList. Alasan kedua adalah tanda tangan pustaka yang menuntut tipe konkret, misalnya konstruktor yang menerima ArrayList sebagai salinan awal, dan dalam kasus seperti itu aturan pustakanya yang diikuti. Yang perlu dihindari adalah menulis tipe konkret tanpa alasan, karena setiap keputusan semacam itu mempersempit kode pemakainya tanpa membawa kemampuan baru.
Kata kunci var memberi catatan tersendiri, karena tipe variabelnya diturunkan dari sisi kanan penugasan, sehingga keuntungan tipe kontraknya hilang diam-diam.
// tidak-dijalankan
var catatan = new ArrayList<String>(); // tipe variabel: ArrayList<String>
List<String> catatanLain = new ArrayList<>(); // tipe variabel: List<String>Bila pemisahan kontrak dan implementasi memang dikehendaki, tuliskan tipe interfacenya secara eksplisit, dan simpan var untuk baris yang tipenya sudah jelas dari konteks.
Kesalahan umum#
- Mendeklarasikan variabel dengan tipe konkret tanpa alasan. Penulisan
ArrayList<String> catatan = new ArrayList<>()mengunci seluruh kode pemakai pada kelas itu. Deklarasikan variabel dan parameter denganList, lalu pilih kelasnya hanya saatnew. - Memanggil metode yang hanya dimiliki kelas konkret lewat variabel interface. Baris
catatan.trimToSize()ditolak kompiler karenaListtidak menjanjikannya. Bila metode itu sungguh dibutuhkan, ubah tipe variabelnya menjadiArrayListdengan sadar. - Mengubah parameter metode menjadi tipe konkret demi kenyamanan. Parameter bertipe
ArrayListmenolak daftar dari implementasi lain, padahal pemanggil biasanya hanya punyaList. Turunkan tipe parameter ke kontrak yang paling longgar yang cukup untuk logikanya. - Mengira tipe interface memperlambat program. Pemanggilan metode lewat tipe interface mengikuti dynamic dispatch yang juga terjadi pada pemanggilan kelas biasa, sehingga perbedaannya tidak terasa pada praktiknya. Biaya kebiasaan ini jauh lebih kecil daripada manfaat fleksibilitasnya.
Ringkasan#
- Deklarasi yang sehat memisahkan kontrak dan wujudnya, yaitu
List<String> catatan = new ArrayList<>(), sehingga pilihan implementasi terkunci di satu baris. - Variabel, parameter, dan tipe kembalian sebaiknya bertipe interface, karena kode pemakai hanya bergantung pada janji metodenya.
- Tipe konkret tetap sah bila kemampuannya memang hanya ada di kelas tersebut atau bila pustaka yang dipakai menuntutnya, tetapi jangan menjadi kebiasaan.
varmenurunkan tipe dari sisi kanan penugasan, sehingga barisvarkehilangan pemisahan kontrak dan implementasi.
Referensi#
- The Java Tutorials: The List Interface — Oracle
— Janji-janji kontrak
Listdan perannya di Collections Framework. - List (Java SE 21 & JDK 21) — Oracle
— Dokumentasi metode yang dijanjikan kontrak
List. - ArrayList (Java SE 21 & JDK 21) — Oracle
— Metode yang hanya dimiliki kelas konkretnya, seperti
ensureCapacitydantrimToSize.