How do I define and use an enum in Java?

An enum defines a fixed set of named constants. You can use it to represent values such as order statuses, directions, or days of the week.

In this example, you will define an enum for order statuses and use a switch expression to select a message.

Source code

Save the following code as EnumExample.java:

public class EnumExample {
    void main() {
        OrderStatus status = OrderStatus.SHIPPED;

        String message = switch (status) {
            case PENDING -> "Your order is waiting for confirmation.";
            case PROCESSING -> "Your order is being prepared.";
            case SHIPPED -> "Your order is on its way.";
            case DELIVERED -> "Your order has arrived.";
        };

        IO.println("Current status: " + status);
        IO.println("Message: " + message);

        IO.println();
        IO.println("Available statuses:");

        for (OrderStatus availableStatus : OrderStatus.values()) {
            IO.println(availableStatus);
        }
    }
}

enum OrderStatus {
    PENDING,
    PROCESSING,
    SHIPPED,
    DELIVERED
}

Explanation

The enum keyword declares a type with a fixed set of constants:

enum OrderStatus {
    PENDING,
    PROCESSING,
    SHIPPED,
    DELIVERED
}

Enum constant names conventionally use uppercase letters, with underscores separating words.

Assigning an enum value

Use the enum type followed by a constant name:

OrderStatus status = OrderStatus.SHIPPED;

The variable status can hold a reference to an OrderStatus constant. You cannot assign a string such as "SHIPPED" directly to it.

This helps prevent mistakes caused by spelling or capitalization differences in string values.

Using an enum in a switch expression

The switch expression selects a message for the current status:

String message = switch (status) {
    case PENDING -> "Your order is waiting for confirmation.";
    case PROCESSING -> "Your order is being prepared.";
    case SHIPPED -> "Your order is on its way.";
    case DELIVERED -> "Your order has arrived.";
};

Inside these case labels, you can use the constant names without the OrderStatus. prefix.

The expression covers every constant declared in OrderStatus, so a default rule is unnecessary.

Looping through enum constants

Every enum provides a values() method that returns an array of its constants in declaration order:

for (OrderStatus availableStatus : OrderStatus.values()) {
    IO.println(availableStatus);
}

Here, the loop prints all four statuses, starting with PENDING.

Run the example

Compile and run the file:

javac EnumExample.java
java EnumExample

Output

Current status: SHIPPED
Message: Your order is on its way.

Available statuses:
PENDING
PROCESSING
SHIPPED
DELIVERED

Use an enum when a value should come from a known set of choices. It gives those choices meaningful names and lets the compiler check their usage.

How do I override a method in Java?

Method overriding lets a subclass provide its own implementation of an inherited instance method.

In this example, you will override the printDetails() method so an ebook displays its file format along with its title and author.

Source code

Save the following code as MethodOverridingExample.java:

public class MethodOverridingExample {
    void main() {
        Book book = new Book("Learning Java", "Alice");
        Book ebook = new Ebook("Exploring Java", "Bob", "PDF");

        book.printDetails();
        IO.println();
        ebook.printDetails();
    }
}

class Book {
    private final String title;
    private final String author;

    Book(String title, String author) {
        this.title = title;
        this.author = author;
    }

    public void printDetails() {
        IO.println("Title: " + title);
        IO.println("Author: " + author);
    }
}

class Ebook extends Book {
    private final String fileFormat;

    Ebook(String title, String author, String fileFormat) {
        super(title, author);
        this.fileFormat = fileFormat;
    }

    @Override
    public void printDetails() {
        super.printDetails();
        IO.println("File format: " + fileFormat);
    }
}

Explanation

The Book class defines a method that prints its title and author:

public void printDetails() {
    IO.println("Title: " + title);
    IO.println("Author: " + author);
}

The Ebook class extends Book and overrides that method:

@Override
public void printDetails() {
    super.printDetails();
    IO.println("File format: " + fileFormat);
}

The overriding method has the same name and parameter list as the inherited method. Here, it also has the same return type, void.

Using the @Override annotation

The @Override annotation tells the compiler that the method is intended to override an inherited method.

If you misspell the method name or use the wrong parameters, the compiler reports an error. This helps catch mistakes before running the program.

Calling the superclass implementation

Inside the overriding method, this statement calls the implementation in Book:

super.printDetails();

It prints the title and author. The next statement adds the ebook’s file format:

IO.println("File format: " + fileFormat);

Calling super.printDetails() is optional. This example uses it to reuse the existing printing code.

Selecting the method at runtime

The following variable has the declared type Book, but refers to an Ebook object:

Book ebook = new Ebook("Exploring Java", "Bob", "PDF");

When you call:

ebook.printDetails();

Java runs the implementation in Ebook because that is the object’s actual class. This behavior is called runtime polymorphism.

Run the example

Compile and run the file:

javac MethodOverridingExample.java
java MethodOverridingExample

Output

Title: Learning Java
Author: Alice

Title: Exploring Java
Author: Bob
File format: PDF

Overriding changes an inherited instance method’s behavior in a subclass. Overloading, covered earlier, defines methods with the same name but different parameter lists.

How do I protect a List inside a Java record?

A record is a convenient way to represent data, but a record containing a mutable list can still change unexpectedly. In this tutorial, you will protect a reading list from changes made through other references.

Records became a final language feature in Java 16. This tutorial uses Java 17 as its baseline and requires no preview or incubator flags. See Java’s language changes by release.

1. Understand the problem

Consider this record:

record ReadingList(String title, List<String> books) {
}

Java generates its constructor and accessors. However, the generated constructor stores the supplied list reference. A caller holding that list can change its contents afterward.

A final field prevents reassignment of the reference; it does not freeze the referenced object. Java’s record documentation explains the fields and methods generated for records.

2. Protect the list during construction

Use a compact constructor to replace the incoming list with an unmodifiable snapshot:

ReadingList {
    books = List.copyOf(books);
}

Assign to the constructor parameter, books. Java assigns the resulting value to the component field after the constructor body completes.

List.copyOf() preserves element order and prevents later changes to the original list from affecting the result. It rejects null collections and null elements. See the Java 17 List API.

3. Create the complete example

Save this code as RecordListExample.java:

import java.util.ArrayList;
import java.util.List;

public class RecordListExample {
    record ReadingList(String title, List<String> books) {
        ReadingList {
            if (title == null || title.isBlank()) {
                throw new IllegalArgumentException(
                        "Title must not be blank");
            }
            books = List.copyOf(books);
        }
    }

    public static void main(String[] args) {
        List<String> original =
                new ArrayList<>(List.of("Java Basics"));

        ReadingList readingList =
                new ReadingList("Weekend", original);

        original.add("Advanced Java");

        System.out.println("Original: " + original);
        System.out.println("Record: " + readingList.books());

        try {
            readingList.books().add("Another Book");
        } catch (UnsupportedOperationException exception) {
            System.out.println("Record list cannot be modified");
        }
    }
}

The example checks two paths: modifying the original list and attempting a modification through the record’s accessor.

4. Compile and run

Open a terminal in the directory containing the file:

javac --release 17 RecordListExample.java
java RecordListExample

Output:

Original: [Java Basics, Advanced Java]
Record: [Java Basics]
Record list cannot be modified

The original list accepts another book. The record keeps its initial contents, and its returned list rejects additions.

5. Know the limit: elements are not copied

This approach protects the list structure. It does not create separate copies of its elements.

Here, the elements are strings, which are immutable. If the list contained mutable Book objects, callers could still change those objects. Use immutable element types or copy mutable elements when your data model requires deeper protection.

Best practices

  • Copy incoming collections at construction boundaries.
  • Document your null policy; this example rejects null lists and null elements.
  • Prefer immutable element types for value objects.
  • Avoid unnecessary copying in the accessor when the stored list is already protected.
  • Treat the caught exception as a demonstration; normal application code should avoid attempting unsupported modifications.

Summary

A record does not automatically make a referenced list immutable. Using List.copyOf() in its compact constructor protects the list against external structural changes while keeping the record concise.

How do I implement an interface in Java?

An interface defines a contract that implementing classes must follow. You can use it to describe a behavior without choosing a specific implementation.

In this example, you will create a Printable interface and implement it in a Book class.

Source code

Save the following code as InterfaceExample.java:

public class InterfaceExample {
    void main() {
        Printable item = new Book("Learning Java", "Alice");

        item.printDetails();
    }
}

interface Printable {
    void printDetails();
}

class Book implements Printable {
    private final String title;
    private final String author;

    Book(String title, String author) {
        this.title = title;
        this.author = author;
    }

    @Override
    public void printDetails() {
        IO.println("Title: " + title);
        IO.println("Author: " + author);
    }
}

Explanation

The Printable interface declares a method:

interface Printable {
    void printDetails();
}

This declaration specifies the method’s name, parameters, and return type. It does not provide a method body.

The method is implicitly public and abstract, so you do not need to write those modifiers here.

Implementing the interface

The Book class uses the implements keyword:

class Book implements Printable {

Because Book is a concrete class, it must provide an implementation of the interface’s abstract method:

@Override
public void printDetails() {
    IO.println("Title: " + title);
    IO.println("Author: " + author);
}

The implementation must be public. Giving it more restrictive access would cause a compilation error.

The @Override annotation lets the compiler check that this method implements an inherited method correctly.

Using an interface as a variable type

The following statement creates a Book object and assigns its reference to a variable of type Printable:

Printable item = new Book("Learning Java", "Alice");

This is valid because Book implements Printable.

The calling code can then use the method declared by the interface:

item.printDetails();

At runtime, Java executes the implementation in Book.

Using an interface as the variable type lets the calling code work with any implementation of that interface.

Run the example

Compile and run the file:

javac InterfaceExample.java
java InterfaceExample

Output

Title: Learning Java
Author: Alice

A class can implement multiple interfaces. Use interfaces when different classes should provide the same behavior, even if those classes do not share a custom superclass.

How do I create a Spirograph animation in Java Swing?

You can create a spirograph-style animation in Java Swing by calculating points on a hypotrochoid curve and connecting them with short line segments. Drawing progressively more segments makes a bright point appear to trace the pattern.

Spirograph Bloom

Spirograph Bloom

In this SwingLoops example, a rainbow flower takes shape over time, pauses, and fades away. The application uses only Java Swing and Java 2D, without external libraries or image assets.

Source code

Save the following code as SpirographBloom.java:

import java.awt.*;
import java.awt.geom.*;
import javax.swing.*;

public class SpirographBloom extends JPanel {
    private static final double CYCLE = 12.0;
    private static final int SAMPLES = 2400;
    private static final double R = 150;
    private static final double r = 40;
    private static final double d = 95;
    private static final double END = 8 * Math.PI;

    private final long start = System.nanoTime();
    private final Timer timer = new Timer(16, _ -> repaint());

    public SpirographBloom() {
        setPreferredSize(new Dimension(540, 960));
        setBackground(new Color(6, 9, 22));
    }

    @Override
    public void addNotify() {
        super.addNotify();
        timer.start();
    }

    @Override
    public void removeNotify() {
        timer.stop();
        super.removeNotify();
    }

    private static Point2D point(double t) {
        double ratio = (R - r) / r;
        double x = (R - r) * Math.cos(t)
                + d * Math.cos(ratio * t);
        double y = (R - r) * Math.sin(t)
                - d * Math.sin(ratio * t);
        return new Point2D.Double(x, y);
    }

    @Override
    protected void paintComponent(Graphics graphics) {
        super.paintComponent(graphics);
        Graphics2D g = (Graphics2D) graphics.create();
        try {
            g.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
                    RenderingHints.VALUE_ANTIALIAS_ON);
            double scale = Math.min(
                    getWidth() / 540.0, getHeight() / 960.0);
            g.translate(getWidth() / 2.0, getHeight() / 2.0);
            g.scale(scale, scale);

            double seconds = (System.nanoTime() - start) / 1e9;
            double phase = (seconds % CYCLE) / CYCLE;
            double progress = Math.min(phase / 0.8, 1.0);
            float fade = phase < 0.9
                    ? 1f
                    : (float) Math.max(0, (1 - phase) / 0.1);
            int visible = (int) (progress * SAMPLES);

            for (int i = 1; i <= visible; i++) {
                double t0 = END * (i - 1) / SAMPLES;
                double t1 = END * i / SAMPLES;
                Point2D a = point(t0);
                Point2D b = point(t1);
                Shape segment = new Line2D.Double(a, b);
                Color color = Color.getHSBColor(
                        i / (float) SAMPLES, 0.75f, 1f);
                g.setColor(color);
                g.setComposite(
                        AlphaComposite.SrcOver.derive(fade * 0.12f));
                g.setStroke(new BasicStroke(9f,
                        BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
                g.draw(segment);
                g.setComposite(
                        AlphaComposite.SrcOver.derive(fade));
                g.setStroke(new BasicStroke(2f,
                        BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
                g.draw(segment);
            }

            Point2D tip = point(END * progress);
            g.setComposite(AlphaComposite.SrcOver.derive(fade));
            g.setColor(new Color(245, 250, 255));
            g.fill(new Ellipse2D.Double(
                    tip.getX() - 5, tip.getY() - 5, 10, 10));
        } finally {
            g.dispose();
        }
    }

    static void main() {
        SwingUtilities.invokeLater(() -> {
            JFrame frame = new JFrame("SwingLoops - Spirograph Bloom");
            frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
            frame.setContentPane(new SpirographBloom());
            frame.pack();
            frame.setLocationRelativeTo(null);
            frame.setVisible(true);
        });
    }
}

Run the example

You need a Java Development Kit version 25 and a desktop environment that supports Swing windows. Open a terminal in the directory containing the source file:

javac SpirographBloom.java
java SpirographBloom

Compilation was verified with JDK 25. Visual playback has not yet been verified.

The window has a preferred drawing area of 540 × 960 pixels. If it is too tall for your screen, change new Dimension(540, 960) to new Dimension(405, 720). Close the window to stop the application.

What is a hypotrochoid?

Imagine a small circle rolling without slipping inside a larger circle. Attach a drawing point to the small circle. As the circle rolls, the point traces a curve called a hypotrochoid.

The point can lie inside, on, or beyond the edge of the rolling circle. In this example, it lies beyond the edge, creating loops that resemble flower petals.

The curve is described by two equations:

k = (R - r) / r

x(t) = (R - r) × cos(t) + d × cos(k × t)
y(t) = (R - r) × sin(t) - d × sin(k × t)

Here, R is the fixed outer circle’s radius, r is the rolling circle’s radius, and d is the distance from the rolling circle’s center to the drawing point. The parameter t is an angle measured in radians; it is not elapsed time in seconds.

The first part of each equation locates the rolling circle’s center. That center travels around a circle of radius R - r.

The second part adds the drawing point’s offset from the rolling center. The minus sign in the vertical term accounts for the opposite rotation associated with rolling inside the outer circle.

The code implements these equations in point():

double ratio = (R - r) / r;
double x = (R - r) * Math.cos(t)
        + d * Math.cos(ratio * t);
double y = (R - r) * Math.sin(t)
        - d * Math.sin(ratio * t);

Swing’s vertical axis points downward, so the result is reflected vertically relative to the usual mathematical coordinate system. Neither construction circle is drawn; only the resulting curve is visible.

Why does the curve close at 8π?

For the chosen radii:

R = 150
r = 40
k = (150 - 40) / 40 = 11/4

Both angular components must return to their starting orientations for the complete tracing state to repeat.

At t = 8π:

First angle:   t = 8π         → 4 complete revolutions
Second angle:  (11/4)t = 22π  → 11 complete revolutions

This is why the example uses:

private static final double END = 8 * Math.PI;

For positive integer radii with R > r, a valid closure interval is:

END = 2π × r / gcd(R, r)

The greatest common divisor of 150 and 40 is 10, giving 2π × 40 / 10 = 8π. If you change the radii, revisit this interval rather than keeping END unchanged.

Revealing the curve over time

The animation converts elapsed seconds into a repeating phase between zero and one:

double phase = (seconds % CYCLE) / CYCLE;

The modulo operator resets the phase after every 12 seconds. Drawing occupies the first 80% of the cycle:

double progress = Math.min(phase / 0.8, 1.0);
int visible = (int) (progress * SAMPLES);

At six seconds, phase is 0.5 and progress is 0.625. The panel draws 1,500 of its 2,400 segments. At 9.6 seconds, the entire curve is visible.

Each segment joins two neighboring samples:

double t0 = END * (i - 1) / SAMPLES;
double t1 = END * i / SAMPLES;

More samples make the polyline approximate the mathematical curve more closely, but also increase the painting work. The bright drawing point is evaluated directly at END * progress, so it moves continuously between the sampled segment endpoints.

Creating the rainbow glow

The hue is based on the segment’s position along the complete curve:

Color color = Color.getHSBColor(i / (float) SAMPLES, 0.75f, 1f);

This assigns a stable color to each part of the drawing. Saturation and brightness remain fixed while the hue travels around the color wheel.

Each segment is drawn twice: first with a wide, low-opacity stroke, then with a thin, brighter stroke. The overlapping translucent strokes approximate a glow without a blur filter.

Rounded stroke caps help adjacent line segments join visually.

Holding and fading the pattern

The completed pattern remains fully visible from 9.6 to 10.8 seconds. During the last 1.2 seconds, opacity decreases linearly:

float fade = phase < 0.9
        ? 1f
        : (float) Math.max(0, (1 - phase) / 0.1);

For example, at phase 0.95, opacity is 0.5. The same fade value is applied to the curve and drawing point.

The background is cleared on every repaint by super.paintComponent(graphics). The application redraws the visible curve from its mathematical definition rather than accumulating pixels between frames.

The sequence repeats, but the transition is not a perfectly seamless fade-in: the drawing point reappears at full brightness when the next cycle starts.

Resizing and Swing painting

The code moves the coordinate origin to the panel’s center and applies a uniform scale:

double scale = Math.min(getWidth() / 540.0, getHeight() / 960.0);
g.translate(getWidth() / 2.0, getHeight() / 2.0);
g.scale(scale, scale);

Using the smaller scale factor preserves the curve’s proportions when the window changes shape. With these parameters, the point stays within a distance of (R - r) + d = 205 logical pixels from the center, leaving room inside the default canvas.

A Swing timer requests repaints approximately every 16 milliseconds. This targets roughly 60 updates per second, but does not guarantee that frame rate. Positions depend on elapsed time rather than the number of timer events.

The interface is created on Swing’s event-dispatch thread. The timer starts when the panel becomes displayable and stops when it is removed. Painting uses a copied Graphics2D context, disposed in a finally block after drawing.

Customize the animation

Parameter Effect
CYCLE Changes the total cycle duration.
SAMPLES Changes the curve’s sampling detail and rendering cost.
R and r Change the curve’s geometry; recalculate END afterward.
d Changes the drawing point’s offset and the shape of the loops.
9f and 2f Change the glow and bright stroke widths.
0.75f in getHSBColor() Changes color saturation.
Background color Changes the contrast behind the glowing curve.

For a first experiment, keep R, r, and END unchanged and reduce d from 95 to 40. The drawing point then lies on the rolling circle’s edge, producing a special case called a hypocycloid.

If you increase the radii or offset, check that the curve still fits inside the drawing area.

Free source code

The complete source code is included above. Save it as SpirographBloom.java to compile and run it without additional dependencies.

Explore the SwingLoops repository for the series’ source code collection.