Java SE · For CS students
Build and Ship a Desktop App with Java SE: A Hands-On Guide for CS Students
Most computer science courses teach Java with programs that print text. Real software has windows, buttons, files and an installer. This guide takes you from an empty folder to a working desktop app, then to something you can email to a friend or put on GitHub as a download. You will write the app with plain Java SE, test the logic, and package it with the tools that ship inside the JDK. A reading map near the start shows which topics of the Deitel and Deitel book (Java How to Program) line up with each step.
- A desktop app is event-driven: a loop waits for clicks and keys, and your code reacts. That idea is the real lesson.
- Swing is already inside the JDK, so a first app needs no downloads. JavaFX is the modern alternative, but it is a separate download.
- Keep logic (save, load, rules) in plain classes and screens in others. Then you can test the logic with JUnit.
- Never do slow work on the event thread, or the window freezes. Use
SwingWorker(orPlatform.runLaterin JavaFX). - Package with
jarfor a quick share, and withjpackagefor a real installer that includes Java, so users do not need to install it. - You must build the installer on each operating system you target. A GitHub Actions matrix can do that for you.
- A project ladder and a portfolio checklist are at the end.
Contents
- What you will build, and why desktop still matters
- Using the Deitel and Deitel book with this guide
- Set up your tools
- How a desktop app works: events
- Your first window
- Build the Notes app, step by step
- Keep the window responsive
- Swing or JavaFX?
- Structure the project and test it
- Polish: look, settings and accessibility
- Package a JAR
- Ship it: jlink and jpackage
- Publish releases and automate builds
- Common mistakes
- Projects to practise, and a portfolio checklist
- Takeaways and glossary
1. What you will build, and why desktop still matters
We will build Notes, a small desktop app. You type a note, press Enter, and it appears in a list. You can delete a note. When you close the app and open it again, your notes are still there, because they are saved to a file in your home folder. It is small enough to finish in one sitting, and it touches almost everything a bigger app needs: a window, buttons, lists, input, files, error messages, and a way to package it.
Why bother with desktop when so much software lives on the web or on phones? Because it teaches things that other platforms hide behind frameworks:
- Event-driven programming. Your
mainmethod returns almost at once, and the program then lives on in listeners. This is how Android, web front ends and games work too. - Threads in the real world. You will meet the rule that the UI thread must never be blocked, with a visible window to prove it.
- Files, paths and the operating system. Where to store data, and how to behave on Windows, macOS and Linux.
- Packaging. What it takes to turn source code into something another person can double-click.
Desktop apps are also still everywhere: IDEs, music tools, lab equipment software, internal business tools, and teaching tools. Java is a good fit because one source code runs on all three major systems.
Java SE, JDK and JRE in one minute
| Term | Meaning |
|---|---|
| Java SE | Java Standard Edition: the core language and libraries, including the GUI toolkit Swing. This is what you use for desktop apps. |
| JDK | Java Development Kit: the compiler (javac), the runtime and the tools (jar, jlink, jpackage). You install this to write programs. |
| JRE / runtime | What runs a compiled program. Users used to install it separately. Today you can bundle it inside your app (section 12). |
| LTS | Long-term support release. Versions 17 and 21 are LTS releases. Pick one for coursework so your code does not move under you. |
2. Using the Deitel and Deitel book with this guide
Java How to Program by Paul Deitel and Harvey Deitel (published by Pearson) is one of the most widely used Java textbooks in university courses. It teaches by showing complete, runnable programs, and it ships its source code online. This guide is a project that sits on top of that kind of course: the book explains each language feature, and this guide shows how they join together in one application.
Which edition? The publisher’s site (deitel.com) currently lists Java How to Program 11th edition in an Early Objects and a Late Objects version, and a newer 12th edition with an “objects-natural” approach. Chapter numbers and titles differ by edition, so use the table of contents of your edition. The topic names below are the stable part.
| Step in this guide | Java idea | Look for this topic in the book |
|---|---|---|
| The Notes model and screens | Classes, objects, methods, constructors | Introduction to classes and objects; methods in depth |
| The list of notes | Collections, ArrayList, generics | Arrays and ArrayLists; generic collections |
| Splitting logic from the screen | Interfaces, composition, inheritance | Inheritance; polymorphism; interfaces; composition vs inheritance |
| Reading and saving notes | Files, paths, streams | Files, input and output streams (the NIO classes) |
| “Could not save” messages | Exceptions, try and catch | Exception handling |
| Buttons, lists and layouts | GUI components and event handling | The JavaFX GUI chapters in recent editions; the optional Swing GUI chapters |
Lambdas such as e -> addNote() | Lambdas and functional interfaces | Lambdas and streams |
| Not freezing the window | Threads and concurrency | Concurrency |
| Storing data in a database (an exercise) | JDBC | Database access with JDBC (online chapter in some editions) |
How to get the most out of the book
- Type the examples. Do not paste them. Your fingers learn the syntax, and typing forces you to read every line.
- Download the book’s source code from the publisher’s site and run the finished programs, then break them on purpose.
- Do the exercises, then this guide’s project. The exercises give you accuracy, and a project gives you finishing power.
- Use JShell (type
jshellin a terminal) to try a single line of Java without writing a class.
Recent editions teach the GUI with JavaFX and keep Swing as optional or online material, while older editions teach Swing. This guide builds in Swing first because it needs nothing extra to install, then shows JavaFX side by side. The ideas (components, layouts, events) carry over almost unchanged.
3. Set up your tools
- Install a JDK (version 21 is a good choice; 17 also works). Free builds include Eclipse Temurin from Adoptium, and the builds from Oracle, Microsoft, Amazon and others. Your course may name one.
- Pick an editor. IntelliJ IDEA Community, NetBeans, Eclipse or VS Code with the Java extension are all fine. An IDE helps with errors and auto-complete. We will also use the terminal, so you understand what the IDE does for you.
- Check the install in a terminal:
java -version
javac -version
jpackage --version
If java works but javac does not, you installed a runtime but not the JDK. Install the JDK. If the commands are not found at all, the JDK’s bin folder is not on your PATH.
Project layout
notes-app/
src/
notes/
NoteStore.java <- saves and loads notes (no GUI code)
NotesApp.java <- the window
test/ <- unit tests, later
out/ <- compiled classes (created by javac)
dist/ <- the finished JAR (created later)
The folder name under src must match the package line at the top of each file. A file starting with package notes; must live in src/notes/.
4. How a desktop app works: events
A console program runs from the first line to the last, and then it ends. A desktop program does something different. After it builds the window, it starts an event loop: a loop that waits for the user to do something. Each click, key press or mouse move becomes an event placed in a queue. A single thread, the event dispatch thread (EDT), takes events from the queue one by one and calls the listener you registered for that event. Your code runs only inside those listeners.
Analogy. A cafe with one waiter. Customers (events) line up, and the waiter serves them one at a time. If the waiter goes to the kitchen to cook a three-hour stew, nobody else gets served, even to be handed a glass of water. That frozen cafe is what a frozen window looks like.
5. Your first window
Save this as src/notes/Hello.java:
package notes;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.SwingUtilities;
import javax.swing.WindowConstants;
import java.awt.FlowLayout;
public class Hello {
public static void main(String[] args) {
SwingUtilities.invokeLater(() -> { // build the window on the event thread
JFrame frame = new JFrame("Hello");
frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
frame.setLayout(new FlowLayout());
JButton button = new JButton("Click me");
int[] clicks = {0};
button.addActionListener(e -> button.setText("Clicked " + (++clicks[0]) + " times"));
frame.add(button);
frame.setSize(300, 120);
frame.setLocationRelativeTo(null); // centre on screen
frame.setVisible(true);
});
}
}
Compile and run it from the project folder:
mkdir -p out
javac -d out src/notes/Hello.java
java -cp out notes.Hello
On Windows PowerShell, use mkdir out if it does not exist and the same javac and java lines. Here is what each part does:
JFrameis the window.setVisible(true)shows it.SwingUtilities.invokeLaterruns the code on the event thread. Swing is not thread-safe, so building the window anywhere else is a bug that shows up randomly.EXIT_ON_CLOSEstops the program when the window closes. Without it the window disappears but the program keeps running.- The
->arrow is a lambda: a short way to write a listener. It is a shortcut for an anonymous class that implementsActionListener. - The array
int[] clicks = {0}is a small trick. A lambda can only use local variables that do not change, but the contents of an array can change.
6. Build the Notes app, step by step
Step 1: the storage class (no GUI)
We begin with the part that has no windows in it. It saves a list of notes to a text file, one line per note, and loads them again. Because it does not touch Swing, it is easy to test, and later you could swap it for a database without changing the screen.
package notes;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
/** Loads and saves notes as one line of text per note. */
public class NoteStore {
private final Path file;
public NoteStore(Path file) {
this.file = file;
}
/** The normal location: a hidden folder in the user's home directory. */
public static NoteStore inUserHome() {
Path dir = Path.of(System.getProperty("user.home"), ".notes-app");
return new NoteStore(dir.resolve("notes.txt"));
}
public List<String> load() throws IOException {
if (Files.notExists(file)) {
return new ArrayList<>(); // first run: no file yet
}
return new ArrayList<>(Files.readAllLines(file));
}
public void save(List<String> notes) throws IOException {
Files.createDirectories(file.getParent()); // make the folder if needed
Files.write(file, notes);
}
}
Notice three decisions that matter for real apps. The file lives in the user’s home folder, not next to the program, because installed apps are often in read-only places. The code uses Path and Files, which handle Windows backslashes and Linux slashes for you. And the methods declare throws IOException, so the caller must decide what to show the user when something fails.
Step 2: the window
package notes;
import javax.swing.BorderFactory;
import javax.swing.DefaultListModel;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextField;
import javax.swing.SwingUtilities;
import javax.swing.WindowConstants;
import java.awt.BorderLayout;
import java.awt.FlowLayout;
import java.io.IOException;
import java.util.Collections;
public class NotesApp {
private final NoteStore store = NoteStore.inUserHome();
private final DefaultListModel<String> model = new DefaultListModel<>(); // the data behind the list
private final JTextField input = new JTextField();
private final JLabel status = new JLabel(" ");
private void show() {
JFrame frame = new JFrame("Notes");
frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
JList<String> list = new JList<>(model);
JButton add = new JButton("Add");
JButton delete = new JButton("Delete");
// Layout: status on top, list in the middle, input row at the bottom
JPanel buttons = new JPanel(new FlowLayout(FlowLayout.RIGHT, 6, 0));
buttons.add(add);
buttons.add(delete);
JPanel bottom = new JPanel(new BorderLayout(6, 6));
bottom.add(input, BorderLayout.CENTER);
bottom.add(buttons, BorderLayout.EAST);
JPanel root = new JPanel(new BorderLayout(8, 8));
root.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
root.add(status, BorderLayout.NORTH);
root.add(new JScrollPane(list), BorderLayout.CENTER);
root.add(bottom, BorderLayout.SOUTH);
// Events
add.addActionListener(e -> addNote());
input.addActionListener(e -> addNote()); // pressing Enter in the text field
delete.addActionListener(e -> deleteNote(list.getSelectedIndex()));
loadNotes();
frame.setContentPane(root);
frame.setSize(420, 360);
frame.setLocationRelativeTo(null);
frame.setVisible(true);
input.requestFocusInWindow();
}
private void addNote() {
String text = input.getText().strip();
if (text.isEmpty()) {
return; // ignore blank notes
}
model.addElement(text);
input.setText("");
saveNotes();
}
private void deleteNote(int index) {
if (index < 0) {
status.setText("Select a note first.");
return;
}
model.remove(index);
saveNotes();
}
private void loadNotes() {
try {
store.load().forEach(model::addElement);
status.setText(model.size() + " notes");
} catch (IOException ex) {
status.setText("Could not read notes: " + ex.getMessage());
}
}
private void saveNotes() {
try {
store.save(Collections.list(model.elements()));
status.setText(model.size() + " notes saved");
} catch (IOException ex) {
status.setText("Could not save: " + ex.getMessage());
}
}
public static void main(String[] args) {
SwingUtilities.invokeLater(() -> new NotesApp().show());
}
}
javac -d out src/notes/NoteStore.java src/notes/NotesApp.java
java -cp out notes.NotesApp
Try it: add a few notes, close the window, run it again. They come back. You can look at the saved file in the .notes-app folder in your home directory.
What to notice in this code
- Model and view.
DefaultListModelholds the data andJListdisplays it. When you change the model, the list updates itself. This idea (data separate from display) is behind almost every GUI library. - Layout managers. We never gave a pixel position.
BorderLayoutputs components in five regions,FlowLayoutlines them up in a row. Layout managers make windows resize and survive different fonts and screen densities. - Errors become messages. A failed save does not crash the app. It shows a message in the status label.
- The class is a bit big. That is fine for a first version, and section 9 shows how to make it more testable.
- Add a Clear all button, and ask for confirmation with
JOptionPane.showConfirmDialog. - Make double-click on a note open it in a dialog for editing.
- Show the note count in the window title.
- Sort the notes alphabetically with a stream:
list.stream().sorted().
7. Keep the window responsive
Our file is tiny, so reading it takes microseconds. Real apps load large files, call web services or crunch numbers, and that can take seconds. If you do it inside a listener, you are the three-hour stew in the cafe: the window cannot repaint, buttons look dead, and the operating system may label the program “not responding”.
The rules are simple:
- Slow work goes on a background thread.
- Touching the UI goes on the event thread.
Swing gives you SwingWorker to do both. It runs doInBackground on a worker thread and done back on the event thread:
// needs: import javax.swing.SwingWorker; import java.util.List;
private void loadNotesInBackground() {
status.setText("Loading...");
new SwingWorker<List<String>, Void>() {
@Override
protected List<String> doInBackground() throws Exception {
return store.load(); // background thread: no UI code here
}
@Override
protected void done() { // event thread again: safe to touch the UI
try {
get().forEach(model::addElement);
status.setText(model.size() + " notes");
} catch (Exception ex) {
status.setText("Could not load: " + ex.getMessage());
}
}
}.execute();
}
| Swing | JavaFX | |
|---|---|---|
| The UI thread is called | Event dispatch thread (EDT) | JavaFX Application Thread |
| Start the UI | SwingUtilities.invokeLater | Application.launch and start |
| Run something on the UI thread | SwingUtilities.invokeLater | Platform.runLater |
| Background task helper | SwingWorker | Task and Service |
The concept (never block the UI thread; only touch components on it) is the same in both. It is also the same rule that Android has for its main thread.
8. Swing or JavaFX?
| Swing | JavaFX | |
|---|---|---|
| Where it comes from | Part of the JDK (java.desktop module) | Separate project (OpenJFX), not in the JDK since Java 11 |
| Setup | None | Add the library with Maven or Gradle, or download the SDK |
| Look | Dated by default, but can be modernised (for example with FlatLaf) | Modern by default, styled with CSS |
| Layout and design tools | Layout managers in code; IDE form designers | Layouts in code or FXML files; Scene Builder drag-and-drop tool |
| Graphics, animation, media | Basic | Strong: 2D, 3D, animation, charts, media |
| Packaging | Simple: a normal JAR | More steps: the libraries must be on the module path |
| Best for | Small tools, coursework, quick delivery, mature codebases | New apps with rich visuals, if you accept the extra setup |
The same Notes window in JavaFX looks like this. Compare it with the Swing version: a Stage replaces JFrame, a Scene holds the content, and setOnAction replaces addActionListener.
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
public class HelloFx extends Application {
@Override
public void start(Stage stage) {
Label label = new Label("0 clicks");
Button button = new Button("Click me");
int[] clicks = {0};
button.setOnAction(e -> label.setText(++clicks[0] + " clicks"));
stage.setScene(new Scene(new VBox(10, label, button), 240, 120));
stage.setTitle("Hello JavaFX");
stage.show();
}
public static void main(String[] args) {
launch(args);
}
}
The easiest way to run JavaFX is through Maven. Add the library and the run plugin to pom.xml (take the version numbers from openjfx.io), then run mvn javafx:run:
<dependencies>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-controls</artifactId>
<version>VERSION</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.openjfx</groupId>
<artifactId>javafx-maven-plugin</artifactId>
<version>VERSION</version>
<configuration><mainClass>HelloFx</mainClass></configuration>
</plugin>
</plugins>
</build>
If your book and course teach JavaFX, follow them: it is the better skill for new work. If you want the shortest path from zero to an installer, build with Swing first: nothing extra to install, and the packaging steps in sections 11 and 12 are simpler. Either way, the event loop, listeners, layouts and threading rules transfer completely.
9. Structure the project and test it
A common beginner problem is a single enormous class where the buttons, the file code and the rules are tangled together. You cannot test any of it without opening a window. The fix is the same separation we used for NoteStore:
- Model: data and rules in plain classes with no GUI imports.
- View: the window, the components and the layout.
- Controller or presenter: the glue that reacts to events by calling the model and updating the view. In a small app, the listeners in the view are enough.
A good test: could you run the model code from a plain main method that shows no window? If yes, you can test it.
A first unit test with JUnit 5
JUnit is the standard Java test library. This test saves notes to a temporary folder and reads them back. The @TempDir parameter gives each test its own clean folder, which is deleted afterwards, so the test never touches your real notes.
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import java.io.IOException;
import java.nio.file.Path;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
class NoteStoreTest {
@Test
void savesAndReloadsNotes(@TempDir Path dir) throws IOException {
NoteStore store = new NoteStore(dir.resolve("notes.txt"));
store.save(List.of("buy milk", "call mum"));
assertEquals(List.of("buy milk", "call mum"), store.load());
}
@Test
void missingFileMeansNoNotes(@TempDir Path dir) throws IOException {
NoteStore store = new NoteStore(dir.resolve("does-not-exist.txt"));
assertTrue(store.load().isEmpty());
}
}
The easiest way to run it is a build tool. Maven and Gradle download JUnit for you and run mvn test or gradle test. If you want the long version of testing (the test pyramid, JUnit 4 and 5, mocking, UI tests and TDD), read Testing Android Apps. It is written for Android, but the JUnit parts apply to any Java project.
Testing the window itself is harder. Libraries such as AssertJ Swing (for Swing) and TestFX (for JavaFX) can click buttons from tests. A small number of those, plus many tests of the model, is a good balance.
10. Polish: look, settings and accessibility
A better look
Swing defaults to a plain cross-platform look. One line changes it to the look of the current operating system, and a free library called FlatLaf gives it a modern, flat style:
// Option A: the system look (call this before creating any window)
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
// Option B: FlatLaf (add the com.formdev:flatlaf library first)
FlatLightLaf.setup();
Remember user settings
Window size, last folder, preferred theme. The simplest built-in tool is java.util.prefs.Preferences, which stores values in the right place on each operating system:
Preferences prefs = Preferences.userNodeForPackage(NotesApp.class);
int width = prefs.getInt("width", 420); // second argument is the default
prefs.putInt("width", frame.getWidth());
Make it usable for everyone
- Keyboard first. Every action should work without a mouse. Use mnemonics (
button.setMnemonic('A')), a default button (frame.getRootPane().setDefaultButton(add)) and a sensible tab order. - Label inputs with
JLabelandsetLabelFor, and set accessible names so screen readers can announce controls. - Do not rely on colour alone to show state, and keep text readable at larger system font sizes.
- Confirm destructive actions (delete, overwrite) and make errors say what to do next.
- A real icon. Provide it as PNG for the window, and as .ico (Windows) and .icns (macOS) for the installer.
11. Package a JAR
A JAR file is a ZIP archive of compiled classes plus a small manifest that names the class to start. It is the simplest thing you can hand to someone who already has Java.
mkdir -p dist
jar --create --file dist/notes.jar --main-class notes.NotesApp -C out .
java -jar dist/notes.jar
Explaining the command: --file is the archive to create, --main-class writes the manifest entry that java -jar reads, and -C out . means “change into out and add everything”. If you use a library such as FlatLaf, the JAR needs it too, either on the class path (java -cp dist/notes.jar:lib/flatlaf.jar ...) or bundled with a build plugin.
- The user must already have a suitable Java installed, and the right version.
- Double-clicking a JAR works on some systems and not on others.
- There is no icon, no Start-menu entry, and no uninstaller.
A JAR is still useful for classmates, graders and developer tools. For everyone else, build an installer.
12. Ship it: jlink and jpackage
Two tools inside the JDK do the heavy work.
jlinkbuilds a custom runtime: a small copy of Java containing only the modules your app needs.jpackagetakes your JAR and a runtime and produces a native installer:.msior.exeon Windows,.dmgor.pkgon macOS,.debor.rpmon Linux. The result carries its own Java, so the user installs nothing else.
Step 1: find out which modules you need
jdeps --print-module-deps --ignore-missing-deps dist/notes.jar
# prints: java.base,java.desktop
Java is split into modules. A Swing app needs java.desktop, plus the always-present java.base. Everything else (database drivers, the compiler, XML tools) can be left out, which keeps your download small.
Step 2: build the installer
Put the JAR (and any libraries) in a folder, here dist. Then run the command for your system. The pieces in common are the app name, version, input folder and main class.
# macOS
jpackage --type dmg --name Notes --app-version 1.0.0 --vendor "Your Name" \
--input dist --main-jar notes.jar --main-class notes.NotesApp \
--add-modules java.desktop --icon packaging/notes.icns --dest installers
# Windows (PowerShell: use ` at the end of a line instead of \)
jpackage --type msi --name Notes --app-version 1.0.0 --vendor "Your Name" `
--input dist --main-jar notes.jar --main-class notes.NotesApp `
--add-modules java.desktop --icon packaging\notes.ico `
--win-menu --win-shortcut --win-dir-chooser --dest installers
# Linux (Debian, Ubuntu)
jpackage --type deb --name notes --app-version 1.0.0 --vendor "Your Name" \
--input dist --main-jar notes.jar --main-class notes.NotesApp \
--add-modules java.desktop --icon packaging/notes.png \
--linux-shortcut --dest installers
Useful options: --type app-image builds a plain folder with the app and its runtime (no installer tools needed, good for a first test), --java-options "-Xmx512m" passes settings to the JVM, and --runtime-image uses a runtime you made yourself with jlink. When your project uses extra libraries, put them in the --input folder and list them in the JAR’s class path, or use the module path options described in the jpackage documentation.
| You build on… | You get | You must also have |
|---|---|---|
| Windows | .msi or .exe installer | The WiX Toolset (see the jpackage documentation for the version your JDK supports) |
| macOS | .dmg or .pkg | Xcode command-line tools. The version number must start with 1 or higher (not 0.x) |
| Linux | .deb or .rpm | dpkg (Debian family) or rpmbuild (Red Hat family) |
No cross-compiling. jpackage builds for the system it runs on. To make a Windows installer you run it on Windows, and a Mac installer on a Mac. If you do not own all three, use a build service (next section).
What your users will see
- Size. A bundled Swing app is typically tens of megabytes, mostly the Java runtime. When I ran the macOS command above on this exact Notes app (JDK 17), the
.dmgwas about 31 MB and the unpacked app about 78 MB. That is normal. - Security warnings. An installer without a digital signature triggers warnings: Windows SmartScreen on Windows, Gatekeeper on macOS. Users can still open it, but they must click through. Removing the warning needs a code-signing certificate on Windows, and an Apple Developer account plus notarization on macOS. Both cost money, and they are optional for coursework and portfolio projects. Mention the warning in your README so users are not scared.
- Updates. A plain installer does not update itself. For a student project, publishing a new version on your releases page is enough.
13. Publish releases and automate builds
Put the project on GitHub and attach your installers to a Release: a page with a version tag, release notes and downloadable files. Anyone can then download your app from a stable link, and it makes a strong item for a CV or portfolio.
Because jpackage must run on each target system, a GitHub Actions matrix job is the practical answer: it runs the same steps on a Mac, a Windows and a Linux machine in the cloud and saves each installer.
# .github/workflows/package.yml
name: Package installers
on:
push:
tags: ["v*"] # run when you push a tag such as v1.0.0
jobs:
package:
strategy:
matrix:
include:
- { os: macos-latest, type: dmg }
- { os: windows-latest, type: msi }
- { os: ubuntu-latest, type: deb }
runs-on: ${{ matrix.os }}
defaults:
run:
shell: bash
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: "21"
- name: Compile and make the JAR
run: |
mkdir -p out dist installers
javac -d out src/notes/*.java
jar --create --file dist/notes.jar --main-class notes.NotesApp -C out .
- name: Build the installer
run: |
jpackage --type ${{ matrix.type }} --name Notes --app-version 1.0.0 \
--input dist --main-jar notes.jar --main-class notes.NotesApp \
--add-modules java.desktop --dest installers
- uses: actions/upload-artifact@v4
with:
name: installer-${{ matrix.os }}
path: installers/*
Check that each runner image has the tools it needs (for example WiX on Windows), and read the logs of your first run carefully, because the first run on a new system usually needs a fix or two. To publish the files on the Release page, add a step that uploads them to the release for the tag.
Things not to use
- Java applets (windows inside a web page) no longer work in browsers.
- Java Web Start (launching apps from a link) was removed from Java in version 11.
- “Please install Java first” as the install instruction. Bundle the runtime with
jpackageinstead.
14. Common mistakes
| Mistake | What happens | Fix |
|---|---|---|
| Long work inside a listener | The window freezes, then the OS says “not responding” | Move it to SwingWorker (or a JavaFX Task) |
| Changing components from another thread | Random glitches or exceptions that appear once in a hundred runs | Update only on the event thread: invokeLater or Platform.runLater |
Building the UI outside invokeLater | Works on your machine, fails on someone else’s | Start every Swing app from SwingUtilities.invokeLater |
Hard-coded file paths such as C:\Users\me\notes.txt | Breaks on any other computer | Use user.home and Path.of, or Preferences |
| Saving next to the program | Fails when installed in a read-only folder | Store data in the user’s home or app-data folder |
Pixel positions (setBounds) everywhere | Broken layouts on other screens and fonts | Use layout managers |
One giant main class | Nothing is testable, and every change is risky | Split model and view, and test the model |
Swallowing exceptions (catch (Exception e) {}) | Failures are silent | Show a message, or at least log the error |
| Only testing from the IDE | The app works in the IDE and fails after packaging | Run the installed app on a clean machine or account |
Forgetting EXIT_ON_CLOSE | The window closes but the process stays alive | Set the default close operation |
| Ignoring high-resolution screens | Blurry or tiny UI | Use a recent JDK, vector or large icons, and layout managers |
15. Projects to practise, and a portfolio checklist
Learning sticks when you build things that grow. Each project adds one new idea:
| # | Project | New ideas |
|---|---|---|
| 1 | Counter and calculator | Buttons, labels, layouts, simple events |
| 2 | Notes (this guide) | Lists, files, exceptions, packaging |
| 3 | To-do list with due dates and filters | Models, sorting, java.time dates, tables |
| 4 | Image viewer or simple drawing tool | Custom painting, mouse events, file dialogs |
| 5 | Expense tracker with a chart | JDBC and SQLite, or CSV files; charts |
| 6 | Chat client for a small server you also write | Sockets, threads, background updates to the UI |
| 7 | Game of life, Sudoku or a board game | Timers, animation, game loops, state machines |
- A README with a one-line description, a screenshot, how to run from source, and how to install.
- A Release with installers for at least one system, and a note about any security warning.
- A clear folder structure and a few unit tests of the model.
- A short “what I learned” section: the hardest bug, and what you would do next.
- A licence file, so others know if they may reuse your code.
16. Takeaways and glossary
- A desktop app is an event loop plus listeners. Keep listeners fast.
- Swing needs nothing beyond the JDK. JavaFX is more modern but needs extra setup.
- Separate logic from screens, so you can test the logic.
- Store data in the user’s folder, and use
Pathinstead of hard-coded strings. - A JAR is for developers.
jpackagemakes the installer for everyone else. - Build on each operating system you support, ideally with a CI matrix.
- Use the Deitel and Deitel book for the language, and projects like this one for finishing skills.
| Term | In plain words |
|---|---|
| Event | Something the user or system did: a click, a key press, a timer firing. |
| Listener | Code you register to run when a particular event happens. |
| Event dispatch thread (EDT) | The one thread that handles Swing events and drawing. |
| Layout manager | An object that positions components for you and adapts to the window size. |
| Lambda | A short, unnamed function, often used as a listener. |
| SwingWorker | A Swing helper that runs work in the background and returns the result to the event thread. |
| JAR | A ZIP-style archive of compiled Java classes and a manifest. |
| Module | A named part of Java or of your app (for example java.desktop). |
| jlink | The JDK tool that builds a small custom Java runtime. |
| jpackage | The JDK tool that builds a native installer with the runtime included. |
| Code signing | Proving who made an installer, so operating systems warn less. |
| CI matrix | A build that runs the same steps on several operating systems. |
Links and further reading
- Deitel & Associates: books and source code (Java How to Program, with downloadable examples)
- dev.java: learn Java · Eclipse Temurin JDK downloads
- Oracle’s Swing tutorial · SwingWorker API
- OpenJFX (JavaFX) · Scene Builder · FlatLaf
- The jpackage guide · jlink · jdeps
- WiX Toolset (Windows installers) · Notarizing macOS software
- GitHub Releases · setup-java action · Maven · Gradle