WELCOME to the Java Developer ConnectionSM
(JDC) Tech Tips, September 12, 2000. This issue covers:
This issue of the JDC Tech Tips is written by Glen McCluskey.
These tips were developed using JavaTM 2 SDK,
Standard Edition, v 1.3.
USING CLASS METHODS AND VARIABLES
Suppose that you're designing a Java class to do some type of time
scheduling. One of the things you need within the class is a list
of the number of days in each month of the year. Another thing you
need is a method that determines if a given calendar year (like
1900) is a leap year. The features might look like this:
class Schedule {
private int days_in_month[] =
{31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
private boolean isLeapYear(int year) {
if (year % 4 != 0) {
return false;
}
if (year % 400 == 0) {
return true;
}
return (year % 100 != 0);
}
}
Implementing the class in this way will work, but there's a better
way to structure the list and the method.
Consider that a table of the number of days in each month is a
fixed set of values that does not change. In other words, January
always has 31 days. In the class above, each instance (object) of
Schedule
will contain the same table of 12 values.
This duplication is wasteful of space, and it gives the false
impression that the table is somehow different in each object,
even though it's not. A better way to structure the table is like
this:
private static final int days_in_month[] =
{31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
The "final" keyword means that the table does not change after
initialization, and the "static" keyword means that there is
a single copy of the table shared across all instances of the
class. A variable declared this way is called a "class variable"
or "static field," as contrasted with "instance variable."
In a similar way, consider that the isLeapYear
method does not
actually do anything with object-specific data. It simply accepts
an integer parameter representing a year, and returns a true/false
value. So it would be better to say:
private static boolean isLeapYear(int year) {
if (year % 4 != 0) {
return false;
}
if (year % 400 == 0) {
return true;
}
return (year % 100 != 0);
}
This is an example of a "class method" or "static method".
There are several interesting points to note about class methods
and variables, some of them obvious, some not. The first point is
that a class variable exists even if no instances of the class
have been created. For example, if you have:
class A {
static int x;
}
You can say:
int i = A.x;
in your program, whether or not you have created any A objects.
Another point is that class methods do not operate on specific
objects, so there's no "this" reference within such methods:
class A {
static int x = 37;
int y = 47;
static void f() {
A aref = this; // invalid
int i = x; // valid
int j = y; // invalid
g(); // invalid
}
void g() {
A aref = this; // valid
int i = x; // valid
int j = y; // valid
f(); // valid
}
}
The invalid cases in the previous example are ones that require
an object. For example, "y" is an instance variable within objects
of type A. Because the static method f does not operate on a
particular object of A, there's no y field to access.
When you access a static field outside of its class, you need to
qualify it with the class name:
class A {
public static int x = 37;
}
class B {
static int i = A.x;
}
A less desirable but legal form of qualification looks like this:
class A {
public static int x = 37;
}
class B {
A aref = new A();
int i = aref.x;
}
This usage gives the false impression that "x" is an instance
variable of an object of A. It's possible to take such usage even
further, and say:
class A {
public static int x = 37;
}
class B {
A aref = null;
int i = aref.x;
}
This usage is legal and will not trigger an exception; since x is
not an instance variable, there is no actual need to access the
object referenced by aref
.
Given these details of how class methods and variables work, where
would you want to use them? One type of usage was illustrated
above with the Schedule
class -- you have some common data shared
by all objects of a particular type. Or perhaps you have some
methods that operate only on class variables. Maybe the methods
don't operate on class data at all, but are somehow related to the
function of the class; the isLeapYear
method illustrates this form
of usage.
You can also use class methods and variables as a packaging
technique. Suppose you have some legacy code that you would like
to convert. Imagine that the code uses some global variables.
Typically you don't want to use global variables (it's impossible
to do so with the Java language). But you'd like to come up with
an equivalent, to help the conversion process along. Here's one
way you can structure the code:
public class Globals {
private Globals() {}
public static int A = 1;
public static int B = 2;
public static int C = 3;
}
Using this class, you have three "pseudo globals" with names
Globals.A, Globals.B, and Globals.C,
which you can use throughout
your application. The private constructor for Globals
emphasizes
that the class is being used simply as a packaging vehicle. It's
not legal to actually create instances of the class.
This particular structuring technique is not always desirable,
because it's easy to change field values from all over your code.
An alternative approach is to make the static fields private, and
allow changes to them only through accessor methods. Using this
approach, you can more readily trap field changes. Here's an
example:
public class Globals {
private Globals() {}
private static int A = 1;
public static void setA(int i) {A = i;}
public static int getA() {return A;}
}
You can also use a class to package methods. For example, two of
the class methods in java.lang.System
are:
arraycopy
currentTimeMillis
These really don't have anything to do with each other, except
that they're both low-level system methods that provide services
to the user. A set of such methods are grouped together in the
System
class.
A final use of class variables is to group together a set of
constants:
public class Constants {
private Constants() {}
public static final int A = 1;
public static final int B = 2;
public static final int C = 3;
}
You can do a similar thing with an interface:
public interface Constants {
int A = 1;
int B = 2;
int C = 3;
}
What are the differences between using classes and interfaces to
group constants? Here are several:
1. Interface fields are implicitly public, static, and final.
2. You cannot change an interface field once it's initialized.
By contrast, you can change a field in a class if the field is
non-final; if you're really establishing a set of constants,
you probably don't want to do this.
3. You can use static initialization blocks to set up the fields
in a class. For example:
class Constants {
public static final int A;
static {
A = 37;
}
}
4. You can implement an interface in a class to gain convenient
access to the interface's constants.
For further information about class methods and class variables,
see sections 2.2.2, Static Fields, and 2.6.1, Static Methods in
"The Java Programming Language Third Edition" by Arnold, Gosling,
and Holmes
(http://java.sun.com/docs/books/javaprog/thirdedition/).
USING PROGRESS BARS AND MONITORS IN JAVA GUI APPLICATIONS
If you have a GUI application that performs a time-consuming task,
it's desirable to let the user know that the task is being
processed. It's also a good idea to give the user a progress
indicator, such as "the task is X% finished."
The Java Swing
library has a couple of mechanisms for displaying
progress. This tip examines them in the context of a real-life
application. The application is one that searches for a string in
all files under a starting directory. For example, if you're on
a UNIX system and you specify "/usr
" as the starting directory,
and a pattern "programming", the application displays a list of
all the files that contain "programming" somewhere within them.
This application is time-consuming. It can take a few seconds
to a few minutes to run, depending on how big the directory
structure is and how fast your computer runs.
The search process has two distinct phases. The first iterates
across the directory structure and makes a list of all the files.
The second phase actually searches the files.
It's not possible in the strictest sense to indicate progress
during the first phase. Progress is based on percentage complete.
Here there's no way to obtain the percentage completed, because
it's not possible to tell in advance how many files are in the
directory. In the second phase, however, it's possible to
get at least a rough idea of progress. The program can determine
that, for example, 59 out of 147 files have been searched so far.
The application code looks like this:
import java.awt.GridLayout;
import java.awt.Cursor;
import java.awt.event.*;
import java.util.*;
import java.io.*;
import javax.swing.*;
import java.lang.reflect.InvocationTargetException;
public class ProgressDemo {
String startdir; // starting directory for search
String patt; // pattern to search for
JTextArea outarea; // output area for file pathnames
JFrame frame; // frame
JProgressBar progbar; // progress bar
JLabel fileslab; // number of files found
boolean search_flag; // true if search in progress
// nested class used to do actual searching
class Search extends Thread {
// do GUI updates
void doUpdate(Runnable r) {
try {
SwingUtilities.invokeAndWait(r);
}
catch (InvocationTargetException e1) {
System.err.println(e1);
}
catch (InterruptedException e2) {
System.err.println(e2);
}
}
// get a list of all the files under a given directory
void getFileList(File f, List list) {
// recurse if a directory
if (f.isDirectory()) {
String entries[] = f.list();
for (int i = 0; i < entries.length; i++) {
getFileList(new File(f, entries[i]),
list);
}
}
// for plain files, add to list and
// update progress bar
else if (f.isFile()) {
list.add(f.getPath());
final int size = list.size();
if (size % 100 != 0) {
return;
}
doUpdate(new Runnable() {
public void run() {
progbar.setValue(size % 1000);
}
});
}
}
// check whether a file contains the specified pattern
boolean fileMatch(String fn, String patt) {
boolean found = false;
try {
FileReader fr = new FileReader(fn);
BufferedReader br = new BufferedReader(fr);
String str;
while ((str = br.readLine()) != null) {
if (str.indexOf(patt) != -1) {
found = true;
break;
}
}
br.close();
}
catch (IOException e) {
System.err.println(e);
}
return found;
}
// perform the search
public void run() {
List filelist = new ArrayList();
final String sep =
System.getProperty("line.separator");
// clear old output
doUpdate(new Runnable() {
public void run() {
outarea.setText("");
fileslab.setText("");
}
});
// get the list of files and display a count
getFileList(new File(startdir), filelist);
final int size = filelist.size();
doUpdate(new Runnable() {
public void run() {
progbar.setValue(0);
fileslab.setText("Found " + size +
" files, now searching ...");
}
});
// set up a progress monitor
final ProgressMonitor pm = new ProgressMonitor(
frame, "Searching files", "", 0, size - 1);
pm.setMillisToDecideToPopup(0);
pm.setMillisToPopup(0);
// iterate across the files, updating
// the progress monitor
for (int i = 0; i < size; i++) {
final String fn = (String)filelist.get(i);
final int curr = i;
if (pm.isCanceled()) {
break;
}
final boolean b = fileMatch(fn, patt);
doUpdate(new Runnable() {
public void run() {
pm.setProgress(curr);
pm.setNote(fn);
if (b) {
outarea.append(fn + sep);
}
}
});
}
// close the progress monitor and
// set the caret position in the output
// area to the beginning of the file list
doUpdate(new Runnable() {
public void run() {
pm.close();
outarea.setCaretPosition(0);
fileslab.setText("");
}
});
search_flag = false;
}
}
public ProgressDemo() {
frame = new JFrame("ProgressDemo");
// set up the window closer for the frame
frame.addWindowListener(new WindowAdapter() {
public void windowClosing(WindowEvent e) {
System.exit(0);
}
});
// set up panels
JPanel paneltop = new JPanel();
JPanel panelbot = new JPanel();
paneltop.setLayout(new GridLayout(5, 1));
JPanel panel1 = new JPanel();
panel1.add(new JLabel("Starting Directory"));
final JTextField dirfield = new JTextField(20);
panel1.add(dirfield);
JPanel panel2 = new JPanel();
panel2.add(new JLabel("Search Pattern"));
final JTextField pattfield = new JTextField(20);
panel2.add(pattfield);
JPanel panel3 = new JPanel();
JButton button = new JButton("Search");
panel3.add(button);
JPanel panel4 = new JPanel();
progbar = new JProgressBar(0, 999);
panel4.add(progbar);
JPanel panel5 = new JPanel();
fileslab = new JLabel();
panel5.add(fileslab);
JPanel panel6 = new JPanel();
outarea = new JTextArea(8, 40);
outarea.setEditable(false);
JScrollPane jsp = new JScrollPane(outarea,
ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED,
ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
panel6.add(jsp);
// processing for "Search" button
button.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
startdir = dirfield.getText();
patt = pattfield.getText();
if (startdir == null ||
startdir.trim().equals("") ||
patt == null ||
patt.trim().equals("")) {
JOptionPane.showMessageDialog(
frame, "Invalid input", "Error",
JOptionPane.ERROR_MESSAGE);
}
else if (search_flag) {
JOptionPane.showMessageDialog(
frame, "Search in progress",
"Error", JOptionPane.ERROR_MESSAGE);
}
else {
search_flag = true;
new Search().start();
}
}
});
paneltop.add(panel1);
paneltop.add(panel2);
paneltop.add(panel3);
paneltop.add(panel4);
paneltop.add(panel5);
panelbot.add(panel6);
JPanel panel = new JPanel();
panel.setLayout(new GridLayout(2, 1));
panel.add(paneltop);
panel.add(panelbot);
// display the frame
frame.getContentPane().add(panel);
frame.pack();
frame.setLocation(200, 200);
frame.setVisible(true);
}
public static void main(String args[]) {
new ProgressDemo();
}
}
The main method creates an object of type ProgressDemo
. This part
of the application sets up the various panels, input areas, and
buttons.
The action listener for the Search
button validates input.
It then creates and starts a thread of type Search
. Search
is an inner class used to do the actual searching.
Searching is done via a separate thread because searching is time-consuming,
and it's a bad idea to perform lengthy processing from the event dispatch
thread. The event dispatch thread is used to handle the Search
button selection and the call of the button's action listener. If
the actual search is also performed in this thread, the thread
cannot immediately respond to other events. An example of another
event might be clicking on the top right of the main window to
terminate the application.
The actual searching is done after control is transferred to the
run method of the Search
class. One piece of code you'll see
repeatedly in this part of the code is:
doUpdate(new Runnable() {
public void run() {
...
}
});
Although searching is not done from the event dispatch thread,
it is desirable that the event dispatch thread be used to update
the GUI. The repeated code above is used because Swing
is not
thread-safe. The code adds a runnable object to the event
dispatch thread queue. The run method for the object is called
when the object gets to the front of the queue.
The doUpdate
method is implemented using
SwingUtilities.invokeAndWait
. This means that doUpdate
does not return until the run method returns. It's also possible to use
SwingUtilities.invokeLater
here, but using invokeAndWait
makes for smoother GUI updating of the progress bar.
The list of files to search is accumulated by doing a recursive
directory walk, using java.io.File
. Because the program doesn't
know how many files there are, it can't indicate a percentage
complete; instead it repeatedly fills a JProgressBar
object.
An object of type JProgressBar
is initially created, with limits
in the range 0 to 999. The bar is updated as files are found
during the directory walk. How "full" the bar is depends on the
result of applying the modulo (%) operator on the count of number
of files. In other words, the progress bar is empty with a value
of 0, and full with a value of 999. If the program finds 500 or
1500 or 2500 files thus far, the bar is half full. This scheme
doesn't indicate a percentage complete, but simply that the
directory enumeration is "doing something".
After tabulating the list of files, a ProgressMonitor
object is
created. You specify a message to be displayed during the
operation (here it's "Searching files", a note describing the
state of the operation (in this example, it's null), and the
minimum and maximum values for this object (here it's
0, and the number of files - 1, respectively). Then
setProgress
(currentvalue) is called to indicate progress has been
made. The logic in the ProgressMonitor
class determines whether
to pop up a display showing how far along the processing is.
Because the program knows the number of files to be searched,
this approach works pretty well.
As each file is searched, ProgressDemo
calls setProgress
and setNote
. These
ProgressMonitor
methods periodically update
the display as progress is being made. Note that the progress
monitor might not display if the searching to be done is very
short. ProgressMonitor
has methods you can call to tailor
the amount of time before the monitor pops up".
Another approach for the progress monitor would be to keep track
of file lengths instead of file counts. This approach is slightly
more complicated, but gives a better indication of progress.
This is especially true if you're searching files of widely
varying lengths. For example, say you have 20 files. The first
10 are one byte long, and the last 10 are each one million bytes
long. In this case, the progress monitor display will be
misleading if it's based on file counts.
There's also a class ProgressMonitorInputStream
designed
specifically for indicating progress while reading files.
A good place to read more about progress bars and progress
monitors is "Graphic Java - Mastering the JFC 3rd Edition
Volume II Swing" by David Geary. See especially "Swing and
Threads" in Chapter 2, and "Progress Bars, Sliders, and
Separators" in Chapter 11.
Note
The names on the JDCSM
mailing list are used for internal Sun MicrosystemsTMpurposes only. To remove your name from the list,
see Subscribe/Unsubscribe below.
Feedback
Comments? Send your feedback on the JDC Tech Tips to: jdc-webmaster@sun.com
Subscribe/Unsubscribe
The JDC Tech Tips are sent to you because you elected to subscribe.
To unsubscribe from this and any other JDC Email, select
"Subscribe to free JDC newsletters" on the JDC front page. This
displays the Subscriptions page, where you can change the current
selections.
You need to be a JDC member to subscribe to the Tech Tips. To
become a JDC member, go to:
http://java.sun.com/jdc/
To subscribe to the Tech Tips and other JDC Email, select
"Subscribe to free JDC newsletters" on the JDC front page.
Archives
You'll find the JDC Tech Tips archives at:
http://developer.java.sun.com/developer/TechTips/index.html
Copyright
Copyright 2000 Sun Microsystems, Inc. All rights reserved.
901 San Antonio Road, Palo Alto, California 94303 USA.
This Document is protected by copyright. For more information, see:
http://developer.java.sun.com/developer/copyright.html