Bagian ini mengangkat seluruh aturan data keluar dari listener tombol dan menempatkannya pada kelas DaftarKontak: validasi masukan, penomoran id yang tidak pernah dipakai ulang, operasi tambah-ubah-hapus, dan pencarian. Kelas tersebut tidak mengimport satu pun kelas Swing, sehingga seluruh perilakunya bisa diuji sebagai Java biasa. Antarmuka dari Bagian 1 kemudian dikurangi perannya: tabel berpindah dari DefaultTableModel ke model kustom yang membaca DaftarKontak, dan setiap tombol hanya memanggil satu operasi lalu melaporkan hasil pada label status. Prinsipnya kita sebut UI tipis, logika tebal.
Logika Inti dan Validasi #
Record Kontak dari Bagian 1 tidak berubah dan tetap menjadi bagian proyek:
/**
* Satu baris data kontak pada aplikasi Buku Kontak.
* Record hanya menyimpan nilai; aturan bisnis hidup di DaftarKontak.
*/
public record Kontak(int id, String nama, String email, String telepon) {
}Aturan kini berkumpul di DaftarKontak bersama dua exception terperiksa: kegagalan validasi adalah hasil normal dari masukan pengguna, jadi pemanggil wajib memutuskan cara menolaknya, bukan membiarkannya lolos diam-diam:
/**
* Masukan kontak yang melanggar aturan validasi:
* field kosong, format email salah, email dipakai kontak lain,
* atau field memuat karakter pemisah penyimpanan.
*/
public class KontakInvalidException extends Exception {
public KontakInvalidException(String pesan) {
super(pesan);
}
}/** Id kontak tidak ada pada daftar saat kontak akan diubah atau dihapus. */
public class KontakTidakDitemukanException extends Exception {
public KontakTidakDitemukanException(int id) {
super("kontak dengan id " + id + " tidak ditemukan");
}
}import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
/**
* Logika inti aplikasi Buku Kontak: daftar kontak dengan validasi,
* operasi CRUD, dan pencarian. Kelas ini tidak mengenal satu pun
* kelas Swing sehingga bisa diuji tanpa layar.
*/
public class DaftarKontak {
private final List<Kontak> kontak = new ArrayList<>();
private int idBerikutnya = 1;
/** Menambah kontak baru setelah seluruh field tervalidasi. */
public Kontak tambah(String nama, String email, String telepon)
throws KontakInvalidException {
periksa(nama, email, telepon);
periksaEmailBelumTerpakai(email, 0);
Kontak baru = new Kontak(idBerikutnya,
nama.strip(), email.strip(), telepon.strip());
kontak.add(baru);
idBerikutnya++;
return baru;
}
/** Menimpa kontak berdasarkan id dengan nilai field yang baru. */
public Kontak ubah(int id, String nama, String email, String telepon)
throws KontakInvalidException, KontakTidakDitemukanException {
periksa(nama, email, telepon);
periksaEmailBelumTerpakai(email, id);
int posisi = indeksDariId(id);
if (posisi < 0) {
throw new KontakTidakDitemukanException(id);
}
Kontak baru = new Kontak(id,
nama.strip(), email.strip(), telepon.strip());
kontak.set(posisi, baru);
return baru;
}
/** Menghapus kontak berdasarkan id dan mengembalikan kontaknya. */
public Kontak hapus(int id) throws KontakTidakDitemukanException {
int posisi = indeksDariId(id);
if (posisi < 0) {
throw new KontakTidakDitemukanException(id);
}
return kontak.remove(posisi);
}
/** Salinan seluruh kontak, terurut sesuai urutan penambahan. */
public List<Kontak> semua() {
return List.copyOf(kontak);
}
/** Kontak berdasarkan id, atau kosong bila tidak ada. */
public Optional<Kontak> cari(int id) {
for (Kontak k : kontak) {
if (k.id() == id) {
return Optional.of(k);
}
}
return Optional.empty();
}
/** Kontak yang nama, email, atau teleponnya memuat kata kunci. */
public List<Kontak> cari(String kataKunci) {
String kunci = kataKunci.strip().toLowerCase(Locale.ROOT);
List<Kontak> hasil = new ArrayList<>();
for (Kontak k : kontak) {
if (k.nama().toLowerCase(Locale.ROOT).contains(kunci)
|| k.email().toLowerCase(Locale.ROOT).contains(kunci)
|| k.telepon().toLowerCase(Locale.ROOT).contains(kunci)) {
hasil.add(k);
}
}
return hasil;
}
public int jumlah() {
return kontak.size();
}
private int indeksDariId(int id) {
for (int i = 0; i < kontak.size(); i++) {
if (kontak.get(i).id() == id) {
return i;
}
}
return -1;
}
/** Validasi seluruh field sebelum kontak disimpan. */
private static void periksa(String nama, String email, String telepon)
throws KontakInvalidException {
if (nama == null || nama.isBlank()) {
throw new KontakInvalidException("nama tidak boleh kosong");
}
if (email == null || email.isBlank()) {
throw new KontakInvalidException("email tidak boleh kosong");
}
if (telepon == null || telepon.isBlank()) {
throw new KontakInvalidException("telepon tidak boleh kosong");
}
periksaFormatEmail(email.strip());
}
/** Email harus berformat [email protected]. */
private static void periksaFormatEmail(String email)
throws KontakInvalidException {
int posisiAt = email.indexOf('@');
if (posisiAt <= 0 || posisiAt != email.lastIndexOf('@')
|| email.indexOf('.', posisiAt) < 0) {
throw new KontakInvalidException(
"email harus berformat [email protected]: " + email);
}
}
/** Email hanya boleh dipakai satu kontak. */
private void periksaEmailBelumTerpakai(String email, int idKecuali)
throws KontakInvalidException {
for (Kontak k : kontak) {
if (k.id() != idKecuali
&& k.email().equalsIgnoreCase(email.strip())) {
throw new KontakInvalidException(
"email sudah dipakai kontak lain: " + email.strip());
}
}
}
}Aturan yang dijaga DaftarKontak dirangkum pada tabel berikut:
| Aturan | Pelanggaran | Tanggapan |
|---|---|---|
| Nama, email, telepon tidak boleh kosong | field kosong atau spasi saja | KontakInvalidException |
Email berformat [email protected] | tanpa @, @ di depan, atau domain tanpa titik | KontakInvalidException |
| Email unik untuk satu kontak | email sudah dipakai kontak lain | KontakInvalidException |
| Id diberikan sistem, bertambah terus | — | id lama tidak pernah dipakai ulang |
Mari kita bedah bagian-bagian kuncinya:
- Field
idBerikutnya: penghitung hanya maju —hapus()tidak menurunkannya, sehingga kontak baru tidak mewarisi id yang sudah dihapus. - Method
tambah(...): memanggilperiksa(...)danperiksaEmailBelumTerpakai(...)lebih dulu; hanya masukan bersih yang sampai keList. - Method
periksaEmailBelumTerpakai(email, idKecuali):equalsIgnoreCasemenolak[email protected]saat[email protected]sudah ada;idKecualimengizinkan kontak menyimpan emailnya sendiri saat diubah. - Dua method
cari: versiintmengembalikanOptional<Kontak>; versiStringmencari kata kunci pada nama, email, atau telepon dengantoLowerCase(Locale.ROOT)agar hasil tidak berubah di lokal lain.
TableModel yang Membaca DaftarKontak #
Tabel kini meninggalkan DefaultTableModel. Model kustom KontakTableModel memperluas AbstractTableModel, membaca DaftarKontak sebagai sumber data, dan meneruskan operasi CRUD ke kelas tersebut — lalu memberi tahu view lewat fireTableRowsInserted, fireTableRowsUpdated, atau fireTableRowsDeleted:
import javax.swing.table.AbstractTableModel;
/**
* TableModel kustom yang membaca DaftarKontak sebagai sumber data.
* Operasi CRUD diteruskan ke DaftarKontak, lalu notifikasi perubahan
* dikirim ke view lewat keluarga method fire... .
*/
public class KontakTableModel extends AbstractTableModel {
private static final String[] KOLUM = { "Id", "Nama", "Email", "Telepon" };
private final DaftarKontak daftar;
public KontakTableModel(DaftarKontak daftar) {
this.daftar = daftar;
}
@Override
public int getRowCount() {
return daftar.jumlah();
}
@Override
public int getColumnCount() {
return KOLUM.length;
}
@Override
public String getColumnName(int kolom) {
return KOLUM[kolom];
}
@Override
public Object getValueAt(int baris, int kolom) {
Kontak k = daftar.semua().get(baris);
return switch (kolom) {
case 0 -> k.id();
case 1 -> k.nama();
case 2 -> k.email();
default -> k.telepon();
};
}
/** Menambah kontak lewat DaftarKontak lalu memberi tahu view. */
public Kontak tambah(String nama, String email, String telepon)
throws KontakInvalidException {
Kontak baru = daftar.tambah(nama, email, telepon);
int baris = daftar.jumlah() - 1;
fireTableRowsInserted(baris, baris);
return baru;
}
/** Mengubah kontak lewat DaftarKontak lalu memberi tahu view. */
public Kontak ubah(int id, String nama, String email, String telepon)
throws KontakInvalidException, KontakTidakDitemukanException {
int baris = barisDariId(id);
Kontak baru = daftar.ubah(id, nama, email, telepon);
fireTableRowsUpdated(baris, baris);
return baru;
}
/** Menghapus kontak lewat DaftarKontak lalu memberi tahu view. */
public Kontak hapus(int id) throws KontakTidakDitemukanException {
int baris = barisDariId(id);
Kontak terhapus = daftar.hapus(id);
fireTableRowsDeleted(baris, baris);
return terhapus;
}
/** Kontak pada baris model — dipakai aksi terhadap baris terpilih. */
public Kontak kontakDiBaris(int baris) {
return daftar.semua().get(baris);
}
/** Jumlah baris saat ini pada model. */
public int jumlahBaris() {
return daftar.jumlah();
}
private int barisDariId(int id) {
for (int i = 0; i < daftar.jumlah(); i++) {
if (daftar.semua().get(i).id() == id) {
return i;
}
}
return -1;
}
}Dua detail menentukan kebenaran tampilan. Operasi tambah, ubah, dan hapus mengubah DaftarKontak lebih dulu, baru memicu notifikasi dengan rentang baris yang tepat — listener yang membaca ulang model harus menemukan data yang sudah berbaris rapi. Method barisDariId mencari posisi baris sebelum penghapusan, karena setelah baris hilang posisi itu tidak bisa dicari lagi.
Panel tabel ikut berevolusi: PanelTabelKontak menerima model dari luar dan memasang TableRowSorter — perantara yang menyaring baris tampilan tanpa menyentuh data model:
import java.util.Locale;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.ListSelectionModel;
import javax.swing.RowFilter;
import javax.swing.table.TableModel;
import javax.swing.table.TableRowSorter;
/**
* Panel tabel kontak: JTable di dalam JScrollPane.
* Panel memasang TableRowSorter agar pencarian bisa menyaring baris
* yang tampil tanpa mengubah data pada model.
*/
public class PanelTabelKontak extends JScrollPane {
private final JTable tabel;
private final TableRowSorter<TableModel> sorter;
public PanelTabelKontak(KontakTableModel model) {
super();
tabel = new JTable(model);
tabel.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
sorter = new TableRowSorter<>(model);
tabel.setRowSorter(sorter);
setViewportView(tabel);
}
/** Tabel yang dipakai panel — pembacaan nilai lewat view ini. */
public JTable tabel() {
return tabel;
}
/** Id kontak pada baris terpilih, atau -1 bila tidak ada yang terpilih. */
public int idTerpilih() {
int barisTampil = tabel.getSelectedRow();
if (barisTampil < 0) {
return -1;
}
int barisModel = tabel.convertRowIndexToModel(barisTampil);
return (int) tabel.getModel().getValueAt(barisModel, 0);
}
/** Menandai baris model tertentu pada tampilan. */
public void pilihBaris(int barisModel) {
int barisTampil = tabel.convertRowIndexToView(barisModel);
if (barisTampil >= 0) {
tabel.setRowSelectionInterval(barisTampil, barisTampil);
} else {
tabel.clearSelection();
}
}
/**
* Menyaring baris yang tampil dengan kata kunci pada kolom nama,
* email, atau telepon. Kata kunci kosong menghapus saringan.
*/
public void saring(String kataKunci) {
String kunci = kataKunci == null ? "" : kataKunci.strip();
if (kunci.isEmpty()) {
sorter.setRowFilter(null);
return;
}
sorter.setRowFilter(new RowFilter<TableModel, Integer>() {
@Override
public boolean include(Entry<? extends TableModel, ? extends Integer> entri) {
for (int kolom = 1; kolom < entri.getValueCount(); kolom++) {
if (entri.getStringValue(kolom)
.toLowerCase(Locale.ROOT).contains(kunci)) {
return true;
}
}
return false;
}
});
}
}Perhatikan tiga method pada panel tersebut:
- Method
saring(kataKunci): memasangRowFilteryang menerima baris bila kolom nama, email, atau telepon memuat kata kunci; kata kunci kosong melepas saringan. - Method
idTerpilih(): baris terpilih dibaca dalam koordinat tampilan lalu diterjemahkan ke koordinat model lewatconvertRowIndexToModel— wajib saat saringan aktif, karena nomor baris tampilan dan model tidak lagi sama. - Method
pilihBaris(barisModel): arah terjemahan sebaliknya, untuk menyorot kontak yang baru ditambahkan.
Jendela yang Menjadi Tipis #
Formulir PanelFormKontak kehilangan hasilKontak(int) — pengambilan keputusan id bukan lagi urusan panel — dan diganti tiga method pembaca sederhana: nama(), email(), telepon():
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JTextField;
/**
* Panel formulir kontak: tiga isian berjajar dengan label rata kanan.
* Panel hanya membaca dan menulis komponen; validasi tetap di DaftarKontak.
*/
public class PanelFormKontak extends JPanel {
private final JTextField isianNama = new JTextField(16);
private final JTextField isianEmail = new JTextField(16);
private final JTextField isianTelepon = new JTextField(16);
public PanelFormKontak() {
super(new GridBagLayout());
GridBagConstraints k = new GridBagConstraints();
k.insets = new Insets(4, 4, 4, 4);
k.anchor = GridBagConstraints.LINE_END;
String[] label = { "Nama:", "Email:", "Telepon:" };
JTextField[] isian = { isianNama, isianEmail, isianTelepon };
for (int i = 0; i < label.length; i++) {
k.gridx = 0;
k.gridy = i;
add(new JLabel(label[i]), k);
k.gridx = 1;
k.fill = GridBagConstraints.HORIZONTAL;
k.weightx = 1.0;
add(isian[i], k);
k.fill = GridBagConstraints.NONE;
k.weightx = 0.0;
}
}
/** Nilai isian nama, sudah dipangkas spasinya. */
public String nama() {
return isianNama.getText().strip();
}
/** Nilai isian email, sudah dipangkas spasinya. */
public String email() {
return isianEmail.getText().strip();
}
/** Nilai isian telepon, sudah dipangkas spasinya. */
public String telepon() {
return isianTelepon.getText().strip();
}
/** Mengisi formulir dari satu kontak — dipakai saat mengubah data. */
public void isi(Kontak kontak) {
isianNama.setText(kontak.nama());
isianEmail.setText(kontak.email());
isianTelepon.setText(kontak.telepon());
}
/** Mengosongkan seluruh isian setelah kontak selesai disimpan. */
public void kosongkan() {
isianNama.setText("");
isianEmail.setText("");
isianTelepon.setText("");
}
}Jendela utama menerima seluruh aksi tombol dengan pola yang sama: baca formulir, panggil satu operasi KontakTableModel, laporkan hasil pada label status. Kegagalan validasi tidak lagi menghentikan aplikasi — pesannya ditampilkan:
import java.awt.BorderLayout;
import java.awt.FlowLayout;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JTextField;
/**
* Jendela utama aplikasi Buku Kontak.
* JFrame tidak bisa dibuat pada lingkungan tanpa layar, sehingga
* perakitan isi jendela dipisahkan ke method static bangunPanelTombol
* yang bisa diuji tanpa membuka jendela nyata.
*/
public class JendelaUtama extends JFrame {
public JendelaUtama() {
super("Buku Kontak");
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
DaftarKontak daftar = new DaftarKontak();
KontakTableModel model = new KontakTableModel(daftar);
PanelFormKontak form = new PanelFormKontak();
PanelTabelKontak tabel = new PanelTabelKontak(model);
JLabel status = new JLabel("Siap.");
JPanel tombol = bangunPanelTombol(form, model, tabel, status);
JPanel selatan = new JPanel(new BorderLayout(4, 4));
selatan.add(tombol, BorderLayout.NORTH);
selatan.add(status, BorderLayout.SOUTH);
JPanel isi = new JPanel(new BorderLayout(8, 8));
isi.add(form, BorderLayout.NORTH);
isi.add(tabel, BorderLayout.CENTER);
isi.add(selatan, BorderLayout.SOUTH);
setContentPane(isi);
pack();
setLocationRelativeTo(null);
}
/**
* Membangun panel tombol beserta seluruh aksinya.
* Setiap tombol hanya membaca formulir, memanggil satu operasi
* KontakTableModel, lalu melaporkan hasil pada label status.
*/
static JPanel bangunPanelTombol(PanelFormKontak form, KontakTableModel model,
PanelTabelKontak tabel, JLabel status) {
JPanel tombol = new JPanel(new FlowLayout(FlowLayout.LEADING));
JButton tambah = new JButton("Tambah");
tambah.addActionListener(e -> {
try {
Kontak baru = model.tambah(form.nama(), form.email(), form.telepon());
form.kosongkan();
tabel.pilihBaris(model.jumlahBaris() - 1);
status.setText("Kontak " + baru.nama() + " (id " + baru.id() + ") ditambahkan.");
} catch (KontakInvalidException ex) {
status.setText("Gagal menambah: " + ex.getMessage());
}
});
JButton ubah = new JButton("Ubah");
ubah.addActionListener(e -> {
int id = tabel.idTerpilih();
if (id < 0) {
status.setText("Pilih kontak pada tabel terlebih dahulu.");
return;
}
try {
Kontak baru = model.ubah(id, form.nama(), form.email(), form.telepon());
status.setText("Kontak id " + baru.id() + " diubah menjadi " + baru.nama() + ".");
} catch (KontakInvalidException | KontakTidakDitemukanException ex) {
status.setText("Gagal mengubah: " + ex.getMessage());
}
});
JButton hapus = new JButton("Hapus");
hapus.addActionListener(e -> {
int id = tabel.idTerpilih();
if (id < 0) {
status.setText("Pilih kontak pada tabel terlebih dahulu.");
return;
}
try {
Kontak terhapus = model.hapus(id);
status.setText("Kontak " + terhapus.nama() + " (id " + id + ") dihapus.");
} catch (KontakTidakDitemukanException ex) {
status.setText("Gagal menghapus: " + ex.getMessage());
}
});
JTextField isianCari = new JTextField(10);
JButton cari = new JButton("Cari");
cari.addActionListener(e -> {
tabel.saring(isianCari.getText());
status.setText(tabel.tabel().getRowCount() + " kontak cocok untuk \""
+ isianCari.getText().strip() + "\".");
});
tombol.add(tambah);
tombol.add(ubah);
tombol.add(hapus);
tombol.add(isianCari);
tombol.add(cari);
return tombol;
}
}Menyimulasikan Alur Lengkap #
Entri berikut menguji tiga lapisan: logika murni DaftarKontak, sinkronisasi notifikasi KontakTableModel, lalu alur tombol jendela yang sesungguhnya — termasuk tombol yang diklik dengan formulir kosong untuk membuktikan jalur kegagalan:
import java.awt.BorderLayout;
import java.awt.Component;
import java.util.ArrayList;
import java.util.List;
import javax.swing.JButton;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JTextField;
import javax.swing.event.TableModelEvent;
import javax.swing.event.TableModelListener;
/**
* Menguji logika DaftarKontak, sinkronisasi KontakTableModel,
* dan alur tombol jendela utama — semuanya tanpa membuka jendela nyata.
*/
public class DemoKontak {
public static void main(String[] args) throws Exception {
System.out.println("== logika inti DaftarKontak ==");
DaftarKontak daftar = new DaftarKontak();
daftar.tambah("Budi", "[email protected]", "0812-1000-1001");
daftar.tambah("Siti", "[email protected]", "0812-1000-1002");
daftar.tambah("Dewi", "[email protected]", "0813-2000-2003");
System.out.println("jumlah = " + daftar.jumlah());
for (Kontak k : daftar.semua()) {
System.out.println(" " + k);
}
System.out.println("validasi:");
tampilkan(() -> daftar.tambah(" ", "[email protected]", "0812"));
tampilkan(() -> daftar.tambah("Rina", "rina.example.com", "0812"));
tampilkan(() -> daftar.tambah("Rina", "[email protected]", "0812"));
System.out.println("ubah dan hapus:");
daftar.ubah(2, "Siti Rahayu", "[email protected]", "0815-3000-3004");
System.out.println(" ubah id 2 -> " + daftar.cari(2).map(Kontak::nama).orElse("-"));
daftar.hapus(1);
System.out.println(" hapus id 1 -> jumlah = " + daftar.jumlah()
+ ", cari(1) ada = " + daftar.cari(1).isPresent());
System.out.println(" tambah berikutnya -> " + daftar.tambah(
"Rina", "[email protected]", "0814-4000-4005").id()
+ " (id lama tidak dipakai ulang)");
System.out.println("pencarian \"SIT\":");
for (Kontak k : daftar.cari("SIT")) {
System.out.println(" " + k.nama());
}
System.out.println();
System.out.println("== sinkronisasi KontakTableModel ==");
DaftarKontak daftarTabel = new DaftarKontak();
KontakTableModel model = new KontakTableModel(daftarTabel);
List<String> kejadian = new ArrayList<>();
model.addTableModelListener((TableModelEvent e) -> {
String jenis = switch (e.getType()) {
case TableModelEvent.INSERT -> "INSERT";
case TableModelEvent.DELETE -> "DELETE";
default -> "UPDATE";
};
kejadian.add(jenis + " baris " + e.getFirstRow());
});
model.tambah("Budi", "[email protected]", "0812-1000-1001");
model.tambah("Siti", "[email protected]", "0812-1000-1002");
model.ubah(2, "Siti Rahayu", "[email protected]", "0815-3000-3004");
model.hapus(1);
System.out.println("kejadian model = " + kejadian);
System.out.println("baris tersisa = " + model.getValueAt(0, 0)
+ " | " + model.getValueAt(0, 1));
System.out.println();
System.out.println("== alur tombol jendela utama ==");
DaftarKontak daftarUi = new DaftarKontak();
KontakTableModel modelUi = new KontakTableModel(daftarUi);
PanelFormKontak form = new PanelFormKontak();
PanelTabelKontak panelTabel = new PanelTabelKontak(modelUi);
JLabel status = new JLabel("Siap.");
JPanel tombol = JendelaUtama.bangunPanelTombol(form, modelUi, panelTabel, status);
form.isi(new Kontak(0, "Dewi", "[email protected]", "0813-2000-2003"));
tombol(tombol, "Tambah").doClick();
System.out.println("setelah Tambah : " + status.getText());
System.out.println(" baris tampil = " + panelTabel.tabel().getRowCount()
+ " | id terpilih = " + panelTabel.idTerpilih());
System.out.println(" form kosong = " + form.nama().isEmpty());
form.isi(new Kontak(0, "Budi", "[email protected]", "0812-1000-1001"));
tombol(tombol, "Tambah").doClick();
System.out.println("setelah Tambah : " + status.getText());
tombol(tombol, "Tambah").doClick();
System.out.println("klik dengan form kosong:");
System.out.println(" " + status.getText());
panelTabel.pilihBaris(0);
tombol(tombol, "Hapus").doClick();
System.out.println("setelah Hapus : " + status.getText());
isianCari(tombol).setText("bu");
tombol(tombol, "Cari").doClick();
System.out.println("setelah Cari : " + status.getText());
System.out.println(" baris tampil = " + panelTabel.tabel().getRowCount());
}
/** Menjalankan perintah dan mencetak exception yang terjadi. */
private static void tampilkan(Perintah perintah) {
try {
perintah.jalankan();
System.out.println(" (tanpa exception)");
} catch (Exception e) {
System.out.println(" " + e.getClass().getSimpleName() + ": " + e.getMessage());
}
}
private static JButton tombol(JPanel panel, String teks) {
for (Component c : panel.getComponents()) {
if (c instanceof JButton b && teks.equals(b.getText())) {
return b;
}
}
throw new IllegalStateException("tombol tidak ada: " + teks);
}
private static JTextField isianCari(JPanel panel) {
for (Component c : panel.getComponents()) {
if (c instanceof JTextField t) {
return t;
}
}
throw new IllegalStateException("isian cari tidak ada");
}
/** Antarmuka satu method agar blok uji bisa melempar exception terperiksa. */
interface Perintah {
void jalankan() throws Exception;
}
}Output yang Dihasilkan:
== logika inti DaftarKontak ==
jumlah = 3
Kontak[id=1, nama=Budi, [email protected], telepon=0812-1000-1001]
Kontak[id=2, nama=Siti, [email protected], telepon=0812-1000-1002]
Kontak[id=3, nama=Dewi, [email protected], telepon=0813-2000-2003]
validasi:
KontakInvalidException: nama tidak boleh kosong
KontakInvalidException: email harus berformat [email protected]: rina.example.com
KontakInvalidException: email sudah dipakai kontak lain: [email protected]
ubah dan hapus:
ubah id 2 -> Siti Rahayu
hapus id 1 -> jumlah = 2, cari(1) ada = false
tambah berikutnya -> 4 (id lama tidak dipakai ulang)
pencarian "SIT":
Siti Rahayu
== sinkronisasi KontakTableModel ==
kejadian model = [INSERT baris 0, INSERT baris 1, UPDATE baris 1, DELETE baris 0]
baris tersisa = 2 | Siti Rahayu
== alur tombol jendela utama ==
setelah Tambah : Kontak Dewi (id 1) ditambahkan.
baris tampil = 1 | id terpilih = 1
form kosong = true
setelah Tambah : Kontak Budi (id 2) ditambahkan.
klik dengan form kosong:
Gagal menambah: nama tidak boleh kosong
setelah Hapus : Kontak Dewi (id 1) dihapus.
setelah Cari : 1 kontak cocok untuk "bu".
baris tampil = 1Bagian logika menunjukkan tiga penolakan berbeda alasan (nama kosong, format email, email duplikat) dan pembuktian bahwa id baru bernilai 4 setelah kontak id 1 dihapus. Bagian model mencatat kejadian [INSERT baris 0, INSERT baris 1, UPDATE baris 1, DELETE baris 0] — notifikasi mengikuti urutan operasi persis. Bagian alur tombol membuktikan jendela berperilaku benar: Tambah mengisi tabel dan mengosongkan formulir, klik dengan formulir kosong berhenti pada pesan kegagalan, Hapus bekerja atas baris terpilih, dan Cari menyaring tampilan tanpa mengubah data.
Data kini hidup di kelas yang teruji, tetapi masih hilang saat aplikasi ditutup: DaftarKontak belum menulis ke mana pun. Bagian 3 menghubungkan logika ini ke penyimpanan lewat pola DAO — kontrak yang memungkinkan berkas teks dan database bertukar tempat tanpa menyentuh kode UI maupun logika.
Referensi #
- Oracle: Kelas AbstractTableModel (JDK 25)
- Oracle: Kelas TableRowSorter (JDK 25)
- Oracle: Kelas RowFilter (JDK 25)
- Oracle: Cara Memakai JTable
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
DaftarKontak.java
Unduh berkasimport java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
/**
* Logika inti aplikasi Buku Kontak: daftar kontak dengan validasi,
* operasi CRUD, dan pencarian. Kelas ini tidak mengenal satu pun
* kelas Swing sehingga bisa diuji tanpa layar.
*/
public class DaftarKontak {
private final List<Kontak> kontak = new ArrayList<>();
private int idBerikutnya = 1;
/** Menambah kontak baru setelah seluruh field tervalidasi. */
public Kontak tambah(String nama, String email, String telepon)
throws KontakInvalidException {
periksa(nama, email, telepon);
periksaEmailBelumTerpakai(email, 0);
Kontak baru = new Kontak(idBerikutnya,
nama.strip(), email.strip(), telepon.strip());
kontak.add(baru);
idBerikutnya++;
return baru;
}
/** Menimpa kontak berdasarkan id dengan nilai field yang baru. */
public Kontak ubah(int id, String nama, String email, String telepon)
throws KontakInvalidException, KontakTidakDitemukanException {
periksa(nama, email, telepon);
periksaEmailBelumTerpakai(email, id);
int posisi = indeksDariId(id);
if (posisi < 0) {
throw new KontakTidakDitemukanException(id);
}
Kontak baru = new Kontak(id,
nama.strip(), email.strip(), telepon.strip());
kontak.set(posisi, baru);
return baru;
}
/** Menghapus kontak berdasarkan id dan mengembalikan kontaknya. */
public Kontak hapus(int id) throws KontakTidakDitemukanException {
int posisi = indeksDariId(id);
if (posisi < 0) {
throw new KontakTidakDitemukanException(id);
}
return kontak.remove(posisi);
}
/** Salinan seluruh kontak, terurut sesuai urutan penambahan. */
public List<Kontak> semua() {
return List.copyOf(kontak);
}
/** Kontak berdasarkan id, atau kosong bila tidak ada. */
public Optional<Kontak> cari(int id) {
for (Kontak k : kontak) {
if (k.id() == id) {
return Optional.of(k);
}
}
return Optional.empty();
}
/** Kontak yang nama, email, atau teleponnya memuat kata kunci. */
public List<Kontak> cari(String kataKunci) {
String kunci = kataKunci.strip().toLowerCase(Locale.ROOT);
List<Kontak> hasil = new ArrayList<>();
for (Kontak k : kontak) {
if (k.nama().toLowerCase(Locale.ROOT).contains(kunci)
|| k.email().toLowerCase(Locale.ROOT).contains(kunci)
|| k.telepon().toLowerCase(Locale.ROOT).contains(kunci)) {
hasil.add(k);
}
}
return hasil;
}
public int jumlah() {
return kontak.size();
}
private int indeksDariId(int id) {
for (int i = 0; i < kontak.size(); i++) {
if (kontak.get(i).id() == id) {
return i;
}
}
return -1;
}
/** Validasi seluruh field sebelum kontak disimpan. */
private static void periksa(String nama, String email, String telepon)
throws KontakInvalidException {
if (nama == null || nama.isBlank()) {
throw new KontakInvalidException("nama tidak boleh kosong");
}
if (email == null || email.isBlank()) {
throw new KontakInvalidException("email tidak boleh kosong");
}
if (telepon == null || telepon.isBlank()) {
throw new KontakInvalidException("telepon tidak boleh kosong");
}
periksaFormatEmail(email.strip());
}
/** Email harus berformat [email protected]. */
private static void periksaFormatEmail(String email)
throws KontakInvalidException {
int posisiAt = email.indexOf('@');
if (posisiAt <= 0 || posisiAt != email.lastIndexOf('@')
|| email.indexOf('.', posisiAt) < 0) {
throw new KontakInvalidException(
"email harus berformat [email protected]: " + email);
}
}
/** Email hanya boleh dipakai satu kontak. */
private void periksaEmailBelumTerpakai(String email, int idKecuali)
throws KontakInvalidException {
for (Kontak k : kontak) {
if (k.id() != idKecuali
&& k.email().equalsIgnoreCase(email.strip())) {
throw new KontakInvalidException(
"email sudah dipakai kontak lain: " + email.strip());
}
}
}
}
DemoKontak.java
Unduh berkasimport java.awt.BorderLayout;
import java.awt.Component;
import java.util.ArrayList;
import java.util.List;
import javax.swing.JButton;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JTextField;
import javax.swing.event.TableModelEvent;
import javax.swing.event.TableModelListener;
/**
* Menguji logika DaftarKontak, sinkronisasi KontakTableModel,
* dan alur tombol jendela utama — semuanya tanpa membuka jendela nyata.
*/
public class DemoKontak {
public static void main(String[] args) throws Exception {
System.out.println("== logika inti DaftarKontak ==");
DaftarKontak daftar = new DaftarKontak();
daftar.tambah("Budi", "[email protected]", "0812-1000-1001");
daftar.tambah("Siti", "[email protected]", "0812-1000-1002");
daftar.tambah("Dewi", "[email protected]", "0813-2000-2003");
System.out.println("jumlah = " + daftar.jumlah());
for (Kontak k : daftar.semua()) {
System.out.println(" " + k);
}
System.out.println("validasi:");
tampilkan(() -> daftar.tambah(" ", "[email protected]", "0812"));
tampilkan(() -> daftar.tambah("Rina", "rina.example.com", "0812"));
tampilkan(() -> daftar.tambah("Rina", "[email protected]", "0812"));
System.out.println("ubah dan hapus:");
daftar.ubah(2, "Siti Rahayu", "[email protected]", "0815-3000-3004");
System.out.println(" ubah id 2 -> " + daftar.cari(2).map(Kontak::nama).orElse("-"));
daftar.hapus(1);
System.out.println(" hapus id 1 -> jumlah = " + daftar.jumlah()
+ ", cari(1) ada = " + daftar.cari(1).isPresent());
System.out.println(" tambah berikutnya -> " + daftar.tambah(
"Rina", "[email protected]", "0814-4000-4005").id()
+ " (id lama tidak dipakai ulang)");
System.out.println("pencarian \"SIT\":");
for (Kontak k : daftar.cari("SIT")) {
System.out.println(" " + k.nama());
}
System.out.println();
System.out.println("== sinkronisasi KontakTableModel ==");
DaftarKontak daftarTabel = new DaftarKontak();
KontakTableModel model = new KontakTableModel(daftarTabel);
List<String> kejadian = new ArrayList<>();
model.addTableModelListener((TableModelEvent e) -> {
String jenis = switch (e.getType()) {
case TableModelEvent.INSERT -> "INSERT";
case TableModelEvent.DELETE -> "DELETE";
default -> "UPDATE";
};
kejadian.add(jenis + " baris " + e.getFirstRow());
});
model.tambah("Budi", "[email protected]", "0812-1000-1001");
model.tambah("Siti", "[email protected]", "0812-1000-1002");
model.ubah(2, "Siti Rahayu", "[email protected]", "0815-3000-3004");
model.hapus(1);
System.out.println("kejadian model = " + kejadian);
System.out.println("baris tersisa = " + model.getValueAt(0, 0)
+ " | " + model.getValueAt(0, 1));
System.out.println();
System.out.println("== alur tombol jendela utama ==");
DaftarKontak daftarUi = new DaftarKontak();
KontakTableModel modelUi = new KontakTableModel(daftarUi);
PanelFormKontak form = new PanelFormKontak();
PanelTabelKontak panelTabel = new PanelTabelKontak(modelUi);
JLabel status = new JLabel("Siap.");
JPanel tombol = JendelaUtama.bangunPanelTombol(form, modelUi, panelTabel, status);
form.isi(new Kontak(0, "Dewi", "[email protected]", "0813-2000-2003"));
tombol(tombol, "Tambah").doClick();
System.out.println("setelah Tambah : " + status.getText());
System.out.println(" baris tampil = " + panelTabel.tabel().getRowCount()
+ " | id terpilih = " + panelTabel.idTerpilih());
System.out.println(" form kosong = " + form.nama().isEmpty());
form.isi(new Kontak(0, "Budi", "[email protected]", "0812-1000-1001"));
tombol(tombol, "Tambah").doClick();
System.out.println("setelah Tambah : " + status.getText());
tombol(tombol, "Tambah").doClick();
System.out.println("klik dengan form kosong:");
System.out.println(" " + status.getText());
panelTabel.pilihBaris(0);
tombol(tombol, "Hapus").doClick();
System.out.println("setelah Hapus : " + status.getText());
isianCari(tombol).setText("bu");
tombol(tombol, "Cari").doClick();
System.out.println("setelah Cari : " + status.getText());
System.out.println(" baris tampil = " + panelTabel.tabel().getRowCount());
}
/** Menjalankan perintah dan mencetak exception yang terjadi. */
private static void tampilkan(Perintah perintah) {
try {
perintah.jalankan();
System.out.println(" (tanpa exception)");
} catch (Exception e) {
System.out.println(" " + e.getClass().getSimpleName() + ": " + e.getMessage());
}
}
private static JButton tombol(JPanel panel, String teks) {
for (Component c : panel.getComponents()) {
if (c instanceof JButton b && teks.equals(b.getText())) {
return b;
}
}
throw new IllegalStateException("tombol tidak ada: " + teks);
}
private static JTextField isianCari(JPanel panel) {
for (Component c : panel.getComponents()) {
if (c instanceof JTextField t) {
return t;
}
}
throw new IllegalStateException("isian cari tidak ada");
}
/** Antarmuka satu method agar blok uji bisa melempar exception terperiksa. */
interface Perintah {
void jalankan() throws Exception;
}
}
JendelaUtama.java
Unduh berkasimport java.awt.BorderLayout;
import java.awt.FlowLayout;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JTextField;
/**
* Jendela utama aplikasi Buku Kontak.
* JFrame tidak bisa dibuat pada lingkungan tanpa layar, sehingga
* perakitan isi jendela dipisahkan ke method static bangunPanelTombol
* yang bisa diuji tanpa membuka jendela nyata.
*/
public class JendelaUtama extends JFrame {
public JendelaUtama() {
super("Buku Kontak");
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
DaftarKontak daftar = new DaftarKontak();
KontakTableModel model = new KontakTableModel(daftar);
PanelFormKontak form = new PanelFormKontak();
PanelTabelKontak tabel = new PanelTabelKontak(model);
JLabel status = new JLabel("Siap.");
JPanel tombol = bangunPanelTombol(form, model, tabel, status);
JPanel selatan = new JPanel(new BorderLayout(4, 4));
selatan.add(tombol, BorderLayout.NORTH);
selatan.add(status, BorderLayout.SOUTH);
JPanel isi = new JPanel(new BorderLayout(8, 8));
isi.add(form, BorderLayout.NORTH);
isi.add(tabel, BorderLayout.CENTER);
isi.add(selatan, BorderLayout.SOUTH);
setContentPane(isi);
pack();
setLocationRelativeTo(null);
}
/**
* Membangun panel tombol beserta seluruh aksinya.
* Setiap tombol hanya membaca formulir, memanggil satu operasi
* KontakTableModel, lalu melaporkan hasil pada label status.
*/
static JPanel bangunPanelTombol(PanelFormKontak form, KontakTableModel model,
PanelTabelKontak tabel, JLabel status) {
JPanel tombol = new JPanel(new FlowLayout(FlowLayout.LEADING));
JButton tambah = new JButton("Tambah");
tambah.addActionListener(e -> {
try {
Kontak baru = model.tambah(form.nama(), form.email(), form.telepon());
form.kosongkan();
tabel.pilihBaris(model.jumlahBaris() - 1);
status.setText("Kontak " + baru.nama() + " (id " + baru.id() + ") ditambahkan.");
} catch (KontakInvalidException ex) {
status.setText("Gagal menambah: " + ex.getMessage());
}
});
JButton ubah = new JButton("Ubah");
ubah.addActionListener(e -> {
int id = tabel.idTerpilih();
if (id < 0) {
status.setText("Pilih kontak pada tabel terlebih dahulu.");
return;
}
try {
Kontak baru = model.ubah(id, form.nama(), form.email(), form.telepon());
status.setText("Kontak id " + baru.id() + " diubah menjadi " + baru.nama() + ".");
} catch (KontakInvalidException | KontakTidakDitemukanException ex) {
status.setText("Gagal mengubah: " + ex.getMessage());
}
});
JButton hapus = new JButton("Hapus");
hapus.addActionListener(e -> {
int id = tabel.idTerpilih();
if (id < 0) {
status.setText("Pilih kontak pada tabel terlebih dahulu.");
return;
}
try {
Kontak terhapus = model.hapus(id);
status.setText("Kontak " + terhapus.nama() + " (id " + id + ") dihapus.");
} catch (KontakTidakDitemukanException ex) {
status.setText("Gagal menghapus: " + ex.getMessage());
}
});
JTextField isianCari = new JTextField(10);
JButton cari = new JButton("Cari");
cari.addActionListener(e -> {
tabel.saring(isianCari.getText());
status.setText(tabel.tabel().getRowCount() + " kontak cocok untuk \""
+ isianCari.getText().strip() + "\".");
});
tombol.add(tambah);
tombol.add(ubah);
tombol.add(hapus);
tombol.add(isianCari);
tombol.add(cari);
return tombol;
}
}
Kontak.java
Unduh berkas/**
* Satu baris data kontak pada aplikasi Buku Kontak.
* Record hanya menyimpan nilai; aturan bisnis hidup di DaftarKontak.
*/
public record Kontak(int id, String nama, String email, String telepon) {
}
KontakInvalidException.java
Unduh berkas/**
* Masukan kontak yang melanggar aturan validasi:
* field kosong, format email salah, email dipakai kontak lain,
* atau field memuat karakter pemisah penyimpanan.
*/
public class KontakInvalidException extends Exception {
public KontakInvalidException(String pesan) {
super(pesan);
}
}
KontakTableModel.java
Unduh berkasimport javax.swing.table.AbstractTableModel;
/**
* TableModel kustom yang membaca DaftarKontak sebagai sumber data.
* Operasi CRUD diteruskan ke DaftarKontak, lalu notifikasi perubahan
* dikirim ke view lewat keluarga method fire... .
*/
public class KontakTableModel extends AbstractTableModel {
private static final String[] KOLUM = { "Id", "Nama", "Email", "Telepon" };
private final DaftarKontak daftar;
public KontakTableModel(DaftarKontak daftar) {
this.daftar = daftar;
}
@Override
public int getRowCount() {
return daftar.jumlah();
}
@Override
public int getColumnCount() {
return KOLUM.length;
}
@Override
public String getColumnName(int kolom) {
return KOLUM[kolom];
}
@Override
public Object getValueAt(int baris, int kolom) {
Kontak k = daftar.semua().get(baris);
return switch (kolom) {
case 0 -> k.id();
case 1 -> k.nama();
case 2 -> k.email();
default -> k.telepon();
};
}
/** Menambah kontak lewat DaftarKontak lalu memberi tahu view. */
public Kontak tambah(String nama, String email, String telepon)
throws KontakInvalidException {
Kontak baru = daftar.tambah(nama, email, telepon);
int baris = daftar.jumlah() - 1;
fireTableRowsInserted(baris, baris);
return baru;
}
/** Mengubah kontak lewat DaftarKontak lalu memberi tahu view. */
public Kontak ubah(int id, String nama, String email, String telepon)
throws KontakInvalidException, KontakTidakDitemukanException {
int baris = barisDariId(id);
Kontak baru = daftar.ubah(id, nama, email, telepon);
fireTableRowsUpdated(baris, baris);
return baru;
}
/** Menghapus kontak lewat DaftarKontak lalu memberi tahu view. */
public Kontak hapus(int id) throws KontakTidakDitemukanException {
int baris = barisDariId(id);
Kontak terhapus = daftar.hapus(id);
fireTableRowsDeleted(baris, baris);
return terhapus;
}
/** Kontak pada baris model — dipakai aksi terhadap baris terpilih. */
public Kontak kontakDiBaris(int baris) {
return daftar.semua().get(baris);
}
/** Jumlah baris saat ini pada model. */
public int jumlahBaris() {
return daftar.jumlah();
}
private int barisDariId(int id) {
for (int i = 0; i < daftar.jumlah(); i++) {
if (daftar.semua().get(i).id() == id) {
return i;
}
}
return -1;
}
}
KontakTidakDitemukanException.java
Unduh berkas/** Id kontak tidak ada pada daftar saat kontak akan diubah atau dihapus. */
public class KontakTidakDitemukanException extends Exception {
public KontakTidakDitemukanException(int id) {
super("kontak dengan id " + id + " tidak ditemukan");
}
}
PanelFormKontak.java
Unduh berkasimport java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JTextField;
/**
* Panel formulir kontak: tiga isian berjajar dengan label rata kanan.
* Panel hanya membaca dan menulis komponen; validasi tetap di DaftarKontak.
*/
public class PanelFormKontak extends JPanel {
private final JTextField isianNama = new JTextField(16);
private final JTextField isianEmail = new JTextField(16);
private final JTextField isianTelepon = new JTextField(16);
public PanelFormKontak() {
super(new GridBagLayout());
GridBagConstraints k = new GridBagConstraints();
k.insets = new Insets(4, 4, 4, 4);
k.anchor = GridBagConstraints.LINE_END;
String[] label = { "Nama:", "Email:", "Telepon:" };
JTextField[] isian = { isianNama, isianEmail, isianTelepon };
for (int i = 0; i < label.length; i++) {
k.gridx = 0;
k.gridy = i;
add(new JLabel(label[i]), k);
k.gridx = 1;
k.fill = GridBagConstraints.HORIZONTAL;
k.weightx = 1.0;
add(isian[i], k);
k.fill = GridBagConstraints.NONE;
k.weightx = 0.0;
}
}
/** Nilai isian nama, sudah dipangkas spasinya. */
public String nama() {
return isianNama.getText().strip();
}
/** Nilai isian email, sudah dipangkas spasinya. */
public String email() {
return isianEmail.getText().strip();
}
/** Nilai isian telepon, sudah dipangkas spasinya. */
public String telepon() {
return isianTelepon.getText().strip();
}
/** Mengisi formulir dari satu kontak — dipakai saat mengubah data. */
public void isi(Kontak kontak) {
isianNama.setText(kontak.nama());
isianEmail.setText(kontak.email());
isianTelepon.setText(kontak.telepon());
}
/** Mengosongkan seluruh isian setelah kontak selesai disimpan. */
public void kosongkan() {
isianNama.setText("");
isianEmail.setText("");
isianTelepon.setText("");
}
}
PanelTabelKontak.java
Unduh berkasimport java.util.Locale;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.ListSelectionModel;
import javax.swing.RowFilter;
import javax.swing.table.TableModel;
import javax.swing.table.TableRowSorter;
/**
* Panel tabel kontak: JTable di dalam JScrollPane.
* Panel memasang TableRowSorter agar pencarian bisa menyaring baris
* yang tampil tanpa mengubah data pada model.
*/
public class PanelTabelKontak extends JScrollPane {
private final JTable tabel;
private final TableRowSorter<TableModel> sorter;
public PanelTabelKontak(KontakTableModel model) {
super();
tabel = new JTable(model);
tabel.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
sorter = new TableRowSorter<>(model);
tabel.setRowSorter(sorter);
setViewportView(tabel);
}
/** Tabel yang dipakai panel — pembacaan nilai lewat view ini. */
public JTable tabel() {
return tabel;
}
/** Id kontak pada baris terpilih, atau -1 bila tidak ada yang terpilih. */
public int idTerpilih() {
int barisTampil = tabel.getSelectedRow();
if (barisTampil < 0) {
return -1;
}
int barisModel = tabel.convertRowIndexToModel(barisTampil);
return (int) tabel.getModel().getValueAt(barisModel, 0);
}
/** Menandai baris model tertentu pada tampilan. */
public void pilihBaris(int barisModel) {
int barisTampil = tabel.convertRowIndexToView(barisModel);
if (barisTampil >= 0) {
tabel.setRowSelectionInterval(barisTampil, barisTampil);
} else {
tabel.clearSelection();
}
}
/**
* Menyaring baris yang tampil dengan kata kunci pada kolom nama,
* email, atau telepon. Kata kunci kosong menghapus saringan.
*/
public void saring(String kataKunci) {
String kunci = kataKunci == null ? "" : kataKunci.strip();
if (kunci.isEmpty()) {
sorter.setRowFilter(null);
return;
}
sorter.setRowFilter(new RowFilter<TableModel, Integer>() {
@Override
public boolean include(Entry<? extends TableModel, ? extends Integer> entri) {
for (int kolom = 1; kolom < entri.getValueCount(); kolom++) {
if (entri.getStringValue(kolom)
.toLowerCase(Locale.ROOT).contains(kunci)) {
return true;
}
}
return false;
}
});
}
}