Tampilan
Kenapa penting
Di aplikasi backend, banyak bagian yang bisa diganti: notifikasi lewat email hari ini, besok lewat WhatsApp; pembayaran lewat satu payment gateway, lalu pindah ke gateway lain; penyimpanan file di disk lokal saat development, di cloud storage saat production. Kode yang memakai bagian-bagian itu sebaiknya hanya tahu apa yang bisa dilakukan, bukan bagaimana caranya. Interface dan abstract class adalah alat Java untuk memisahkan "apa" dari "bagaimana". Spring Boot dibangun di atas ide ini — hampir setiap komponen yang kamu suntikkan (Repository, Service, Client) dipakai lewat interface.
Konsep inti
Interface sebagai kontrak
Interface berisi daftar method yang wajib disediakan oleh class yang mengimplementasikannya. Method tanpa badan di interface disebut method abstrak dan otomatis public. Class menandatangani kontrak itu dengan kata kunci implements, dan boleh mengimplementasikan lebih dari satu interface.
java
public class NotifierDemo {
public static void main(String[] args) {
Notifier email = new EmailNotifier();
Notifier sms = new SmsNotifier();
email.send("budi@contoh.com", "Pesanan INV-001 dikirim");
sms.send("081234567890", "Kode OTP: 482913");
EmailNotifier concrete = new EmailNotifier();
System.out.println("Audit: " + concrete.auditLabel());
}
}
interface Notifier {
void send(String recipient, String message); // method abstrak: tanpa badan
}
interface Auditable {
String auditLabel();
}
class EmailNotifier implements Notifier, Auditable {
@Override
public void send(String recipient, String message) {
System.out.println("EMAIL ke " + recipient + ": " + message);
}
@Override
public String auditLabel() {
return "notifikasi-email";
}
}
class SmsNotifier implements Notifier {
@Override
public void send(String recipient, String message) {
System.out.println("SMS ke " + recipient + ": " + message);
}
}Output:
text
EMAIL ke budi@contoh.com: Pesanan INV-001 dikirim
SMS ke 081234567890: Kode OTP: 482913
Audit: notifikasi-emailPerhatikan bahwa variabel email bertipe Notifier, bukan EmailNotifier. Kode yang memakai variabel itu hanya bisa memanggil method yang ada di kontrak Notifier — dan memang hanya itu yang perlu ia tahu. Interface sendiri tidak bisa dibuat object-nya (new Notifier() ditolak compiler), tidak punya constructor, dan tidak punya field instance.
Implementasi method interface wajib public. Bila kamu lupa menulisnya, compiler menolak:
text
Weaker.java:5: error: send(String,String) in PushNotifier cannot implement send(String,String) in Notifier
void send(String recipient, String message) {
^
attempting to assign weaker access privileges; was publicdefault dan static method
Sejak Java 8, interface boleh berisi method yang sudah punya badan:
defaultmethod — implementasi bawaan yang diwarisi semua class pengimplementasi, dan boleh ditimpa.staticmethod — milik interface itu sendiri, dipanggil lewat nama interface (PriceFormatter.rupiah()), sering dipakai sebagai factory atau helper.
java
public class DefaultStatic {
public static void main(String[] args) {
PriceFormatter rupiah = PriceFormatter.rupiah();
System.out.println(rupiah.format(125_000));
System.out.println(rupiah.formatWithLabel("Total", 125_000));
PriceFormatter usd = new UsdFormatter();
System.out.println(usd.format(49));
System.out.println(usd.formatWithLabel("Total", 49));
}
}
interface PriceFormatter {
String symbol(); // abstrak: wajib diisi implementasi
default String format(long amount) { // default: implementasi bawaan
return symbol() + amount;
}
default String formatWithLabel(String label, long amount) {
return label + ": " + format(amount);
}
static PriceFormatter rupiah() { // static: dipanggil lewat nama interface
return new RupiahFormatter();
}
}
class RupiahFormatter implements PriceFormatter {
@Override
public String symbol() {
return "Rp";
}
}
class UsdFormatter implements PriceFormatter {
@Override
public String symbol() {
return "$";
}
@Override
public String format(long amount) { // boleh menimpa default
return symbol() + amount + ".00";
}
}Output:
text
Rp125000
Total: Rp125000
$49.00
Total: $49.00Baris terakhir menarik: formatWithLabel adalah default method, tetapi di dalamnya ia memanggil format(...) — dan yang terpanggil adalah versi milik UsdFormatter. Inilah awal dari polymorphism yang dibahas di bab berikutnya.
Default method dibuat terutama agar interface bisa berkembang tanpa merusak semua implementasi lama. Risikonya: bila sebuah class mengimplementasikan dua interface yang punya default method dengan signature sama, compiler menolak:
text
Conflict.java:7: error: types Logger and Tracer are incompatible;
class RequestHandler implements Logger, Tracer {
^
class RequestHandler inherits unrelated defaults for prefix() from types Logger and TracerSolusinya, class menimpa method itu dan memilih secara eksplisit dengan NamaInterface.super.method():
java
public class Resolved {
public static void main(String[] args) {
System.out.println(new RequestHandler().prefix());
}
}
interface Logger {
default String prefix() {
return "[LOG]";
}
}
interface Tracer {
default String prefix() {
return "[TRACE]";
}
}
class RequestHandler implements Logger, Tracer {
@Override
public String prefix() {
return Logger.super.prefix() + Tracer.super.prefix(); // pilih/gabungkan secara eksplisit
}
}Output: [LOG][TRACE]. Jangan memakai default method untuk menaruh logika bisnis besar di interface — interface tetap paling baik sebagai kontrak yang ramping.
Konstanta di interface: sebaiknya dihindari
Field di interface otomatis public static final. Dulu orang membuat "interface konstanta" berisi puluhan nilai, lalu class meng-implements interface itu hanya untuk memakai konstantanya. Pola ini dianggap buruk: class jadi "mengaku" memenuhi kontrak yang sebenarnya bukan kontrak. Taruh konstanta di class tempat ia dipakai (bab Static & Final), atau gunakan enum (bab Record & Enum).
Abstract class: state dan alur bersama
Abstract class adalah class yang tidak bisa dibuat object-nya dan boleh berisi method abstrak. Bedanya dengan interface: abstract class boleh punya field instance, constructor, dan method protected. Cocok saat beberapa subclass berbagi data dan langkah kerja yang sama.
Pola yang sering memakainya adalah template method: abstract class menulis urutan langkah dalam satu method final, sementara detail tiap langkah diisi subclass.
java
public class ExporterDemo {
public static void main(String[] args) {
String[] rows = {"INV-001,125000", "INV-002,98000"};
ReportExporter csv = new CsvExporter();
ReportExporter markdown = new MarkdownExporter();
System.out.print(csv.export(rows));
System.out.print(markdown.export(rows));
System.out.println(csv.exportedCount() + " " + markdown.exportedCount());
}
}
abstract class ReportExporter {
private int exportedCount; // state bersama untuk semua subclass
// Template method: urutan langkah dikunci di sini, detailnya diisi subclass.
public final String export(String[] rows) {
StringBuilder sb = new StringBuilder();
sb.append(header());
for (String row : rows) {
sb.append(formatRow(row.split(",")));
}
exportedCount = exportedCount + rows.length;
return sb.toString();
}
public int exportedCount() {
return exportedCount;
}
protected abstract String header();
protected abstract String formatRow(String[] cells);
}
class CsvExporter extends ReportExporter {
@Override
protected String header() {
return "invoice,total\n";
}
@Override
protected String formatRow(String[] cells) {
return cells[0] + "," + cells[1] + "\n";
}
}
class MarkdownExporter extends ReportExporter {
@Override
protected String header() {
return "| Invoice | Total |\n| --- | --- |\n";
}
@Override
protected String formatRow(String[] cells) {
return "| " + cells[0] + " | " + cells[1] + " |\n";
}
}Output:
text
invoice,total
INV-001,125000
INV-002,98000
| Invoice | Total |
| --- | --- |
| INV-001 | 125000 |
| INV-002 | 98000 |
2 2Karena export ditandai final, subclass tidak bisa mengacaukan urutannya — ia hanya mengisi header dan formatRow. Mencoba new ReportExporter() langsung menghasilkan error ReportExporter is abstract; cannot be instantiated.
Interface atau abstract class?
| Aspek | Interface | Abstract class |
|---|---|---|
| Jumlah yang boleh "diwarisi" | banyak (implements A, B) | satu (extends) |
| Field instance & constructor | tidak | boleh |
| Method berisi | default, static, private | method biasa apa saja |
| Akses method abstrak | otomatis public | bebas (public, protected, ...) |
| Cocok untuk | kontrak/kemampuan ("bisa mengirim notifikasi") | basis bersama berisi state & alur ("kurir per kg") |
Aturan praktis: mulai dengan interface. Tambahkan abstract class hanya bila beberapa implementasi benar-benar berbagi state atau langkah kerja — dan biarkan abstract class itu tetap mengimplementasikan interface, sehingga kode lain tetap bergantung pada kontraknya.
Sekilas functional interface
Interface yang punya tepat satu method abstrak disebut functional interface dan boleh ditandai @FunctionalInterface. Contohnya Notifier di atas. Interface seperti ini bisa diisi dengan lambda — cara ringkas menulis implementasi tanpa membuat class — yang akan kamu pelajari di M04.
Contoh di dunia backend
Layanan checkout perlu menghitung ongkos kirim dari beberapa penyedia. Kontraknya ditulis sebagai interface; dua kurir yang tarifnya "biaya dasar + per kg" berbagi implementasi lewat abstract class; layanan ambil sendiri cukup mengimplementasikan interface langsung.
java
public class ShippingDemo {
public static void main(String[] args) {
ShippingProvider[] providers = {new RegularCourier(), new SameDayCourier(), new PickupPoint()};
int weightGrams = 2_500;
for (ShippingProvider provider : providers) {
if (provider.supports(weightGrams)) {
System.out.println(provider.code() + " -> Rp" + provider.cost(weightGrams));
} else {
System.out.println(provider.code() + " -> tidak tersedia");
}
}
}
}
/** Kontrak: apa pun penyedianya, layanan checkout cukup tahu tiga method ini. */
interface ShippingProvider {
String code();
long cost(int weightGrams);
default boolean supports(int weightGrams) {
return weightGrams > 0 && weightGrams <= 30_000;
}
}
/** Implementasi bersama untuk kurir yang tarifnya "biaya dasar + per kg". */
abstract class PerKgCourier implements ShippingProvider {
private final long baseFee;
private final long feePerKg;
protected PerKgCourier(long baseFee, long feePerKg) {
this.baseFee = baseFee;
this.feePerKg = feePerKg;
}
@Override
public long cost(int weightGrams) {
int billableKg = (weightGrams + 999) / 1000; // pembulatan ke atas
return baseFee + billableKg * feePerKg;
}
}
class RegularCourier extends PerKgCourier {
RegularCourier() {
super(5_000, 9_000);
}
@Override
public String code() {
return "REGULER";
}
}
class SameDayCourier extends PerKgCourier {
SameDayCourier() {
super(15_000, 12_000);
}
@Override
public String code() {
return "SAME_DAY";
}
@Override
public boolean supports(int weightGrams) {
return weightGrams > 0 && weightGrams <= 5_000; // motor: maksimal 5 kg
}
}
class PickupPoint implements ShippingProvider {
@Override
public String code() {
return "AMBIL_SENDIRI";
}
@Override
public long cost(int weightGrams) {
return 0;
}
}Output untuk paket 2,5 kg:
text
REGULER -> Rp32000
SAME_DAY -> Rp51000
AMBIL_SENDIRI -> Rp0Untuk paket 7,2 kg, SAME_DAY menampilkan tidak tersedia karena ia menimpa default supports. Menambah kurir baru cukup dengan satu class baru — loop di main (yang di aplikasi nyata ada di service checkout) tidak perlu diubah.
Pertanyaan interview
1. Apa beda interface dan abstract class? Kapan memakai masing-masing?
Interface adalah kontrak tanpa state; satu class boleh mengimplementasikan banyak interface. Abstract class boleh punya field, constructor, dan method biasa, tetapi class hanya boleh meng-extends satu. Pakai interface untuk mendefinisikan kemampuan atau batas antar-komponen; pakai abstract class untuk berbagi state dan alur di antara implementasi yang mirip — idealnya abstract class itu sendiri mengimplementasikan interface.
2. Kenapa Java menambahkan default method, dan apa risikonya?
Agar interface yang sudah banyak dipakai bisa ditambah method baru tanpa merusak semua implementasinya (contoh terkenal: method baru di interface collection Java 8). Risikonya: konflik bila dua interface punya default method yang sama — class wajib menimpanya dan memilih dengan A.super.method() — serta godaan menaruh logika bisnis di interface.
3. Apa itu template method pattern?
Pola di mana abstract class menulis urutan langkah sebuah proses dalam method (biasanya final), sementara langkah yang berbeda-beda diserahkan ke subclass lewat method abstrak. Contohnya exporter laporan: urutan header lalu baris sama, format CSV atau Markdown berbeda.
Jebakan umum
- Lupa
publicsaat mengimplementasikan method interface →attempting to assign weaker access privileges. - Interface "raksasa" berisi belasan method yang tidak semuanya relevan bagi setiap implementasi. Pecah menjadi beberapa interface kecil yang fokus.
- Menambah method abstrak ke interface yang sudah dipakai luas membuat semua implementasi gagal dikompilasi. Rencanakan perubahan kontrak, atau beri default method sebagai masa transisi.
- Interface konstanta — meng-
implementsinterface hanya demi konstantanya. Taruh konstanta di class atauenum. - Abstract class untuk sekadar berbagi satu-dua method helper. Pewarisan mengikat subclass selamanya; method helper biasa (atau komposisi) sering lebih sederhana.
Ringkasan
- Interface adalah kontrak: method abstraknya otomatis
publicdan wajib diisi class yang meng-implements. - Satu class boleh mengimplementasikan banyak interface, tetapi hanya meng-
extendssatu class. defaultmethod memberi implementasi bawaan yang boleh ditimpa;staticmethod dipanggil lewat nama interface.- Abstract class boleh punya field, constructor, dan method konkret; cocok untuk template method.
- Mulai dari interface; tambahkan abstract class hanya bila ada state atau alur yang benar-benar dibagi.