Program konsol yang kita tulis di Section awal berjalan dari atas ke bawah: baca input, hitung, cetak, selesai. Aplikasi desktop menolak pola itu — pengguna bisa mengklik tombol Simpan, lalu menggeser slider, lalu mengetik di kolom nama, dalam urutan yang tidak bisa kita tebak. Swing menjawabnya dengan pemrograman berbasis kejadian (event-driven programming): kita tidak menunggu input, melainkan mendaftarkan listener — objek berisi kode yang dijalankan hanya ketika kejadian tertentu tiba. Chapter ini membahas anatomi listener di Swing, bentuk lambda yang mempersingkat penulisannya, kelas adapter untuk listener bermethod banyak, dan menutup dengan bus kejadian buatan sendiri agar mekanisme di balik layar tidak lagi terasa seperti kotak gelap.
Dari Program Berurut ke Antrean Kejadian #
Ketika setVisible(true) membuka jendela, method main kita boleh selesai — namun aplikasi tetap hidup. Yang menjaganya adalah loop di dalam EDT: mengambil satu kejadian dari antrean kejadian (event queue), mengirimkannya ke listener yang bersangkutan, mengambil kejadian berikutnya, dan seterusnya. Setiap klik mouse, tekan tombol keyboard, atau perubahan isi JTextField diubah toolkit menjadi objek kejadian (ActionEvent, MouseEvent, DocumentEvent) lalu didorong ke antrean tersebut. Kode listener kita hanyalah penumpang yang menunggu giliran dipanggil — dan karena seluruh kejadian diserahkan EDT satu per satu, dua listener tidak pernah berjalan bersamaan di dalam aplikasi Swing yang sehat.
ActionListener dan Lambda #
ActionListener adalah listener yang paling sering kita pasang — milik tombol, item menu, dan menekan Enter pada kolom teks. Interfacenya hanya berisi satu method actionPerformed(ActionEvent e), sehingga sempurna untuk lambda. Berikut tampilan aplikasi penghitung sederhana yang lengkap:
import java.awt.BorderLayout;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.SwingUtilities;
public class PenghitungKlik {
private int jumlah = 0;
private void bangunUi() {
JFrame jendela = new JFrame("Penghitung Klik");
jendela.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
JLabel label = new JLabel("Belum ada klik", JLabel.CENTER);
JButton tombol = new JButton("Klik aku");
tombol.addActionListener(e -> {
jumlah++;
label.setText("Klik ke-" + jumlah);
});
jendela.add(label, BorderLayout.CENTER);
jendela.add(tombol, BorderLayout.SOUTH);
jendela.setSize(300, 150);
jendela.setVisible(true);
}
public static void main(String[] args) {
SwingUtilities.invokeLater(() -> new PenghitungKlik().bangunUi());
}
}Tiga keputusan desain pada kode tersebut layak dicermati. Pertama, variabel jumlah dideklarasikan sebagai field, bukan variabel lokal — lambda hanya boleh menangkap variabel lokal yang effectively final, jadi penghitung yang berubah perlu hidup di field. Kedua, lambda (e -> ...) adalah bentuk ringkas dari kelas anonim new ActionListener() { public void actionPerformed(...) {...} } dengan compiler yang membuat implementasinya otomatis — semua method pada ActionListener diimplementasi oleh lambda itu. Ketiga, listener mengubah label lewat setText, dan pengecatan ulang terjadi otomatis pada putaran EDT berikutnya; kita tidak perlu memanggil repaint manual.
Anatomi ActionEvent #
Objek ActionEvent yang diterima listener membawa data kejadian: sumber kejadian, string perintah, dan identitas jenis kejadian. Karena doClick() pada JButton memicu listener tanpa layar, kita bisa membedah isi objek tersebut secara runnable:
import java.awt.event.ActionEvent;
import javax.swing.JButton;
/** Memicu ActionListener lewat doClick tanpa layar nyata. */
public class KlikTombol {
public static void main(String[] args) {
JButton simpan = new JButton("Simpan");
simpan.setActionCommand("SIMPAN_DATA");
simpan.addActionListener((ActionEvent e) -> {
Object sumber = e.getSource();
System.out.println("sumber = " + ((JButton) sumber).getText());
System.out.println("perintah = " + e.getActionCommand());
System.out.println("id kejadian = " + e.getID());
});
simpan.doClick(); // seolah-olah pengguna menekan tombol
}
}Output yang Dihasilkan:
sumber = Simpan
perintah = SIMPAN_DATA
id kejadian = 1001Mari kita bedah tiap baris intinya:
- Baris 8 (
setActionCommand("SIMPAN_DATA")): secara bawaan, action command sama dengan teks tombol. Menyetelnya secara eksplisit memisahkan label tampilan dari perintah logika — teks tombol boleh berganti jadi “Save” tanpa merusak kode yang membandingkan string perintah. - Baris 9-14 (lambda dengan parameter bertipe): tanda kurung
(ActionEvent e)diperlukan ketika kita ingin menuliskan tipe parameter secara eksplisit; isi lambda membaca tiga bagian objek kejadian. - Baris 10-11 (
e.getSource()): mengembalikan objek sumber kejadian — tombol yang diklik. Objek tersebut perlu dikembalikan keJButton(lewat cast pada pencetakan) sebelum methodgetText()bisa dipanggil, karena tipe statis kembaliannyaObject. - Baris 13 (
e.getID()): konstanta identitas jenis kejadian;1001adalahActionEvent.ACTION_PERFORMED. Nilai ini stabil antar eksekusi karena ditetapkan oleh spesifikasi AWT, bukan dihitung dari kondisi runtime.
Adapter Class untuk Listener yang Bermethod Banyak #
ActionListener hanya punya satu method, tetapi listener lain tidak seberuntung itu: MouseListener mendefinisikan lima method (mouseClicked, mousePressed, mouseReleased, mouseEntered, mouseExited) dan WindowListener mendefinisikan tujuh. Memaksa kita mengimplementasikan semuanya padahal hanya satu yang dibutuhkan adalah pemborosan. Paket java.awt.event menyediakan kelas adapter — MouseAdapter, WindowAdapter, KeyAdapter — yang sudah mengimplementasikan semua method dengan tubuh kosong. Cukup perluas kelas adapter lalu timpa method yang dikehendaki:
import javax.swing.JLabel;
import javax.swing.JPanel;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
public class PapanTikus {
public static JPanel bangun() {
JLabel info = new JLabel("Gerakkan mouse ke sini");
JPanel panel = new JPanel();
panel.add(info);
panel.addMouseListener(new MouseAdapter() {
@Override public void mouseEntered(MouseEvent e) {
info.setText("Kursor masuk di " + e.getX() + "," + e.getY());
}
@Override public void mouseExited(MouseEvent e) {
info.setText("Kursor keluar");
}
});
return panel;
}
}Karena lambda hanya cocok untuk interface bermethod tunggal (functional interface), listener banyak method memang harus memakai kelas anonim seperti di atas — atau kelas bernama tersendiri bila logikanya panjang.
Bus Kejadian Buatan Sendiri #
Bagaimana sebenarnya “pendaftaran” dan “pengiriman” bekerja? Kita bisa meniru mekanisme intinya dengan sebuah kelas Bus: daftar listener, method daftar, dan method kirim yang memanggil listener satu per satu. Pola ini juga muncul di banyak aplikasi nyata untuk komunikasi antarbagian yang saling tidak mengenal:
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
/** Kejadian sederhana yang mengalir lewat bus. */
record Kejadian(String jenis, String data) {
}
/** Bus kejadian buatan sendiri: listener dipanggil urut pendaftaran. */
class Bus {
private final List<Consumer<Kejadian>> pendengar = new ArrayList<>();
void daftar(Consumer<Kejadian> listener) {
pendengar.add(listener);
}
void kirim(Kejadian kejadian) {
for (Consumer<Kejadian> p : pendengar) {
p.accept(kejadian);
}
}
}
/** Tombol simulasi yang meneruskan klik ke bus. */
class TombolSimulasi {
private final String nama;
private final Bus bus;
TombolSimulasi(String nama, Bus bus) {
this.nama = nama;
this.bus = bus;
}
void klik() {
bus.kirim(new Kejadian("klik", nama));
}
}
public class BusKejadian {
public static void main(String[] args) {
Bus bus = new Bus();
bus.daftar(k -> System.out.println("[log] " + k.jenis() + " pada " + k.data()));
bus.daftar(k -> {
if (k.jenis().equals("klik")) {
System.out.println("[aksi] layar diperbarui karena " + k.data());
}
});
TombolSimulasi simpan = new TombolSimulasi("Simpan", bus);
TombolSimulasi hapus = new TombolSimulasi("Hapus", bus);
simpan.klik();
hapus.klik();
}
}Output yang Dihasilkan:
[log] klik pada Simpan
[aksi] layar diperbarui karena Simpan
[log] klik pada Hapus
[aksi] layar diperbarui karena HapusStruktur program ini memetakan lurus ke mekanisme Swing. Method daftar adalah padanan addActionListener — keduanya hanya menyimpan referensi listener ke dalam daftar internal. Method kirim adalah padanan momen doClick di dalam Swing: kejadian dibungkus objek (record Kejadian), lalu setiap listener menerima objek yang sama berurutan. Perbedaan pentingnya satu: bus kita menjamin urutan pendaftaran secara eksplisit dalam satu loop for, sehingga dua listener berjalan sesuai urutan yang terbaca pada keluaran.
Antarmuka kini hidup — tombol memicu kode, kode memperbarui label. Namun begitu data yang ditampilkan berupa baris-baris (daftar siswa, tabel transaksi), komponen label tidak lagi memadai. Chapter berikutnya menghadirkan JTable dan kontrak TableModel: struktur data yang menjadi sumber kebenaran bagi tabel di layar.
Referensi #
- Oracle: Menulis Event Listeners
- Oracle: Kelas ActionListener (JDK 25)
- Oracle: Kelas ActionEvent (JDK 25)
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
BusKejadian.java
Unduh berkasimport java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
/** Kejadian sederhana yang mengalir lewat bus. */
record Kejadian(String jenis, String data) {
}
/** Bus kejadian buatan sendiri: listener dipanggil urut pendaftaran. */
class Bus {
private final List<Consumer<Kejadian>> pendengar = new ArrayList<>();
void daftar(Consumer<Kejadian> listener) {
pendengar.add(listener);
}
void kirim(Kejadian kejadian) {
for (Consumer<Kejadian> p : pendengar) {
p.accept(kejadian);
}
}
}
/** Tombol simulasi yang meneruskan klik ke bus. */
class TombolSimulasi {
private final String nama;
private final Bus bus;
TombolSimulasi(String nama, Bus bus) {
this.nama = nama;
this.bus = bus;
}
void klik() {
bus.kirim(new Kejadian("klik", nama));
}
}
public class BusKejadian {
public static void main(String[] args) {
Bus bus = new Bus();
bus.daftar(k -> System.out.println("[log] " + k.jenis() + " pada " + k.data()));
bus.daftar(k -> {
if (k.jenis().equals("klik")) {
System.out.println("[aksi] layar diperbarui karena " + k.data());
}
});
TombolSimulasi simpan = new TombolSimulasi("Simpan", bus);
TombolSimulasi hapus = new TombolSimulasi("Hapus", bus);
simpan.klik();
hapus.klik();
}
}
KlikTombol.java
Unduh berkasimport java.awt.event.ActionEvent;
import javax.swing.JButton;
/** Memicu ActionListener lewat doClick tanpa layar nyata. */
public class KlikTombol {
public static void main(String[] args) {
JButton simpan = new JButton("Simpan");
simpan.setActionCommand("SIMPAN_DATA");
simpan.addActionListener((ActionEvent e) -> {
Object sumber = e.getSource();
System.out.println("sumber = " + ((JButton) sumber).getText());
System.out.println("perintah = " + e.getActionCommand());
System.out.println("id kejadian = " + e.getID());
});
simpan.doClick(); // seolah-olah pengguna menekan tombol
}
}