1

Тема: SwingWorker (виконання довгих розрахунків в SWINGу)

Під час створення графічного інтерфейса, часто виникає питання, а що ж робити, коли при натисканні на якусь кнопку виконується "важка" (довга) процедура (підключення до бази даних, читання даних з великого файла, виконання довгих розрахунків, тощо). Пам'ятаю, як дивувався, чому ж не поновлюється JProgressBar, при підключенні до бази даних.  :D  Весь інтерфейс, так би мовити "завмирає" на певний час і навіть кнопка, яку ми натиснули, так і відображається натиснутою. І ось тут, ми стоїмо на роздоріжжі, потрібно застосовувати багатопоточність.

Є два варіанти:
1. Реалізувати даний механізм власноруч (напевно, найкращий варіант, проте, значно важчий, аніж 2 варіант).
2. Використати абстрактний клас SwingWorker (public abstract class SwingWorker<T,V> extends Object implements RunnableFuture<T>).

Що ж то за Worker, такий?
Даний, клас з'явився в 6 версії Java, тобто, ще раз підкреслюю, що розробники, раніше і без нього справлялися.  :)
Проте він був створений, для полегшення вирішення даних задач. Тобто, давайте поглянем на мінімум, який потрібно зробити, для виконання задачі в іншому потоці:
1. Написати клас, який наслідується від класа SwingWorker.
2. Не забуваєм, що SwingWorker абстрактний клас, тому потрібно реалізувати абстрактний метод doInBackground() (protected abstract T doInBackground() throws Exception).
3. Створити об'єкт нашого класу та викликати метод execute() (public final void execute()).
Тобто, як бачимо, нічого надзвичайного не має. А тепер, давайте більш детально пройдемось, по даному класу.

SwingWorker<T,V>
Що таке T та V?
T - тип результату, що повертається методом doInBackground() (можна сказати кінцевий результат).
V - тип проміжкових результатів (відправляється методом publish(V... chunks) (protected final void publish(V... chunks)) з методу doInBackground(), ці результати потрапляють (передаються) до методу process(List<V> chunks) (protected void process(List<V> chunks))).

Самий, цікавий, для нас метод - doInBackground(). Ось тут і потрібно виконувати, нашу "важку" роботу (фонову задачу). Оскільки даний метод буде запущено в окремому потоці. Також треба наголосити, на те, що з елементами GUI працювати в даному методі НЕ РЕКОМЕНДУЄТЬСЯ, проте є деякі виключення. Методи - thread safe (потокозахищені) - JTextComponent.setText(), JTextComponent.print(), JTextArea.insert(), JTextArea.append(), JTextPane.insertIcon(),... (див. офіційну документацію).

Не обов'язково, але при бажанні, з даного метода можемо передавати проміжкові результати методом publish(V... chunks).

Перевизначення методу process(List<V> chunks) використовується, для обробки (показу) проміжних результатів. Ось в цьому методі вже можна працювати, з елементами GUI, оскільки даний метод викликається з Event Dispatch Thread ("рідного" для SWINGа - потоку надсилання подій).

Метод done() (protected void done()) викликається після завершення виконання методу doInBackground(). Тобто, "довга" робота скінчена, ми знову повертаємося в Event Dispatch Thread. І якщо, ми щось хочемо змінити в інтерфейсі, після "довгої" роботи слід перевизначити метод done().

Ну і звичайно, не слід забувати про метод execute(). Який і запускає виконнаня в окремому робочому потоці.

Також, потрібно підкреслити, що об'єкт даного класу, скажімо так, для одноразового використання. Ще один нюанс, даного класу: максимальна кількість потоків - 10. Це означає, що за допомогою класу SwingWorker паралельно можуть виконуватися тільки 10 фонових задач. Якщо потоків створено більше, SwingWorker, ставить нові потоки в чергу очікування.

Тобто, нічого, складного в класі SwingWorker немає. А головне, що даний клас сам потурбується про переключення потоків.

Ось простенький, код, де можна побачити, як застосовувати SwingWorker:

import java.util.List;

import java.awt.FlowLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

import javax.swing.JFrame;
import javax.swing.JButton;
import javax.swing.JLabel;
import javax.swing.SwingUtilities;
import javax.swing.SwingWorker;

public class Example extends JFrame {

    private JLabel label = new JLabel();

    public Example() {
        setTitle("Тест SwingWorker");
        setDefaultCloseOperation(EXIT_ON_CLOSE);
        JButton button = new JButton("Виконання");
        button.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent arg0) {
                new MyWork().execute();
            }
        });
        setLayout(new FlowLayout());
        add(button);
        add(label);
        setSize(300, 300);
        setLocationRelativeTo(null);
        setVisible(true);
    }

    public static void main(String[] args) {
        SwingUtilities.invokeLater(new Runnable() {
            @Override
            public void run() {
                new Example();
            }
        });
    }

    class MyWork extends SwingWorker<Void, String> {

        // даний метод запускається в окремому потоці, не в Event Dispatch
        // Thread, тут виконується фонова задача
        @Override
        protected Void doInBackground() throws Exception {
            int i = 0;
            // надсилання проміжкових результатів
            publish("Підраховуємо i");
            while (i < Integer.MAX_VALUE) {
                i++;
            }
            // надсилання проміжкових результатів
            publish("i=" + i);
            Thread.sleep(1000);
            // надсилання проміжкових результатів
            publish("Заснули");
            Thread.sleep(5000);
            // надсилання проміжкових результатів
            publish("Прокинулися");
            Thread.sleep(1000);
            return null;
        }

        // ось цей метод запускається в Event Dispatch Thread, для обробки
        // проміжкових результатів
        @Override
        protected void process(List<String> chunks) {
            for (String x : chunks) {
                label.setText(x);
            }
        }

        // даний метод запускається в Event Dispatch Thread, після виконання
        // методу doInBackground()
        @Override
        protected void done() {
            label.setText("");
        }
    }
}

P.S. Ну і звичайно, давайте, не забувати користуватися офіційною документацією Class SwingWorker<T,V>
P.P.S. Звичайно, дана стаття призначена, для початківців, якщо в когось є якісь доповнення, буду радий їх почитати.

Подякували: leofun01, Replace, Lujok3

2

Re: SwingWorker (виконання довгих розрахунків в SWINGу)

Частина II

Ще при написанні статті, вагався, писати "по мінімуму" чи описати "не обов'язкові методи". Вибрав перший варіант, реалізація SwingWorker "по мінімуму". Проте, згодом, все ж таки, вирішив зробити опис деяких "цікавих" методів.

public final T get() throws InterruptedException, ExecutionException
public final boolean cancel(boolean mayInterruptIfRunning)
protected final void setProgress(int progress)
public final int getProgress()

T get() - повертає результат роботи методу T doInBackground(). Тобто, якщо ми хочемо отримати посиланя на об'єкт, який повертає метод T doInBackground(), то слід викликати метод T get(). Проте слід пам'ятати, що при виклику метода T get() блокується потік, з якого даний метод викликаний, доки не буде отриманий результат. Тому викликати даний метод в Event Dispatch Thread, НЕ ПОТРІБНО, оскільки, тоді, не має ніякого сенсу реалізовувати, Ваш клас SwingWorker. Найкраще, викликати метод T get() в методі void done().

boolean cancel(boolean mayInterruptIfRunning) - що ж робити, якщо користувач, спочатку, запустив "довгий" процес, а згодом, чекати стало не цікаво і ВИРІШИВ ЗУПИНИТИ ЙОГО. Ось тут, даний, метод стане в нагоді. Тобто, набираймо:

cancel(true);

Але, як ми дізнаємось, чи "важка" робота, дійсно, скінчилася? А, досить легко, як тільки ми вказуємо на зупинку виконання нашої задачі, метод T get() згенерує виключення CancellationException, яке нам слід обробити.

void setProgress(int progress) та int getProgress() - дані методи застосовуються, для задавання та отримання "індикатора виконанної роботи". Тобто, нічого, важкого не має, головне слід запам'ятати void setProgress(int progress) викликається в робочому потоці (метод T doInBackground()), а int getProgress() - в Event Dispatch Thread (методи void process(List<V> chunks) та void done())

Давайте, тепер пройдемося по коду (прикладу). Код розділимо на частинки, щоб легше сприймався, а в кінці статті буде повний код.

1. Спочатку створюємо фрейм з двома кнопками (старт/стоп), індикатором виконаної роботи, та однієї лейбочкою, де будем писати, як виконується робота. Кнопка "Зупинити", спочатку не активна, оскільки не має, що зупиняти  :) :

import java.awt.BorderLayout;

import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JButton;
import javax.swing.JProgressBar;
import javax.swing.JLabel;
import javax.swing.SwingConstants;

public class TestWorker extends JFrame {

    private JButton butStart = new JButton("Виконати");
    private JButton butStop = new JButton("Зупинити");
    private JLabel label = new JLabel("", SwingConstants.CENTER);
    private JProgressBar progress = new JProgressBar();

    public TestWorker() {
        setTitle("Тест SwingWorker");
        setDefaultCloseOperation(EXIT_ON_CLOSE);
        add(label, BorderLayout.PAGE_START);
        JPanel panel = new JPanel();
        panel.add(butStart);
        panel.add(butStop);
        butStop.setEnabled(false);
        add(panel);
        add(progress, BorderLayout.PAGE_END);
        progress.setStringPainted(true);
        setSize(300, 200);
        setLocationRelativeTo(null);
        setVisible(true);
    }

    public static void main(String[] args) {
        SwingUtilities.invokeLater(new Runnable() {
            @Override
            public void run() {
                new TestWorker();
            }
        });
    }
}

2. Ось тепер, головне, пишемо свій клас, який наслідується від SwingWorker - MyWork.
а) Реалізуєм, метод doInBackground(). Спочатку виставляємо індикатор виконаної роботи=0, потім відправляєм сповіщення, що робота розпочалась, далі йде виконання роботи+сповіщення про виконання і в кінці отримуємо об'єкт класа String, проте, що робота успішно виконана:

@Override
protected String doInBackground() throws Exception {
    setProgress(0);
    publish("Виконання роботи розпочато");
    Thread.sleep(1000);
    publish("Довгенька робота");
    for (int i = 0; i <= 100; i += 10) {
        setProgress(i);
        publish();
        Thread.sleep(500);
    }
    return "Робота успішно виконана";
}

б) Перевизначемо метод process(). Ось тут, ми, будем періодично вносити зміни в наш GUI:

@Override
protected void process(List<String> chunks) {
    for (String x : chunks) {
        label.setText(x);
    }
    progress.setValue(getProgress());
}

в) Перевизначемо метод done(). Даний, метод викликається, по завершенню виконнання метода doInBackground(), автоматично. І ось тут, ми маєм два варіанти: Робота виконана -  все добре, в лейбочку записуєм стрічку, яку отримаєм при виклику метода get(), інша справа, якщо користувач, вирішив завершити роботу завчасно. Тоді при виклику метода get(), ми отримає виключення класа CancellationException, при обробці зазначаєм в лейбочці, що виконання роботи зупинено.:

@Override
protected void done() {
    try {
        label.setText(get());
    } catch (CancellationException e) {
        progress.setValue(0);
        label.setText("Виконання роботи зупинено");
    } catch (Exception e) {
        e.printStackTrace();
    }
    butStop.setEnabled(false);
    butStart.setEnabled(true);
}

Ось повний код класу MyWork:

Прихований текст

class MyWork extends SwingWorker<String, String> {

    @Override
    protected String doInBackground() throws Exception {
        setProgress(0);
        publish("Виконання роботи розпочато");
        Thread.sleep(1000);
        publish("Довгенька робота");
        for (int i = 0; i <= 100; i += 10) {
            setProgress(i);
            publish();
            Thread.sleep(500);
        }
        return "Робота успішно виконана";
    }

    @Override
    protected void process(List<String> chunks) {
        for (String x : chunks) {
            label.setText(x);
        }
        progress.setValue(getProgress());
    }

    @Override
    protected void done() {
        try {
            label.setText(get());
        } catch (CancellationException e) {
            progress.setValue(0);
            label.setText("Виконання роботи зупинено");
        } catch (Exception e) {
            e.printStackTrace();
        }
        butStop.setEnabled(false);
        butStart.setEnabled(true);
    }
}

3. Останні дії. В нас вже все є, треба тільки з'єднати. До кожної кнопки прикручуємо обробника подій, для цього створюємо окремий метод, який будем викликати в конструкторі нашого класу TestWorker (не забуваймо активовувати/деактивовувати кнопки):

private void setListeners() {
    butStart.addActionListener(new ActionListener() {
        @Override
        public void actionPerformed(ActionEvent arg0) {
            butStart.setEnabled(false);
            work = new MyWork();
            work.execute();
            butStop.setEnabled(true);
        }
    });
    butStop.addActionListener(new ActionListener() {
        @Override
        public void actionPerformed(ActionEvent arg0) {
            work.cancel(true);
        }
    });
}

Повний код:

Прихований текст

import java.util.List;
import java.util.concurrent.CancellationException;

import java.awt.BorderLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JButton;
import javax.swing.JProgressBar;
import javax.swing.JLabel;
import javax.swing.SwingUtilities;
import javax.swing.SwingConstants;
import javax.swing.SwingWorker;

public class TestWorker extends JFrame {

    private JButton butStart = new JButton("Виконати");
    private JButton butStop = new JButton("Зупинити");
    private JLabel label = new JLabel("", SwingConstants.CENTER);
    private JProgressBar progress = new JProgressBar();
    private MyWork work;

    public TestWorker() {
        setTitle("Тест SwingWorker");
        setDefaultCloseOperation(EXIT_ON_CLOSE);
        add(label, BorderLayout.PAGE_START);
        JPanel panel = new JPanel();
        panel.add(butStart);
        panel.add(butStop);
        butStop.setEnabled(false);
        add(panel);
        add(progress, BorderLayout.PAGE_END);
        progress.setStringPainted(true);
        setListeners();
        setSize(300, 200);
        setLocationRelativeTo(null);
        setVisible(true);
    }

    private void setListeners() {
        butStart.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent arg0) {
                butStart.setEnabled(false);
                work = new MyWork();
                work.execute();
                butStop.setEnabled(true);
            }
        });
        butStop.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent arg0) {
                work.cancel(true);
            }
        });
    }

    public static void main(String[] args) {
        SwingUtilities.invokeLater(new Runnable() {
            @Override
            public void run() {
                new TestWorker();
            }
        });
    }

    class MyWork extends SwingWorker<String, String> {

        @Override
        protected String doInBackground() throws Exception {
            setProgress(0);
            publish("Виконання роботи розпочато");
            Thread.sleep(1000);
            publish("Довгенька робота");
            for (int i = 0; i <= 100; i += 10) {
                setProgress(i);
                publish();
                Thread.sleep(500);
            }
            return "Робота успішно виконана";
        }

        @Override
        protected void process(List<String> chunks) {
            for (String x : chunks) {
                label.setText(x);
            }
            progress.setValue(getProgress());
        }

        @Override
        protected void done() {
            try {
                label.setText(get());
            } catch (CancellationException e) {
                progress.setValue(0);
                label.setText("Виконання роботи зупинено");
            } catch (Exception e) {
                e.printStackTrace();
            }
            butStop.setEnabled(false);
            butStart.setEnabled(true);
        }
    }
}

Подякували: Lujok, leofun012