本文承接上一篇,針對同一個 JTable 綁定數據庫的需求,提供更詳細的「從方案一遷移到方案二」的重構實戰步驟,並補充 MyEclipse 可視化設計器的具體操作截圖流程、異常處理模式、以及生產環境常用的分頁加載擴展。
| 維度 | 方案一:二維數組 + DefaultTableModel | 方案二:自定義 TableModel |
|---|---|---|
| 代碼量 | 極少(~10 行) | 中等(~80 行) |
| 內存佔用 | 全量載入 | 可按需載入/分頁 |
| 編輯支持 | 需手動監聽模型事件 | 原生支持 setValueAt |
| 排序/過濾 | 需外部實現 | TableRowSorter 原生支持 |
| 渲染定制 | 困難 | 覆寫 getColumnClass/渲染器 |
| 適用階段 | 原型/演示/小數據 | 生產/中大數據/複雜交互 |
public class User {
private Long id;
private String username;
private String email;
private Integer status; // 1=啟用, 0=禁用
// Getter/Setter 省略
// 建議加 toString() 方便調試
}
public class UserTableModel extends AbstractTableModel {
private static final String[] COLUMNS = {"ID", "用戶名", "郵箱", "狀態"};
private final List data = new ArrayList<>();
private final UserDao dao = new UserDao();
@Override public int getRowCount() { return data.size(); }
@Override public int getColumnCount() { return COLUMNS.length; }
@Override public String getColumnName(int col) { return COLUMNS[col]; }
@Override
public Class> getColumnClass(int col) {
// 讓 JTable 自動選擇渲染器:Long→右對齊,Boolean→勾選框
if (col == 0) return Long.class;
if (col == 3) return Boolean.class;
return String.class;
}
@Override
public Object getValueAt(int row, int col) {
User u = data.get(row);
switch (col) {
case 0: return u.getId();
case 1: return u.getUsername();
case 2: return u.getEmail();
case 3: return u.getStatus() == 1; // Boolean 顯示為勾選框
default: return null;
}
}
// --- 編輯支持 ---
@Override public boolean isCellEditable(int row, int col) { return col == 3; }
@Override
public void setValueAt(Object val, int row, int col) {
if (col == 3 && val instanceof Boolean) {
User u = data.get(row);
u.setStatus((Boolean) val ? 1 : 0);
dao.updateStatus(u.getId(), u.getStatus()); // 同步 DB
fireTableCellUpdated(row, col);
}
}
// --- 數據操作 ---
public void reload() {
try (ResultSet rs = dao.query("SELECT id, username, email, status FROM t_user")) {
data.clear();
while (rs.next()) {
User u = new User();
u.setId(rs.getLong("id"));
u.setUsername(rs.getString("username"));
u.setEmail(rs.getString("email"));
u.setStatus(rs.getInt("status"));
data.add(u);
}
fireTableDataChanged();
} catch (SQLException e) {
throw new RuntimeException("加載用戶數據失敗", e);
}
}
public void add(User u) {
dao.insert(u);
data.add(u);
fireTableRowsInserted(data.size() - 1, data.size() - 1);
}
public void remove(int row) {
User u = data.remove(row);
dao.delete(u.getId());
fireTableRowsDeleted(row, row);
}
}
*.java 設計視圖,選中 JTable 組件。com.example.model.UserTableModel.createInstance()。


public class UserTableModel extends AbstractTableModel {
// ... 前面代碼 ...
/** 設計器調用的靜態工廠方法 */
public static UserTableModel createInstance() {
UserTableModel model = new UserTableModel();
// 異步加載避免阻塞 UI 初始化
SwingUtilities.invokeLater(model::reload);
return model;
}
}
public class UserManageFrame extends JFrame {
private JTable table;
private UserTableModel tableModel;
public UserManageFrame() {
initComponents(); // 設計器生成代碼,含 table.setModel(UserTableModel.createInstance())
tableModel = (UserTableModel) table.getModel();
// 啟用排序
TableRowSorter sorter = new TableRowSorter<>(tableModel);
table.setRowSorter(sorter);
// 工具欄按鈕事件
btnRefresh.addActionListener(e -> loadDataAsync());
btnAdd.addActionListener(e -> showAddDialog());
btnDelete.addActionListener(e -> deleteSelected());
}
private void loadDataAsync() {
new SwingWorker() {
@Override protected Void doInBackground() {
tableModel.reload();
return null;
}
@Override protected void done() {
JOptionPane.showMessageDialog(UserManageFrame.this, "刷新完成");
}
}.execute();
}
private void deleteSelected() {
int[] rows = table.getSelectedRows();
if (rows.length == 0) return;
// 從大到小刪除避免索引偏移
for (int i = rows.length - 1; i >= 0; i--) {
int modelRow = table.convertRowIndexToModel(rows[i]);
tableModel.remove(modelRow);
}
}
}
當表數據超過 1 萬行,全量載入會導致界面卡頓、內存飆升。改造 UserTableModel 實現分頁:
public class PagedUserTableModel extends AbstractTableModel {
private static final int PAGE_SIZE = 100;
private final List pageData = new ArrayList<>();
private int currentPage = 0;
private int totalRows = 0;
private final UserDao dao = new UserDao();
// getRowCount 返回總行數,JTable 滾動條按總行數渲染
@Override public int getRowCount() { return totalRows; }
@Override
public Object getValueAt(int row, int col) {
// 計算行所在頁
int page = row / PAGE_SIZE;
int offset = row % PAGE_SIZE;
// 若未緩存該頁,後台加載
if (page != currentPage) {
loadPageAsync(page);
return "載入中..."; // 佔位文本
}
// 頁面已緩存,返回數據
if (offset < pageData.size()) {
User u = pageData.get(offset);
return switch (col) {
case 0 -> u.getId();
case 1 -> u.getUsername();
case 2 -> u.getEmail();
case 3 -> u.getStatus() == 1;
default -> null;
};
}
return null;
}
private void loadPageAsync(int page) {
currentPage = page;
pageData.clear();
int offset = page * PAGE_SIZE;
new SwingWorker() {
@Override protected Void doInBackground() {
try (ResultSet rs = dao.queryPage("SELECT ... LIMIT ?, ?", offset, PAGE_SIZE)) {
while (rs.next()) {
User u = new User();
u.setId(rs.getLong("id"));
u.setUsername(rs.getString("username"));
u.setEmail(rs.getString("email"));
u.setStatus(rs.getInt("status"));
pageData.add(u);
}
// 首次加載需查總數
if (totalRows == 0) {
totalRows = dao.count("SELECT COUNT(*) FROM t_user");
}
} catch (SQLException e) {
e.printStackTrace();
}
return null;
}
@Override protected void done() {
fireTableDataChanged(); // 觸發視圖重繪
}
}.execute();
}
}
| 異常場景 | 處理策略 |
|---|---|
| 數據庫連接失敗 | DAO 拋出運行時異常,Model 捕獲並顯示錯誤對話框,不阻塞 UI |
| 編輯時數據庫更新失敗 | setValueAt 中捕獲異常,回滾模型值,彈出錯誤提示 |
| 並發修改衝突 | 引入版本號字段,更新時樂觀鎖檢查 |
| 大量數據導出 Excel | 後台線程分批查詢寫入 SXSSFWorkbook,進度條反饋 |
通過「領域模型 → 自定義 TableModel → 設計器綁定 → 異步加載 → 分頁擴展」五步重構,可將簡單的 JTable 綁定升級為生產級組件。關鍵在于:數據與視圖分離、異步非阻塞、事件驅動刷新、按需加載。掌握這套模式,應對各類 Swing 表格開發需求皆得心應手。