不正確的Swing線程是運行緩慢、無響應(yīng)和不穩(wěn)定的Swing應(yīng)用的主要原因之一。這是許多原因造成的,從開發(fā)人員對Swing單線程模型的誤解,到保證正確的線程執(zhí)行的困難。即使對Swing線程進行了很多努力,應(yīng)用線程邏輯也是很難理解和維護的。本文闡述了如何在開發(fā)Swing應(yīng)用中使用事件驅(qū)動編程,以大大簡化開發(fā)、維護,并提供高靈活性。 

  背景

  既然我們是要簡化Swing應(yīng)用的線程,首先讓我們來看看Swing線程是怎么工作的,為什么它是必須的。Swing API是圍繞單線程模型設(shè)計的。這意味著Swing組件必須總是通過同一個線程來修改和操縱。為什么采用單線程模型,這有很多原因,包括開發(fā)成本和同步Swing的復(fù)雜性--這都會造成一個遲鈍的API。為了達到單線程模型,有一個專門的線程用于和Swing組件交互。這個線程就是大家熟知的Swing線程,AWT(有時也發(fā)音為“ought”)線程,或者事件分派線程。在本文的下面的部分,我選用Swing線程的叫法。
既然Swing線程是和Swing組件進行交互的唯一的線程,它就被賦予了很多責(zé)任。所有的繪制和圖形,鼠標(biāo)事件,組件事件,按鈕事件,和所有其它事件都發(fā)生在Swing線程。因為Swing線程的工作已經(jīng)非常沉重了,當(dāng)太多其它工作在Swing線程中進行處理時就會發(fā)生問題。會引起這個問題的最常見的位置是在非Swing處理的地方,像發(fā)生在一個事件監(jiān)聽器方法中,比如JButton的ActionListener,的數(shù)據(jù)庫查找。既然ActionListener的actionPerformed()方法自動在Swing線程中執(zhí)行,那么,數(shù)據(jù)庫查找也將在Swing線程中執(zhí)行。這將占用了Swing的工作,阻止它處理它的其它任務(wù)--像繪制,響應(yīng)鼠標(biāo)移動,處理按鈕事件,和應(yīng)用的縮放。用戶以為應(yīng)用死掉了,但實際上并不是這樣。在適當(dāng)?shù)木€程中執(zhí)行代碼對確保系統(tǒng)正常地執(zhí)行非常重要。 

  既然我們已經(jīng)看到了在適當(dāng)?shù)木€程中執(zhí)行Swing應(yīng)用的代碼是多么重要,現(xiàn)在讓我們?nèi)绾螌崿F(xiàn)這些線程。我們看看將代碼放入和移出Swing線程的標(biāo)準(zhǔn)機制。在講述過程中,我將突出幾個和標(biāo)準(zhǔn)機制有關(guān)的問題和難點。正如我們看到的,大部分的問題都來自于企圖在異步的Swing線程模型上實現(xiàn)同步的代碼模型。從那兒,我們將看到如何修改我們的例子到事件驅(qū)動--移植整個方式到異步模型。




通用Swing線程解決方案

  讓我們以一個最常用的Swing線程錯誤開始。我們將企圖使用標(biāo)準(zhǔn)的技術(shù)來修正這個問題。在這個過程中,我們將看到實現(xiàn)正確的Swing線程的復(fù)雜性和常見困難。并且,注意在修正這個Swing線程問題中,許多中間的例子也是不能工作的。在例子中,我在代碼失敗的地方以//broken開頭標(biāo)出。好了,現(xiàn)在,讓我們進入我們的例子吧。

  假設(shè)我們在執(zhí)行圖書查找。我們有一個簡單的用戶界面,包括一個查找文本域,一個查找按鈕,和一個輸出的文本區(qū)域。這個接口如圖1所示。不要批評我的UI設(shè)計,這個確實很丑陋,我承認。


圖 1. 基本查詢用戶界面

  用戶輸入書的標(biāo)題,作者或者其它條件,然后顯示一個結(jié)果的列表。下面的代碼例子演示了按鈕的ActionListener在同一個線程中調(diào)用lookup()方法。在這些例子中,我使用了thread.sleep()休眠5秒來作為一個占位的外部查找。線程休眠的結(jié)果等同于一個耗時5秒的同步的服務(wù)器調(diào)用。

private void searchButton_actionPerformed()
{
 outputTA.setText("Searching for: " + searchTF.getText());
 //Broken!! Too much work in the Swing
 thread String[] results = lookup(searchTF.getText());
 outputTA.setText("");
 for (int i = 0; i < results.length; i++)
 {
  String result = results[i];
  outputTA.setText(outputTA.getText() + '\n' + result);
  }
}

  如果你運行這段代碼(完整的代碼可以在這兒下載),你會立即發(fā)現(xiàn)存在一些問題。圖2顯示了查找運行中的一個屏幕截圖。


圖 2. 在Swing線程中進行查找

  注意Go按鈕看起來是被按下了。這是因為actionPerformed方法通知了按鈕繪制為非按下外觀,但是還沒有返回。你也會發(fā)現(xiàn)要查找的字串“abcde”并沒有出現(xiàn)在文本區(qū)域中。searchButton_actionPerformed的第1行代碼將文本區(qū)域設(shè)置為要查找的字串。但是,注意Swing重畫并不是立即執(zhí)行的。而是把重畫請求放置到Swing事件隊列中等待Swing線程處理。但是這兒,我們因查找處理占用了Swing線程,所以,它還不能馬上進行重畫。

  要修正這些問題,讓我們把查找操作移入非Swing線程中。我們第一個想到的就是讓整個方法在一個新的線程中執(zhí)行。這樣作的問題是Swing組件,本例中的文本區(qū)域,只能從Swing線程中進行編輯。下面是修改后的searchButton_actionPerformed方法:

private void searchButton_actionPerformed()
{
 outputTA.setText("Searching for: " + searchTF.getText());
 //the String[][] is used to allow access to
 // setting the results from an inner class
 final String[][] results = new String[1][1];
 new Thread()
 {
  public void run()
  {
   results[0] = lookup(searchTF.getText());
   }
  }.start();
 outputTA.setText("");
 for (int i = 0; i < results[0].length; i++)
  {
  String result = results[0][i];
  outputTA.setText(outputTA.getText() + '\n' + result);
  }
}

  這種方法有很多問題。注意final String[][] 。這是一個處理匿名內(nèi)部類和作用域的不得已的替代。基本上,在匿名內(nèi)部類中使用的,但在外部環(huán)繞類作用域中定義的任何變量都需要定義為final。你可以通過創(chuàng)建一個數(shù)組來持有變量解決這個問題。這樣的話,你可以創(chuàng)建數(shù)組為final的,修改數(shù)組中的元素,而不是數(shù)組的引用自身。既然我們已經(jīng)解決這個問題,讓我們進入真正的問題所在吧。圖3顯示了這段代碼運行時發(fā)生的情況:


圖 3. 在Swing線程外部進行查找

  界面顯示了一個null,因為顯示代碼在查找代碼完成前被處理了。這是因為一旦新的線程啟動了,代碼塊繼續(xù)執(zhí)行,而不是等待線程執(zhí)行完畢。這是那些奇怪的并發(fā)代碼塊中的一個,下面將把它編寫到一個方法中使其能夠真正執(zhí)行。

  在SwingUtilities類中有兩個方法可以幫助我們解決這些問題:invokerLater()和invokeAndWait()。每一個方法都以一個Runnable作為參數(shù),并在Swing線程中執(zhí)行它。invokeAndWait()方法阻塞直到Runnnable執(zhí)行完畢;invokeLater()異步地執(zhí)行Runnable。invokeAndWait()一般不贊成使用,因為它可能導(dǎo)致嚴(yán)重的線程死鎖,對你的應(yīng)用造成嚴(yán)重的破壞。所以,讓我們把它放置一邊,使用invokeLater()方法。
要修正最后一個變量變量scooping和執(zhí)行順序的問題,我們必須將文本區(qū)域的getText()和setText()方法調(diào)用移入一個Runnable,只有在查詢結(jié)果返回后再執(zhí)行它,并且在Swing線程中執(zhí)行。我們可以這樣作,創(chuàng)建一個匿名Runnable傳遞給invokeLater(),包括在新線程的Runnable后的文本區(qū)域操作。這保證了Swing代碼不會在查找結(jié)束之前執(zhí)行。下面是修正后的代碼:

private void searchButton_actionPerformed()
{
 outputTA.setText("Searching for: " + searchTF.getText());
 final String[][] results = new String[1][1];
 new Thread()
 {
  public void run()
  { //get results.
   results[0] = lookup(searchTF.getText())
   // send runnable to the Swing thread
   // the runnable is queued after the
   // results are returned
   SwingUtilities.invokeLater(
    new Runnable()
    {
     public void run()
     {
      // Now we're in the Swing thread
      outputTA.setText("");
      for (int i = 0; i < results[0].length; i++)
      {
       String result = results[0][i];
       outputTA.setText( outputTA.getText() + '\n' + result);
       }
      }
    }
   );
  }
 }.start();}

  這可以工作,但是這樣做令人非常頭痛。我們不得不對通過匿名線程執(zhí)行的順序,我們還不得不處理困難的scooping問題。問題并不少見,并且,這只是一個非常簡單的例子,我們已經(jīng)遇到了作用域,變量傳遞,和執(zhí)行順序等一系列問題。相像一個更復(fù)雜的問題,包含了幾層嵌套,共享的引用和指定的執(zhí)行順序。這種方法很快就失控了。

問題

  我們在企圖強制通過異步模型進行同步執(zhí)行--企圖將一個方形的螺栓放到一個圓形的空中。只有我們嘗試這樣做,我們就會不斷地遭遇這些問題。從我的經(jīng)驗,可以告訴你這些代碼很難閱讀,很難維護,并且易于出錯。

  這看起來是一個常見的問題,所以一定有標(biāo)準(zhǔn)的方式來解決,對嗎?出現(xiàn)了一些框架用于管理Swing的復(fù)雜性,所以讓我們來快速預(yù)覽一下它們可以做什么。

  一個可以得到的解決方案是Foxtrot,一個由Biorn Steedom寫的框架,可以在SourceForge上獲取。它使用一個叫做Worker的對象來控制非Swing任務(wù)在非Swing線程中的執(zhí)行,阻塞直到非Swing任務(wù)執(zhí)行完畢。它簡化了Swing線程,允許你編寫同步代碼,并在Swing線程和非Swing線程直接切換。下面是來自它的站點的一個例子:

public void actionPerformed(ActionEvent e)
{
 button.setText("Sleeping..."); 
 String text = null; 
 try 
 {
  text = (String)Worker.post(new Task() {
   public Object run() throws Exception { 
    Thread.sleep(10000); return "Slept !";
    } 
   }
  );
 }
 catch (Exception x) ... button.setText(text); somethingElse();} 

  注意它是如何解決上面的那些問題的。我們能夠非常容易地在Swing線程中傳入傳出變量。并且,代碼塊看起來也很正確--先編寫的先執(zhí)行。但是仍然有一些問題障礙阻止使用從準(zhǔn)同步異步解決方案。Foxtrot中的一個問題是異常管理。使用Foxtrot,每次調(diào)用Worker必須捕獲Exception。這是將執(zhí)行代理給Worker來解決同步對異步問題的一個產(chǎn)物。

  同樣以非常相似的方式,我此前也創(chuàng)建了一個框架,我稱它為鏈接運行引擎(Chained Runnable Engine) ,同樣也遭受來自類似同步對異步問題的困擾。使用這個框架,你將創(chuàng)建一個將被引擎執(zhí)行的Runnable的集合。每一個Runnable都有一個指示器告訴引擎是否應(yīng)該在Swing線程或者另外的線程中執(zhí)行。引擎也保證Runnable以正確的順序執(zhí)行。所以Runnable #2將不會放入隊列直到Runnable #1執(zhí)行完畢。并且,它支持變量以HashMap的形式從Runnable到Runnable傳遞。

  表面上,它看起來解決了我們的主要問題。但是當(dāng)你深入進去后,同樣的問題又冒出來了。本質(zhì)上,我們并沒有改變上面描述的任何東西--我們只是將復(fù)雜性隱藏在引擎的后面。因為指數(shù)級增長的Runnable而使代碼編寫將變得非常枯燥,也很復(fù)雜,并且這些Runnable常常相互耦合。Runnable之間的非類型的HashMap變量傳遞變得難于管理。問題的列表還有很多。

  在編寫這個框架之后,我意識到這需要一個完全不同的解決方案。這讓我重新審視了問題,看別人是怎么解決類似的問題的,并深入的研究了Swing的源代碼。




解決方案:事件驅(qū)動編程

  所有前面的這些解決方案都存在一個共同的致命缺陷--企圖在持續(xù)地改變線程的同時表示一個任務(wù)的功能集。但是改變線程需要異步的模型,而線程異步地處理Runnable。問題的部分原因是我們在企圖在一個異步的線程模型之上實現(xiàn)一個同步的模型。這是所有Runnable之間的鏈和依賴,執(zhí)行順序和內(nèi)部類scooping問題的根源。如果我們可以構(gòu)建真正的異步,我們就可以解決我們的問題并極大地簡化Swing線程。
在這之前,讓我們先列舉一下我們要解決的問題:

  1. 在適當(dāng)?shù)木€程中執(zhí)行代碼 

  2. 使用SwingUtilities.invokeLater()異步地執(zhí)行. 

  異步地執(zhí)行導(dǎo)致了下面的問題:

  1. 互相耦合的組件 

  2. 變量傳遞的困難 

  3. 執(zhí)行的順序 

  讓我們考慮一下像Java消息服務(wù)(JMS)這樣的基于消息的系統(tǒng),因為它們提供了在異步環(huán)境中功能組件之間的松散耦合。消息系統(tǒng)觸發(fā)異步事件,正如在Enterprise Integration Patterns 中描述的。感興趣的參與者監(jiān)聽該事件,并對事件做成響應(yīng)--通常通過執(zhí)行它們自己的一些代碼。結(jié)果是一組模塊化的,松散耦合的組件,組件可以添加到或者從系統(tǒng)中去除而不影響到其它組件。更重要的,組件之間的依賴被最小化了,而每一個組件都是良好定義的和封裝的--每一個都僅對自己的工作負責(zé)。它們簡單地觸發(fā)消息,其它一些組件將響應(yīng)這個消息,并對其它組件觸發(fā)的消息進行響應(yīng)。

  現(xiàn)在,我們先忽略線程問題,將組件解耦并移植到異步環(huán)境中。在我們解決了異步問題后,我們將回過頭來看看線程問題。正如我們所將要看到的,那時解決這個問題將非常容易。

  讓我們還拿前面引入的例子,并把它移植到基于事件的模型。首先,我們把lookup調(diào)用抽象到一個叫LookupManager的類中。這將允許我們將所有UI類中的數(shù)據(jù)庫邏輯移出,并最終允許我們完全將這兩者脫耦。下面是LookupManager類的代碼:

class LookupManager { 
 private String[] lookup(String text) {
  String[] results = ... // database lookup code return results 
 }
} 

  現(xiàn)在我們開始向異步模型轉(zhuǎn)換。為了使這個調(diào)用異步化,我們需要抽象調(diào)用的返回。換句話,方法不能返回任何值。我們將以分辨什么相關(guān)的動作是其它類所希望知道的開始。在我們這個例子中最明顯的事件是搜索結(jié)束事件。所以讓我們創(chuàng)建一個監(jiān)聽器接口來響應(yīng)這些事件。該接口含有單個方法lookupCompleted()。下面是接口的定義:

interface LookupListener { public void lookupCompleted(Iterator results);} 

  遵守Java的標(biāo)準(zhǔn),我們創(chuàng)建另外一個稱作LookupEvent的類包含結(jié)果字串?dāng)?shù)組,而不是到處直接傳遞字串?dāng)?shù)組。這將允許我們在不改變LookupListener接口的情況下傳遞其它信息。例如,我們可以在LookupEvent中同時包括查找的字串和結(jié)果。下面是LookupEvent類:

public class LookupEvent {
 String searchText; 
 String[] results;
 public LookupEvent(String searchText) { 
  this.searchText = searchText;
 }
 public LookupEvent(String searchText, String[] results) { 
  this.searchText = searchText;
  this.results = results; 
 }
 public String getSearchText() {
  return searchText; 
 }
 public String[] getResults() {
  return results; 
 }
} 

  注意LookupEvent類是不可變的。這是很重要的,因為我們并不知道在傳遞過程中誰將處理這些事件。除非我們創(chuàng)建事件的保護拷貝來傳遞給每一個監(jiān)聽者,我們需要把事件做成不可變的。如果不這樣,一個監(jiān)聽者可能會無意或者惡意地修訂事件對象,并破壞系統(tǒng)。

  現(xiàn)在我們需要在LookupManager上調(diào)用lookupComplete()事件。我們首先要在LookupManager上添加一個LookupListener的集合:

List listeners = new ArrayList(); 

  并提供在LookupManager上添加和去除LookupListener的方法:

public void addLookupListener(LookupListener listener){ 
 listeners.add(listener);
}
public void removeLookupListener(LookupListener listener){
 listeners.remove(listener);
} 

  當(dāng)動作發(fā)生時,我們需要調(diào)用監(jiān)聽者的代碼。在我們的例子中,我們將在查找返回時觸發(fā)一個lookupCompleted()事件。這意味著在監(jiān)聽者集合上迭代,并使用一個LookupEvent事件對象調(diào)用它們的lookupCompleted()方法。



我喜歡把這些代碼析取到一個獨立的方法fire[event-method-name] ,其中構(gòu)造一個事件對象,在監(jiān)聽器集合上迭代,并調(diào)用每一個監(jiān)聽器上的適當(dāng)?shù)姆椒ā_@有助于隔離主要邏輯代碼和調(diào)用監(jiān)聽器的代碼。下面是我們的fireLookupCompleted方法:

private void fireLookupCompleted(String searchText, String[] results){
 LookupEvent event = new LookupEvent(searchText, results); 
 Iterator iter = new ArrayList(listeners).iterator();
 while (iter.hasNext()) {
  LookupListener listener = (LookupListener) iter.next();
  listener.lookupCompleted(event); 
 }
} 

  第2行代碼創(chuàng)建了一個新的集合,傳入原監(jiān)聽器集合。這在監(jiān)聽器響應(yīng)事件后決定在LookupManager中去除自己時將發(fā)揮作用。如果我們不是安全地拷貝集合,在一些監(jiān)聽器應(yīng)該 被調(diào)用而沒有被調(diào)用時發(fā)生令人厭煩的錯誤。

  下面,我們將在動作完成時調(diào)用fireLookupCompleted輔助方法。這是lookup方法的返回查詢結(jié)果的結(jié)束處。所以我們可以改變lookup方法使其觸發(fā)一個事件而不是返回字串?dāng)?shù)組本身。下面是新的lookup方法:

public void lookup(String text) { 
 //mimic the server call delay... 
 try { 
  Thread.sleep(5000);
 } catch (Exception e){ 
  e.printStackTrace(); 
 }
 //imagine we got this from a server 
 String[] results = new String[]{"Book one", "Book two", "Book three"}; 
 fireLookupCompleted(text, results);
} 

  現(xiàn)在讓我們把監(jiān)聽器添加到LookupManager。我們希望當(dāng)查找返回時更新文本區(qū)域。以前,我們只是直接調(diào)用setText()方法。因為文本區(qū)域是和數(shù)據(jù)庫調(diào)用一起都在UI中執(zhí)行的。既然我們已經(jīng)將查找邏輯從UI中抽象出來了,我們將把UI類作為一個到LookupManager的監(jiān)聽器,監(jiān)聽lookup事件并相應(yīng)地更新自己。首先我們將在類定義中實現(xiàn)監(jiān)聽器接口:

public class FixedFrame implements LookupListener 

  接著我們實現(xiàn)接口方法:

public void lookupCompleted(final LookupEvent e) {
 outputTA.setText("");
 String[] results = e.getResults(); 
 for (int i = 0; i < results.length; i++) {
  String result = results[i]; 
  outputTA.setText(outputTA.getText() + "\n" + result); 
  }
} 

  最后,我們將它注冊為LookupManager的一個監(jiān)聽器:

public FixedFrame() { 
 lookupManager = new LookupManager(); 
 //here we register the listener 
 lookupManager.addListener(this);
 initComponents(); 
 layoutComponents();} 

  為了簡化,我在類的構(gòu)造器中將它添加為監(jiān)聽器。這在大多數(shù)系統(tǒng)上都允許良好。當(dāng)系統(tǒng)變得更加復(fù)雜時,你可能會重構(gòu)、從構(gòu)造器中提煉出監(jiān)聽器注冊代碼,以允許更大的靈活性和擴展性。

  到現(xiàn)在為止,你看到了所有組件之間的連接,注意職責(zé)的分離。用戶界面類負責(zé)信息的顯示--并且僅負責(zé)信息的顯示。另一方面,LookupManager類負責(zé)所有的lookup連接和邏輯。并且,LookupManager負責(zé)在它變化時通知監(jiān)聽器--而不是當(dāng)變化發(fā)生時應(yīng)該具體做什么。這允許你連接任意多的監(jiān)聽器。

  為了演示如何添加新的事件,讓我們回頭添加一個lookup開始的事件。我們可以添加一個稱作lookupStarted()的事件到LookupListener,我們將在查找開始執(zhí)行前觸發(fā)它。我們也創(chuàng)建一個fireLookupStarted()事件調(diào)用所有LookupListener的lookupStarted()。現(xiàn)在lookup方法如下:

public void lookup(String text) { 
  fireLookupStarted(text); 
  //mimic the server call delay... 
  try { 
   Thread.sleep(5000);
  } catch (Exception e){ 
    e.printStackTrace(); 
  }
  //imagine we got this from a server 
  String[] results = new String[]{"Book one", "Book two", "Book three"}; 
  fireLookupCompleted(text, results);} 

  我們也添加新的觸發(fā)方法fireLookupStarted()。這個方法等同于fireLookupCompleted()方法,除了我們調(diào)用監(jiān)聽器上的lookupStarted()方法,并且該事件也不包含結(jié)果集。下面是代碼:

private void fireLookupStarted(String searchText){
 LookupEvent event = new LookupEvent(searchText); 
 Iterator iter = new ArrayList(listeners).iterator();
 while (iter.hasNext()) { 
  LookupListener listener = (LookupListener) iter.next();
  listener.lookupStarted(event);
 }
} 

  最后,我們在UI類上實現(xiàn)lookupStarted()方法,設(shè)置文本區(qū)域提示當(dāng)前搜索的字符串。

public void lookupStarted(final LookupEvent e) {
 outputTA.setText("Searching for: " + e.getSearchText());
} 

  這個例子展示了添加新的事件是多么容易。現(xiàn)在,讓我們看看展示事件驅(qū)動脫耦的靈活性。我們將通過創(chuàng)建一個日志類,當(dāng)一個搜索開始和結(jié)束時在命令行中輸出信息來演示。我們稱這個類為Logger。下面是它的代碼:

public class Logger implements LookupListener { 
 public void lookupStarted(LookupEvent e) { 
  System.out.println("Lookup started: " + e.getSearchText());
 }
 public void lookupCompleted(LookupEvent e) {
  System.out.println("Lookup completed: " + e.getSearchText() + " " + e.getResults()); 
 }
} 

  現(xiàn)在,我們添加Logger作為在FixedFrame構(gòu)造方法中的LookupManager的一個監(jiān)聽器。

public FixedFrame() {
 lookupManager = new LookupManager();
 lookupManager.addListener(this); 
 lookupManager.addListener(new Logger());
 initComponents();
 layoutComponents();
}  

  現(xiàn)在你已經(jīng)看到了添加新的事件、創(chuàng)建新的監(jiān)聽器--向您展示了事件驅(qū)動方案的靈活性和擴展性。你會發(fā)現(xiàn)隨著你更多地開發(fā)事件集中的程序,你會更加嫻熟地在你的應(yīng)用中創(chuàng)建通用動作。像其它所有事情一樣,這只需要時間和經(jīng)驗。看起來在事件模型上已經(jīng)做了很多研究,但是你還是需要把它和其它替代方案相比較。考慮開發(fā)時間成本;最重要的,這是一次性成本。一旦你創(chuàng)建好了監(jiān)聽器模型和它們的動作,以后向你的應(yīng)用中添加監(jiān)聽器將是小菜一蝶。



線程

  到現(xiàn)在,我們已經(jīng)解決了上面的異步問題;通過監(jiān)聽器使組件脫耦,通過事件對象傳遞變量,通過事件產(chǎn)生和監(jiān)聽器的注冊的組合決定執(zhí)行的順序。讓我們回到線程問題,因為正是它把我們帶到了這兒。實際上非常容易:因為我們已經(jīng)有了異步功能的監(jiān)聽器,我們可以簡單地讓監(jiān)聽器自己決定它們應(yīng)該在哪個線程中執(zhí)行。考慮UI類和LookupManager的分離。UI類基于事件,決定需要什么處理。并且,該類也是Swing,而日志類不是。所以讓UI類負責(zé)決定它應(yīng)該在什么線程中執(zhí)行將更加有意義。所以,讓我們再次看看UI類。下面是沒有線程的lookupCompleted()方法:

public void lookupCompleted(final LookupEvent e) {
 outputTA.setText(""); 
 String[] results = e.getResults(); 
 for (int i = 0; i < results.length; i++) {
  String result = results[i]; 
  outputTA.setText(outputTA.getText() + "\n" + result); 
 }
} 

  我們知道這將在非Swing線程中調(diào)用,因為該事件是直接在LookupManager中觸發(fā)的,這將不是在Swing線程中執(zhí)行。因為所有的代碼功能上都是異步的(我們不必等待監(jiān)聽器方法允許結(jié)束后才調(diào)用其它代碼),我們可以通過SwingUtilities.invokeLater()將這些代碼改道到Swing線程。下面是新的方法,傳入一個匿名Runnable到SwingUtilities.invokeLater():

public void lookupCompleted(final LookupEvent e) { 
 //notice the threading 
 SwingUtilities.invokeLater( new Runnable() { 
  public void run() { 
   outputTA.setText("");
   String[] results = e.getResults(); 
   for (int i = 0; i < results.length; i++) {
    String result = results[i]; 
    outputTA.setText(outputTA.getText() + "\n" + result);
   }
  }
 }
);
} 

  如果任何LookupListener不是在Swing線程中執(zhí)行,我們可以在調(diào)用線程中執(zhí)行監(jiān)聽器代碼。作為一個原則,我們希望所有的監(jiān)聽器都迅速地接到通知。所以,如果你有一個監(jiān)聽器需要很多時間來處理自己的功能,你應(yīng)該創(chuàng)建一個新的線程或者把耗時代碼放入ThreadPool中等待執(zhí)行。

  最后的步驟是讓LookupManager在非Swing線程中執(zhí)行l(wèi)ookup。當(dāng)前,LookupManager是在JButton的ActionListener的Swing線程中被調(diào)用的。現(xiàn)在是我們做出決定的時候,或者我們在JButton的ActionListener中引入一個新的線程,或者我們可以保證lookup自己在非Swing線程中執(zhí)行,自己開始一個新的線程。我選擇盡可能和Swing類貼近地管理Swing線程。這有助于把所有Swing邏輯封裝在一起。如果我們把Swing線程邏輯添加到LookupManager,我們將引入了一層不必要的依賴。并且,對于LookupManager在非Swing線程環(huán)境中孵化自己的線程是完全沒有必要的,比如一個非繪圖的用戶界面,在我們的例子中,就是Logger。產(chǎn)生不必要的新線程將損害到你應(yīng)用的性能,而不是提高性能。LookupManager執(zhí)行的很好,不管Swing線程與否--所以,我喜歡把代碼集中在那兒。

  現(xiàn)在我們需要將JButton的ActionListener執(zhí)行l(wèi)ookup的代碼放在一個非Swing線程中。我們創(chuàng)建一個匿名的Thread,使用一個匿名的Runnable執(zhí)行這個lookup。

private void searchButton_actionPerformed() {
 new Thread(){ 
  public void run() {
   lookupManager.lookup(searchTF.getText());
  }
 }.start();
} 

  這就完成了我們的Swing線程。簡單地在actionPerformed()方法中添加線程,確保監(jiān)聽器在新的線程中執(zhí)行照顧到了整個線程問題。注意,我們不用處理像第一個例子那樣的任何問題。通過把時間花費在定義一個事件驅(qū)動的體系,我們在和Swing線程相關(guān)處理上節(jié)約了更多的時間。

  結(jié)論

  如果你需要在同一個方法中執(zhí)行大量的Swing代碼和非Swing代碼,很容易將某些代碼放錯位置。事件驅(qū)動的方式將迫使你將代碼放在它應(yīng)該在的地方--它僅應(yīng)該在的地方。如果你在同一個方法中執(zhí)行數(shù)據(jù)庫調(diào)用和更新UI組件,那么你就在一個類中寫入了太多的邏輯。分析你系統(tǒng)中的事件,創(chuàng)建底層的事件模型將迫使你將代碼放到正確的地方。將費時的數(shù)據(jù)庫調(diào)用代碼放在非UI類中,也不要在非UI組件中更新UI組件。采用事件驅(qū)動的體系,UI負責(zé)UI更新,數(shù)據(jù)庫管理類負責(zé)數(shù)據(jù)庫調(diào)用。在這一點上,每一個封裝的類都只用關(guān)心自己的線程,不用擔(dān)心系統(tǒng)其它部分如何動作。當(dāng)然,設(shè)計、構(gòu)建一個事件驅(qū)動的客戶端也很有用,但是需要花費的時間代價遠超過帶來的結(jié)果系統(tǒng)的靈活性和可維護性的提高。