在 Java Swing 開發中,構建一個「永不凍結」的用戶介面是核心目標。無論用戶進行何種交互,程式都必須即時回應。要達成此目標,開發者必須精通 Swing 框架下的多線程運作模型。本文將系統性剖析 Swing 三大線程類型——初始線程、事件分派線程(EDT)與工作線程,並深入講解 SwingWorker 的進階應用,以及頂層容器的架構細節。
每個 Java 程式啟動時都會產生初始線程(標準程式為 main 執行緒,Applet 為 init 執行緒)。在 Swing 應用中,初始線程的職責極為單一:建立一個 Runnable 任務,排程至事件分派線程(EDT)初始化 GUI。一旦 GUI 建立完成,程式即進入事件驅動模式。
關鍵方法:
SwingUtilities.invokeLater(Runnable):非阻塞排程,適合大多數啟動場景。SwingUtilities.invokeAndWait(Runnable):阻塞等待 GUI 建立完成,Applet 必須使用此法避免競態條件。SwingUtilities.invokeLater(() -> {
createAndShowGUI(); // 所有 Swing 元件建立與操作必須在 EDT 中進行
});
核心原則:幾乎所有建立或操作 Swing 元件的代碼必須在 EDT 執行,否則會導致線程安全問題。
EDT 專責處理所有 Swing 事件(如按鈕點擊、滑鼠移動)與繪製更新。絕大多數 Swing 方法呼叫必須在此線程執行,因為 Swing 元件非線程安全。
EDT 執行模型可視為一系列短小任務隊列。若任務耗時過長(超過數百毫秒),會導致事件堆積,介面出現「回應緩慢」甚至凍結。開發者可透過 SwingUtilities.isEventDispatchThread() 驗證當前線程身份。
長時間運算(資料庫查詢、檔案 I/O、網路請求)絕不可 放在 EDT,必須委派給工作線程。javax.swing.SwingWorker<T,V> 是專為 Swing 設計的抽象類,提供以下強大特性:
doInBackground()(背景執行)、done()(EDT 執行,任務完成後自動回呼)。get() 取得回傳值、cancel() 取消任務、isDone()/isCancelled() 狀態查詢。publish(V) 發送進度資料,process(List<V>) 在 EDT 批次處理並更新 UI。progress(0-100 整數)、state(PENDING/STARTED/DONE)變更可觸發屬性變更事件,便於綁定進度條等 UI。TumbleItem Applet 需載入大量圖片。若在初始線程或 EDT 載入,會導致啟動延遲或介面凍結。解決方案:
SwingWorker<ImageIcon[], Void> worker = new SwingWorker<>() {
@Override
protected ImageIcon[] doInBackground() {
ImageIcon[] imgs = new ImageIcon[nimgs];
for (int i = 0; i < nimgs; i++) imgs[i] = loadImage(i + 1);
return imgs;
}
@Override
protected void done() {
try { imgs = get(); } // 取得結果並更新 UI
catch (InterruptedException | ExecutionException e) { handleError(e); }
}
};
worker.execute(); // 啟動任務
泛型說明:第一個型參 ImageIcon[] 為 doInBackground 回傳型別(亦為 get() 型別);第二個 Void 表示無中間結果。
背景任務頻繁發布中間結果 FlipPair,EDT 透過 process() 批次更新介面:
private class FlipTask extends SwingWorker<Void, FlipPair> {
@Override
protected Void doInBackground() {
long heads = 0, total = 0;
Random rnd = new Random();
while (!isCancelled()) {
total++;
if (rnd.nextBoolean()) heads++;
publish(new FlipPair(heads, total)); // 發布中間結果
}
return null;
}
@Override
protected void process(List<FlipPair> pairs) {
FlipPair latest = pairs.get(pairs.size() - 1); // 僅取最新值更新 UI
headsText.setText(String.valueOf(latest.heads));
totalText.setText(String.valueOf(latest.total));
devText.setText(String.format("%.10g", (double)latest.heads/total - 0.5));
}
}
doInBackground 定期檢查 isCancelled(),或處理 InterruptedException。cancel(true) 會發送中斷信號;cancel(false) 僅設定取消旗標。Swing 提供三種頂層容器:JFrame(視窗)、JDialog(對話框)、JApplet(Applet)。它們構成「包含層級」的根節點,所有可視元件必須屬於某個包含層級才能顯示。
BorderLayout,實際可視元件皆置於此。JMenuBar)置於內容面板之外,由 setJMenuBar() 設定。JPanel contentPane = new JPanel(new BorderLayout());
contentPane.setBorder(BorderFactory.createEmptyBorder(10,10,10,10));
contentPane.add(mainComponent, BorderLayout.CENTER);
contentPane.add(statusBar, BorderLayout.PAGE_END);
contentPane.setOpaque(true); // 必須不透明,否則繪製錯誤
frame.setContentPane(contentPane);
避坑指南:切勿使用透明容器(JScrollPane、JSplitPane 等)作為內容面板,會導致元件重疊混亂。
每個頂層容器內部擁有一個 JRootPane,管理四大層級:
進階開發(如全域事件攔截、拖曳視覺回饋)需直接操作根面板。
掌握 Swing 併發模型的精髓在於:初始線程只負責啟動、EDT 專職 UI 事件與繪製、耗時任務全權交給 SwingWorker。配合頂層容器的標準結構,即可構建穩健、流暢的桌面應用。實務上請牢記:SwingWorker 的 publish/process 機制是即時進度回報的利器,而 propertyChangeListener 監聽 progress/state 則是綁定進度條的標準作法。