要修正这些问题,让我们把查找操作移入非Swing线程中。我们第一个想到的就是让整个方法在一个新的线程中执行。这样作的问题是Swing组件,本例中的文本区域,只能从Swing线程中进行编辑。下面是修改后的searchButton_actionPerformed方法: privatevoid 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(){ publicvoid 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); } }
图 3. 在Swing线程外部进行查找 界面显示了一个null,因为显示代码在查找代码完成前被处理了。这是因为一旦新的线程启动了,代码块继续执行,而不是等待线程执行完毕。这是那些奇怪的并发代码块中的一个,下面将把它编写到一个方法中使其能够真正执行。 在SwingUtilities类中有两个方法可以帮助我们解决这些问题:invokerLater()和invokeAndWait()。每一个方法都以一个Runnable作为参数,并在Swing线程中执行它。invokeAndWait()方法阻塞直到Runnnable执行完毕;invokeLater()异步地执行Runnable。invokeAndWait()一般不赞成使用,因为它可能导致严重的线程死锁,对你的应用造成严重的破坏。所以,让我们把它放置一边,使用invokeLater()方法。 要修正最后一个变量变量scooping和执行顺序的问题,我们必须将文本区域的getText()和setText()方法调用移入一个Runnable,只有在查询结果返回后再执行它,并且在Swing线程中执行。我们可以这样作,创建一个匿名Runnable传递给invokeLater(),包括在新线程的Runnable后的文本区域操作。这保证了Swing代码不会在查找结束之前执行。下面是修正后的代码: privatevoid searchButton_actionPerformed() { outputTA.setText("Searching for: " + searchTF.getText()); final String[][] results = new String[1][1]; new Thread() { publicvoid 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() { publicvoid 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(); }
public String getSearchText() { return searchText; }
public String[] getResults() { return results; }
}
注意LookupEvent类是不可变的。这是很重要的,因为我们并不知道在传递过程中谁将处理这些事件。除非我们创建事件的保护拷贝来传递给每一个监听者,我们需要把事件做成不可变的。如果不这样,一个监听者可能会无意或者恶意地修订事件对象,并破坏系统。 现在我们需要在LookupManager上调用lookupComplete()事件。我们首先要在LookupManager上添加一个LookupListener的集合: List listeners = new ArrayList();
当动作发生时,我们需要调用监听者的代码。在我们的例子中,我们将在查找返回时触发一个lookupCompleted()事件。这意味着在监听者集合上迭代,并使用一个LookupEvent事件对象调用它们的lookupCompleted()方法。 我喜欢把这些代码析取到一个独立的方法fire[event-method-name] ,其中构造一个事件对象,在监听器集合上迭代,并调用每一个监听器上的适当的方法。这有助于隔离主要逻辑代码和调用监听器的代码。下面是我们的fireLookupCompleted方法: privatevoid 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行代码创建了一个新的集合,传入原监听器集合。这在监听器响应事件后决定在LookupManager中去除自己时将发挥作用。如果我们不是安全地拷贝集合,在一些监听器应该 被调用而没有被调用时发生令人厌烦的错误。 下面,我们将在动作完成时调用fireLookupCompleted辅助方法。这是lookup方法的返回查询结果的结束处。所以我们可以改变lookup方法使其触发一个事件而不是返回字串数组本身。下面是新的lookup方法: publicvoid 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); }
现在让我们把监听器添加到LookupManager。我们希望当查找返回时更新文本区域。以前,我们只是直接调用setText()方法。因为文本区域是和数据库调用一起都在UI中执行的。既然我们已经将查找逻辑从UI中抽象出来了,我们将把UI类作为一个到LookupManager的监听器,监听lookup事件并相应地更新自己。首先我们将在类定义中实现监听器接口: public class FixedFrame implements LookupListener
接着我们实现接口方法: publicvoid 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的一个监听器: public FixedFrame() { lookupManager = new LookupManager(); //here we register the listener lookupManager.addListener(this); initComponents(); layoutComponents(); }
//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); }
我们也添加新的触发方法fireLookupStarted()。这个方法等同于fireLookupCompleted()方法,除了我们调用监听器上的lookupStarted()方法,并且该事件也不包含结果集。下面是代码: privatevoid 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); } }