为什么在Java中要使用嵌套的InvokeLater调用?

3

我正在重构一些运行多阶段进程的代码。每个步骤都在嵌套的java.awt.EventQueue.invokeLAter调用中。它看起来有点像这样:

   import java.awt.EventQueue;


public class NestedInvokeLater {

    /**
     * @param args
     */
    public static void main(String[] args) {
        java.awt.EventQueue.invokeLater(new Runnable() {

            @Override
            public void run() {
                changeTabPanel();
                copySomeFiles();
                enableNextButton1();
                upDateProgressBar(10);
                java.awt.EventQueue.invokeLater(new Runnable() {

                    @Override
                    public void run() {
                        readInFiles();
                        doSomethingToFiles();
                        upDateProgressBar(15);
                        java.awt.EventQueue.invokeLater(new Runnable() {

                            @Override
                            public void run() {
                                doSomethingElse();
                                upDateProgressBar(100);

                            }
                        });
                    }
                });

            }

        });

    };
}

我对Java的了解还不够深入,无法理解为什么需要将这些调用嵌套起来,我也不太敢随意更改这些调用。但是我认为我理解了invokeLater调用所做的事情以及每个步骤的作用。如果我的理解有误,请纠正我:

invokeLater被用于添加一些调用到待完成的任务列表中,这些调用会在事件分配线程中执行。Java会处理何时以及如何执行每个调用,确保事件分配线程和GUI在执行“后台”任务时不会锁定。

我认为这种嵌套调用意味着我们应该排队一组任务,其中之一是排队某些内容,这又会排队一些任务......其中之一是排队某些内容。但是只有在前一个任务完成后,第一个内部调用才会被排队。一切都按顺序发生(这符合我对整个过程的理解),但我不明白为什么要使用嵌套请求来排队任务。如果我从头开始编写此代码,我会简单地为每个调用创建函数并依次调用它们。

我承认,由于我只是Java的初学者,可能错过了很重要的一点,这使得嵌套调用很重要。但是没有关于嵌套的文档,代码中也没有注释。

我错过了什么?这段代码有什么意义吗?


注意:只有一个EDT线程。invokeLater不会创建新的线程。在这种情况下,它的功能类似于JavaScript中的setTimeout(fn, 0)——将要做的下一件事放在队列中,确保在doSomethingElse开始运行之前进行UI(和进度条百分比)更新。假设soSomethingdoSomethingElse需要“明显的时间”(例如100毫秒到10秒)。 - user166390
啊,好的,抱歉。我弄混了。我会回去编辑我的问题。 - AncientSwordRage
如果代码调用了invokeAndWait方法,那么这种结构会更有意义,尽管Runnables可以是顺序的而不是嵌套的。整个代码块可以缩减为一个Runnable。但这样做有点违背进度条的目的。 - Gilbert Le Blanc
EDT在“前台”运行。在EDT上执行的代码应该快速高效,这样就不会阻塞GUI并防止其重新绘制。如果您有一个长时间运行的任务,那么您可以创建一个单独的线程,它将在“后台”运行。通常情况下,您不需要使用invokeLater()嵌套调用,因为EDT上的所有代码都是顺序执行的。偶尔一些Swing内部处理也可能使用invokeLater(),这意味着它将在您的代码之后执行。因此结果可能不如预期。在这种情况下,您可以嵌套invokeLater()。我通过试错学到了这一点。 - camickr
@GilbertLeBlanc 这是我在想的,但我不确定。它可能意味着一系列嵌套的invokeAndWait调用,但我不知道我是否会知道。 - AncientSwordRage
4个回答

4

做这么多嵌套调用是没有意义的。虽然基于良好的意图,但实现很糟糕。

如果你想正确地做到这一点,请使用SwingWorker

SwingWorker的文档有一个很好的示例,展示了如何在应用程序的后台执行多个任务(其中显示了PrimeNumbersTask类)。

编辑:以下是在您的情况下使用SwingWorker的示例。

class SequentialInvoker extends SwingWorker<Void, Integer> {
    @Override
    public void doInBackground() {

        changeTabPanel();
        copySomeFiles();
        enableNextButton1();
        setProgress(10);

        readInFiles();
        doSomethingToFiles();
        setProgress(15);

        doSomethingElse();
        setProgress(100);
    }
}

为了在进度条上显示实际进度,可以参考以下代码,该代码摘自SwingWorker文档:
JTextArea textArea = new JTextArea();
JProgressBar progressBar = new JProgressBar(0, 100);
SequentialInvoker task = new SequentialInvoker();
task.addPropertyChangeListener(
    new PropertyChangeListener() {
        public  void propertyChange(PropertyChangeEvent evt) {
            if ("progress".equals(evt.getPropertyName())) {
                progressBar.setValue((Integer)evt.getNewValue());
            }
        }
    }); 

使用这段代码,你的进度条将显示SwingWorker工作的进度。


这只是嵌套调用数量的一小部分,我大约会计算25-30个。这会产生什么影响?它与一个调用中所有方法的效果相同吗?我现在正在阅读SwingWorker,但将其作为我的当前工作的一部分可能太多了。 - AncientSwordRage

2
以这种方式进行操作的一个优点是,其他排队等待的事情得以在其间运行。因此,在执行changeTabPanel()部分和readInFiles()部分之间,GUI将能够响应用户点击按钮等操作...
实际实现有些混乱,说明了匿名函数并不是一个好主意(依我之见)。你想把这三个部分变成“真正”的函数并按顺序调用它们,这是个好主意。但是,为了保持相同的逻辑,你需要将它们变成三个可运行的对象,并让每个对象invokeLater后续的对象。
而@Cyrille正确指出,在EDT上执行这些重要任务是不好的做法。

这是我想的某种方式。让每个“子任务”都可以运行起来对我来说很有意义。 - AncientSwordRage
1
如果只有几个子任务,我会说去做。但当嵌套调用多达30层时,您可能需要像@ogregoire建议的SwingWorker,甚至是一些完整的工作流系统,执行器队列等。 - user949300

1
这里使用了三个与invokeLater有关的任务。每个任务都执行一项昂贵的操作,调用updateProgressBar,然后将下一个任务添加到EDT中。
问题在于,如果代码只是继续执行下一个昂贵的操作而不调用invokeLater来完成它,EDT将没有机会重绘进度条以显示其新值。这可能就是为什么工作被分为三个invokelater调用的原因。
现在,这并不是我所谓的好代码。这是相当糟糕的实践:不应该在EDT中执行长时间的处理,因为它会阻塞一切并使GUI无响应。应该将此更改为在单独的线程中执行进程,然后仅调用invokeLater来更新进度条。

编辑:更一般地回答标题中的问题:几乎没有明智的理由嵌套调用invokeLater。当你这样做时,你说“将此作业排队,以便在同一线程中但稍后执行”。因此,它给了GUI的其余部分重新绘制自己的机会,就像这里一样。但只有在EDT中有长时间运行的进程时才有意义,而这是您应该始终避免的。


知道这个“真正”的程序会禁用所有按钮,所以用户无论如何都不能在GUI上执行任何操作可能会有所帮助。 - AncientSwordRage
哦,这也太糟糕了。应该使用模态对话框,并将进度条放在其中。 - Cyrille Ka
1
是的,但是“模态”的,因此它会阻止对下面的GUI的访问。 - Cyrille Ka

0

你发布的代码对我来说毫无意义 - 你可以按顺序编写所有内容,因为你没有运行可能在EDT上发布事件的并行线程。但是你需要第一个invokeLater(),因为你使用了Swing组件。

但是,由于你的代码表明你正在执行一些相对较长的操作:读取文件,对它们进行处理,...你应该在新的工作线程中运行这些方法,而不是在EDT中运行。在这些工作线程的run()方法中,你需要调用EventQueue.invokeLater()来更新你的GUI。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接