Lecture #10: Graphical User Interfaces in Java
Introduction
Almost every non-trivial program eventually needs to talk to a human being, and for decades the dominant way of doing that has been the graphical user interface: windows, buttons, sliders, text fields, menus. Java has shipped with GUI support since its very first release, and the way that support is designed turns out to be one of the best real-world illustrations of object-oriented ideas you will meet in this course — inheritance hierarchies of components, polymorphism through event listeners, and the Composite and Observer design patterns all show up naturally once you start building windows.
This chapter looks at Java’s two historical GUI toolkits, AWT and Swing, and uses them as a vehicle to revisit and reinforce ideas you have already seen: class hierarchies, interfaces, inner and anonymous classes, and — since Java 8 — lambda expressions. The running theme is that a GUI is not a monolithic block of code but a society of objects collaborating through well-defined interfaces, and understanding that society is what lets you build interfaces that are both flexible and maintainable.
Two Toolkits, One Set of Ideas: AWT and Swing
Java’s original windowing library is java.awt (Abstract Window Toolkit). AWT components are “heavyweight”: each one is backed by a native “peer” object supplied by the underlying operating system’s windowing system. A Java Button on Windows is, under the hood, wired to an actual Windows button; on macOS it is wired to a native Cocoa button. This gives AWT applications a very authentic native look, but it also means the toolkit is only as rich, and as consistent across platforms, as the lowest common denominator of the native widget sets it wraps.
javax.swing, introduced a couple of years later, takes a different approach. Swing components are “lightweight”: instead of delegating drawing to the operating system, they paint themselves using Java2D, Java’s own 2D graphics engine. Because Swing does not depend on native peers, it can offer a much richer and more uniform component library across every platform (tables, trees, styled text, and dozens of other widgets), and it supports pluggable “look and feel” — the same application can be made to look like a Windows app, a macOS app, or a custom-skinned app, all without changing a line of business logic. Swing classes generally extend or complement their AWT ancestors and are conventionally prefixed with a capital J — JFrame, JButton, JLabel, and so on — precisely to distinguish them from their older AWT counterparts (Frame, Button, Label).
There is also a third library worth knowing about by name, SWT (Standard Widget Toolkit), used by the Eclipse IDE. SWT goes back to the AWT philosophy of native peers, implemented through JNI (Java Native Interface), trading portability for pixel-perfect native fidelity. You are unlikely to use SWT directly in this course, but it is useful to know that “wrap the native widgets” and “draw everything yourself” are the two fundamental strategies every GUI toolkit chooses between, in Java or in any other language.
One practical piece of advice: avoid mixing AWT and Swing components inside the same window. Because AWT components are painted by the native system and Swing components are painted by Java2D, mixing them can produce inconsistent stacking (“Z-order”) between overlapping windows and popups. It works in simple cases, but it is a trap worth knowing about before you hit it.
Separating the Model from the Interface
Before writing a single line of GUI code, it pays to design the functional core of your application — often called the “model” — completely independently of how a user will eventually interact with it. This core should know nothing about buttons or windows; it should simply expose the operations and data that make your application useful. Only once that core exists and works on its own do you design the graphical layer on top of it: choosing which components you need, arranging them on screen (layout), and finally wiring up the “dynamics” of the interface — the event-driven logic that connects user actions to the model.
This separation is not just good style, it is what allows the same functional core to be reused behind several different interfaces: a full graphical window today, a stripped-down mobile screen tomorrow, or even a plain command-line interface for scripting and testing. We will come back to this idea, sometimes called MVC (Model-View-Controller) in its fuller form, at the end of the chapter.
As a working example throughout this chapter, imagine a small functional core that converts a temperature between Celsius and Fahrenheit and additionally reports which of a small set of comfort bands it falls into. Notice that this class below has no dependency at all on any GUI package — it is pure, testable Java:
public class TemperatureModel {
private double celsius; // single normalized state variable
public void setCelsius(double c) { this.celsius = c; }
public void setFahrenheit(double f) { this.celsius = (f - 32) * 5.0 / 9.0; }
public double getCelsius() { return celsius; }
public double getFahrenheit() { return celsius * 9.0 / 5.0 + 32; }
public String getComfortBand() {
if (celsius < 0) return "FREEZING";
if (celsius < 15) return "COLD";
if (celsius < 25) return "COMFORTABLE";
return "HOT";
}
}
Notice the same design trick you will see throughout this kind of code: a single internal state variable (celsius) is kept normalized in one unit, and conversions to and from the other unit are computed on the fly through simple formulas. This avoids the bugs that come from keeping two representations of the same quantity in sync by hand.
Building Blocks: Components and the Composite Pattern
A Swing (or AWT) interface is built out of components. Some components are “elementary”: a JButton, a JLabel, a JTextField, a JSlider — things that display or collect a single piece of information and cannot themselves contain other components. Others are “composite”: a JFrame (a top-level application window) or a JPanel (a sub-panel used to group components) — things whose entire purpose is to hold and arrange other components, including other composite ones.
A simple Swing interface combining a slider, labels, text fields, and a button — the kind of small toolkit of elementary components most GUI screens are built from.
This elementary/composite split is a textbook application of the Composite design pattern: a hierarchy in which a “leaf” and a “container of leaves” share a common supertype, so that client code can treat a single component and an entire tree of components uniformly. In Swing this shared vocabulary is expressed through the Container class (which knows how to hold and arrange child components) and, at the very top, an interface implemented by every top-level window:
// package javax.swing; — a simplified sketch, not the real (much larger) interface
public interface RootPaneContainer {
Container getContentPane();
}
Every JFrame and JDialog implements this interface, meaning that no matter what kind of “root” window you are building, you always retrieve its content area the same way — getContentPane() — and add children to that content pane using the same add(Component) method inherited all the way from java.awt.Container. This is exactly the value that the Composite pattern provides: uniform treatment of the whole tree through a shared interface, regardless of how deep or how simple that tree actually is.
Elementary components and composite (container) components both descend from a common Component/Container lineage — the classic shape of the Composite pattern.
The class hierarchy under java.awt.Component and javax.swing.JComponent is deep and, frankly, a good specimen to study if you want to see inheritance used well in a real library: buttons, checkboxes, and radio buttons all extend a common AbstractButton (because they share so much behavior — a label, an icon, a pressed/armed state); text areas and text fields both extend a common JTextComponent (because they share editing, selection, and caret behavior); and dozens of other widgets each specialize the generic JComponent contract in their own way.
A fragment of the java.awt.Component / javax.swing.JComponent hierarchy — buttons, checkboxes and radio buttons share AbstractButton; text fields and text areas share JTextComponent.
Positioning Components: Layout Managers
Creating a component does not position it. If you simply construct a dozen JButton and JLabel objects and add them to a frame with no further instructions, Swing has to decide somehow where each one goes, and by default it will not produce anything usable. This is the job of a LayoutManager, an object attached to a Container that is responsible for computing the size and position of every child component whenever the container is resized, shown, or otherwise needs to be laid out again.
// package java.awt; — a simplified sketch of the two methods that matter here;
// both are concrete (implemented), not abstract, in the real Container class
class Container extends Component {
void add(Component c) { /* ... */ }
void setLayout(LayoutManager lay) { /* ... */ }
}
Java ships with several ready-made layout managers, each embodying a different positioning strategy:
BorderLayout— the default layout of aJFrame’s content pane — divides the container into five regions:NORTH,SOUTH,EAST,WEST, andCENTER. It is perfect when you have one dominant central component (say, a text area) surrounded by a handful of secondary ones (a toolbar on top, a status bar at the bottom).FlowLayoutsimply places components left to right, wrapping to a new row when it runs out of horizontal space — much like words wrapping in a paragraph.GridLayoutarranges components into a strict grid of L rows by C columns, all cells the same size.GridBagLayoutis the most powerful and most verbose: each component is placed according to a detailed set of topological constraints (which cell, how many rows/columns it spans, how much extra space it should absorb when the window grows, and so on).BoxLayout(fromjavax.swing) stacks components in a single row or column, similar in spirit toGridLayoutbut allowing cells of different sizes.CardLayoutstacks components like a deck of cards, showing only one “card” at a time — useful for wizard-style screens or tabbed views implemented manually.
Choosing a layout manager, rather than hand-computing pixel coordinates, is what allows a Swing window to resize gracefully, adapt to different fonts and screen densities, and remain reasonably portable across platforms.
Worked Example: Assembling a Window
Let’s put this together with a small, original example — a temperature converter window built on top of the TemperatureModel class from earlier. First, we declare the frame, the model it wraps, and the components it will need:
import javax.swing.*;
import javax.swing.event.*;
import java.awt.*;
import java.awt.event.*;
public class TemperatureFrame extends JFrame {
// the system being interfaced — the functional core
private final TemperatureModel model = new TemperatureModel();
// the components, created but not yet positioned
private final JLabel celsiusLabel = new JLabel("Celsius", JLabel.CENTER);
private final JLabel fahrenheitLabel = new JLabel("Fahrenheit", JLabel.CENTER);
private final JLabel comfortLabel = new JLabel("COMFORT BAND: ...", JLabel.CENTER); // set for real in the constructor
private final JTextField celsiusField = new JTextField("0.0");
private final JTextField fahrenheitField = new JTextField("32.0");
private final JButton resetButton = new JButton("Reset");
private final JSlider celsiusSlider = new JSlider(JSlider.HORIZONTAL, -20, 45, 0);
// constructor continues below...
}
Notice that constructing these objects has, so far, done nothing visible: none of them has been placed anywhere yet. Positioning happens explicitly, in the constructor, by choosing a layout manager and calling add for each component in the order we want them to appear:
public TemperatureFrame() {
Container contentPane = getContentPane();
contentPane.setLayout(new GridLayout(7, 1));
add(celsiusLabel);
add(celsiusSlider);
add(celsiusField);
add(fahrenheitLabel);
add(fahrenheitField);
add(comfortLabel);
add(resetButton);
refreshDisplay(); // sync the labels with the model's actual initial state
// event wiring will go here — see the next section
}
At this point we have a window with the right shape, but it does nothing: typing a new value and pressing Enter has no effect, moving the slider has no effect, and clicking “Reset” has no effect. That is because we have only described the static structure of the interface. Making it react to the user is the subject of the next section, and it is where object-oriented programming really earns its keep.
Event-Driven Programming and the Observer Pattern
Here is the fundamental problem GUI toolkits have to solve: a JButton object has absolutely no idea, when it is written, what any particular application is going to want to do when it is clicked. The people who wrote JButton could not possibly anticipate every use case. So instead of baking application logic into the button, Swing (like essentially every GUI toolkit in every language) turns the problem inside out: components publish the fact that something happened, and interested application objects subscribe to be told about it. This is event-driven programming, and structurally it is a direct application of the Observer design pattern.
A GUI component (the “subject”) keeps a list of registered listener objects (the “observers”) and notifies them, through a common interface, whenever a relevant event occurs.
In the classic Observer pattern, a subject keeps a list of interested observers and notifies all of them whenever its state changes, without needing to know anything about what each observer will actually do with that notification. In Swing, GUI components play the role of the subject, and listener objects play the role of the observer. The two sides communicate purely through interfaces — this is the same idea of “programming to an interface, not an implementation” that underlies polymorphism generally: the button does not call MyApplicationLogic.handleClick(), it calls listener.actionPerformed(event) on every object registered as an ActionListener, and it is entirely up to each concrete listener class to decide, through overriding, what that call actually does.
Events themselves are represented as objects, all ultimately descending from java.awt.AWTEvent. There is a whole small hierarchy of event classes — ActionEvent, MouseEvent, KeyEvent, WindowEvent, ChangeEvent, and more — reflecting the fact that different kinds of user interaction carry different kinds of information (a MouseEvent needs coordinates; an ActionEvent mostly just needs to say “this happened”). Each component class documents which events it is capable of generating: a JButton fires ActionEvents (and, like any component, low-level MouseEvents and KeyEvents too); a JSlider fires ChangeEvents whenever its value changes; a JTextField fires an ActionEvent when the user presses Enter while it has focus.
For every kind of event XXEvent, Swing defines a matching listener interface XXListener that declares the method(s) called when that event occurs. ActionListener declares one method, actionPerformed(ActionEvent e). ChangeListener declares stateChanged(ChangeEvent e). WindowListener declares several methods (windowClosing, windowActivated, windowIconified, and so on) because a window can experience several distinct lifecycle events — and because implementing all of those methods just to handle one of them would be tedious, Swing also provides convenience Adapter classes (WindowAdapter, MouseAdapter, …) with empty default implementations of every method, so your own listener class only needs to override the one method it actually cares about.
Registering a listener on a component follows one consistent naming convention across the whole library: component.addXXListener(listener), with a matching removeXXListener to unsubscribe later. JButton and JTextField both support addActionListener(ActionListener l); JSlider supports addChangeListener(ChangeListener l); every component supports addMouseListener, addKeyListener, and so on, because those apply universally.
Let’s wire up our temperature converter. Because the listeners need to react by touching model, celsiusField, fahrenheitField, and the other fields of TemperatureFrame, the natural place to define them is as inner classes of TemperatureFrame — nested classes that automatically retain a reference to the enclosing frame instance, so they can freely read and modify its fields:
public class TemperatureFrame extends JFrame {
// ... fields as before ...
private class CelsiusFieldListener implements ActionListener {
public void actionPerformed(ActionEvent e) {
double value = Double.parseDouble(celsiusField.getText());
model.setCelsius(value);
refreshDisplay();
}
}
private class SliderListener implements ChangeListener {
public void stateChanged(ChangeEvent e) {
model.setCelsius(celsiusSlider.getValue());
refreshDisplay();
}
}
private class ResetListener implements ActionListener {
public void actionPerformed(ActionEvent e) {
model.setCelsius(0.0);
celsiusSlider.setValue(0);
refreshDisplay();
}
}
private void refreshDisplay() {
celsiusField.setText(String.format("%.2f", model.getCelsius()));
fahrenheitField.setText(String.format("%.2f", model.getFahrenheit()));
comfortLabel.setText("COMFORT BAND: " + model.getComfortBand());
}
}
And back in the constructor, after positioning the components, we simply subscribe each listener to its component:
public TemperatureFrame() {
// ... layout code from before ...
celsiusField.addActionListener(new CelsiusFieldListener());
celsiusSlider.addChangeListener(new SliderListener());
resetButton.addActionListener(new ResetListener());
}
Notice fahrenheitField never gets a listener here — in this example it’s treated purely as an output, refreshed by refreshDisplay() whenever something else changes the model, not as a second way to input a value. Wiring it up symmetrically (parsing typed Fahrenheit, converting back to Celsius, updating everything else) is a natural extension exercise: it would need its own FahrenheitFieldListener, mirroring CelsiusFieldListener below but converting in the other direction.
Trace through what happens when the user types 10.0 into celsiusField and presses Enter: the text field fires an ActionEvent; Swing looks up the list of registered ActionListeners for that field and finds our CelsiusFieldListener; it calls actionPerformed on it; that method reads the new text, pushes the value into the model, and asks the frame to refresh every dependent label and field. The button, the field, and the slider never call any of this logic directly by name — they only ever call the single abstract method declared by the interface they were handed. That indirection through an interface is what makes it possible to plug in any listener, for any purpose, onto any component that fires that kind of event, without the component’s own code ever changing. It is polymorphism doing exactly the job it was designed for.
Anonymous Classes and Lambda Expressions
Writing a whole named inner class for a listener that is only ever used once, in one place, is often more ceremony than the logic deserves. Java lets you collapse the class declaration and its single instantiation into one expression using an anonymous class:
resetButton.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
model.setCelsius(0.0);
celsiusSlider.setValue(0);
refreshDisplay();
}
});
This defines an unnamed class implementing ActionListener and immediately creates one instance of it, all inline. Behind the scenes the compiler still generates a real .class file for it (numbered, like TemperatureFrame$1.class, rather than named).
Since Java 8, interfaces that declare exactly one abstract method — like ActionListener and ChangeListener — are recognized as functional interfaces, and any anonymous implementation of one that does not need extra instance state can be written even more tersely as a lambda expression:
resetButton.addActionListener(e -> {
model.setCelsius(0.0);
celsiusSlider.setValue(0);
refreshDisplay();
});
This is exactly the same behavior as the anonymous class above, just without the boilerplate of naming the interface and the method — the compiler infers both from the context (the parameter type expected by addActionListener). It is worth pointing out explicitly what made this shortcut possible: the shape of the interface (one method, no required state) is what determines whether a lambda can stand in for a full listener class. A WindowListener, which declares five methods, cannot be replaced by a bare lambda for exactly this reason.
A Few More Components You’ll Meet
TemperatureFrame only needed JLabel, JTextField, JButton, and JSlider, but a few other components come up often enough to be worth a quick mention before you meet them in the wild:
JTextArea— a multi-line text component, unlike the single-lineJTextField. It’sContainer-agnostic (it doesn’t scroll on its own — wrap it in aJScrollPaneif the content might overflow), and it’s frequently used as a read-only output/status area rather than for input:area.setEditable(false)turns off user editing while still letting your code update its contents withsetText(...).JComboBox<T>— a dropdown selection list. Construct it from a fixed set of items and register a listener the same way as any other component:JComboBox<String> combo = new JComboBox<>(new String[]{"a", "b", "c"}); combo.addActionListener(e -> ...);— inside the listener,combo.getSelectedItem()tells you what the user picked.ImageIcon— loads an image (PNG, JPEG, GIF) from a file or classpath resource, and can be attached to any component that accepts an icon:new JLabel(new ImageIcon("diagram.png"))displays a static picture as a label with no text.
These follow the same component/layout/listener discipline as everything else in this chapter — nothing new conceptually, just a larger vocabulary of concrete JComponent subclasses to draw on.
Reading Someone Else’s GUI Code
Everything so far has been building a window from an empty class outward. In practice — and in this course’s own lab work — you’ll just as often be handed a partially built GUI (someone else’s JFrame subclass, already wired up for most of its behavior) and asked to read through it, understand how its pieces fit together, and complete a specific missing piece.
The reading strategy is the mirror image of the writing process covered in this chapter:
- Find the fields first — the component declarations tell you the static shape of the window, exactly like the field-declaration block at the top of
TemperatureFrame. - Find the constructor — this is where
add(...)calls reveal the actual layout, and whereaddXXListener(...)calls reveal which components are already interactive. - Match each listener back to the component it’s registered on, and read its body to see what state it touches — this tells you which fields of the enclosing class are “live,” i.e. actually wired into the running application, versus merely declared.
- A component with no registered listener is either intentionally static (a label, a picture) or a gap you’re meant to fill in — exactly the situation you’ll be in in this course’s final lab, where a provided GUI class is missing one listener and one display component, and your job is to identify the gap using this same reading process before writing a single new line.
Activating the Application
A GUI window, once fully built and wired, still is not visible until you explicitly say so. The typical shape of a Java main method for a Swing application is: construct the frame, configure its size and title, and finally call setVisible(true):
public class Main {
public static void main(String[] args) {
TemperatureFrame frame = new TemperatureFrame();
frame.setSize(420, 320);
frame.setTitle("Temperature Converter");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setVisible(true);
}
}
setDefaultCloseOperation deserves a mention: by default, closing a JFrame’s window only hides it — the Java process keeps running. EXIT_ON_CLOSE tells the frame to terminate the whole application when its close button is clicked, which is what you usually want for a standalone desktop application.
Conclusion: Model and View, Separately
The thread running through this whole chapter is a single design discipline: keep your application’s real logic — its “model” or functional core — cleanly separated from whichever interface happens to present it to a user. The TemperatureModel class never imports javax.swing; it can be unit-tested, reused from a command-line tool, or driven by a completely different graphical front end without a single change. The TemperatureFrame class, conversely, contains no conversion arithmetic at all — it only creates components, arranges them, and translates user actions into calls on the model plus a refresh of the display.
This separation is what makes it realistic to support several interfaces for the same application (a full desktop window, a simplified interface for a different category of users, a plain console mode for testing or scripting), to swap one interface implementation for another without touching business logic, and — in a client-server or mobile setting — to run the functional core on one machine while its interface lives somewhere else entirely, on a browser or a phone. That layered idea, of a model observed and driven by one or more independent views, is usually given a name of its own: MVC, Model-View-Controller — a pattern you will very likely meet again, in Java and beyond.
Recap
- AWT wraps native platform widgets (heavyweight, one native peer per component); Swing draws its own components with Java2D (lightweight, richer and more portable, supports pluggable look-and-feel). Avoid mixing the two in one window.
- Design the functional core independently of any interface; only then design the GUI on top of it.
- GUI components form a Composite-pattern hierarchy: elementary components (
JButton,JLabel,JTextField, …) versus composite/container components (JFrame,JPanel, …), unified throughContainerandRootPaneContainer. - Layout managers (
BorderLayout,FlowLayout,GridLayout,GridBagLayout,BoxLayout,CardLayout) compute component positions automatically; creating a component never positions it. - GUI interaction is event-driven, an application of the Observer pattern: components are subjects, listener objects are observers, and they communicate solely through
XXListenerinterfaces declaringactionPerformed,stateChanged, and similar callback methods — a direct, practical use of polymorphism. - Listeners are commonly written as inner classes (when they need access to enclosing-frame state), anonymous classes (when used only once), or — for single-method listener interfaces — lambda expressions (the most concise form, available since Java 8).
- An application becomes visible only once you explicitly call
setVisible(true)on its top-level frame, typically frommain. - Keeping the model and the view separate is what lets you reuse the same functional core behind different, interchangeable interfaces — the essence of the MVC pattern.