Pemrograman ke Interface

Membiasakan tipe List untuk variabel ArrayList, kebiasaan kecil yang memisahkan kode profesional.

Daftar isi

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.

java DemoCatatan.java
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());
    }
}
text Keluaran
- Beli beras
- Bayar tagihan air
- Kembalikan buku perpustakaan
Jumlah catatan: 3

Tipe 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.

java Contoh: pergantian implementasi cukup di satu baris
// tidak-dijalankan
List<String> catatan = new ArrayList<>();   // versi semula
List<String> catatan = new LinkedList<>();  // versi pengganti, baris lain tetap

Kebiasaan 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.

java RekapKata.java
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;
    }
}
text Keluaran
Kata berukuran 4 huruf atau kurang: 2

Diagram 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.

Kontrak List diikuti kelas ArrayList dan LinkedListinterface List<E>add(), get(), size()class ArrayList<E>elemen dalam arrayclass LinkedList<E>elemen dalam simpul berantaiimplementsimplements
Variabel bertipe List boleh menampung objek ArrayList maupun LinkedList

Pola yang sama berlaku pada kontrak-kontrak koleksi lain di pustaka standar, dan tabel berikut menunjukkan padanan yang paling sering dipakai.

KontrakImplementasi yang lazim dipakai
ListArrayList
SetHashSet
MapHashMap
DequeArrayDeque

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.

java Contoh: var menurunkan tipe konkret
// 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 dengan List, lalu pilih kelasnya hanya saat new.
  • Memanggil metode yang hanya dimiliki kelas konkret lewat variabel interface. Baris catatan.trimToSize() ditolak kompiler karena List tidak menjanjikannya. Bila metode itu sungguh dibutuhkan, ubah tipe variabelnya menjadi ArrayList dengan sadar.
  • Mengubah parameter metode menjadi tipe konkret demi kenyamanan. Parameter bertipe ArrayList menolak daftar dari implementasi lain, padahal pemanggil biasanya hanya punya List. 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.
  • var menurunkan tipe dari sisi kanan penugasan, sehingga baris var kehilangan pemisahan kontrak dan implementasi.

Referensi#