Timers are used to perform actions periodically, after an initial delay, or both. Also, actions can be performed only once, if desired.
Here are some reminders regarding the two flavors of timers, as used in Swing applications :
- suitable for simpler cases, using low numbers of timers (say less than a dozen)
- runs ActionListener objects on the event dispatch thread
- can directly update the GUI, without using EventQueue.invokeLater
- if the task runs entirely in the event dispatch thread (that is, if it does not spawn a worker thread), then the GUI will remain responsive only if the task does not take very long (say under 300 milliseconds)
- more scalable than javax.swing.Timer, and with additional scheduling features
- runs TimerTask objects on a private thread
- needs to use EventQueue.invokeLater to update the GUI
For a simple example using java.util.Timer and java.util.TimerTask in a non-Swing context, please see this related topic.
Example 2
Here, an About Box includes a JLabel which displays the current object heap size. This JLabel is updated every few seconds, to reflect any activity of the garbage collector. (Note that the timer itself does not itself request garbage collection.)
This task is not an intensive one (see getHeapSize below),
so it may be performed directly on the event dispatch thread without causing
the GUI to become unresponsive. That is, a Swing timer may be used.
package hirondelle.stocks.help; import java.awt.event.*; import java.awt.*; import javax.swing.*; import java.util.*; import java.util.logging.*; import hirondelle.stocks.util.Consts; import hirondelle.stocks.util.Args; import hirondelle.stocks.util.FileUtil; import hirondelle.stocks.util.ui.UiConsts; import hirondelle.stocks.util.ui.UiUtil; import hirondelle.stocks.preferences.LoggingPreferencesEditor; import hirondelle.stocks.util.Util; /** * Display a modal dialog, centered on the main window, which * contains general information about both this application and * the system on which it is running. * *<P> The system information includes a running snapshot of the * object heap size. A button is provided to coax the JVM to * perform garbage collection. */ public final class AboutAction extends AbstractAction { /** * Constructor. * * @param aFrame parent window to which this dialog is attached. */ public AboutAction(JFrame aFrame) { super("About " + Consts.APP_NAME); Args.checkForNull(aFrame); fFrame = aFrame; putValue(SHORT_DESCRIPTION, "About this application"); putValue(LONG_DESCRIPTION, "Displays details regarding the StocksMonitor application."); putValue(MNEMONIC_KEY, new Integer(KeyEvent.VK_A) ); } public void actionPerformed(ActionEvent e) { fLogger.info("Showing the about box."); showAboutBox(); } // PRIVATE // private JFrame fFrame; /** * Displays the size of the object heap. * * In a typical GUI application, the breakdown for memory consumption is roughly * <ul> * <li> classes - 70% * <li> objects - 15% * <li> other - 15% *</ul> * * There is no API for displaying total memory use. The memory consumed by objects, * however, is available, and is displayed by this label; the display is updated * periodically. */ private JLabel fObjectHeapSize; /** Periodically updates the display of <tt>fObjectHeapSize</tt>. */ private javax.swing.Timer fTimer; private ActionListener fHeapSizeUpdater; private static final int UPDATE_FREQ = 2 * Consts.MILLISECONDS_PER_SECOND; private static final long SLEEP_INTERVAL = 100; private static final String ABOUT_TEXT_FILE = "About.txt"; private static final Dimension ABOUT_TEXT_SIZE = new Dimension(100,250); private static final Logger fLogger = Util.getLogger(AboutAction.class); private void showAboutBox(){ JTabbedPane aboutPane = new JTabbedPane(); aboutPane.addTab( "About" , getAboutPanel() ); aboutPane.setMnemonicAt(0, KeyEvent.VK_A); aboutPane.addTab( "System Info" , getSystemInfoPanel() ); aboutPane.setMnemonicAt(1, KeyEvent.VK_S); startHeapSizeTimer(); Icon image = UiUtil.getImageIcon("xray-small.jpg", this.getClass()) ; String title = UiUtil.getDialogTitle("About"); JOptionPane.showMessageDialog(fFrame, aboutPane, title, JOptionPane.OK_OPTION, image); stopHeapSizeTimer(); } private JComponent getAboutPanel(){ JPanel aboutPanel = new JPanel(); aboutPanel.setLayout( new BoxLayout(aboutPanel, BoxLayout.Y_AXIS) ); String appNameAndVersion = Consts.APP_NAME + Consts.SPACE + Consts.APP_VERSION; String text = appNameAndVersion + Consts.NEW_LINE + getAboutFileContents(); JTextArea aboutText = UiUtil.getStandardTextAreaHardNewLines( text ); JScrollPane scrollPane = new JScrollPane(aboutText); scrollPane.setPreferredSize( ABOUT_TEXT_SIZE ); aboutPanel.add( scrollPane ); return aboutPanel; } private String getAboutFileContents(){ java.io.InputStream contents = AboutAction.class.getResourceAsStream(ABOUT_TEXT_FILE); return FileUtil.getStreamAsString(contents); } private JComponent getSystemInfoPanel(){ JPanel infoPanel = getStandardPanel(); Map<String, String> info = new HashMap<String, String>(); addSysProperty(info, "Java Version","java.version"); addSysProperty(info, "Java VM", "java.vm.info"); addSysProperty(info, "Java Home", "java.home"); addSysProperty(info, "Java Vendor", "java.vendor"); addSysProperty(info, "User Current Directory", "user.dir" ); addSysProperty(info, "User Home Directory", "user.home" ); StringBuilder osInfo = new StringBuilder(); osInfo.append( getProperty("os.arch") ); osInfo.append( Consts.SPACE); osInfo.append( getProperty("os.name") ); osInfo.append( Consts.SPACE); osInfo.append( getProperty("os.version") ); info.put("Operating System", osInfo.toString()); LoggingPreferencesEditor loggingPrefs = new LoggingPreferencesEditor(); info.put("Logging Config File", loggingPrefs.getLogConfigFile().toString() ); //Note that a HashMap is used to populate the tree (bit faster), but a TreeMap is the //final version presented to the user (bit slower, but has desired iteration order) UiUtil.addSimpleDisplayFields( infoPanel, new TreeMap<String, String>(info) ); fObjectHeapSize = UiUtil.addSimpleDisplayField( infoPanel, "Object Heap Size", getHeapSize(), UiUtil.getConstraints(8,0), true ); fObjectHeapSize.setToolTipText("Total memory consumption is much larger"); addGarbageCollectionButton(infoPanel); return infoPanel; } private void addGarbageCollectionButton(JPanel aInfoPanel){ JButton collectGarbage = new JButton("Collect Garbage"); collectGarbage.setToolTipText("Request garbage collection by JVM"); collectGarbage.setMnemonic(KeyEvent.VK_C); collectGarbage.addActionListener( new ActionListener() { public void actionPerformed(ActionEvent event) { putOutTheGarbage(); updateHeapSizeDisplay(); } }); GridBagConstraints constraints = UiUtil.getConstraints(9,1); constraints.insets = new Insets(UiConsts.ONE_SPACE, 0,0,0); aInfoPanel.add(collectGarbage, constraints ); } /** * Return a <tt>JPanel</tt> which has a <tt>GridBagLayout</tt>, and * a border as specified in {@link UiUtil#getStandardBorder}. */ private JPanel getStandardPanel(){ JPanel result = new JPanel(); result.setLayout( new GridBagLayout() ); result.setBorder( UiUtil.getStandardBorder() ); return result; } private void updateHeapSizeDisplay(){ fLogger.fine("Updating heap size..."); fObjectHeapSize.setText( getHeapSize() ); } /** Return a measure of the current heap size in kilobytes. */ private String getHeapSize(){ long totalMemory = Runtime.getRuntime().totalMemory(); long freeMemory = Runtime.getRuntime().freeMemory(); Long memoryUseKB = new Long( (totalMemory - freeMemory)/Consts.ONE_KILOBYTE ); StringBuilder result = new StringBuilder(); result.append( UiUtil.getLocalizedInteger(memoryUseKB) ); result.append(" KB"); return result.toString(); } /** Periodically update the display of object heap size. */ private void startHeapSizeTimer(){ fHeapSizeUpdater = new ActionListener() { public void actionPerformed(ActionEvent evt) { updateHeapSizeDisplay(); } }; fTimer = new javax.swing.Timer(UPDATE_FREQ, fHeapSizeUpdater); fTimer.start(); fLogger.fine("Starting timer..."); } /** * Must be called when the About Box is closed - otherwise the timer will continue * to operate. */ private void stopHeapSizeTimer(){ fLogger.fine("Stopping timer..."); fTimer.stop(); //stops notifying registered listeners fTimer.removeActionListener(fHeapSizeUpdater); //removes the one registered listener fHeapSizeUpdater = null; fTimer = null; } private static void putOutTheGarbage() { collectGarbage(); collectGarbage(); } private static void collectGarbage() { try { System.gc(); Thread.currentThread().sleep(SLEEP_INTERVAL); System.runFinalization(); Thread.currentThread().sleep(SLEEP_INTERVAL); } catch (InterruptedException ex){ ex.printStackTrace(); } } private void addSysProperty(Map<String, String> aMap, String aKey, String aPropertyName){ aMap.put(aKey, getProperty(aPropertyName)); } private String getProperty(String aName){ return System.getProperty(aName); } }
Example 3
This example is unusual, in that it uses a javax.swing.Timer to perform a task which is not short lived. This is acceptable, however, since this Timer is combined with a worker thread, and most of the task is not performed on the event dispatching thread. (Note the construction of the Timer at the end of the constructor using a this reference, and the actionPerformed method.)
This is an example of a common requirement - that of performing an intensive,
periodic task, using both a timer and worker thread. Another example would
be a graphical network management application, which polls the state of
many devices every few minutes, and then updates the user interface with
the results.
package hirondelle.stocks.quotes; import java.awt.event.*; import java.awt.*; import javax.swing.*; import java.util.logging.*; import java.util.*; import java.text.MessageFormat; import hirondelle.stocks.util.Consts; import hirondelle.stocks.util.Util; import hirondelle.stocks.util.Args; import hirondelle.stocks.util.DataAccessException; import hirondelle.stocks.util.ui.UiConsts; import hirondelle.stocks.util.ui.UiUtil; import hirondelle.stocks.portfolio.CurrentPortfolio; import hirondelle.stocks.table.QuoteTable; import hirondelle.stocks.preferences.QuoteTablePreferencesEditor; /** * Fetch current quote data for the {@link CurrentPortfolio} from a data * source on the web. * * <P>This action is performed at many different times : *<ul> * <li>once upon startup * <li>periodically, at an interval which is configured by the user * <li>when the end user explicitly requests a refresh of quote info * <li>when the user makes a change to the current portfolio *</ul> * * <P>This class performs most of its work in a background daemon thread, and uses * <tt>EventQueue.invokeLater</tt> to update the user interface with the result. * The user interface remains responsive, regardless of the time taken for its * work to complete. * * <P>A daemon thread is used since daemon threads do not prevent an application * from terminating. */ public final class FetchQuotesAction extends AbstractAction implements Observer { /** * Constructor. * * @param aCurrentPortfolio an <tt>Observable</tt> which notifies this * object when the {@link CurrentPortfolio} is changed * @param aQuoteTablePrefEditor allows this class to read the user preference * for the frequency of periodic fetches * @param aQuoteTable a GUI element which is updated when a fetch is performed * @param aSummaryView a GUI element which is updated when a fetch is performed */ public FetchQuotesAction ( CurrentPortfolio aCurrentPortfolio, QuoteTablePreferencesEditor aQuoteTablePrefEditor, QuoteTable aQuoteTable, SummaryView aSummaryView ) { super("Update", UiUtil.getImageIcon("/toolbarButtonGraphics/general/Refresh")); Args.checkForNull(aQuoteTable); Args.checkForNull(aSummaryView); fCurrentPortfolio = aCurrentPortfolio; fCurrentPortfolio.addObserver(this); fQuoteTablePrefEditor = aQuoteTablePrefEditor; fQuoteTablePrefEditor.addObserver(this); fQuoteTable = aQuoteTable; fSummaryView = aSummaryView; putValue(SHORT_DESCRIPTION, "Fetch updated stock quotes from web"); putValue( ACCELERATOR_KEY, KeyStroke.getKeyStroke(KeyEvent.VK_F5, UiConsts.NO_KEYSTROKE_MASK) ); putValue( LONG_DESCRIPTION, "Retrieves fresh stock quotes and displays it to the user in a table." ); putValue(MNEMONIC_KEY, new Integer(KeyEvent.VK_U) ); fUpdateFreq = fQuoteTablePrefEditor.getUpdateFrequency(); fTimer = new javax.swing.Timer(fUpdateFreq * CONVERSION_FACTOR, this); fTimer.start(); } /** Fetch quotes from the web for the <tt>CurrentPortfolio</tt>. */ public void actionPerformed(ActionEvent e) { fLogger.info("Fetching quotes from web."); fSummaryView.showStatusMessage("Fetching quotes..."); Thread worker = new Thread(new HardWorker()); worker.setDaemon(true); worker.start(); } /** * Listens for changes to the <tt>CurrentPortfolio</tt> or the user * preference for update frequency, and calls {@link #actionPerformed}. * * <P>In the case of changes to the update frequency, <tt>actionPerformed</tt> is * called only if the udate frequency has been assigned a new value. */ public void update(Observable aPublisher, Object aData) { fLogger.fine("Notified ..."); if ( aPublisher == fQuoteTablePrefEditor ) { fLogger.fine("By StocksTablePrefEditor..."); boolean hasChangedFreq = fQuoteTablePrefEditor.getUpdateFrequency()!= fUpdateFreq; if ( hasChangedFreq ) restartTimer(); } else { fLogger.fine("By Current Portfolio..."); actionPerformed(null); } } // PRIVATE // private static final Logger fLogger = Util.getLogger(FetchQuotesAction.class); /** * The set of {@link Stock} objects in which the user * is currently interested. */ private CurrentPortfolio fCurrentPortfolio; /** * Edits user preferences attached to a table which presents quote data, and * allows read-only programmatic access to such preferences. */ private QuoteTablePreferencesEditor fQuoteTablePrefEditor; /** * GUI element which is updated whenever a new set of quotes is obtained. */ private QuoteTable fQuoteTable; /** * GUI element which is updated whenever a new set of quotes is obtained. */ private SummaryView fSummaryView; /** * Periodically fetches quote data. * * <P>Use of a Swing Timer is acceptable here, in spite of the fact that the task * takes a long time to complete, since the task does <em>not</em> in fact get * executed on the event-dispatch thread (See below.) */ private javax.swing.Timer fTimer; /** * The number of minutes to wait between fetches of quote information. */ private int fUpdateFreq; private static final int CONVERSION_FACTOR = Consts.MILLISECONDS_PER_SECOND * Consts.SECONDS_PER_MINUTE ; /** * Perform the fetch, and update the GUI elements using the event dispatch * thread. * * <P>Should be run as a daemon thread, such that it never prevents the application * from exiting. */ private final class HardWorker implements Runnable { public void run() { //simulateLongDelay(); try { java.util.List<Quote> quotes = fCurrentPortfolio.getPortfolio().getQuotes(); EventQueue.invokeLater( new GuiUpdater(quotes) ); } catch(DataAccessException ex) { EventQueue.invokeLater( new GuiUpdater(ex) ); } } } /** * Update the user interface after the fetch completes. * * <P>Must be run on the event dispatch thread. If the fetch fails for any reason, * then any current quote data is left as is, and an error message is displayed in * a status message. */ private final class GuiUpdater implements Runnable { GuiUpdater( java.util.List<Quote> aQuotes ){ fQuotes = aQuotes; } GuiUpdater(DataAccessException ex){ //the exception is not used in this implementation } public void run(){ if (fQuotes != null){ fQuoteTable.setQuoteTable( fQuotes ); fSummaryView.setQuotes( fQuotes ); StringBuilder warning = new StringBuilder(); if ( hasNoZeroPrices(warning) ){ fSummaryView.showStatusMessage("Done."); } else { fSummaryView.showStatusMessage(warning.toString()); } } else { fSummaryView.showStatusMessage("Failed - Please connect to the web."); } } private java.util.List<Quote> fQuotes; private MessageFormat fTickerWarningFormat = new MessageFormat("Warning - no price for ticker {0} ({1})") ; private boolean hasNoZeroPrices(StringBuilder aMessage){ for(Quote quote: fQuotes){ if ( Util.isZeroMoney(quote.getPrice()) ) { Object[] params = { quote.getStock().getTicker(), quote.getStock().getExchange() }; aMessage.append(fTickerWarningFormat.format(params)); return false; } } return true; } } /** Use for testing purposes only. */ private void simulateLongDelay(){ try { Thread.sleep(20000); } catch (InterruptedException ex) { System.out.println(ex); } } private void restartTimer(){ fLogger.fine("Resetting initial delay and delay to: " + fUpdateFreq + " minutes."); fUpdateFreq = fQuoteTablePrefEditor.getUpdateFrequency(); fTimer.setInitialDelay(fUpdateFreq * CONVERSION_FACTOR); fTimer.setDelay(fUpdateFreq * CONVERSION_FACTOR); fLogger.fine("Cancelling pending tasks, and restarting timer..."); fTimer.restart(); } }
|