Pada Swing kita menulis label.setText(...) setiap kali data berubah — dan wajib mengingat setiap tempat yang perlu diperbarui. JavaFX menggantikan kewajiban itu dengan property: nilai bermekanisme pengamatan (observable) yang bisa diikuti listener dan diikat (bind) ke property lain. Begitu totalProperty diikat ke hargaProperty dikali jumlahProperty, label total memperbarui dirinya sendiri setiap kali harga atau jumlah berubah — tanpa satu baris kode penyinkron manual. Chapter ini membedah tiga lapis sistem tersebut: property sebagai nilai teramati, dua jenis listener dengan semantik berbeda, dan binding satu arah maupun dua arah — lalu meneguhkan pemahaman lewat implementasi mini yang berjalan di JDK telanjang.
Properti sebagai Nilai yang Teramati #
Di JavaFX, Label tidak menyimpan String polos melainkan StringProperty yang diambil lewat textProperty(). Property menyimpan nilai sekaligus daftar pendengar yang ingin tahu saat nilai berganti. Bentuk umumnya:
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleStringProperty;
public class PropertiNilai {
public static void tampilkan() {
SimpleIntegerProperty harga = new SimpleIntegerProperty(5000);
SimpleIntegerProperty jumlah = new SimpleIntegerProperty(3);
SimpleStringProperty label = new SimpleStringProperty("Total: 15000");
harga.addListener((obs, lama, baru) ->
label.set("Total: " + harga.get() * jumlah.get()));
}
}Kelas-kelas Simple<Type>Property ini adalah pembungkus nilai dengan mesin notifikasi; untuk tipe primitif tersedia IntegerProperty, DoubleProperty, BooleanProperty, dan untuk objek StringProperty atau ObjectProperty<T>. Hampir setiap field UI JavaFX diekspos sebagai property — textProperty() pada Label, valueProperty() pada Slider — sehingga pola di atas berlaku seragam di seluruh toolkit.
Invalidation Listener versus Change Listener #
JavaFX menyediakan dua jenis pendengar yang sering tertukar pemula. ChangeListener menerima nilai lama dan nilai baru setiap kali nilai benar-benar berbeda. InvalidationListener hanya menerima pemberitahuan “nilai ini sudah usang” — tanpa nilai baru — dan dirancang untuk pola malas (lazy): pendengar menandai sesuatu perlu dihitung ulang, dan perhitungan baru terjadi jika nilainya memang nanti dibaca.
| Aspek | ChangeListener | InvalidationListener |
|---|---|---|
| Callback | (obs, lama, baru) | (obs) |
| Data yang dibawa | nilai lama + baru | tidak ada |
| Waktu notifikasi | saat set dengan nilai berbeda | saat set dengan nilai berbeda |
| Gaya pemakaian | bereaksi pada tiap perubahan | tunda pekerjaan sampai nilai dibaca |
| Cocok untuk | log, validasi per nilai | caching, binding ekspresi |
Satu perilaku penting yang keduanya bagi: set dengan nilai yang sama nilainya tidak memicu apa pun. Bandingkan dengan setter biasa yang selalu menjalankan tubuh method — property menghemat pekerjaan dengan membandingkan dahulu.
Bindings Satu Arah dan Dua Arah #
Binding adalah pernyataan hubungan antarproperty. Satu arah (a.bind(b)): a selalu mengikuti b; menulis a.set(...) langsung melempar RuntimeException karena a bukan lagi pemilik nilainya. Dua arah (a.bindBidirectional(b)): keduanya saling mengikuti dan keduanya boleh ditulis. Untuk aritmetika, pabrik Bindings menghasilkan binding ekspresi yang menghitung ulang otomatis:
import javafx.beans.binding.Bindings;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.property.SimpleStringProperty;
public class IkatTotal {
public static void tampilkan() {
IntegerProperty harga = new SimpleIntegerProperty(5000);
IntegerProperty jumlah = new SimpleIntegerProperty(3);
IntegerProperty total = new SimpleIntegerProperty();
total.bind(harga.multiply(jumlah));
StringProperty label = new SimpleStringProperty();
label.bind(Bindings.format("Total: %d", total));
harga.set(6000); // label otomatis menjadi "Total: 18000"
jumlah.set(4); // label otomatis menjadi "Total: 24000"
}
}Method harga.multiply(jumlah) membangun objek NumberBinding yang mendengarkan kedua property sumber; Bindings.format(...) mengubah nilai menjadi teks dengan java.util.Formatter — pola format sama dengan String.format. Rantai harga → total → label inilah yang membuat UI JavaFX reaktif: satu set bergema sampai piksel terakhir tanpa kode antara.
Semantik Properti di Tangan Sendiri #
Mesin di balik property bisa kita tiru dalam kelas Java biasa, dan hasil tiruannya menguji semua klaim di atas. Program berikut membangun Properti<T> dengan dua daftar pendengar, mengikat total satu arah ke harga dan jumlah, memasang ikatan dua arah antara milimeter dan sentimeter, lalu mencatat seluruh kejadian yang terjadi:
import java.util.ArrayList;
import java.util.List;
/** Properti teramati mini: meniru semantik JavaFX di kelas Java biasa. */
class Properti<T> {
private T nilai;
private final List<Runnable> pendengarInvalidasi = new ArrayList<>();
private final List<ListenerPerubahan<T>> pendengarPerubahan = new ArrayList<>();
interface ListenerPerubahan<T> {
void saatBerubah(T lama, T baru);
}
Properti(T awal) {
this.nilai = awal;
}
T get() {
return nilai;
}
void set(T baru) {
if (baru.equals(nilai)) {
return; // nilai sama: tidak ada kejadian apa pun
}
T lama = nilai;
nilai = baru;
pendengarInvalidasi.forEach(Runnable::run);
pendengarPerubahan.forEach(p -> p.saatBerubah(lama, baru));
}
void saatInvalid(Runnable r) {
pendengarInvalidasi.add(r);
}
void saatBerubah(ListenerPerubahan<T> l) {
pendengarPerubahan.add(l);
}
}
public class PropertiMini {
public static void main(String[] args) {
Properti<Integer> harga = new Properti<>(5000);
Properti<Integer> jumlah = new Properti<>(3);
harga.saatInvalid(() -> System.out.println("[invalidasi] harga ditandai usang"));
harga.saatBerubah((lama, baru) ->
System.out.println("[perubahan ] harga " + lama + " -> " + baru));
// Ikat satu arah: total selalu harga x jumlah.
Runnable hitungTotal = () -> System.out.println(
"[binding ] total kini = " + (harga.get() * jumlah.get()));
harga.saatBerubah((l, b) -> hitungTotal.run());
jumlah.saatBerubah((l, b) -> hitungTotal.run());
System.out.println("total awal = " + (harga.get() * jumlah.get()));
harga.set(6000); // invalidasi + perubahan + hitung ulang
jumlah.set(4); // perubahan jumlah -> hitung ulang
harga.set(6000); // nilai sama: senyap sepenuhnya
// Ikat dua arah: milimeter dan sentimeter saling mengikuti.
Properti<Double> mm = new Properti<>(2500.0);
Properti<Double> cm = new Properti<>(250.0);
mm.saatBerubah((l, b) -> {
double target = b / 10;
if (cm.get() != target) {
cm.set(target);
}
});
cm.saatBerubah((l, b) -> {
double target = b * 10;
if (mm.get() != target) {
mm.set(target);
}
});
mm.set(3200.0);
System.out.println("setelah mm=3200 : cm = " + cm.get());
cm.set(41.0);
System.out.println("setelah cm=41 : mm = " + mm.get());
}
}Output yang Dihasilkan:
total awal = 15000
[invalidasi] harga ditandai usang
[perubahan ] harga 5000 -> 6000
[binding ] total kini = 18000
[binding ] total kini = 24000
setelah mm=3200 : cm = 320.0
setelah cm=41 : mm = 410.0Mari kita bedah kelakuan yang tercetak:
- Baris 24-28 (
setdengan pembandingan):set(6000)kedua menghasilkan nol baris keluaran karenabaru.equals(nilai)menghentikan seluruh proses — persis semantik property JavaFX yang menghemat notifikasi untuk nilai kembar. - Baris 31-32 (urutan dua daftar): dalam implementasi mini kita, pendengar invalidasi dipanggil lebih dulu, disusul pendengar perubahan. Keluaran
harga.set(6000)memperlihatkan urutan itu:[invalidasi],[perubahan], lalu[binding]. - Baris 53-54 (binding satu arah):
hitungTotaldidaftarkan sebagai pendengar perubahan pada dua property sumber. Saat harga menjadi 6000, binding menghitung 6000×3 = 18000 denganjumlahyang masih lama; saat jumlah menjadi 4, binding yang sama menghitung 6000×4 = 24000. Satu pendengar yang membaca ulang kedua sumber adalah bentuk paling sederhana dari binding ekspresi. - Baris 66-80 (binding dua arah): tiap pendengar menulis balik ke pasangannya dengan hasil konversi, dan perbandingan
cm.get() != targetmenjadi penjaga agar penulisan balik berhenti — tanpa penjaga itu, dua listener akan saling memicu tanpa henti. Keluaransetelah mm=3200 : cm = 320.0dansetelah cm=41 : mm = 410.0membuktikan arah kedua-duanya hidup.
Kapan Memakai Property di Luar UI #
Property dan binding tidak terikat UI — javafx.beans berdiri sendiri sebagai kerangka reaksi. Model domain yang ditampilkan di banyak tempat (formulir, tabel, grafik) paling diuntungkan: satu ObjectProperty<StatusPesanan> yang diikat ke tiga label dan satu warna tombol berarti satu set menyegarkan semuanya. Batas praktisnya adalah ketergantungan: aplikasi konsol murni tanpa JavaFX biasanya memilih Consumer/listener ringan seperti bus kejadian pada Chapter Event-Driven Programming, karena menarik modul javafx.base hanya demi property jarang sepadan.
Dengan data yang bereaksi, tersisa satu hambatan klasik antarmuka: pekerjaan berat di thread UI. Chapter berikutnya membahas Task dan Service — cara JavaFX (dan SwingWorker di Swing) menjaga jendela tetap responsif sambil bekerja di latar.
Referensi #
- OpenJFX: Javadoc paket javafx.beans.property
- OpenJFX: Javadoc ObservableValue
- OpenJFX: Javadoc Bindings
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
PropertiMini.java
Unduh berkasimport java.util.ArrayList;
import java.util.List;
/** Properti teramati mini: meniru semantik JavaFX di kelas Java biasa. */
class Properti<T> {
private T nilai;
private final List<Runnable> pendengarInvalidasi = new ArrayList<>();
private final List<ListenerPerubahan<T>> pendengarPerubahan = new ArrayList<>();
interface ListenerPerubahan<T> {
void saatBerubah(T lama, T baru);
}
Properti(T awal) {
this.nilai = awal;
}
T get() {
return nilai;
}
void set(T baru) {
if (baru.equals(nilai)) {
return; // nilai sama: tidak ada kejadian apa pun
}
T lama = nilai;
nilai = baru;
pendengarInvalidasi.forEach(Runnable::run);
pendengarPerubahan.forEach(p -> p.saatBerubah(lama, baru));
}
void saatInvalid(Runnable r) {
pendengarInvalidasi.add(r);
}
void saatBerubah(ListenerPerubahan<T> l) {
pendengarPerubahan.add(l);
}
}
public class PropertiMini {
public static void main(String[] args) {
Properti<Integer> harga = new Properti<>(5000);
Properti<Integer> jumlah = new Properti<>(3);
harga.saatInvalid(() -> System.out.println("[invalidasi] harga ditandai usang"));
harga.saatBerubah((lama, baru) ->
System.out.println("[perubahan ] harga " + lama + " -> " + baru));
// Ikat satu arah: total selalu harga x jumlah.
Runnable hitungTotal = () -> System.out.println(
"[binding ] total kini = " + (harga.get() * jumlah.get()));
harga.saatBerubah((l, b) -> hitungTotal.run());
jumlah.saatBerubah((l, b) -> hitungTotal.run());
System.out.println("total awal = " + (harga.get() * jumlah.get()));
harga.set(6000); // invalidasi + perubahan + hitung ulang
jumlah.set(4); // perubahan jumlah -> hitung ulang
harga.set(6000); // nilai sama: senyap sepenuhnya
// Ikat dua arah: milimeter dan sentimeter saling mengikuti.
Properti<Double> mm = new Properti<>(2500.0);
Properti<Double> cm = new Properti<>(250.0);
mm.saatBerubah((l, b) -> {
double target = b / 10;
if (cm.get() != target) {
cm.set(target);
}
});
cm.saatBerubah((l, b) -> {
double target = b * 10;
if (mm.get() != target) {
mm.set(target);
}
});
mm.set(3200.0);
System.out.println("setelah mm=3200 : cm = " + cm.get());
cm.set(41.0);
System.out.println("setelah cm=41 : mm = " + mm.get());
}
}