Label dan kolom teks cocok untuk satu nilai; begitu data berbentuk baris-kolom — daftar siswa, transaksi penjualan, log peristiwa — komponen yang tepat adalah JTable. Keputusan desain terpenting Swing pada komponen ini adalah pemisahan tegas antara view dan model: JTable tidak menyimpan satu baris data pun. Seluruh data hidup di objek TableModel terpisah, dan tabel hanya membacanya lewat kontrak method yang sama untuk semua model. Pemisahan ini memungkinkan data ditampilkan serentak oleh beberapa view, diuji tanpa layar sama sekali, dan diubah lewat jalur yang memberi tahu view secara otomatis. Chapter ini membedah kontrak TableModel, membangun model kustom dari List berisi record, lalu menyusun tampilan tabel yang lengkap.

Kontrak TableModel #

Setiap kelas yang mengimplementasikan antarmuka javax.swing.table.TableModel wajib menyediakan method berikut:

MethodPertanyaan yang dijawab
getRowCount()Berapa baris data tersedia saat ini?
getColumnCount()Berapa kolom yang ditampilkan?
getColumnName(int kolom)Apa judul kolom ke-kolom?
getValueAt(int baris, int kolom)Apa nilai pada sel tersebut?
isCellEditable(int baris, int kolom)Bolehkah pengguna mengubah sel itu?
setValueAt(Object nilai, int baris, int kolom)Bagaimana menyimpan hasil suntingan?
addTableModelListener(...)Siapa yang diberi tahu saat data berubah?

Tiga method pertama dan getValueAt adalah inti pembacaan: begitu JTable perlu menggambar layar, view memanggil keempatnya berkali-kali — dan tidak pernah menyimpan salinan hasilnya. Saat data berubah, model wajib memberi tahu view lewat fireTableRowsInserted, fireTableRowsDeleted, atau fireTableDataChanged dari kelas induk AbstractTableModel; tanpa panggilan itu, layar menampilkan data basi meskipun model sudah berubah. Menulis semua method kontrak plus seluruh pengiriman notifikasi dari nol itu banyak bekerja, sehingga dalam praktik model kustom cukup memperluas AbstractTableModel yang sudah menyiapkan manajemen listener dan keluarga method fire....

TableModel Kustom untuk Daftar Tugas #

Program berikut membangun model untuk daftar tugas di atas List<BarisTugas> — record dua field yang berfungsi sebagai satu baris. Model menguji dirinya sendiri tanpa satu pun jendela: menghitung dimensi, membaca nilai sel, mencatat notifikasi perubahan, lalu diakhiri membuktikan bahwa JTable membaca model yang sama:

java ModelTugas.java
import java.util.ArrayList;
import java.util.List;
import javax.swing.JTable;
import javax.swing.event.TableModelEvent;
import javax.swing.event.TableModelListener;
import javax.swing.table.AbstractTableModel;

/** Satu baris data tugas. */
record BarisTugas(String nama, boolean selesai) {
}

/** TableModel kustom: sumber data berupa List of record. */
class DaftarTugasModel extends AbstractTableModel {
    private final List<BarisTugas> baris = new ArrayList<>(List.of(
            new BarisTugas("Belajar Java", true),
            new BarisTugas("Kerjakan latihan", false)));
    private final String[] kolom = { "Tugas", "Selesai" };

    @Override public int getRowCount() { return baris.size(); }
    @Override public int getColumnCount() { return kolom.length; }
    @Override public String getColumnName(int c) { return kolom[c]; }

    @Override public Object getValueAt(int r, int c) {
        BarisTugas t = baris.get(r);
        return c == 0 ? t.nama() : t.selesai();
    }

    void tambah(String nama) {
        baris.add(new BarisTugas(nama, false));
        fireTableRowsInserted(baris.size() - 1, baris.size() - 1);
    }

    void hapus(int r) {
        baris.remove(r);
        fireTableRowsDeleted(r, r);
    }
}

public class ModelTugas {
    public static void main(String[] args) {
        DaftarTugasModel model = new DaftarTugasModel();
        List<String> catatan = new ArrayList<>();

        model.addTableModelListener((TableModelEvent e) -> {
            String jenis = switch (e.getType()) {
                case TableModelEvent.INSERT -> "INSERT";
                case TableModelEvent.DELETE -> "DELETE";
                default -> "UPDATE";
            };
            catatan.add(jenis + " baris " + e.getFirstRow());
        });

        System.out.println("ukuran awal   = " + model.getRowCount() + "x" + model.getColumnCount());
        System.out.println("header kolom  = " + model.getColumnName(0) + ", " + model.getColumnName(1));

        model.tambah("Baca dokumentasi JTable");
        model.hapus(0);

        System.out.println("kejadian model = " + catatan);
        for (int r = 0; r < model.getRowCount(); r++) {
            System.out.println("baris " + r + ": " + model.getValueAt(r, 0)
                    + " | selesai=" + model.getValueAt(r, 1));
        }

        // View tipis: JTable hanya membaca model, tanpa menyimpan data sendiri.
        JTable tabel = new JTable(model);
        System.out.println("dimensi JTable = " + tabel.getRowCount() + "x" + tabel.getColumnCount());
        System.out.println("nilai via view = " + tabel.getValueAt(1, 0)
                + " | editable(1,1)=" + tabel.isCellEditable(1, 1));
    }
}

Output yang Dihasilkan:

text
ukuran awal   = 2x2
header kolom  = Tugas, Selesai
kejadian model = [INSERT baris 2, DELETE baris 0]
baris 0: Kerjakan latihan | selesai=false
baris 1: Baca dokumentasi JTable | selesai=false
dimensi JTable = 2x2
nilai via view = Baca dokumentasi JTable | editable(1,1)=false

Mari kita bedah bagian-bagian kuncinya:

  1. Baris 14-16 (record BarisTugas): record menjadikan satu baris tabel sebagai nilai yang tidak bisa diubah — menambah baris berarti menambah objek ke List, bukan memodifikasi baris lama.
  2. Baris 19-21 (extends AbstractTableModel): kelas induk menyimpan daftar TableModelListener dan menyediakan seluruh method fire...; kita hanya menulis logika data.
  3. Baris 28-32 (getValueAt): terjemahan koordinat tabel ke field record. Kolom 0 mengembalikan String, kolom 1 mengembalikan Boolean — JTable otomatis merender Boolean sebagai kotak centang.
  4. Baris 35-41 (tambah dan hapus): kedua method mengubah List lalu memicu notifikasi dengan rentang baris yang tepat. Urutan ini penting: listener yang menerima notifikasi langsung membaca ulang model, sehingga data harus sudah berbaris rapi sebelum notifikasi terkirim.
  5. Baris 46-56 (listener switch): e.getType() membedakan jenis perubahan; e.getFirstRow() menyebut baris pertama yang terdampak. Keluaran [INSERT baris 2, DELETE baris 0] membuktikan notifikasi bergerak sesuai urutan operasi.
  6. Baris 66-69 (new JTable(model)): tabel dibuat hanya dari model — tanpa data, tanpa kolom hard-coded. Dimensi 2x2 dan nilai Baca dokumentasi JTable yang dibaca lewat view sama persis dengan yang dibaca lewat model, dan editable(1,1) bernilai false karena kita tidak menimpa isCellEditable (bawaan AbstractTableModel: semua sel tidak bisa disunting).

DefaultTableModel untuk Kasus Cepat #

Untuk data kecil yang tidak butuh kelas khusus, Swing menyertakan DefaultTableModel: model serbaguna yang menyimpan data di dalam Vector internal dan menyediakan addRow, removeRow, serta setValueAt siap pakai. Model ini cocok untuk contoh cepat dan alat bantu internal; untuk aplikasi nyata dengan domain data sendiri, model kustom di atas biasanya lebih sehat karena tipe datanya ketat (record, bukan Object) dan logika bisnisnya teruji tanpa view.

Tampilan Lengkap dengan JScrollPane #

Satu kebiasaan JTable yang mengejutkan pemula: tabel tidak menggulir sendiri. JTable memanggil scroll bila diletakkan di dalam JScrollPane, dan header kolom hanya tampil pada wadah tersebut. Berikut tampilan jendela lengkap yang memakai model kita:

java JendelaTugas.java — tampilan aplikasi lengkap
import java.awt.BorderLayout;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.SwingUtilities;

public class JendelaTugas {
    public static void main(String[] args) {
        SwingUtilities.invokeLater(() -> {
            DaftarTugasModel model = new DaftarTugasModel();
            JTable tabel = new JTable(model);
            JScrollPane gulir = new JScrollPane(tabel);

            JButton tambah = new JButton("Tambah tugas");
            tambah.addActionListener(e -> model.tambah("Tugas baru "
                    + model.getRowCount()));

            JPanel bawah = new JPanel();
            bawah.add(tambah);

            JFrame jendela = new JFrame("Daftar Tugas");
            jendela.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
            jendela.add(gulir, BorderLayout.CENTER);
            jendela.add(bawah, BorderLayout.SOUTH);
            jendela.setSize(420, 260);
            jendela.setVisible(true);
        });
    }
}

Tombol tambah sama sekali tidak menyentuh tabel — tombol hanya memanggil model.tambah(...), dan baris baru muncul di layar karena notifikasi fireTableRowsInserted yang kita tulis sebelumnya. Itulah kekuatan pola model-view: satu jalur perubahan, semua view ikut sinkron.

Dengan model sebagai sumber kebenaran, aplikasi kita siap menyimpan data domain yang teruji. Bagian desktop ini belum menutup satu topik besar: JavaFX dan cara pandangnya yang berbeda tentang antarmuka. Chapter berikutnya melompat ke toolkit modern tersebut — Stage, Scene, dan pohon node.

Referensi #

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

ModelTugas.java

Unduh berkas
java
import java.util.ArrayList;
import java.util.List;
import javax.swing.JTable;
import javax.swing.event.TableModelEvent;
import javax.swing.event.TableModelListener;
import javax.swing.table.AbstractTableModel;

/** Satu baris data tugas. */
record BarisTugas(String nama, boolean selesai) {
}

/** TableModel kustom: sumber data berupa List of record. */
class DaftarTugasModel extends AbstractTableModel {
    private final List<BarisTugas> baris = new ArrayList<>(List.of(
            new BarisTugas("Belajar Java", true),
            new BarisTugas("Kerjakan latihan", false)));
    private final String[] kolom = { "Tugas", "Selesai" };

    @Override public int getRowCount() { return baris.size(); }
    @Override public int getColumnCount() { return kolom.length; }
    @Override public String getColumnName(int c) { return kolom[c]; }

    @Override public Object getValueAt(int r, int c) {
        BarisTugas t = baris.get(r);
        return c == 0 ? t.nama() : t.selesai();
    }

    void tambah(String nama) {
        baris.add(new BarisTugas(nama, false));
        fireTableRowsInserted(baris.size() - 1, baris.size() - 1);
    }

    void hapus(int r) {
        baris.remove(r);
        fireTableRowsDeleted(r, r);
    }
}

public class ModelTugas {
    public static void main(String[] args) {
        DaftarTugasModel model = new DaftarTugasModel();
        List<String> catatan = new ArrayList<>();

        model.addTableModelListener((TableModelEvent e) -> {
            String jenis = switch (e.getType()) {
                case TableModelEvent.INSERT -> "INSERT";
                case TableModelEvent.DELETE -> "DELETE";
                default -> "UPDATE";
            };
            catatan.add(jenis + " baris " + e.getFirstRow());
        });

        System.out.println("ukuran awal   = " + model.getRowCount() + "x" + model.getColumnCount());
        System.out.println("header kolom  = " + model.getColumnName(0) + ", " + model.getColumnName(1));

        model.tambah("Baca dokumentasi JTable");
        model.hapus(0);

        System.out.println("kejadian model = " + catatan);
        for (int r = 0; r < model.getRowCount(); r++) {
            System.out.println("baris " + r + ": " + model.getValueAt(r, 0)
                    + " | selesai=" + model.getValueAt(r, 1));
        }

        // View tipis: JTable hanya membaca model, tanpa menyimpan data sendiri.
        JTable tabel = new JTable(model);
        System.out.println("dimensi JTable = " + tabel.getRowCount() + "x" + tabel.getColumnCount());
        System.out.println("nilai via view = " + tabel.getValueAt(1, 0)
                + " | editable(1,1)=" + tabel.isCellEditable(1, 1));
    }
}