, there is no mixing. Audio engineers will control the values ofa
andb
to create the desired echo effect.Write a threaded program that computes the values in
out
in parallel for an arbitrary number of threads. -
Write a program which takes a number of minutes and seconds as input. In this program, implement a timer using
Thread.sleep()
calls. Each second, print the remaining time to the screen.  -
As you know, π ≈ 3.1416. A more precise value can be found by writing a program which approximates the area of a circle. The area of a circle can be approximated by summing up the area of rectangles filling curve of the arc of the circle. As the width of the rectangle goes to zero, the approximation becomes closer and closer to the true area. If a circle with radius r is centered at the origin, its height y at a particular distance x is given by the following formula.
Write a parallel implementation of this problem which divides up portions of the arc of the circle among several threads and then sums the results after they all finish. By setting r = 2, you need only sum one quadrant of a circle to get π. You’ll need to use a very small rectangle width to get an accurate answer. When your program finishes running, you can compare your value against
Math.PI
for accuracy.
Experiments
-
Use the
currentTimeMillis()
method to measure the time taken to execute a relatively long-running piece of Java code you’ve written. Execute your program several times and compare the execution time you obtain during different executions. Why do you think the execution times are different? -
Thread creation overhead is an important consideration in writing efficient parallel programs. Write a program which creates a large number of threads which do nothing. Test how long it takes to create and join various numbers of threads. See if you can determine how long a single thread creation operation takes on your system, on average.
-
Create serial and concurrent implementations of matrix multiplication like those described in Example 13.11.
-
Experiment with different matrix sizes and thread counts to see how the speedup performance changes. If possible, run your tests on machines with different numbers of cores or processors.
-
Given a machine with k > 1 cores, what is the maximum speedup you can expect to obtain?
-
-
Repeatedly run the code in Example 13.7 which creates several
NumberedThread
objects. Can you discover any patterns in the order that the threads print? Add a loop and some additional instrumentation to theNumberedThread
class which will allow you to measure how long each thread runs before the next thread has a turn. -
Create serial and parallel implementations of the array summing problem solved in Example 13.10. Experiment with different array sizes and thread counts to see how performance changes. How does the speedup differ from matrix multiply? What happens if you simply sum the numbers instead of taking the sine first?
-
The solution to the array summing problem in Example 13.10 seems to use concurrency half-heartedly. After all the threads have computed their sums, the main thread sums up the partial sums sequentially.
An alternative approach is to sum up the partial sums concurrently. Once a thread has computed the sum of the sines of each partition, the sums of each pair of neighboring partitions should be merged into a single sum. The process can be repeated until the final sum has been computed. At each step, half of the remaining threads will have nothing left to do and will stop. The pattern of summing is like a tree which starts with k threads working at the first stage, k/2 working at the second stage, k/4 working at the third, and so on, until a single thread completes the summing process.
Figure 13.8 Example of concurrent tree-style summation with 8 threads.Update the
run()
method in theSumThread
class so that it adds its assigned elements as before and then adds its neighbor’s sum to its own. To do so, it must use thejoin()
method to wait for the neighboring thread. It should perform this process repeatedly. After summing their own values, each even numbered thread should add in the partial sum from its neighbor. At the next step, each thread with a number divisible by 4 should add the partial sum from its neighbor. At the next step, each thread with a number divisible by 8 should add the partial sum from its neighbor, and so on. Thread 0 will perform the final summation. Consequently, the main thread only needs to wait for thread 0. So that each thread can wait for other threads, thethreads
array will need to be a static field. Figure 13.8 illustrates this process.Once you’ve implemented this design, test it against the original
SumThread
class to see how it performs. Restrict the number of threads you create to a power of 2 to make it easier to determine which threads wait and which threads terminate.
14. Synchronization
Sharing is sometimes more demanding than giving.
14.1. Introduction
Concurrent programs allow multiple threads to be scheduled and executed, but the programmer doesn’t have a great deal of control over when threads execute. As explained in Section 13.6, the JVM and the underlying OS are responsible for scheduling threads onto processor cores.
While writing a concurrent program, you have to ensure that the program will work correctly even though different executions of the same program will likely lead to different sequences of thread execution. The problem we introduce next illustrates why one thread execution sequence might be perfectly fine while another might lead to unexpected and incorrect behavior. (And even people starving!)
14.2. Problem: Dining philosophers
Concurrency gives us the potential to make our programs faster but introduces a number of other problems. The way that threads interact can be unpredictable. Because they share memory, one thread can corrupt a value in another thread’s variables. We introduce synchronization tools in this chapter that can prevent threads from corrupting data, but these tools create new pitfalls. To explore these pitfalls, we give you another problem to solve.
Imagine a number of philosophers sitting at a round table with plates that are periodically filled with rice. Between adjacent philosophers are single chopsticks so that there are exactly the same number of chopsticks as there are philosophers. These philosophers only think and eat. In order to eat, a philosopher must pick up both the chopstick on her left and the chopstick on her right. Figure 14.1 illustrates this situation.
Your goal is to write a class called DiningPhilosopher
which extends
Thread
. Each thread created in the main()
method should be a
philosopher who thinks for some random amount of time, then acquires the
two necessary chopsticks and eats. No philosopher should starve. No
philosophers should be stuck indefinitely fighting over chopsticks.
Although this problem sounds simple, the solution is not. Make sure that you understand the concepts and Java syntax in this chapter thoroughly before trying to implement your solution. It’s important that no two philosophers try to use the same chopstick at the same time. Likewise, we need to avoid a situation where every philosopher is waiting for every other philosopher to give up a chopstick.
14.3. Concepts: Thread interaction
This dining philosopher problem highlights some difficulties which were emerging toward the end of the last chapter. In Exercise 13.10 two snippets of code could run concurrently and modify the same shared variable, potentially producing incorrect output. Because of the nondeterministic nature of scheduling, we have to assume that the code executing in two or more threads can be interleaved in any possible way. When the result of computation changes depending on the order of thread execution, it’s called a race condition. Below is a simple example of a race condition in Java.
public class RaceCondition extends Thread {
private static int counter = 0;
public static final int THREADS = 4;
public static final int COUNT = 1000000;
public static void main(String[] args) {
RaceCondition[] threads = new RaceCondition[THREADS];
for(int i = 0; i < THREADS; i++) {
threads[i] = new RaceCondition();
threads[i].start();
}
try {
for(int i = 0; i < THREADS; i++)
threads[i].join();
}
catch(InterruptedException e) {
e.printStackTrace();
}
System.out.println("Counter:\t" + counter);
}
public void run() {
for(int i = 0; i < COUNT/THREADS; i++)
counter++;
}
}
This short (and pointless) class attempts to increment the variable
counter
until it reaches 1000000
. To illustrate the race condition,
we’ve divided the work of incrementing counter
evenly among a number
of threads. If you run this program, the final value of counter
will
often not be 1000000
. Depending on which JVM, OS, and how many cores
you have, you may never get 1000000
, and the answer you do get will
vary a lot. On all systems, if you change the value of THREADS
to 1
,
the answer should always be correct.
Looking at the code, the problem might not be obvious. Everything
centers on the statement counter++
in the for
loop inside the
run()
method. But, this statement appears to execute in a single step!
Each thread should increase the value of counter
a total of
COUNT/THREADS
times, adding up to 1000000
. The trouble is that
counter++
is not a single step. Recall that counter++
is shorthand
for counter = counter + 1
. To be even more explicit we could write it
as follows.
temp = counter;
counter = temp + 1;
One thread might get as far as storing counter
into a temporary
location, but then it runs out of time, allowing the next thread in the
schedule to run. When that’s the case, this next thread may do a series of
increments to count
which are all overwritten when the first thread
runs again. Because the first thread had an old value of counter
stored in temp
, adding 1 to temp
has the effect of ignoring
increments that happened in the interim. This situation can happen on a single
processor with threads switching back and forth, but it’s even more dangerous
on a multicore system.
The primary lesson here is that threads can switch between each other at any time with unpredictable effects. The secondary lesson is that the source code is too coarse-grained to show atomic operations. An atomic operation is one which cannot be interrupted by a context switch to another thread. The actual code that the JVM runs is much lower level than source code.
We can’t easily force a non-atomic operation to be atomic, but there are
ways to restrict access to certain pieces of code under certain
conditions. The name we give to a piece of code which should not be
accessed by more than one thread at a time is a critical section. In
the example above, the single line of code which increments counter
is
a critical section, and the error in the program would be removed if
only one thread were able to run that line of code at a time.
Protecting a critical section is done with mutual exclusion tools. They’re called mutual exclusion tools because they enforce the requirement that one thread executing a critical section excludes the possibility of another. There are many different techniques, algorithms, and language features in computer science which can be used to create mutual exclusion. Java relies heavily on a tool called a monitor which hides some of the details of enforcing mutual exclusion from the user. Mutual exclusion is a deeply researched topic with many approaches other than monitors. If you plan to write concurrent programs in another language, you may need to brush up on its mutual exclusion features.
14.4. Syntax: Thread synchronization
14.4.1. The synchronized
keyword
In Java, the language feature which allows you to enforce mutual
exclusion is the synchronized
keyword. There are two ways to use this
keyword: with a method or with an arbitrary block of code. In the method
version, you add the synchronized
modifier before the return type.
Let’s imagine a class with a private String
field called message
which is set to be "Will Robinson!"
by the constructor. Now, we define
the following method.
public synchronized void danger() {
message = "Danger, " + message;
}
If danger()
is called five times from different threads, message
will contain
"Danger, Danger, Danger, Danger, Danger, Will Robinson!"
Without the
synchronized
keyword, danger()
would suffer from a race condition
similar to the one in RaceCondition
. Some of the String
concatenations might be overwritten by other calls to danger()
. You
would never have more than five copies of "Danger, "
appended to the
beginning of message
, but you might have fewer.
Any time a thread enters a piece of code protected by the synchronized
keyword, it implicitly acquires a lock which only a single thread can
hold. If another thread tries to access the code, it’s forced to wait
until the lock is released. This lock is re-entrant. Re-entrant means
that, when a thread currently holds a lock and tries to get it again, it
succeeds. This situation frequently occurs with synchronized methods
which call other synchronized methods.
Consider method safety()
which does the “opposite” of danger()
, by
removing occurrences of "Danger, "
from the beginning of message
.
public synchronized void safety() {
if(message.startsWith("Danger, "))
message = message.substring(8);
}
Will the danger()
and safety()
methods play nicely together on the
same object? In other words, will a thread be blocked from entering
safety()
if another thread is already in danger()
? Yes! The locks
in Java are connected to objects. When you use the synchronized
keyword on a
method, the object the method is being called on (whichever object
this
refers to inside the method) serves as the lock. Thus, only one
thread can be inside of either of these methods on a given object. If you have 10
synchronized methods in an object, only one of them can execute at a
time for that object.
Perhaps this level of control is too restrictive. You may have six
methods which conflict with each other and four others which conflict
with each other but not the first six. Using synchronized
in each
method declaration would unnecessarily limit the amount of concurrency
your program could have.
Although it takes a little more work, using synchronized
with a block
of code allows more fine-grained control. The following version of
danger()
is equivalent to the earlier one.
public void danger() {
synchronized(this) {
message = "Danger, " + message;
}
}
Using synchronized
on a block of code gives us more flexibility in two
ways. First, we can choose exactly how much code we want to control,
instead of the whole method. Second, we can choose which object we want
to use for synchronization. For the block style, any arbitrary object
can be used as a lock. Objects keep a list of threads which are waiting
to get the lock and do all the other management needed to make the
synchronized
keyword work.
If you have two critical sections which are unrelated to each other, you can use the fine-grained control the block style provides. First, you’ll need some objects to use as locks, probably declared so that they can easily be shared, perhaps as static fields of a class.
private static Object lock1 = new Object();
private static Object lock2 = new Object();
Then, wherever you need control over concurrency, you use them as locks.
synchronized(lock1) {
// Do dangerous thing 1
}
// Do safe things
synchronized(lock2) {
// Do dangerous thing 2, unrelated to dangerous thing 1
}
Since declaring a method with synchronized
is equivalent to having its
body enclosed in a block beginning with synchronized(this)
, what about
static
methods? Can they be synchronized
? Yes, they can. Whenever a
class is loaded, Java creates an object of type Class
which
corresponds to that class. This object is what synchronized static
methods inside the class will use as a lock. For example, a synchronized
static method inside of the Eggplant
class will lock on the object
Eggplant.class
.
14.4.2. The wait()
and notify()
methods
Protecting critical sections with the synchronized
keyword is a
powerful technique, and many other synchronization tools can be built
using just this tool. However, efficiency demands a few more options.
Sometimes a thread is waiting for another thread to finish a task so
that it can process the results. Imagine one thread collecting votes
while another one’s waiting to count them. In this example, the
counting thread must wait for all votes to be cast before it can begin
counting. We could use a synchronized block and an indicator boolean
called votingComplete
to allow the collector thread to signal to the
counting thread.
while(true) {
synchronized(this) {
if(votingComplete)
break;
}
}
countVotes();
What’s the problem with this design? The counting thread is
running through the while
loop over and over waiting for
votingComplete
to become true
. On a single processor, the counting
thread would slow down the job of the collecting thread which is trying
to process all the votes. On a multicore system, the counting thread is
still wasting CPU cycles that some other thread could use. This
phenomenon is known as busy waiting, for obvious reasons.
To combat this problem, Java provides the wait()
method. When a thread’s
executing synchronized code, it can call wait()
. Instead of busy
waiting, a thread which has called wait()
will be removed from the
list of running threads. It will wait in a dormant state until someone
comes along and notifies the thread that its waiting is done. If you
recall the thread state diagram from Chapter 13,
there’s a Not Runnable state which threads enter by
calling sleep()
, calling wait()
, or performing blocking I/O. Using
wait()
, we can rewrite the vote counting thread.
synchronized(this) {
while(!votingComplete) {
wait();
}
}
countVotes();
Note that the while
loop has moved inside the synchronized block.
Doing so before might have kept our program from terminating: As long as
the vote counting thread held the lock, the vote collecting thread would
not be allowed to modify votingComplete
. When a thread calls wait()
,
however, it gives up the corresponding lock it’s holding until it wakes
up and runs again. Why use the while
loop at all now? There’s no
guarantee that the condition you’re waiting for is true
. Many threads
might be waiting on this particular lock. We use the while
loop to check
that votingComplete
is true
and wait again if it isn’t.
In order to notify a waiting thread, the other thread calls the
notify()
method. Like wait()
, notify()
must be called within a
synchronized block or method. Here is corresponding code the vote
collecting thread might use to notify the counting thread that voting is
complete.
// Finish collecting votes
synchronized(this) {
votingComplete = true;
notifyAll();
}
A call to notify()
will wake up one thread waiting on the lock object.
If there are many threads waiting, the method notifyAll()
used above
can be called to wake them all up. In practice, it’s usually safer to
call notifyAll()
. If a particular condition changes and a single
waiting thread is notified, that thread might need to notify the next
waiting thread when it’s done. If your code isn’t very carefully
designed, some thread might end up waiting forever and never be notified
if you only rely on notify()
.
To illustrate the use of wait()
and notify()
calls inside of
synchronized code, we give a simple solution to the producer/consumer
problem below. This problem is a classic example in the concurrent
programming world. Often one thread (or a group of threads) is
producing data, perhaps from some input operation. At the same time, one
thread (or, again, a group of threads) is taking these chunks of
data and consuming them by performing some computational or output task.
Every resource inside of a computer is finite. Producer/consumer problems often assume a bounded buffer which stores items from the producer until the consumer can take them away. Our solution does all synchronization on this buffer. Many different threads can share this buffer, but all accesses will be controlled.
public class Buffer {
public final static int SIZE = 10;
private Object[] objects = new Object[SIZE];
private int count = 0;
public synchronized void addItem(Object object) throws InterruptedException { (1)
while(count == SIZE) (2)
wait();
objects[count] = object;
count++;
notifyAll(); (3)
}
public synchronized Object removeItem() throws InterruptedException { (4)
while(count == 0) (5)
wait();
count--;
Object object = objects[count];
notifyAll(); (6)
return object;
}
}
| 1 | When adding an item, producers enter the synchronized addItem()
method. |
| 2 | If count shows that the buffer is full, the producer must wait
until the buffer has at least one open space. |
| 3 | After adding an item to the buffer, the producer then notifies all waiting threads. |
| 4 | The consumer
performs mirrored operations in removeItem() . |
| 5 | A consumer thread can’t consume anything if the buffer is empty and must then wait. |
| 6 | After there’s an object to consume, the consumer removes it and notifies all other threads. |
Both methods are synchronized, making access to the buffer completely sequential. Although it seems undesirable, sequential behavior is precisely what’s needed for the producer/consumer problem. All synchronized code is a protection against unsafe concurrency. The goal is to minimize the amount of time spent in synchronized code and get threads back to parallel execution as quickly as possible.
Although producer/consumer is a good model to keep in mind, there are other ways that reading and writing threads might interact. Consider the following programming problem, similar to one you might find in real life.
As a rising star in a bank’s IT department, you’ve been given the job
of creating a new bank account class called SynchronizedAccount
. This
class must have methods to support the following operations: deposit,
withdraw, and check balance. Each method should print a status message
to the screen on completion. Also, the method for withdraw should return
false
and do nothing if there are insufficient funds. Because the
latest system is multi-threaded, these methods must be designed so that
the bookkeeping is consistent even if many threads are accessing a
single account. No money should magically appear or disappear.
There’s an additional challenge. To maximize concurrency,
SynchronizedAccount
should be synchronized differently for read and
write accesses. Any number of threads should be able to
check the balance on an account simultaneously, but only one thread can
deposit or withdraw at a time.
To solve this problem, our implementation of the class has a balance
variable to record the balance, but it also has a readers
variable to
keep track of the number of threads which are reading from the account
at any given time.
public class SynchronizedAccount {
private double balance = 0.0;
private int readers = 0;
Next, the getBalance()
method is called by threads which wish to read
the balance.
public double getBalance() throws InterruptedException {
double amount;
synchronized(this) { (1)
readers++;
}
amount = balance; (2)
synchronized(this) {
if(--readers == 0) (3)
notifyAll(); (4)
}
return amount;
}
| 1 | Access to the readers variable is synchronized. |
| 2 | After passing that first synchronized block, the code which stores the
balance is no longer synchronized. In this way, multiple readers can access
the data at the same time. For this example, the concurrency controls we
have are overkill. The command amount = balance does not take a great
deal of time. If it did, however, it would make sense for readers to
execute it concurrently as we do. |
| 3 | After reading the balance, this method
decrements readers . |
| 4 | If readers reaches 0, a call to notifyAll() is
made, signaling that threads trying to deposit to or withdraw from the
account can continue. |
public void deposit(double amount) throws InterruptedException {
changeBalance(amount);
System.out.println("Deposited $" + amount + ".");
}
public boolean withdraw(double amount)
throws InterruptedException {
boolean success = changeBalance(-amount);
if(success)
System.out.println("Withdrew $" + amount + ".");
else
System.out.println("Failed to withdraw $" +
amount + ": insufficient funds.");
return success;
}
The deposit()
and withdraw()
methods are wrappers for the
changeBalance()
method, which has all the interesting concurrency
controls.
protected synchronized boolean changeBalance(double amount) (1)
throws InterruptedException {
boolean success;
while(readers > 0) (2)
wait();
if(success = (balance + amount > 0)) (3)
balance += amount;
return success;
}
}
| 1 | The changeBalance() method is synchronized so that it can have
exclusive access to the readers variable. It’s also marked protected
because SynchronizedAccount will be used as a parent class in
Chapter 17. |
| 2 | As long as readers is
greater than 0, this method will wait. |
| 3 | Eventually, the readers should finish their job and notify the waiting writer which can finish changing the balance of the account. |
14.5. Pitfalls: Synchronization challenges
As you can see from the dining philosophers problem, synchronization tools help us get the right answer but also create other difficulties.
14.5.1. Deadlock
Deadlock is the situation when two or more threads are both waiting for the others to complete, forever. Some combination of locks or other synchronization tools has forced a blocking dependence onto a group of threads which will never be resolved.
In the past, people have described four conditions which must exist for deadlock to happen.
-
Mutual Exclusion: Only one thread can access the resource (often a lock) at a time.
-
Hold and Wait: A thread holding a resource can ask for additional resources.
-
No Preemption: A thread holding a resource cannot be forced to release it by another thread.
-
Circular Wait: Two or more threads hold resources which make up a circular chain of dependency.
We illustrate deadlock with an example of how not to solve the dining philosophers problem. What if all the philosophers decided to pick up the chopstick on her left and then the chopstick on her right? If the timing was just right, each philosopher would be holding one chopstick in her left hand and be waiting forever for her neighbor on the right to give up a chopstick. No philosopher would ever be able to eat. Here’s that scenario illustrated in code.
public class DeadlockPhilosopher extends Thread {
public static final int SEATS = 5; (1)
private static boolean[] chopsticks = new boolean[SEATS]; (2)
private int seat;
public DeadlockPhilosopher(int seat) { (3)
this.seat = seat;
}
| 1 | We define a constant for the number of seats. |
| 2 | We create a shared boolean array called chopsticks so that all philosophers
can know which chopsticks are in use. |
| 3 | The constructor assigns each philosopher a seat number. |
public static void main(String args[]) {
DeadlockPhilosopher[] philosophers = new DeadlockPhilosopher[SEATS];
for(int i = 0; i < SEATS; i++) {
philosophers[i] = new DeadlockPhilosopher(i);
philosophers[i].start(); (1)
}
try {
for(int i = 0; i < SEATS; i++)
philosophers[i].join(); (2)
}
catch(InterruptedException e) {
e.printStackTrace();
}
System.out.println("All philosophers done.");
}
| 1 | In main() , we create and start a thread for each
philosopher. |
| 2 | Then, we wait for them to finish which, sadly, will never happen. |
After setting up the class and the main()
method, things get interesting
in the run()
method.
public void run() {
try {
getChopstick(seat); (1)
Thread.sleep(50); (2)
getChopstick((seat + 1) % SEATS); (3)
}
catch(InterruptedException e) {
e.printStackTrace();
}
eat();
}
| 1 | First a philosopher tries to get her left chopstick. |
| 2 | Then she sleeps for 50 milliseconds. |
| 3 | Finally, she tries to get her right chopstick. We mod by SEATS so that the
last philosopher will try to get the chopstick at the beginning of the array. |
Without sleeping, this code would usually run just fine. Every once in a while, the philosophers would become deadlocked, but it would be hard to predict when. By introducing the sleep, we can all but guarantee that the philosophers will deadlock every time.
The remaining two methods are worth examining to see how the synchronization is done, but by getting the two chopsticks separately above, we’ve already gotten ourselves into trouble.
private void getChopstick(int location) throws InterruptedException {
if(location < 0)
location += SEATS;
synchronized(chopsticks) {
while(chopsticks[location])
chopsticks.wait();
chopsticks[location] = true;
}
System.out.println("Philosopher " + seat +
" picked up chopstick " + location + ".");
}
private void eat() {
// Done eating, put back chopsticks
synchronized(chopsticks) {
chopsticks[seat] = false;
if(seat == 0)
chopsticks[SEATS - 1] = false;
else
chopsticks[seat - 1] = false;
chopsticks.notifyAll();
}
}
}
Here’s another example of deadlock. We emphasize deadlock because it’s one of the most common and problematic issues with using synchronization carelessly.
Consider two threads which both need access to two separate resources.
In our example, the two resources are random number generators. The goal
of each of these threads is to acquire locks for the two shared random
number generators, generate two random numbers each, and sum the numbers
generated. (Note that locks are totally unnecessary for this problem
since access to Random
objects is synchronized.)
import java.util.Random;
public class DeadlockSum extends Thread {
private static Random random1 = new Random();
private static Random random2 = new Random();
private boolean reverse;
private int sum;
The class begins by creating shared static
Random
objects
random1
and random2
. Then, in the main()
method, the main thread
spawns two new threads, passing true
to one and false
to the other.
public static void main(String[] args) {
Thread thread1 = new DeadlockSum(true);
Thread thread2 = new DeadlockSum(false);
thread1.start();
thread2.start();
try {
thread1.join();
thread2.join();
}
catch(InterruptedException e) {
e.printStackTrace();
}
}
Next, the mischief begins to unfold. One of the two threads stores
true
in its reverse
field.
public DeadlockSum(boolean reverse) {
this.reverse = reverse;
}
Finally, we have the run()
method where all the action happens. If the
two running threads were both to acquire locks for random1
and random2
in
the same order, everything would work out fine. However, the reversed
thread locks on random2
and then random1
, with a sleep()
in
between. The non-reversed thread tries to lock on random1
and then
random2
.
public void run() {
if(reverse) {
synchronized(random2) {
System.out.println("Reversed Thread: locked random2");
try{ Thread.sleep(50); }
catch(InterruptedException e) {
e.printStackTrace();
}
synchronized(random1) {
System.out.println("Reversed Thread: locked random1");
sum = random1.nextInt() + random2.nextInt();
}
}
}
else {
synchronized(random1) {
System.out.println("Normal Thread: locked random1");
try { Thread.sleep(50); }
catch(InterruptedException e) {
e.printStackTrace();
}
synchronized(random2) {
System.out.println("Normal Thread: locked random2");
sum = random1.nextInt() + random2.nextInt();
}
}
}
}
}
If you run this code, it should invariably deadlock with thread1
locked on random2
and thread2
locked on random1
. No sane
programmer would intentionally code the threads like this. In fact, the
extra work we did to acquire the locks in opposite orders is exactly
what causes the deadlock. For more complicated programs, there may be
many different kinds of threads and many different resources. If two
different threads (perhaps written by different programmers) need
both resource A and resource B at the same time but try to acquire them
in reverse order, this kind of deadlock can occur without such an
obvious cause.
For deadlock of this type, the circular wait condition can be broken by ordering the resources and always locking the resources in ascending order. Of course, this solution only works if there is some universal way of ordering the resources and the ordering is always followed by all threads in the program.
Ignoring the deadlock problems with the example above, it gives a nice example of the way Java intended synchronization to be done: when possible, use the resource you need as its own lock. Many other languages require programmers to create additional locks or semaphores to protect a given resource, but this approach causes problems if the same lock is not consistently used. Using the resource itself as a lock is an elegant solution.
14.5.2. Starvation and livelock
Starvation is another problem which can occur with careless use of synchronization tools. Starvation is a general term which covers any situation in which some thread never gets access to the resources it needs. Deadlock can be viewed as a special case of starvation since none of the threads which are deadlocking makes progress.
The dining philosophers problem was framed around the idea of eating with humorous intent. If a philosopher is never able to acquire chopsticks, that philosopher will quite literally starve.
Starvation doesn’t necessarily mean deadlock, however. Examine the
implementation in Example 14.2 for the bank account.
That solution is correct in the sense that it preserves mutual
exclusion. No combination of balance checks, deposits, or withdrawals
will cause the balance to be incorrect. Money will neither be created
nor destroyed. A closer inspection reveals that the solution is not
entirely fair. If a single thread is checking the balance, no other
thread can make a deposit or a withdrawal. Balance checking threads
could be coming and going constantly, incrementing and decrementing the
readers
variable, but if readers
never goes down to zero, threads
waiting to make deposits and withdrawals will wait forever.
Another kind of starvation is livelock. In deadlock, two or more threads get stuck and wait forever, doing nothing. Livelock is similar except that the two threads keep executing code and waiting for some condition that never arrives. A classic example of livelock is two polite (but oddly predictable) people speaking with each other: Both happen to start talking at exactly the same moment and then stop to hear what the other has to say. After exactly one second, they both begin again and immediately stop. Lather, rinse, repeat.
Imagine three friends going to a party. Each of them starts getting ready at different times. They follow the pattern of getting ready for a while, waiting for their friends to get ready, and then calling their friends to see if the other two are ready. If all three are ready, then the friends will leave. Unfortunately, if a friend calls and either of the other two aren’t ready, he’ll become frustrated and stop being ready. Perhaps he’ll realize that he’s got time to take a shower or get involved in some other activity for a while. After finishing that activity, he’ll become ready again and wait for his friends to become ready.
If the timing is just right, the three friends will keep becoming ready, waiting for a while, and then becoming frustrated when they realize that their friends aren’t ready. Here’s a rough simulation of this process in code.
public class Livelock extends Thread {
private static int totalReady = 0; (1)
private static Object lock = new Object(); (2)
public static void main(String[] args) { (3)
Livelock friend1 = new Livelock();
Livelock friend2 = new Livelock();
Livelock friend3 = new Livelock();
| 1 | First, we create a shared variable called totalReady which
tracks the total number of friends ready. |
| 2 | To avoid race conditions, a shared Object called lock will be used to control
access to totalReady . |
| 3 | Then, the main() method creates Livelock
objects representing each of the friends. |
try {
friend1.start();
Thread.sleep(100);
friend2.start();
Thread.sleep(100);
friend3.start();
friend1.join();
friend2.join();
friend3.join();
}
catch(InterruptedException e) {
e.printStackTrace();
}
System.out.println("All ready!");
}
The rest of the main()
method starts each of the threads representing
the friends running, with a 100 millisecond delay before the next thread
starts . Then, it waits for them all to finish. If
successful, it’ll print All ready!
to the screen.
public void run() {
boolean done = false;
try {
while(!done) { (1)
Thread.sleep(75); // Prepare for party (2)
synchronized(lock) {
totalReady++; (3)
}
Thread.sleep(75); // Wait for friends (4)
synchronized(lock) {
if(totalReady >= 3) (5)
done = true;
else
totalReady--; (6)
}
}
}
catch(InterruptedException e) {
e.printStackTrace();
}
}
}
| 1 | In the run() method, each friend goes through a loop until the done
variable is true . |
| 2 | In this loop, an initial call to Thread.sleep()
for 75 milliseconds represents preparing for the party. |
| 3 | After that,
totalReady is incremented by one. |
| 4 | Then, the friend waits for another 75 milliseconds. |
| 5 | Finally, he checks to see if everyone else is ready by
testing whether totalReady is 3 . |
| 6 | If not, he decrements totalReady and repeats the process. |
At roughly 75 milliseconds into the simulation, the first friend becomes ready, but he doesn’t check with his friends until 150 milliseconds. Unfortunately, the second friend doesn’t become ready until 175 milliseconds. He then checks with his friends at 225 milliseconds, around which time the first friend is becoming ready a second time. However, the third friend isn’t ready until 275 milliseconds. When he then checks at 350 milliseconds, the first friend isn’t ready anymore. On some systems the timing might drift such that the friends all become ready at the same time, but it could take a long, long while.
In reality, human beings would not put off going to a party indefinitely. Some people would decide that it was too late to go. Others would go alone. Others would go over to their friends' houses and demand to know what was taking so long. Computers are not nearly as sensible and must obey instructions, even if they cause useless repetitive patterns. Realistic examples of livelock are hard to show in a short amount of code, but they do crop up in real systems and can be very difficult to predict.
14.5.3. Sequential execution
When designing a parallel program, you might notice that synchronization tools are necessary to get a correct answer. Then, when you run this parallel version and compare it to the sequential version, it runs no faster or, worse, runs slower than the sequential version. Too much zeal with synchronization tools can produce a program which gives the right answer but doesn’t exploit any parallelism.
For example, we can take the run()
method from the parallel
implementation of matrix multiply given in Example 13.11
and use the synchronized
keyword to lock on the matrix itself.
public void run() {
synchronized(c) {
for(int i = lower; i < upper; i++)
for(int j = 0; j < c[i].length; j++)
for(int k = 0; k < b.length; k++)
c[i][j] += a[i][k] * b[k][j];
}
}
In this case, only a single thread would have access to the matrix at any given time, and all speedup would be lost.
For the parallel version of matrix multiply we gave earlier, no synchronization is needed. In
the case of the producer/consumer problem, synchronization is necessary,
and the only way to manage the buffer properly is to enforce sequential
execution. Sometimes sequential execution can’t be avoided, but you
should always know which pieces of code are truly executing in parallel
and which aren’t if you hope to get the maximum amount of speedup. The
synchronized
keyword should be used whenever it’s needed, but no
more.
14.5.4. Priority inversion
In Chapter 13 we suggest that you rarely use thread priorities. Even good reasons to use priorities can be thwarted by priority inversion. In priority inversion, a lower priority thread holds a lock needed by a higher priority thread, potentially for a long time. Because the high priority thread cannot continue, the lower priority thread gets more CPU time, as if it were a high priority thread.
Worse, if there are some medium priority threads in the system, the low priority thread could hold the lock needed by the high priority thread for even longer because those medium priority threads reduce the amount of CPU time the low priority thread has to finish its task.
14.6. Solution: Dining philosophers
Here we give our solution to the dining philosophers problem. Although deadlock was the key pitfall we were trying to avoid, many other issues can crop up in solutions to this problem. A single philosopher might be forced into starvation, or all philosophers might experience livelock through some pattern of picking up and putting down chopsticks which never quite works out. A very simple solution could allow the philosophers to eat, one by one, in order. Then, the philosophers would often and unnecessarily be waiting to eat, and the program would approach sequential execution.
The key element that makes our solution work is that we force a philosopher to pick up two chopsticks atomically. The philosopher will either pick up both chopsticks or neither.
import java.util.Random;
public class DiningPhilosopher extends Thread {
public static final int SEATS = 5;
private static boolean[] chopsticks = new boolean[SEATS];
private int seat;
public DiningPhilosopher(int seat) {
this.seat = seat;
}
We begin with a similar setup as the deadlocking version given in Example 14.3.
public static void main(String args[]) {
DiningPhilosopher[] philosophers = new DiningPhilosopher[SEATS];
for(int i = 0; i < SEATS; i++) {
philosophers[i] = new DiningPhilosopher(i);
philosophers[i].start(); (1)
}
try {
for(int i = 0; i < SEATS; i++)
philosophers[i].join(); (2)
}
catch(InterruptedException e) {
e.printStackTrace();
}
System.out.println("All philosophers done.");
}
| 1 | In main() , we create and start a thread for each
philosopher. |
| 2 | Then, we wait for them to finish. |
public void run() { (1)
for(int i = 0; i < 100; i++) { (2)
think();
getChopsticks();
eat();
}
}
private void think() { (3)
Random random = new Random();
try {
sleep(random.nextInt(20) + 10);
}
catch(InterruptedException e) {
e.printStackTrace();
}
}
| 1 | This run() method is different from the deadlocking version but not in
a way that prevents deadlock. |
| 2 | We added the for loop so that you could
see the philosophers eat and think many different times without
problems. |
| 3 | We also added the think() method to randomize the amount of
time between eating so that each run of the program is less
deterministic. |
private void getChopsticks() { (1)
int location1 = seat;
int location2 = (seat + 1) % SEATS;
synchronized(chopsticks) { (2)
while(chopsticks[location1] || chopsticks[location2]) { (3)
try {
chopsticks.wait(); (4)
}
catch(InterruptedException e) {
e.printStackTrace();
}
}
chopsticks[location1] = true;
chopsticks[location2] = true;
}
System.out.println("Philosopher " + seat + " picked up chopsticks " +
location1 + " and " + location2 + ".");
}
| 1 | The real place where deadlock is prevented is in the getChopsticks()
method. As in Example 14.3, we mod by SEATS so that the last philosopher
tries to get the first chopstick in the array. |
| 2 | The philosopher acquires the chopsticks lock. |
| 3 | Then, she picks up the two chopsticks she needs only if both are available. |
| 4 | Otherwise, she waits. |
private void eat() { (1)
// Done eating, put back chopsticks
synchronized(chopsticks) { (2)
chopsticks[seat] = false;
if(seat == 0)
chopsticks[SEATS - 1] = false;
else
chopsticks[seat - 1] = false;
chopsticks.notifyAll(); (3)
}
}
}
| 1 | Finally, in the eat() method, the philosopher eats the rice. We would
assume that some other computation would be done here in a realistic
problem before entering the synchronized block. The eating itself
does not require a lock. |
| 2 | After eating’s done, the lock is acquired to give back the chopsticks (hopefully after some cleaning). |
| 3 | Then, all waiting philosophers are notified that some chopsticks may have become available. |
Our solution prevents deadlock and livelock because some philosopher will get control of two chopsticks eventually, yet there are still issues. Note that each philosopher only eats and thinks 100 times. If, instead of philosophers sharing chopsticks, each thread were a server sharing network storage units, the program could run for an unspecified amount of time: days, weeks, even years. If starvation is happening to a particular philosopher in our program, the other philosophers will finish after 100 rounds, and the starved philosopher can catch up. If there were no limitation on the loop, a starving philosopher might never catch up.
Even if we increase the number of iterations of the loop quite a lot,
we probably wouldn’t see starvation of an individual thread because we’re
cheating in another way. Some unlucky sequence of chopstick accesses
by two neighboring philosophers could starve the philosopher between
them. By making the think()
method wait a random amount of time, such
a sequence will probably be interrupted. If all philosophers thought for
exactly the same amount of time each turn, an unlucky pattern could
repeat. It’s not unreasonable to believe that the amount
of thinking a philosopher (or a server) will do at any given time will
vary, but the behavior depends on the system.
It’s very difficult to come up with a perfect answer to some synchronization problems. Such problems have been studied for many years, and research continues to find better solutions.
14.7. Exercises
Conceptual Problems
-
What’s the purpose of the
synchronized
keyword? How does it work? -
The language specification for Java makes it illegal to use the
synchronized
keyword on constructors. During the creation of an object, it’s possible to leak data to the outside world by adding a reference to the object under construction to some shared data structure. What’s the danger of leaking data in this way? -
If you call
wait()
ornotify()
on an object, it must be inside of a block synchronized on the same object. If not, the code will compile, but anIllegalMonitorStateException
may be thrown at run time. Why is it necessary to own the lock on an object before callingwait()
ornotify()
on it? -
Why is it safer to call
notifyAll()
thannotify()
? If it’s generally safer to callnotifyAll()
, are there any scenarios in which there are good reasons to callnotify()
? -
Imagine a simulation of a restaurant with many waiter and chef objects. The waiters must submit orders to the kitchen staff, and the chefs must divide the work among themselves. How would you design this system? How would information and food be passed from waiter to chef and chef to waiter? How would you synchronize the process?
-
Let’s reexamine the code that increments a variable with several threads from Section 14.3. We can rewrite the
run()
method as follows.public synchronized void run() { for(int i = 0; i < COUNT/THREADS; i++) counter++; }
Will this change fix the race condition? Why or why not?
-
Examine our deadlock example from Example 14.4. Explain why this example fulfills all four conditions for deadlock. Be specific about which threads and which resources are needed to show each condition.
-
What’s priority inversion? Why can a low priority thread holding a lock be particularly problematic?
Programming Practice
-
In Example 14.1 the
Buffer
class used to implement a solution to the producer/consumer problem only has a single lock. When the buffer is empty and a producer puts an item in it, both producers and consumers are woken up. A similar situation happens whenever the buffer is full and a consumer removes an item. Re-implement this solution with two locks so that a producer putting an item into an empty buffer only wakes up consumers and a consumer removing an item from a full buffer only wakes up producers. -
In Example 14.2 we used the class
SynchronizedAccount
to solve a bank account problem. As we mention in Section 14.5.2, depositing and withdrawing threads can be starved out by a steady supply of balance checking threads. Add additional synchronization tools toSynchronizedAccount
so that balance checking threads will take turns with depositing and withdrawing threads. If there are no depositing or withdrawing threads, make your implementation continue to allow an unlimited number of balance checking threads to read concurrently. -
The solution to the dining philosophers problem given in Section 14.6 suffers from the problem that a philosopher could be starved by the two philosophers on either side of her, if she happened to get unlucky. Add variables to each philosopher which indicate hunger and the last time a philosopher has eaten. If a given philosopher is hungry and hasn’t eaten for longer than her neighbor, her neighbor shouldn’t pick up the chopstick they share. Add synchronization tools to enforce this principle of fairness. Note that your solution must not cause deadlock. Although one philosopher may be waiting on another who is waiting on another and so on, some philosopher in the circle must have gone hungry the longest, breaking circular wait.
Experiments
-
Critical sections can slow down your program by preventing parallel computation. However, the locks used to enforce critical sections can add extra delays on top of that. Design a simple experiment which repeatedly acquires a lock and does some simple operation. Test the running time with and without the lock. See if you can estimate the time needed to acquire a lock in Java on your system.
-
Design a program which experimentally determines how much time a thread is scheduled to spend running on a CPU before switching to the next thread. To do this, first create a tight loop which runs a large number of iterations, perhaps 1,000,000 or more. Determine how much time it takes to run a single run of those iterations. Then, write an outer loop which runs the tight loop several times. Each iteration of the outer loop, test to see how much time has passed. When you encounter a large jump in time, typically at least 10 times the amount of time the tight loop usually takes to run to completion, record that time. If you run these loops in multiple threads and average the unusually long times together for each thread, you should be able to find out about how long each thread waits between runs. Using this information, you can estimate how much time each thread is allotted. Bear in mind that your this average is only an estimation. Some JVMs will change the amount of CPU time allotted to threads for various reasons. If you’re on a multicore machine, it will be more difficult to interpret your data since some threads will be running concurrently.
-
Create an experiment to investigate priority inversion in the following way.
-
Create two threads, setting the priority of the first to
MIN_PRIORITY
and the priority of the second toMAX_PRIORITY
. Start the first thread running but wait 100 milliseconds before starting the second thread. The first thread should acquire a shared lock and then perform some lengthy process such as finding the sum of the sines of the first million integers. After it finishes its computation, it should release the lock, and print a message. The second thread should try to acquire the lock, print a message, and then release the lock. Time the process. Because the lock is held by the lower priority thread, the higher priority thread will have to wait until the other thread is done for it to finish. -
Once you have a feel for the time it takes for these two threads to finish alone, create 10 more threads that must also perform a lot of computation. However, do not make these threads try to acquire the lock. How much do they delay completion of the task? How does this delay relate to the number of cores in your processor? How much does the delay change if you set the priorities of these new threads to
MAX_PRIORITY
orMIN_PRIORITY
?
-
15. Constructing Graphical User Interfaces
A good sketch is better than a long speech.
15.1. Problem: Math tutor
Most people are used to using interacting with programs through a GUI rather than a command-line interface. In this chapter, our problem is to write a GUI program that can allow young math students to practice their arithmetic. Specifically, we’re interested in addition, subtraction, multiplication, and division with small, positive integers. For this program, we’ll consider addition and subtraction basic and multiplication and division advanced. Our program should allow the user to select a check box in a menu setting the mode to advanced or basic.
The user should then be able to select one of the four operations from another menu. Once the operation is selected, the program should generate a random problem testing that operation. The problem should be displayed as a label on the program with a text field to one side. The user should be able to enter an answer in the text field and hit a button to submit it. The program should check the answer and display the updated number of correct and incorrect answers.
MathTutor
. (a) No menu selected. (b) Type selected. (c) Operations selected. (d) Attempting to answer an addition problem.Figure 15.1 shows the final program in four different states. The window on the top left appears when the program is first initialized, with the “Submit” button disabled until a problem is generated. The top right and the bottom left show each of the two menus open. The bottom right shows the program when an addition problem has been generated and the user is about to answer.
15.2. Concepts: Graphical user interfaces
The program shown above with its menus, labels, buttons, and other interactive components is called a graphical user interface or GUI. A GUI is a means of communication between a computer program and a (usually human) user. Although it’s possible for some programs such as scripts to interact with a GUI, most program-to-program communication is done in other ways.
While communication through input and output on the command line is one way to communicate with a program, GUIs offer a user-friendly alternative that has become extremely commonplace. In fact, GUIs are so common that many people have never used anything else to interact with programs and may not even suspect that other kinds of interaction are possible.
This chapter will teach you how to write programs with GUIs. Although GUIs can make input and output easier for the user, the programmer has to shoulder the burden of arranging the layout and appearance of the GUI and making it function properly. Chapter 7 introduced a way to make simple GUIs, but those GUIs came in preset flavors designed for displaying a message, getting a range of responses in the form of buttons or lists, or reading a short piece of text as input. In this chapter we’ll explore ways to make GUIs of arbitrary complexity with no limitations on the size or shape of the GUI or the components it contains.
A typical GUI consists of a frame (also known as a window) on which are displayed one or more components (known as widgets), such as panels, buttons, and text boxes. Panels are used to organize the contents within the frame. A frame contains at least one panel, but additional ones can be added. Each panel can also contain components: buttons, labels, text boxes, and even other panels. Using code to create a GUI with all the components laid out exactly where you want them is half the work of making a GUI-driven program in Java.
Some components like labels are read-only to the user. They display information such as status messages. Other components allow the user to give input to the program. Reading GUI input is usually done in response to an event. Handling events inside of a program is the other half of writing a GUI program in Java. Layout is concerned with appearance, but event handling is concerned with functionality.
Some IDEs such as IntelliJ have graphical tools that automatically generate GUI layout code for you. It’s fast to create a GUI with such tools, but the GUIs they create are often inflexible and look terrible when the window is resized. We focus on how to write all the code yourself because doing so gives you more control and helps you understand Java GUIs better.
15.2.1. Swing and AWT
Most of the components you’ll use to create GUIs are defined in
classes that belong to the Java Swing library. This library contains
many interfaces and classes. In earlier chapters, you have seen and
used one class from the Swing library, the JOptionPane
class.
Some components of the Swing library are
built on another library known as the Abstract Window Toolkit or AWT.
The AWT is an older library which provides direct access to OS
components. Thus, an AWT Button
object in Microsoft Windows creates a
Windows button. Swing, however, draws its own button. AWT GUIs look
exactly like other GUIs from the same OS. Swing GUIs can be configured
to look similar using look and feel settings, or developers can choose
to use a default Java look and feel that will look similar across all platforms.
We’ll discuss many Swing components such as JButton
and JTextField
. Swing
components usually have a J
at the beginning of their names to
distinguish them from similar AWT components. (The AWT contains Button
and TextField
. If you see examples from other sources using components
that don’t start with J
, they’re probably using AWT.) Although Swing
is built on top of the older AWT library, it’s not a good idea to mix
Swing and AWT components in a single GUI.
The Swing library is far too large for us to cover in its entirety. Instead, our goal is to show you how to construct GUIs using some of the most common Java components. Once you’ve grasped the material in this chapter, you’ll be able to read and understand how to use many other interesting Swing components, as well as other Java libraries for constructing GUIs. The JavaFX library is a newer library that was intended to replace Swing, but it hasn’t been widely adopted. Although it’s still available, JavaFX is no longer included with Java 11 and higher and must be downloaded as a separate library.
15.3. Syntax: GUIs in Java
15.3.1. Creating a frame
A frame is the Java terminology for a window. GUI components in Java will
usually be found on a frame. In Swing, a frame is an object whose type is
derived from the JFrame
class. Here’s a line of code that creates a JFrame
object.
JFrame frame = new JFrame("Empty Frame");
The above statement declares and creates a JFrame
object named
frame
. The title of the frame is “Empty Frame” and is given as
an argument to the JFrame
constructor. Although calling the
constructor creates the frame, you need to make it visible for it to
show up on the screen.
frame.setVisible(true);
The setVisible()
method causes the frame
frame to be visible on
the screen as a window. You can specify its size as follows.
frame.setSize(350,200);
The setSize()
method sets the width and height of the frame in pixels.
In the above example, the width of frame
is set to 350
pixels and its height to 200 pixels. If you don’t set the size (or use a
method like pack()
to make a JFrame
resize itself to the appropriate
size for its contents), it might be so tiny that you at first don’t see it.
The window created by the above code is resizable. If you don’t want the user to be able to resize the window, you can specify that as well.
frame.setResizable(false);
Your entire GUI will often be a frame you create and the components inside that frame. You might want your application to end when you close the frame by clicking on the close button toward the top. The actual location of this button depends on the operating system and the look and feel managers you’re using. Regardless, the following statement can be used to set the behavior of the application when you close the frame window.
frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
|
Pitfall: Closing frames
Some books and online tutorials suggest setting Using By using It’s still necessary to select some default closing operation. By
default, the operation is set to |
Program 15.1 creates and displays an empty frame with the title “Empty Frame,” much like the code above.
import javax.swing.*; (1)
public class EmptyFrame {
public static void main(String[] args){
JFrame frame = new JFrame("Empty Frame"); (2)
frame.setSize(350,200); (3)
frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE); (4)
frame.setVisible(true); (5)
}
}
| 1 | This import statement lets the compiler know we’re using the Swing library. |
| 2 | The first statement inside the main() method declares and creates a JFrame object
and assigns a title to it. |
| 3 | Then, we set its size. |
| 4 | Next, we set the default close operation. |
| 5 | Finally, we make the frame visible. |
The frame so created is shown in Figure 15.2.
You may resize the frame at any point in the program even after the frame has been created and made visible. The initial size may or may not be set prior to making the frame visible. Similarly, the frame title can be set and reset at any point in the program.
A single frame might need to open other subsidiary windows from time to time
to ask a question or display some information. To create windows dependent
on a parent frame, use JDialog
instead of JFrame
. A dialog window made
with JDialog
can either be modal (meaning that the user must close it before they
can interact with the parent frame) or non-modal (allowing the user
to interact with it and its parent frame at the same time).
15.3.2. Components
A component (or widget) is an element of a GUI. Java provides a large variety of components including panels, buttons, text boxes, check boxes, radio buttons, and menus. When laying out a GUI, one or more of the components are created and then placed on a frame. A component is declared and created like any other object.
Widget w = new Widget(arguments);
Here we use class Widget
to represent any Swing component class like
JButton
or JTextField
. Arguments supplied while constructing a
component allow you to set attributes such as its icon, color, size, or
text to display. Most of these attributes can be changed after creation.
Although components can be added directly to a frame, it’s often convenient to lay out a GUI by adding panels to a frame and adding components to those panels. Each panel can hold zero or more components. A panel is also referred to as a container object. Next, we show you how to create a panel, populate it with components, add it to a frame, and display the completed frame.
In Swing, a panel is an instance of the JPanel
class and can be created as
follows.
JPanel panel = new JPanel();
This statement creates a panel named panel
. Thus far, the panel
is empty. We can create two buttons and add them to panel
.
JButton thisButton = new JButton("This");
JButton thatButton = new JButton("That");
panel.add(thisButton);
panel.add(thatButton);
Here, we create two buttons named thisButton
and thatButton
.
It’s common for component variables to reflect what kind of component
they are by appending a description, in this case Button
, to their names.
These buttons are labeled "This"
and
"That"
, but their labels could be any String
values. Then, we add
the two buttons to the panel
.
frame.add(panel);
This statement adds the panel
to an existing frame called frame
.
Another useful component is JTextField
. It creates a text field that can
be used by a program for both input and output of String
data.
JTextField field = new JTextField("This is not a pipe.");
This statement creates a JTextField
component named field
.
When displayed, it’ll show the text “This is not a pipe.”
The user can change this text by typing, but we won’t know when the text
has been change without the event handling tools discussed in
Section 15.3.3. The following example combines several
of the components we’ve introduced into one program.
We can write an application with a GUI that contains three buttons labeled “This,” “That,” and “Exit.” In addition, it contains a text field that initially displays the text, “Text input and output area”
In this example the buttons are only for display. You can click each one, but the program won’t do anything. The text field won’t be changed by the program after it’s initialized either. In the next subsection we’ll add actions to each button and make the program more useful.
import javax.swing.*;
public class FrameWithPanel {
public static void main(String[] args){
JFrame frame = new JFrame("Button Example"); (1)
JPanel panel = new JPanel(); (2)
JButton thisButton = new JButton("This"); (3)
JButton thatButton = new JButton("That");
JButton exitButton = new JButton("Exit");
JTextField field = new JTextField("Text input and output area");
panel.add(thisButton); (4)
panel.add(thatButton);
panel.add(field);
panel.add(exitButton);
frame.add(panel); (5)
frame.setSize(350,200);
frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
frame.setVisible(true); (6)
}
}
| 1 | We first create a frame named frame . |
| 2 | Then, we make a panel named panel . |
| 3 | Next, we create three buttons named thisButton , thatButton , and exitButton and a text field named field . |
| 4 | We add the buttons and the text field to panel . |
| 5 | We add panel to frame . |
| 6 | After setting the size and the closing operation, the frame is made visible. |
The final GUI is shown in Figure 15.3. The sequence in which you add the buttons to the panel determines the appearance of the GUI. That’s why the text field appears before the “Exit” button. Note that the same GUI may look different on different platforms.
In addition to JTextField
, JTextArea
and JPasswordField
are two other
components useful for entering text. The JTextArea
component is designed
for larger passages of text and allows multiple lines. The JPasswordField
component functions the same as a JTextField
except that each character
of input text is displayed as a meaningless echo character designed to hide
sensitive information like a password.
Figure 15.4(a) shows a GUI with a JTextArea
, and
Figure 15.4(b) shows a GUI with a JPasswordField
.
15.3.3. Adding actions to components
We can design a GUI so that when a user clicks a button, the program responds by printing a message to the terminal, changing a text field, playing a sound, or any other action that a Java program can perform. Clicking a button generates an event. In Swing, an event is processed by one or more listeners. Java provides various types of listeners, some of which are introduced here. Next, we show you how to write listeners to handle events generated by a few different kinds of components.
The ActionListener
interface
Java provides an ActionListener
interface. This interface has a single
method named actionPerformed()
. This method takes an ActionEvent
as
input and performs a suitable action based on the event. A JButton
object generates ActionEvent
when it’s pressed. Any class that
implements the ActionListener
interface can be registered as an action
listener on a JButton
or any other component that generates an
ActionEvent
.
As discussed in Chapter 10, an interface is a set of
method signatures. If a class implements an interface, it promises that its objects will
have all of the methods in that interface. If a class implements
ActionListener
, it’s saying that it knows what to do when an action is
performed. The following statements show how to add an ActionListener
to a button and implement its actionPerformed()
method.
JButton thisButton = new JButton("This");
thisButton.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
// Code to perform an action goes here
}
});
The first line above creates a button named thisButton
. The remaining code
adds an action listener to the button. The process of adding an action
listener to an object is also known as registering a listener on the
object. Note that the sole argument to this addActionListener()
method
is a newly created ActionListener
object. Inside this newly created
and anonymous ActionListener
object, we implement the
actionPerformed()
method. Whatever code we want to execute in response
to the clicking of the thisButton
button goes inside the
actionPerformed()
method.
This syntax may look strange to you. ActionListener
is an interface,
which can’t be instantiated. What’s that new
keyword doing? It’s
doing something pretty amazing by creating an instance of an anonymous
class. On the fly, we’re creating a class that has never existed
before. It doesn’t even have a name! All we know about it is that it
implements the interface ActionListener
.
Note that there are braces after the constructor call, defining what’s
inside of this class. Inside, we have only created an
actionPerformed()
method, but we could have created fields as well as
other methods. It’s a little ugly to create a whole new class and
instantiate it in the middle of calling the addActionListener()
method, but it’s also very convenient. We need to supply an object that
reacts to the event exactly the way we want it to. Since one doesn’t
exist yet, we have to create it. Of course, it’s possible to supply any
object that implements the ActionListener
interface, not just
instances of anonymous classes. For more information about nested
classes, inner classes, and anonymous classes, refer to
Section 9.4 and Section 10.4.
Now we can modify Program 15.2 to respond to button
clicks. When the thisButton
button is clicked, the program will display the
message “You can get with this.” in the text field. Similarly, when the
thatButton
button is clicked, the program will display “Or you can get with that.”
import javax.swing.*;
import java.awt.event.*;
public class FrameWithPanelAndActions {
public static void main(String[] args){
JFrame frame = new JFrame("Button Example");
JPanel panel = new JPanel();
JButton thisButton = new JButton("This");
JButton thatButton = new JButton("That");
JButton exitButton = new JButton("Exit");
JTextField field = new JTextField("Text input and output area");
panel.add(thisButton);
panel.add(thatButton);
panel.add(field);
panel.add(exitButton);
// Add action listeners to various buttons
thisButton.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e){
field.setText("You can get with this.");
}
});
thatButton.addActionListener(new ActionListener(){
public void actionPerformed(ActionEvent e){
field.setText("Or you can get with that.");
}
});
exitButton.addActionListener(new ActionListener(){
public void actionPerformed(ActionEvent e){
System.out.println("Exit");
frame.dispose();
}
});
frame.add(panel);
frame.setSize(350,200);
frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
frame.setVisible(true);
}
}
Program 15.3 is largely the same as
Program 15.2. It adds the same buttons and text
field but then adds an action listener to each button. The action
performed when the thisButton
and thatButton
buttons are clicked is to display
a message in the text box. When the exitButton
button is clicked, the
listener displays a message on the terminal and exits the program.
The action listeners can be added either before or after the panel has been set up but should be added before the frame is made visible.
In the previous example, we added an ActionListener
object to each
button and implemented its actionPerformed()
method with anonymous
inner classes. An alternate way to use ActionListener
is to implement
ActionListener
on the surrounding class and include an
actionPerformed()
method exactly once instead of creating several
individual anonymous inner classes which each handle an event. Let’s
examine one such implementation in
Program 15.4 and contrast it with
Program 15.3. Note that both programs
generate the same GUI and exhibit identical behavior.
ActionListener
at the class level.import javax.swing.*;
import java.awt.event.*;
public class AlternateActionListener implements ActionListener {
private JFrame frame = new JFrame("Button Example");
private JPanel panel = new JPanel();
private JButton thisButton = new JButton("This");
private JButton thatButton = new JButton("That");
private JButton exitButton = new JButton("Exit");
private JTextField field = new JTextField("Text input and output area");
public AlternateActionListener (){ (1)
thisButton.addActionListener(this);
thatButton.addActionListener(this);
exitButton.addActionListener(this);
panel.add(thisButton);
panel.add(thatButton);
panel.add(field);
panel.add(exitButton);
frame.add(panel);
frame.setSize(350,200);
frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
frame.setVisible(true);
}
public void actionPerformed(ActionEvent e){ (2)
Object button = e.getSource(); (3)
if(button == thisButton) (4)
field.setText("You can get with this.");
else if(button == thatButton)
field.setText("Or you can get with that.");
else {
System.out.println("Exit");
frame.dispose();
}
}
public static void main(String[] args){ (5)
new AlternateActionListener();
}
}
| 1 | The constructor adds an ActionListener to each button. The listener added is this , specifying that the AlternateActionListener object is the one that will process any action event generated by the buttons. The remainder of the code for the constructor is essentially the same as that from the main() method in Program 15.3. |
| 2 | A class that implements ActionListener must include an actionPerformed() method. It has exactly one parameter, an ActionEvent object. Whenever an action event occurs, its attributes are bundled into an ActionEvent object and passed into the actionPerformed() method. |
| 3 | The getSource() method returns a reference to the object that generated the event. Variable button holds the object returned by
getSource() . |
| 4 | These if statements compare button with thisButton and thatButton to determine if either of these generated the event. Then, a suitable message is displayed in the field text field. If neither button generated the event, it must have been exitButton , so the frame disposes itself after displaying “Exit” on the terminal. |
| 5 | The main() method creates an instance of AlternateActionListener and finishes. The program doesn’t end there because the threads of the GUI are already running. |
There are a few differences between
Program 15.3 and
Program 15.4. In
the second program, most of the code has been
moved from the main()
method to the constructor. Various objects,
namely the frame, the panel, all three
buttons, and the text box are now fields of the object instead of local
variables in the main()
method. The buttons need to be fields so that the
actionPerformed()
method can compare against them, and the text box needs
to be a field so that the method can change it.
We’ve examined two styles of adding an ActionListener
to a Java
program. The choice of style depends on your needs. Adding
an anonymous ActionListener
to each component makes it easy to specify an
action for each one, but the syntax is ugly. Using a named class (often the main program class or a
subclass of JFrame
) as the ActionListener
allows you to handle many
events in a centralized location. It can be easier to find errors when
all events are handled in one actionPerformed()
method, but the method
can become complex since it needs to determine which component generated the event.
The ItemListener
interface
An ItemListener
can be attached to a component such as a check box or a
radio button to listen for when it’s selected or deselected. When you select
a check box, a ✓ sign appears to its left, and an ItemEvent
is
generated. When you select an already checked check box, the sign
disappears, and another ItemEvent
is generated.
Creating a check box is very much like creating a button, except
that we use the JCheckBox
component instead of JButton
.
When it’s initially created, a check box is unchecked.
JCheckBox checkBox = new JCheckBox("Check the oven");
Just as the ActionListener
interface only has a single method, the
ItemListener
interface only has one as well, itemStateChanged()
, which
takes a single parameter of type ItemEvent
.
An ActionEvent
and an ItemEvent
are similar, but one reason that Java
has two different interfaces with two different kinds of events is
because an ItemEvent
has more information: By using the
getStateChange()
method, it’s possible to tell whether the component
that fired the ItemEvent
is now selected or deselected.
checkBox.addItemListener(new ItemListener(){
public void itemStateChanged(ItemEvent e) {
if(e.getStateChange() == ItemEvent.SELECTED)
checkBox.setText("We checked the oven!");
else
checkBox.setText("No one checked the oven!");
}
});
Below is an example of a GUI with three check boxes.
The following program shows a GUI with three check boxes labeled “Nasty”, “Brutish”, and “Short”. When each is clicked, it’ll update a text field saying that your life either is or isn’t nasty, brutish, or short, depending on whether or not the clicked check box is currently selected.
import javax.swing.*;
import java.awt.event.*;
public class CheckBoxExample {
public static void main(String[] args){
JFrame frame = new JFrame("Check Box Example"); (1)
JPanel panel = new JPanel();
JCheckBox nastyCheckBox = new JCheckBox("Nasty"); (2)
JCheckBox brutishCheckBox = new JCheckBox("Brutish");
JCheckBox shortCheckBox = new JCheckBox("Short");
JTextField field = new JTextField("Here's what your life is like.");
panel.add(nastyCheckBox); (3)
panel.add(brutishCheckBox);
panel.add(shortCheckBox);
panel.add(field);
// Add item listeners to the check boxes
nastyCheckBox.addItemListener(new ItemListener() { (4)
public void itemStateChanged(ItemEvent e) {
if(e.getStateChange() == ItemEvent.SELECTED)
field.setText("Your life is nasty.");
else
field.setText("Your life isn't nasty.");
}
});
brutishCheckBox.addItemListener(new ItemListener(){
public void itemStateChanged(ItemEvent e) {
if(e.getStateChange() == ItemEvent.SELECTED)
field.setText("Your life is brutish.");
else
field.setText("Your life isn't brutish.");
}
});
shortCheckBox.addItemListener(new ItemListener(){
public void itemStateChanged(ItemEvent e) {
if(e.getStateChange() == ItemEvent.SELECTED)
field.setText("Your life is short.");
else
field.setText("Your life isn't short.");
}
});
frame.add(panel); (5)
frame.setSize(350,200);
frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
frame.setVisible(true);
}
}
| 1 | Just as in previous examples, we create a frame and a panel. |
| 2 | We make three check boxes and a text field. |
| 3 | Then we add all four components to the panel. |
| 4 | We create an ItemListener for each check box
to update the text field with an appropriate message
based on whether the check box is now checked or unchecked. Note that
the most recent message will overwrite whatever text was previously
in the text field. |
| 5 | Like previous examples, we add the panel to the frame, set the size, set the default close operation, and show the frame. |
Figure 15.5 shows this GUI after the “Brutish” check box has been clicked.
All JCheckBox
objects could have an ActionListener
added to them
as well. We could change the listener for the nastyCheckBox
above to the following,
and it would function the same. To do so, we must ask the nastyCheckBox
if it’s
currently selected by calling its isSelected()
method.
nastyCheckBox.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
if(nastyCheckBox.isSelected())
field.setText("Your life is nasty.");
else
field.setText("Your life isn't nasty.");
}
});
Another Swing component that can be used with an ItemListener
is JRadioButton
.
Radio buttons are similar to check boxes in that clicking one will select it.
However, radio buttons are almost always used in a group. When one radio button
in the group is selected, all others are deselected, guaranteeing that only one
radio button can be selected at a time.
For example, the following code will create three radio buttons, only one of which can be selected.
JRadioButton smarterButton = new JRadioButton("Smarter than the average bear");
JRadioButton lessSmartButton = new JRadioButton("Less smart than the average bear");
JRadioButton asSmartButton = new JRadioButton("As smart as the average bear");
ButtonGroup group = new ButtonGroup();
group.add(smarterButton);
group.add(lessSmartButton);
group.add(asSmartButton);
The ButtonGroup
object provides the logical grouping that guarantees a maximum
of one JRadioButton
will be selected. This grouping is only for the purposes of
selection, and the radio buttons still need to be added to a panel or a frame or some other
GUI container to display them.
With check boxes, an ItemListener
didn’t provide much of an advantage over an
ActionListener
. A radio button, however, can be deselected not by clicking on it
but by clicking on another radio button. Thus, using an ItemListener
allows us
to handle the event when a radio button is deselected, even though it wasn’t clicked
itself, as illustrated in the following example.
The program below shows a GUI with three radio buttons labeled “Half empty”, “Half full”, and “Twice the needed size”, describing a partially filled glass of water from the perspective of an optimist, a pessimist, or an engineer, respectively. When each radio button is selected, it’ll update the first text field describing the glass from the given perspective. However, when a radio button is deselected by clicking another one, it’ll update the second text field to describe what the glass isn’t, now that we’re in a different perspective.
import javax.swing.*;
import java.awt.event.*;
public class RadioButtonExample {
public static void main(String[] args){
JFrame frame = new JFrame("Radio Button Example");
JPanel panel = new JPanel();
JRadioButton halfEmptyButton = new JRadioButton("Half empty"); (1)
JRadioButton halfFullButton = new JRadioButton("Half full");
JRadioButton twiceTheSizeButton = new JRadioButton("Twice the needed size");
JTextField positiveField = new JTextField("Positive statement about the glass.");
JTextField negativeField = new JTextField("Negative statement about the glass.");
ButtonGroup group = new ButtonGroup(); (2)
group.add(halfEmptyButton);
group.add(halfFullButton);
group.add(twiceTheSizeButton);
panel.add(halfEmptyButton); (3)
panel.add(halfFullButton);
panel.add(twiceTheSizeButton);
panel.add(positiveField);
panel.add(negativeField);
// Add item listeners to the radio buttons
halfEmptyButton.addItemListener(new ItemListener() { (4)
public void itemStateChanged(ItemEvent e) {
if(e.getStateChange() == ItemEvent.SELECTED)
positiveField.setText("The glass is half empty.");
else
negativeField.setText("And it's not half empty.");
}
});
halfFullButton.addItemListener(new ItemListener(){
public void itemStateChanged(ItemEvent e) {
if(e.getStateChange() == ItemEvent.SELECTED)
positiveField.setText("The glass is half full.");
else
negativeField.setText("And it's not half full.");
}
});
twiceTheSizeButton.addItemListener(new ItemListener(){
public void itemStateChanged(ItemEvent e) {
if(e.getStateChange() == ItemEvent.SELECTED)
positiveField.setText("The glass is twice the needed size.");
else
negativeField.setText("And it's not twice the needed size.");
}
});
frame.add(panel); (5)
frame.setSize(350,200);
frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
frame.setVisible(true);
}
}
| 1 | We make three radio buttons and two text fields. |
| 2 | We add the three radio buttons to a group. |
| 3 | Then we add the five components to the panel. |
| 4 | We create an ItemListener for each radio button. When the event
is a selection, it will update the first text field with a message
that matches the radio button. When the event is a deselection,
it will update the second text field with a message that suggests
the opposite of the radio button. |
| 5 | Like previous examples, we add the panel to the frame, set the size, set the default close operation, and show the frame. |
Figure 15.6(a) shows this GUI before any radio buttons have been selected. Figure 15.6(b) shows this GUI after the “Half full” radio button’s been selected. Figure 15.6(c) shows this GUI after the “Twice the needed size” radio button’s been selected. In this final step, note that the second text field reads “And it’s not half full.” because the “Half full” button was just deselected.
The MouseListener
interface
Clicking a button or a check box is useful, but a mouse can generate events
in other ways too. For example, in a screen full of pictures, you might want to
highlight a picture when the cursor hovers over it. Or you might want to
create a drawing program which uses a mouse as a pen. To process general
mouse events, we need an object that implements the MouseListener
interface, which defines the following methods.
-
mouseClicked()
-
mouseEntered()
-
mouseExited()
-
mousePressed()
-
mouseReleased()
The function of each method is implied by its name. The mouseEntered()
event fires when the mouse cursor moves into the area above a component.
Conversely, the mouseExited()
event fires when a mouse cursor was over
a component and has just moved away. The mousePressed()
event fires
when a mouse button is pressed over a component. The mouseReleased()
event fires when a mouse button is released over a component.
The mouseClicked()
event is a combination of
both the mousePressed()
and mouseReleased()
events, occurring only
if a mouse button was pressed and then released while the cursor was
over a component. As you can see, a component only fires events when the
cursor is over it (or has just left). Thus, a component only
reports events that have to do with it, not the general state of the
mouse.
Each method in MouseListener
receives a MouseEvent
object as its
argument. To handle mouse events, a class must implement the
MouseListener
interface. Doing so is similar to the implementation of the
ActionListener
interface from the previous section, but implementing
MouseListener
requires a definition for each of the five methods
listed above. The next example illustrates MouseListener
in use.
We can write a program that displays a GUI containing two buttons labeled “One” and “Two.” A text box will display a suitable message when the cursor enters a button. When a button is clicked, the text box should display the total number of times that button has been clicked.
MouseListener
interface.import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
public class SimpleMouseEvents implements MouseListener { (1)
private JFrame frame = new JFrame("Mouse Events");
private JTextField status = new JTextField("Mouse status comes here.");
private JButton oneButton = new JButton("One");
private JButton twoButton = new JButton("Two");
private int oneClicks = 0, twoClicks = 0; // Number of clicks
public SimpleMouseEvents() { (2)
JPanel panel = new JPanel();
oneButton.addMouseListener(this);
twoButton.addMouseListener(this);
panel.add(oneButton);
panel.add(twoButton);
panel.add(status);
frame.add(panel);
frame.setSize(275,200);
frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
frame.setVisible(true);
}
// Implement all abstract methods in MouseListener
public void mouseEntered(MouseEvent e) { (3)
if (e.getSource() == oneButton)
status.setText("Mouse enters One.");
else
status.setText("Mouse enters Two.");
}
public void mouseClicked(MouseEvent e) { (4)
if (e.getSource() == oneButton) {
oneClicks++;
status.setText("One clicked "+ oneClicks + " times.");
}
else {
twoClicks++;
status.setText("Two clicked "+ twoClicks + " times.");
}
}
public void mouseExited(MouseEvent e) {} // Unused methods (5)
public void mousePressed(MouseEvent e) {}
public void mouseReleased(MouseEvent e) {}
public static void main(String[] args){ (6)
new SimpleMouseEvents();
}
}
| 1 | We declare class SimpleMouseEvents , implementing the MouseListener interface. The following few lines declare frame frame , buttons one and two , and a text box status . Two integers oneClicks and twoClicks are initialized to 0 and are used to keep track of the number of times each button has been clicked. |
| 2 | The first line of the SimpleMouseEvents constructor creates the panel
panel . It doesn’t need to be a field, and it’s always preferable to
keep a variable local if it can be. The next two lines add a MouseListener to the two buttons. Note the use of this in the argument to addMouseListener() which refers to the object being created by the constructor. Next, the panel is set up and added to the frame. Finally the frame size and its default close operations are set, and the frame is made visible. |
| 3 | The mouseEntered() method is invoked when the cursor enters either of the two buttons. First, we retrieve the source of the event using the getSource() method and identify which object generated the event. A suitable message is displayed in the status box using the setText() method. |
| 4 | The mouseClicked() method is invoked when the mouse cursor is clicked above a button. As before, we retrieve the source of the event using the getSource() method. A suitable message, including the number of clicks, is displayed in the text box. Of course, recording button clicks could have been done with an ActionListener instead. |
| 5 | MouseListener methods that aren’t used must be implemented, but their bodies can be left empty. |
| 6 | The only job of the main() method is to create an instance of
SimpleMouseEvents . |
Program 15.5 generates the GUI shown in Figure 15.7.
Mouse adapter
Creating a MouseListener
requires all five methods in the interface to
be implemented. In many cases, as in Example 15.7,
there’s no need to implement all the methods because we’re not
interested in all the corresponding events. In such situations we’re
forced to include empty methods.
However, you might want to include the methods only when they’re
needed. The MouseAdapter
class helps us avoid implementing
methods we don’t need.
MouseAdapter
is an abstract class, unlike the MouseListener
interface. The advantage of using MouseAdapter
is that it already
provides a skeletal implementation of each method needed to process
mouse events. We can override these implementations as needed, and we
don’t need to provide an implementation of a method that’s not used.
Program 15.6 is a revised version of Program 15.5. Remember that an abstract class is extended whereas an interface is implemented.
MouseAdapter
abstract class.import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
public class SimpleMouseAdapter extends MouseAdapter { (1)
private JFrame frame = new JFrame("Mouse Events");
private JTextField status = new JTextField("Mouse status comes here.");
private JButton oneButton = new JButton("One");
private JButton twoButton = new JButton("Two");
private int oneClicks = 0, twoClicks = 0;
public SimpleMouseAdapter () {
JPanel panel = new JPanel();
oneButton.addMouseListener(this);
twoButton.addMouseListener(this);
panel.add(oneButton);
panel.add(twoButton);
panel.add(status);
frame.add(panel);
frame.setSize(275,200);
frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
frame.setVisible(true);
}
// Override only those methods we want
public void mouseEntered(MouseEvent e) { (2)
if (e.getSource() == oneButton)
status.setText("Mouse enters One.");
else
status.setText("Mouse enters Two.");
}
public void mouseClicked(MouseEvent e) { (3)
if (e.getSource() == oneButton) {
oneClicks++;
status.setText("One clicked "+ oneClicks + " times.");
}
else {
twoClicks++;
status.setText("Two clicked "+ twoClicks + " times.");
}
}
public static void main(String[] args){
new SimpleMouseAdapter ();
}
}
| 1 | Class SimpleMouseAdapter extends the abstract class MouseAdapter . Thus, it inherits all the empty methods defined in MouseAdapter . |
| 2 | We override the method mouseEntered() . |
| 3 | We override the method mouseClicked() . No other methods need to be overridden in this example. |
Other event listeners
In this chapter we describe three types of listeners in Java,
ActionListener
, ItemListener
and MouseListener
. You may have noticed that none of
the mouse events we discussed involved the movement of the mouse inside
of the component, only whether it was entering or exiting the component.
Because tracking mouse movement is more computationally expensive than
tracking simple presses, releases, enters, and exits, Java uses yet another
listener to handle mouse movement, MouseMotionListener
. It contains
the methods mouseDragged()
and mouseMoved()
, which are used to
handle mouse movement with or without the button pressed.
Java provides several other listeners to handle a variety of events. For
example, the DocumentListener
can be attached to a JTextField
or a
JTextArea
object to listen to document events, which include the
insertUpdate()
event that’s fired when a character is inserted the
text box. A KeyListener
can also be attached to text boxes to listen
to key events such as the return key being typed, which can have similar
functionality. These events could be useful when developing a text editor
application, for example.
After you’ve mastered the contents of this chapter, you may plan to write more complex GUIs than the ones we discuss. For further information, you might want to follow the Java tutorial on writing event listeners at the Oracle Swing Events tutorial site.
15.3.4. Adding sounds and images
Sounds and images can also be added to a Java GUI application. While
Java offers a rich set of sound APIs, we restrict our examples to
playing sound clips from audio files that come in au
or wav
formats.
We also introduce the ImageIcon
class to create icons from image
files.
Sounds
There are many ways to play sounds in Java, but the simplest is through the Clip
interface. Let’s see how we can create an appropriate Clip
object.
Clip clip = AudioSystem.getClip();
This statement creates a new Clip
object using a static method from
the AudioSystem
class in the javax.sound.sampled
package. To use
clip
, we need an audio file to play.
File soundFile = new File("sounds/trumpet.wav"); (1)
clip.open(AudioSystem.getAudioInputStream(soundFile)); (2)
| 1 | Here we create a File object corresponding to the location of an audio
file we want to play. In this case, we’re trying to play a file called
trumpet.wav in a sounds directory that’s in the same directory as
our program. The location of the file could be much more complicated
or even entered by the user. |
| 2 | Then, we use the AudioSystem class to make an audio stream from the
file and open it with clip . Note that the two methods on this line can
throw a number of exceptions, depending on whether the file is in a usable
format and your computer’s audio system is ready. |
clip.start();
This command will play the clip in clip
loaded from the specified
file, exactly once. If you want to play the clip in a loop, use the
loop()
method. You can specify a specific number of times to loop
or use the Clip.LOOP_CONTINUOUSLY
constant to make it loop an unlimited
number of times.
clip.loop(Clip.LOOP_CONTINUOUSLY);
To stop a clip from playing, use the stop()
method.
clip.stop();
Now that we’ve seen how to create, open, play, and stop an audio clip, we’re ready to write a program that can play sounds.
 Our program only has two sounds, but more could be added.
We can include a button labeled “Stop Sound” that stops the playback of sounds when clicked. When it starts, the GUI will look like Figure 15.8(a). Note that the “Stop Sound” button is gray, showing that it’s disabled. The complete program is shown below.
import java.io.*; (1)
import java.awt.event.*;
import javax.swing.*;
import javax.sound.sampled.*;
public class AnimalSounds {
public static void main (String[] args) throws Exception {
JFrame frame = new JFrame("Animal Sounds");
JPanel panel = new JPanel();
JButton chirpButton = new JButton("Chirp");
JButton barkButton = new JButton("Bark");
JButton stopButton = new JButton("Stop Sound");
JTextField field = new JTextField("Click Chirp or Bark.");
File chirpFile = new File("sounds/chirp.wav"); (2)
File barkFile = new File("sounds/bark.wav");
Clip chirpClip = AudioSystem.getClip(); (3)
chirpClip.open(AudioSystem.getAudioInputStream(chirpFile));
Clip barkClip = AudioSystem.getClip();
barkClip.open(AudioSystem.getAudioInputStream(barkFile));
panel.add(chirpButton);
panel.add(barkButton);
panel.add(stopButton);
panel.add(field);
frame.add(panel);
stopButton.setEnabled(false); (4)
| 1 | Many import statements are needed to cover all the library classes and interfaces used to play sounds. |
| 2 | After creating a number of GUI elements, we define the files for the two sounds. |
| 3 | Then, we create chirpClip and barkClip and open audio streams
corresponding to their sound files. |
| 4 | Note that we start with the stop button disabled. |
chirpButton.addActionListener(new ActionListener() { (1)
public void actionPerformed(ActionEvent e){
field.setText("Playing chirp.");
barkButton.setEnabled(false);
chirpClip.loop(Clip.LOOP_CONTINUOUSLY);
stopButton.setEnabled(true);
}
});
barkButton.addActionListener(new ActionListener(){ (2)
public void actionPerformed(ActionEvent e){
field.setText("Playing bark.");
chirpButton.setEnabled(false);
barkClip.loop(Clip.LOOP_CONTINUOUSLY);
stopButton.setEnabled(true);
}
});
stopButton.addActionListener(new ActionListener(){ (3)
public void actionPerformed(ActionEvent e){
field.setText("Click Chirp or Bark.");
chirpClip.stop();
barkClip.stop();
barkButton.setEnabled(true);
chirpButton.setEnabled(true);
stopButton.setEnabled(false);
}
});
frame.setSize(275,200); // Set size in pixels
frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
frame.setVisible(true); // Display it
}
}
| 1 | Here we define the action listener for chirpButton , which
disables barkButton , tells the user that the chirp sound is playing, plays
the sound, and enables the stop button. |
| 2 | The action listener for barkButton button is similar. |
| 3 | The action listener for stopButton doesn’t know which sound is playing,
so it stops both sounds and then disables the stopButton . |
When chirpButton
is clicked, the GUI looks like Figure 15.8(b).
Images and icons
Images are often useful when creating GUIs. In this section we show you
how to use images to create icons and then use those icons to decorate
buttons and labels. First, let’s see how an icon object can be created.
Suppose we have a picture file named smile.jpg
in a directory named
pictures
. Note that the pictures
directory should be located in same
directory as the class files for your program. The following statement
creates an object of type ImageIcon
from this picture.
ImageIcon smileIcon = new ImageIcon("pictures/smile.jpg");
The file name, along with its path, is passed to the
ImageIcon
constructor as a string. Now we can add the image to a
button.
JButton smile = new JButton();
smile.add(smileIcon);
Similarly, you can add an image to a label. The next example gives a simple program that creates a button with an image.
Figure 15.9 shows a GUI with a button decorated with a
picture. Program 15.8 gives the code to create this
GUI using the ImageIcon
class.
import javax.swing.*;
import java.awt.*;
public class IconExample {
public static void main(String[] args) {
JFrame frame = new JFrame("Icon Example");
ImageIcon smileIcon = new ImageIcon("pictures/smile.jpg"); (1)
JButton smileButton = new JButton(smileIcon); (2)
frame.add(smileButton);
frame.setSize(325,250);
frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
frame.setVisible(true);
}
}
| 1 | An image icon is created from a JPEG file named smile.jpg located in
the pictures directory. |
| 2 | We create a button named smileButton and decorate it with an icon
by supplying it as an argument to the JButton constructor. If
the file can’t be found, the program will fail quietly. This means that
no exception is thrown. Instead, the button will appear without an image. |
Labels, icons, and text
In some applications you might want to show a picture with an attached
text label. For example, a shopping cart application for an online clothing
store might shows pictures of clothes, each labeled with a name
and a price. The JLabel
class is flexible, able to display text alone,
an image alone, or both. However, a JLabel
is designed for displaying
information and can’t read user input.
Here are three different ways to create a label.
ImageIcon hibiscus = new ImageIcon("pictures/hibiscus.jpg")
JLabel textOnly = new JLabel("Text only");
JLabel flower = new JLabel(hibiscus);
JLabel labeledflower = new JLabel("Red Hibiscus", hibiscus, JLabel.CENTER);
The first JLabel
constructor above creates a label displaying only
text, namely, “Text only.” The second constructor creates a label
decorated with an icon created from a picture. The third constructor
creates a label with the same icon and additional text. The last
argument in this third case is JLabel.CENTER
, a constant that
specifies that the content of the label (both the image and the text)
should be placed horizontally in the center of the label. A horizontal
alignment of left or right could also be specified using the constants
JLabel.LEFT
or JLabel.RIGHT
, respectively.
Sometimes you might want to place the text below the icon that decorates the label. To do so, you could set the horizontal and vertical positions of the text as follows.
flower.setVerticalTextPosition(JLabel.BOTTOM);
flower.setHorizontalTextPosition(JLabel.CENTER);
Figure 15.10(a) is generated by Program 15.9.
import javax.swing.*;
import java.awt.*;
public class LabelExample {
public static void main(String[] args) {
JFrame frame = new JFrame("Label Example");
ImageIcon hibiscusIcon = new ImageIcon("pictures/hibiscus.jpg");
JLabel flower = new JLabel("Red Hibiscus", hibiscusIcon, JLabel.CENTER);
flower.setVerticalTextPosition(JLabel.BOTTOM); (1)
flower.setHorizontalTextPosition(JLabel.CENTER); (2)
frame.add(flower);
frame.setSize(300,250);
frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
frame.setVisible(true);
}
}
| 1 | Vertically position the text on the bottom of the label. |
| 2 | Horizontally center the text on the label. |
You will see the GUI shown in Figure 15.10(b) if the alignment instructions on these two lines are omitted.
15.3.5. Layout managers
Java provides a number of layout managers to assist with the design of
GUIs. A layout manager controls the placement of
components on a frame or panel. Every container has a default layout
manager, but it’s possible to change that manager to a different one.
In this section we’ll introduce the three layout managers FlowLayout
,
GridLayout
, and BorderLayout
. Java also provides several other layout
managers, each designed for different situations.
FlowLayout
The FlowLayout
manager is one of simplest. When a
container is using the FlowLayout
manager, components will be added in
order from left to right. When there’s no more space, subsequent
components will be added starting on the next row. In addition, each row of
components is centered within the container. The JPanel
container uses
FlowLayout
by default, but it’s possible to set it explicitly as
well.
JPanel panel = new JPanel(new FlowLayout());
When we’ve added more than one component to a JFrame
in previous
examples, we’ve first added them to a JPanel
. The reason we did so is
because FlowLayout
is the default layout manager for
JPanel
containers. Although every JFrame
has a container, it uses
the BorderLayout
manager by default, which would have complicated our
earlier examples. The next example illustrates FlowLayout
further.
FlowLayout
Program 15.10 creates a GUI with several buttons.
FlowLayout
.import javax.swing.*;
import java.awt.*;
public class FlowLayoutExample {
public static void main(String[] args) {
JFrame frame = new JFrame("FlowLayout Example"); (1)
JPanel panel = new JPanel(new FlowLayout()); (2)
final int MAX_BUTTONS = 6;
for(int i = 0; i < MAX_BUTTONS; i++) (3)
panel.add(new JButton(" " + i + " "));
frame.add(panel);
frame.setSize(300,200);
frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
frame.setResizable(false);
frame.setVisible(true);
}
}
| 1 | We first create a frame. |
| 2 | We create a panel and set its layout manager to FlowLayout . |
| 3 | This for loop is used to create new buttons, label them appropriately, and add them to the panel. |
The FlowLayout
manager places the buttons along a number of rows depending on the width of the frame.
If the container changes size, the components will react by "'flowing'" into different rows, hence the name.
The GUI created is shown in Figure 15.11.
FlowLayout
manager to generate a GUI containing six buttons.GridLayout
The GridLayout
manager lays out components in a grid with a set number
of rows and columns. As with other layout managers, GridLayout
can be
applied to frames and panels.
JFrame frame = new JFrame("Grid Layout Example");
frame.setLayout(new GridLayout(3, 2, 5, 5));
This snippet creates a frame named frame
and sets its layout manager
to GridLayout
. The first two arguments to GridLayout
give the number
of rows and columns, respectively. The last two arguments (which are optional)
give the horizontal and vertical gaps between the neighboring cells in the
grid. In this example the frame will contain a total of six cells organized
into three rows with two columns.
Figure 15.12 shows how frame
will look after six
buttons, labeled 0 through 5, have been added to it in order. The buttons
were created in the same way as the buttons in Program 15.10, but
they look different because of the GridLayout
manager. A key
feature of using GridLayout
is that all cells in the grid will be
the same size and will stretch to fill the entire container. Also note
the equal spacing between the neighboring cells. It’s possible to add
more cells or fewer cells than specified in the GridLayout
constructor, but
the layout manager will be forced to guess at your intentions.
We can write a program to display pictures of animals and identify which animal the mouse is current hovering over. The animal’s name will be displayed in the title of the frame. Figure 15.13 shows this GUI.
Program 15.11 creates the GUI shown in Figure 15.13.
GridLayout
manager.import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
public class AnimalIdentifier extends MouseAdapter { (1)
private JLabel bison, dove, gecko, spider; (2)
private JFrame frame = new JFrame("Animal Identifier");
public AnimalIdentifier() {
JPanel panel = new JPanel(new GridLayout(2,2,5,5)); (3)
ImageIcon bisonIcon = new ImageIcon("pictures/bison.jpg");
ImageIcon doveIcon = new ImageIcon("pictures/dove.jpg");
ImageIcon geckoIcon = new ImageIcon("pictures/gecko.jpg");
ImageIcon spiderIcon = new ImageIcon("pictures/spider.jpg");
bison = new JLabel(bisonIcon); (4)
bison.addMouseListener(this);
dove = new JLabel(doveIcon);
dove.addMouseListener(this);
gecko = new JLabel(geckoIcon);
gecko.addMouseListener(this);
spider = new JLabel(spiderIcon);
spider.addMouseListener(this);
panel.add(bison); (5)
panel.add(dove);
panel.add(gecko);
panel.add(spider);
frame.add(panel);
frame.setSize(400,400); (6)
frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
frame.setVisible(true);
}
| 1 | Class AnimalIdentifier extends MouseAdapter so that it can
be added as a MouseListener without the need to implement all of its methods. |
| 2 | We declare four labels and a frame as fields. The labels are named bison , dove , gecko , and spider . |
| 3 | Inside the constructor, we create a panel with a 2 × 2 GridLayout . Then, we create four image icons, one to decorate each label. |
| 4 | Each of the four labels is created with its respective icon. We also add the AnimalIdentifier object we’re constructing as a MouseListener for each label. |
| 5 | The buttons are added to the panel, and the panel is added to the frame. |
| 6 | The last few lines set the size of the frame, set its close operation, and make it visible. |
public void mouseEntered(MouseEvent e) { (1)
Object label = e.getSource(); (2)
if(label == bison)
frame.setTitle("Animal Identifier: Bison");
else if(label == dove)
frame.setTitle("Animal Identifier: Dove");
else if (label == gecko)
frame.setTitle("Animal Identifier: Gecko");
else if (label == spider)
frame.setTitle("Animal Identifier: Spider");
}
public static void main(String[] args) { (3)
new AnimalIdentifier();
}
}
| 1 | Since AnimalIdentifier extends MouseListener , we only need to implement the single MouseListener method we care about, mouseEntered() . |
| 2 | We get the label the mouse entered. We compare this label to the four labels and change the frame title correspondingly. |
| 3 | The main() method creates a new instance of
class AnimalIdentifier , launching the GUI. |
Play with Program 15.11. What happens when you resize the window?
BorderLayout
The BorderLayout
manager is the default one for a JFrame
. It allows
components to be laid out spatially in regions of a container. These
regions are north, south, east, west, and center. This layout is
intuitively easy to understand, but it’s difficult to describe
precisely.
You can only add one component to each region of the layout, and adding a component to any region is optional. The regions will stretch or shrink to accommodate the components inside. The north and south regions will only be as tall as needed to hold their contents, but their width will stretch as wide as the entire container. The east and west regions will only be as wide as needed to hold their contents, but their height will stretch as tall as needed to fit the remaining height of the container. Both the height and the width of the center region will stretch as big as it needs to fill the container.
BorderLayout
Here’s an example of a frame using BorderLayout
. Five buttons have
been added, one to each region, using the program shown below. Since the default layout manager of a JFrame
is a BorderLayout
,
we don’t need to state it explicitly, although we do in this case.
BorderLayout
.import javax.swing.*;
import java.awt.*;
public class BorderLayoutExample {
public static void main(String[] args) {
JFrame frame = new JFrame("BorderLayout Example");
frame.setLayout(new BorderLayout());
frame.add(new JButton("North"), BorderLayout.NORTH);
frame.add(new JButton("South"), BorderLayout.SOUTH);
frame.add(new JButton("East"), BorderLayout.EAST);
frame.add(new JButton("West"), BorderLayout.WEST);
frame.add(new JButton("Center"), BorderLayout.CENTER);
frame.setSize(400,250);
frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
frame.setVisible(true);
}
}
BorderLayout
generated by Program 15.12.Unlike FlowLayout
or GridLayout
, the location must be specified to
add a component to a BorderLayout
. To do so, the add()
method
takes a second parameter, which is one of the constants BorderLayout.NORTH
,
BorderLayout.SOUTH
, BorderLayout.EAST
, BorderLayout.WEST
, or
BorderLayout.CENTER
, depending on where you want to add the component.
If you don’t specify a second parameter, the component will be added to
the center region. Since only one component can be in each region,
adding a component to a region that’s already occupied will replace the
old component with the new one.
At first glance, BorderLayout
seems that it would rarely be useful.
However, this layout is frequently used because it establishes a spatial
relationship between different parts of a GUI. A
container with a BorderLayout
generally has other containers with
their own layouts added to its regions, as shown in the following
example.
We can make a GUI application that functions as a simple calculator. The calculator has the ten digits 0-9, a plus button, a minus button, and an enter button. At the top is a display that shows the current value.
We create ten JButton
objects for the digits and three more JButton
objects for plus, minus, and enter. The display is a JLabel
. The code
is given below.
import javax.swing.*;
import java.awt.*;
public class CalculatorLayout {
public static void main(String[] args) {
JFrame frame = new JFrame("Calculator Layout");
frame.setLayout(new BorderLayout());
JPanel numbers = new JPanel(new GridLayout(4,3));
numbers.add(new JButton("7"));
numbers.add(new JButton("8"));
numbers.add(new JButton("9"));
numbers.add(new JButton("4"));
numbers.add(new JButton("5"));
numbers.add(new JButton("6"));
numbers.add(new JButton("1"));
numbers.add(new JButton("2"));
numbers.add(new JButton("3"));
numbers.add(new JButton("0"));
numbers.add(new JButton("+"));
numbers.add(new JButton("-"));
frame.add(numbers, BorderLayout.CENTER);
JButton enter = new JButton("Enter");
frame.add(enter, BorderLayout.SOUTH);
JLabel display = new JLabel("0");
frame.add(display, BorderLayout.NORTH);
frame.setSize(300,350);
frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
frame.setVisible(true);
}
}
This program uses a frame with a BorderLayout
to arrange components around
a central panel with a GridLayout
. First, we
put the ten digit buttons in a panel with a GridLayout
having 4 rows
and 3 columns. We put the plus button and the minus button in the
remaining two cells of the grid. We add this grid panel to the center region
of the frame. We put the enter button in the south region
and the display in the north region.
Note that this program only creates the layout of a simple calculator, not the functionality.
There’s no limit to how deeply you can nest containers within each
other. Sometimes you must use many BorderLayout
managers to achieve the
appearance you want. The value of establishing these spatial relationships
is that they’re still valid even when the GUI is resized. Even so, it’s
challenging to make a usable GUI that looks attractive no matter how
the user resizes it or what the resolution of the user’s screen is.
Although there’s not enough space here to discuss strategies for
developing attractive GUIs, it’s worth noting the value of the
setPreferredSize()
method that all components have. This method is
one of the best ways to suggest a size for particular components,
especially if other components and containers are going to stretch
as needed. When key components have their sizes set using
setPreferredSize()
, the entire frame can be packed using the
pack()
method, which sizes the frame to fit its components.
The three layout managers discussed in this section are the simplest,
but there are others. The BoxLayout
manager is a useful tool for
laying out components in a line, whether horizontal like a row or
vertical like a column. The GridBagLayout
manager can be used to create complex layouts in a single container
using a grid-based framework that’s much more flexible than
GridLayout
, but the complexity of programming GridBagLayout
is
significant. The SpringLayout
and GroupLayout
managers are also
powerful, but they’re designed for use with a GUI builder utility.
15.3.6. Menus
Menus provide a useful form of interface that’s expected from most GUI applications. In this section we show how to create menus and respond to the selection of menu items. Menus are placed on a menu bar. Each menu usually consists of several menu items that could be selected by the user. In addition to simple text, a menu item can be a button, a radio button, check box, or an icon. A menu can have one or more sub-menus opening out of a menu item.
Creating menus
To use menus we have to create a menu bar.
JMenuBar menuBar = new JMenuBar();
This statement creates an object of type JMenuBar
named menuBar
which
can hold menus. A JFrame
has only one menu bar. We can create several
menus and add them to the menu bar.
JMenu frenchMenu = new JMenu("French Menu");
JMenu germanMenu = new JMenu("German Menu");
menuBar.add(frenchMenu);
menuBar.add(germanMenu);
These statements create two menus named frenchMenu
and germanMenu
labeled
“French Menu” and “German Menu,” respectively. The two menus can be added to the existing menu bar using the add()
method. Menus can be populated
with menu items as follows.
JMenuItem coqAuVin = new JMenuItem("Coq au vin");
JMenuItem moulesFrites = new JMenuItem("Moules-frites");
frenchMenu.add(coqAuVin);
frenchMenu.add(moulesFrites);
These statements create two menu items named coqAuVin
and
moulesFrites
. These menu items are then added to the menu frenchMenu
.
After having created a menu bar together with its menus and their
respective menu items, we need a frame so that we can set its menu bar
to the one we created.
JFrame frame = new JFrame("Menu Example");
frame.setJMenuBar(menuBar);
These statements create a frame and set its menu bar to menuBar
. It’s
possible to use the add()
method instead of the setJMenuBar()
method
to add a JMenuBar
to a JFrame
. However, doing so will add the
JMenuBar
to the regular content area, not to the menu area.
Figure 15.16 shows a GUI with the menus and menu items added above.
Sometimes you might need to disable a menu item and enable it only under certain conditions.
JMenuItem unusable = new JMenuItem("Currently unusable");
unusable.setEnabled(false);
These statements create a menu item named unusable
and disable it.
A disabled menu item shows as a gray item and doesn’t respond to
attempts to select it. As we showed in Example 15.9, JButton
objects, like many other components, can be disabled in the same way.
Adding events to menus
An action listener can be added to each menu item, just like a
JButton
. Then, when a menu item is clicked by the user, an action
event is generated. A JCheckBoxMenuItem
object can be added to a
JMenu
as well. This object will have a check box which can be selected
or unselected. A regular JMenuItem
object generates an ActionEvent
which is handled by an ActionListener
. Like a JCheckBox
, a
JCheckBoxMenuItem
can also generate an ItemEvent
handled by an
ItemListener
. Here are examples of both situations.
JMenuItem clickHere = new JMenuItem("Click here");
JCheckBoxMenuItem checkBox = new JCheckBoxMenuItem("Check yourself");
clickHere.addActionListener(this);
checkBox.addItemListener(this);
A JMenuItem
works just like a JButton
. In fact, the same action
listener code could handle events for both buttons and menu items.
Similar to JCheckBox
objects, a JCheckBoxMenuItem
can be handled with an ActionListener
if you only want to know when it’s clicked on. However, you can add an ItemListener
if you want to write an event handler that runs whenever the state of its check box changes.
15.4. Solution: Math tutor
Here we give the code to generate the GUI shown in Figure 15.1 which displays basic (addition and subtraction) as well as advanced (multiplication and division) problems.
As shown in Figure 15.1, this GUI has a menu bar consisting of two menus labeled “Type” and “Operations.” The “Type” menu contains a check box labeled “Advanced” while the “Operations” menu contains four menu items labeled “Add,” “Subtract,” “Multiply,” and “Divide.” Note that the “Multiply” and “Divide” menu items start disabled. They’ll be enabled when the user selects the “Advanced” check box.
Before we introduce the program that creates this GUI, we need a helper
class called ProblemGenerator
that can randomly generate arithmetic
problems. The class is designed so that the answers are always positive
integers.
import java.util.Random;
import javax.swing.*;
public class ProblemGenerator {
private Random random = new Random();
public int addPractice(JLabel label) {
int a = random.nextInt(12) + 1;
int b = random.nextInt(12) + 1;
label.setText(a + " + " + b + " = ");
return a + b;
}
public int subtractPractice(JLabel label) {
int a = random.nextInt(12) + 1;
int b = a + random.nextInt(12) + 1;
label.setText(b + " - " + a + " = ");
return b - a;
}
public int multiplyPractice(JLabel label) {
int a = random.nextInt(12) + 1;
int b = random.nextInt(12) + 1;
label.setText(a + " \u00D7 " + b + " = ");
return a * b;
}
public int dividePractice(JLabel label) {
int a = random.nextInt(12) + 1;
int b = a*(random.nextInt(12) + 1);
label.setText(b + " \u00F7 " + a + " = ");
return b / a;
}
}
The code listed above has methods addPractice()
,
subtractPractice()
, multiplyPractice()
, and dividePractice()
. Each
method generates an appropriate math problem, sets an input JLabel
to
display the problem, and returns the solution. Note that \u00D7
and
\u00F7
are the Unicode values for the multiplication (×) and division (÷)
symbols.
Program 15.15 generates the GUI in Figure 15.1.
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
public class MathTutor implements ActionListener, ItemListener {
private JMenuItem add = new JMenuItem("Addition"); (1)
private JMenuItem subtract = new JMenuItem("Subtraction");
private JMenuItem multiply = new JMenuItem("Multiply");
private JMenuItem divide = new JMenuItem ("Divide");
private JLabel score = new JLabel("Score: 0 Correct 0 Incorrect");
private JLabel label = new JLabel();
private JTextField field = new JTextField(10);
private JButton submitButton = new JButton("Submit");
private ProblemGenerator generator = new ProblemGenerator(); (2)
private int correct = 0; (3)
private int incorrect = 0;
private int answer = -1;
| 1 | We begin by creating the GUI components that need to interact with event handlers: four menu items, a label, a text field, and a button. |
| 2 | We also create a ProblemGenerator object to help create new problems. |
| 3 | Next, we add int fields to store the number of correct and incorrect answers as well as the answer to the current problem. |
public MathTutor() { (1)
JFrame frame = new JFrame("Math Tutor");
JMenuBar menuBar = new JMenuBar();
JMenu typeMenu = new JMenu("Type");
JMenu operationsMenu = new JMenu("Operations");
JCheckBoxMenuItem advanced = new JCheckBoxMenuItem("Advanced");
// Add listeners to menu items (2)
add.addActionListener(this);
subtract.addActionListener(this);
multiply.addActionListener(this);
divide.addActionListener(this);
advanced.addItemListener(this); (3)
//Add anonymous ActionListener to submitButton (4)
submitButton.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
int response = Integer.parseInt(field.getText());
if(response == answer)
correct++;
else
incorrect++;
label.setText("");
score.setText("Score: " + correct + " Correct " +
incorrect + " Incorrect");
submitButton.setEnabled(false);
}
});
| 1 | Inside the MathTutor constructor, we create the frame and the remaining components. |
| 2 | Action listeners are added to the four operations menu items. |
| 3 | An item listener is added to the “Advanced” check box menu item because it requires a different kind of event handler. Note that MathTutor implements both the ActionListener and ItemListener interfaces, allowing it to handle both kinds of events. |
| 4 | Next, we add an anonymous ActionListener just for the submit button. It
reads the response from the text field, converts it into an integer, and checks against the answer. If the response is correct, it increments the counter for correct answers. Otherwise, it increments the counter for incorrect answers. Finally, it updates the score label and disables the submit
button until another operation is chosen from the menu. |
typeMenu.add(advanced); (1)
operationsMenu.add(add); (2)
operationsMenu.add(subtract);
operationsMenu.add(multiply);
operationsMenu.add(divide);
multiply.setEnabled(false); (3)
divide.setEnabled(false);
menuBar.add(typeMenu); (4)
menuBar.add(operationsMenu);
frame.setJMenuBar(menuBar);
// Add components to frame and display GUI (5)
frame.add(score, BorderLayout.NORTH);
frame.add(label, BorderLayout.WEST);
frame.add(field, BorderLayout.EAST);
frame.add(submitButton, BorderLayout.SOUTH);
submitButton.setEnabled(false);
frame.setSize(300, 150);
frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
frame.setVisible(true);
}
| 1 | We add the advanced menu item to the type menu. |
| 2 | Then, we add the four kinds of operations to the operations menu. |
| 3 | The multiply and divide menu items are disabled because the application starts in basic mode. |
| 4 | The menus are then added to the menu bar, and the menu bar is set on the frame. |
| 5 | Finally, the labels and text field are added to the frame, which is made visible. Note that these components can be added directly to the frame with the parameters BorderLayout.NORTH , BorderLayout.EAST , and BorderLayout.WEST because JFrame objects use the BorderLayout manager by default. |
public void itemStateChanged(ItemEvent e) {
if(e.getStateChange() == ItemEvent.SELECTED) {
add.setEnabled(false);
subtract.setEnabled(false);
multiply.setEnabled(true);
divide.setEnabled(true);
}
else {
add.setEnabled(true);
subtract.setEnabled(true);
multiply.setEnabled(false);
divide.setEnabled(false);
}
}
The itemStateChanged()
method enables the multiply
and divide
menus and
disables the add
and subtract
menus if the advanced
check box is
selected and does the reverse if it’s been unselected.
public void actionPerformed(ActionEvent e){
Object menuItem = e.getSource();
if(menuItem == add)
answer = generator.addPractice(label);
else if(menuItem == subtract)
answer = generator.subtractPractice(label);
else if(menuItem == multiply)
answer = generator.multiplyPractice(label);
else if(menuItem == divide)
answer = generator.dividePractice(label);
submitButton.setEnabled(true);
field.setText("");
}
Depending on which menu item fired the event, the actionPerformed()
method calls the
appropriate method from the ProblemGenerator
object to create and then
display a math problem on label
. The correct answer is stored in answer
.
Note that this actionPerformed()
method doesn’t handle the submit button
because it has its own anonymous class handler. We use both styles of listener
to keep the code simpler: Handling the submit button separately makes sense
because it’s different, but handling the operations menu items together makes
sense because they’re similar.
public static void main(String[] args){
new MathTutor();
}
}
Last but not least, the main()
method creates an instance of MathTutor
, initiating GUI construction and display.
15.5. Concurrency: GUIs
Stand-alone Java programs have at least one thread, the main thread. Applications with GUIs create additional threads to manage the GUI behind the scenes.
Although a GUI will create several threads, the most relevant of these
is the event dispatch thread (EDT). This thread handles events like
button clicks. When you write actionPerformed()
methods, remember
that the EDT is the one that will actually execute the code inside.
If you’re writing a complex program, the EDT may interact with many other threads, and the synchronization issues discussed in Chapter 14 will become important. However, only the EDT is allowed to change the state of components in a GUI. Using other threads to do so will work some of the time, but it’s not thread-safe and violates the design of Swing.
15.5.1. Worker threads
Thread safety is not the most common multi-threaded GUI problem, however. Unresponsive GUIs can be found on almost every platform, as you’ve no doubt experienced. In Java, unresponsive GUIs usually happen when the programmer uses the event dispatch thread to perform some task that takes too long. Because the EDT is responsible for updating the GUI, the GUI freezes, and the user has to wait.
This problem presents quite a conundrum. On the one hand, the EDT is the only thread allowed to update components. On the other, it has to do its work quickly so that the GUI is responsive. The solution is to spawn worker threads to do the job. When they’re done, they can tell the EDT to update the GUI.
Let’s look at a GUI with two JButton
components and two JLabel
components.
When one button’s pressed, the EDT goes to sleep for 5 seconds before