How do I convert byte arrays to hexadecimal and back in Java 17?

When you inspect binary data, a hexadecimal string can be easier to read than a list of decimal byte values. You might use it when troubleshooting a file format or checking a message exchanged between applications.

This tutorial shows a complete conversion in both directions, including readable formatting and invalid input handling.

1. Choose a hexadecimal format

Java 17 introduced java.util.HexFormat as a standard API. No preview or incubator flags are needed.

Use HexFormat.of() for lowercase output without separators. For uppercase output with colons between bytes, use:

HexFormat readable =
        HexFormat.ofDelimiter(":").withUpperCase();

Each byte becomes two hexadecimal digits. These options are documented in the Java 17 HexFormat API.

2. Create the complete example

Save the following as HexFormatExample.java:

import java.util.Arrays;
import java.util.HexFormat;

public class HexFormatExample {
    public static void main(String[] args) {
        byte[] packet = {
                0, 15, 16, 127, (byte) 128, (byte) 255
        };

        HexFormat plain = HexFormat.of();
        HexFormat readable =
                HexFormat.ofDelimiter(":").withUpperCase();

        System.out.println(
                "Compact: " + plain.formatHex(packet));

        String formatted = readable.formatHex(packet);
        System.out.println("Readable: " + formatted);

        byte[] restored = readable.parseHex(formatted);
        System.out.println(
                "Same bytes: " + Arrays.equals(packet, restored));
        System.out.println(
                "Java bytes: " + Arrays.toString(restored));

        try {
            readable.parseHex("00:GG");
        } catch (IllegalArgumentException exception) {
            System.out.println(
                    "Invalid input: expected hexadecimal byte pairs");
        }
    }
}

The program first creates sample data. It then prints two representations, parses the colon-separated version, and checks whether the recovered bytes match the original.

3. Compile and run

Open a terminal in the directory containing the file:

javac --release 17 HexFormatExample.java
java HexFormatExample

Output:

Compact: 000f107f80ff
Readable: 00:0F:10:7F:80:FF
Same bytes: true
Java bytes: [0, 15, 16, 127, -128, -1]
Invalid input: expected hexadecimal byte pairs

4. Understand the result

The two formatted strings represent identical data. Java’s signed byte representation explains the negative decimal values in the recovered array: the bit patterns 80 and FF correspond to -128 and -1.

Arrays.equals() confirms that the conversion preserved the bytes.

Parsing must use the expected delimiters. Invalid digits, such as GG, cause IllegalArgumentException; hexadecimal digit letters themselves are case-insensitive. See HexFormat.parseHex().

Best practices

  • Define the expected representation when exchanging data with another application.
  • Use a matching formatter for encoding and decoding.
  • Keep the original bytes when your next operation requires binary data.
  • Handle invalid external input at the point where it enters your application.
  • Remember that hexadecimal encoding makes data readable; it does not encrypt it.

Summary

Use formatHex() to turn bytes into hexadecimal text and parseHex() to recover them. A short round-trip example helps verify your chosen representation before integrating it into a larger application.

How do I write a lambda expression in Java?

A lambda expression provides a concise implementation of a functional interface. A functional interface has one abstract method.

In this example, you will use a lambda expression to greet someone by name.

Source code

Save the following code as LambdaExample.java:

public class LambdaExample {
    void main() {
        Greeting greeting = name -> "Hello, " + name + "!";

        IO.println(greeting.greet("Alice"));
        IO.println(greeting.greet("Bob"));
    }
}

@FunctionalInterface
interface Greeting {
    String greet(String name);
}

Explanation

The Greeting interface declares one abstract method:

@FunctionalInterface
interface Greeting {
    String greet(String name);
}

The method accepts a name and returns a greeting.

The @FunctionalInterface annotation lets the compiler check that the interface meets the requirements of a functional interface.

Creating the lambda expression

The following statement assigns a lambda expression to a Greeting variable:

Greeting greeting = name -> "Hello, " + name + "!";

Each part has a purpose:

Part Purpose
name Receives the method argument
-> Separates the parameter from the body
"Hello, " + name + "!" Produces the returned value

The compiler knows that name is a String because the greet() method declares a String parameter.

For a single parameter with an inferred type, parentheses are optional. You can also write:

Greeting greeting = (name) -> "Hello, " + name + "!";

Returning a value

This lambda has an expression body:

name -> "Hello, " + name + "!"

The expression’s result is returned automatically. You do not need braces or a return statement.

If you use a block body instead, you must return the value explicitly:

Greeting greeting = name -> {
    return "Hello, " + name + "!";
};

Both forms provide the same behavior.

Calling the implementation

Call the interface method through the variable:

greeting.greet("Alice")

This passes "Alice" to the lambda and returns "Hello, Alice!". The program then prints the returned string using IO.println().

Run the example

Compile and run the file:

javac LambdaExample.java
java LambdaExample

Output

Hello, Alice!
Hello, Bob!

A lambda expression needs a target functional interface, such as Greeting in this example. You can also use lambdas with standard functional interfaces from java.util.function, including Predicate, Function, and Consumer.

How do I morph text with particles in Java Swing?

You can turn text into a particle animation by drawing each word into a transparent image and treating its visible pixels as destinations. In this example, 840 glowing dots form JAVA, change into SwingLoops, and return to the original cloud. Everything is drawn with Java Swing and Java 2D.

Particle Text Morph

Particle Text Morph

Source code

Save the following code as ParticleTextMorph.java:

import java.awt.*;
import java.awt.geom.Ellipse2D;
import java.awt.image.BufferedImage;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Random;
import javax.swing.*;

public class ParticleTextMorph extends JPanel {
    private static final int WIDTH = 540, HEIGHT = 960, COUNT = 840;
    private static final double CYCLE = 12.0;

    private final Point[] cloud = new Point[COUNT];
    private final Point[] javaText = textPoints("JAVA", 132, 7);
    private final Point[] logoText = textPoints("SwingLoops", 90, 19);
    private final Color[] colors = new Color[COUNT];
    private final Timer timer = new Timer(16, _ -> repaint());
    private long start;

    public ParticleTextMorph() {
        setPreferredSize(new Dimension(WIDTH, HEIGHT));
        setBackground(new Color(7, 10, 24));
        Random random = new Random(42);
        for (int i = 0; i < COUNT; i++) {
            cloud[i] = new Point(
                    40 + random.nextInt(WIDTH - 80),
                    190 + random.nextInt(HEIGHT - 380));
            float hue = 0.47f + 0.25f * random.nextFloat();
            colors[i] = Color.getHSBColor(hue, 0.55f, 1.0f);
        }
    }

    private static Point[] textPoints(String text, int size, long seed) {
        BufferedImage mask = new BufferedImage(
                WIDTH, HEIGHT, BufferedImage.TYPE_INT_ARGB);
        Graphics2D g = mask.createGraphics();
        try {
            g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING,
                    RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
            Font font = new Font(Font.SANS_SERIF, Font.BOLD, size);
            while (font.getSize() > 1
                    && g.getFontMetrics(font).stringWidth(text) > WIDTH - 80) {
                font = font.deriveFont(font.getSize2D() - 1);
            }
            g.setFont(font);
            FontMetrics metrics = g.getFontMetrics();
            int x = (WIDTH - metrics.stringWidth(text)) / 2;
            int y = (HEIGHT + metrics.getAscent() - metrics.getDescent()) / 2;
            g.setColor(Color.WHITE);
            g.drawString(text, x, y);
        } finally {
            g.dispose();
        }

        List<Point> pixels = new ArrayList<>();
        for (int y = 0; y < HEIGHT; y += 2) {
            for (int x = 0; x < WIDTH; x += 2) {
                if ((mask.getRGB(x, y) >>> 24) > 180) {
                    pixels.add(new Point(x, y));
                }
            }
        }
        if (pixels.isEmpty()) {
            throw new IllegalArgumentException("No visible pixels for: " + text);
        }
        Collections.shuffle(pixels, new Random(seed));
        Point[] points = new Point[COUNT];
        for (int i = 0; i < COUNT; i++) {
            points[i] = pixels.get(i % pixels.size());
        }
        return points;
    }

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

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

    private static double smooth(double u) {
        return u * u * (3 - 2 * u);
    }

    @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() / (double) WIDTH, getHeight() / (double) HEIGHT);
            g.translate((getWidth() - WIDTH * scale) / 2,
                    (getHeight() - HEIGHT * scale) / 2);
            g.scale(scale, scale);

            double t = ((System.nanoTime() - start) / 1e9) % CYCLE;
            Point[] from;
            Point[] to;
            double u;

            if (t < 3) {
                from = cloud; to = javaText; u = t / 3;
            } else if (t < 5) {
                from = javaText; to = javaText; u = 0;
            } else if (t < 8) {
                from = javaText; to = logoText; u = (t - 5) / 3;
            } else if (t < 9) {
                from = logoText; to = logoText; u = 0;
            } else {
                from = logoText; to = cloud; u = (t - 9) / 3;
            }

            double blend = smooth(u);
            double arc = 46 * Math.sin(Math.PI * blend);
            Ellipse2D.Double dot = new Ellipse2D.Double();

            for (int i = 0; i < COUNT; i++) {
                double x = from[i].x + (to[i].x - from[i].x) * blend;
                double y = from[i].y + (to[i].y - from[i].y) * blend;
                x += arc * Math.cos(i * 2.39996);
                y += arc * Math.sin(i * 2.39996);

                g.setColor(colors[i]);
                g.setComposite(AlphaComposite.SrcOver.derive(0.12f));
                dot.setFrame(x - 5, y - 5, 10, 10);
                g.fill(dot);

                g.setComposite(AlphaComposite.SrcOver);
                dot.setFrame(x - 1.6, y - 1.6, 3.2, 3.2);
                g.fill(dot);
            }
        } finally {
            g.dispose();
        }
    }

    static void main() {
        SwingUtilities.invokeLater(() -> {
            JFrame frame = new JFrame("SwingLoops - Particle Text Morph");
            frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
            frame.setContentPane(new ParticleTextMorph());
            frame.pack();
            frame.setLocationRelativeTo(null);
            frame.setVisible(true);
        });
    }
}

Run the example

Open a terminal in the directory containing the file and run:

javac ParticleTextMorph.java
java ParticleTextMorph

The application opens a portrait window with a preferred drawing area of 540 × 960. If it is too tall for your screen, change the preferred size to 405 × 720. The drawing scales to the window while preserving its proportions. Close the window to stop the animation.

Turning letters into particle destinations

The textPoints() method calls drawString() on a transparent BufferedImage. Swing never displays this image directly. Its purpose is to tell the program where the letters are.

The program visits every second pixel on both axes. A pixel is eligible when its alpha channel, a value from 0 (transparent) to 255 (opaque), exceeds 180:

if ((mask.getRGB(x, y) >>> 24) > 180) {
    pixels.add(new Point(x, y));
}

The unsigned shift extracts alpha from the packed ARGB value. The threshold mostly keeps the solid part of each letter and discards faint pixels along antialiased edges.

The program shuffles the eligible coordinates with a fixed seed so it can pick points across the word instead of filling it from top to bottom. If fewer coordinates are available than particles, modulo indexing reuses some coordinates. This work happens once for each word when the panel is created.

Moving a particle between two points

Each particle has three destinations: a point in the cloud, a point in JAVA, and a point in SwingLoops. All three arrays use the same particle index. During a transition, the program interpolates between the current and next destinations:

double x = from[i].x + (to[i].x - from[i].x) * blend;
double y = from[i].y + (to[i].y - from[i].y) * blend;

A blend of 0 gives the starting position. A blend of 0.5 gives the midpoint, and a blend of 1 gives the destination. Instead of using elapsed progress directly, the code passes it through a smoothstep function:

return u * u * (3 - 2 * u);

The curve starts and ends with a slope of zero. That gives the particles time to accelerate away from a word and slow down as they settle into the next one.

The sine term adds a temporary offset to the straight path. It is zero at both endpoints and largest halfway through. The offset direction depends on the particle index, so the dots bend in different directions without missing their destinations.

Drawing the glow and repeating the motion

Each particle is drawn twice: a wide translucent circle creates a halo and a smaller opaque circle forms the bright center. Both use the same color. The panel clears the background on every repaint, so this glow does not depend on retaining previous frames.

A Swing timer requests a repaint approximately every 16 milliseconds. The code calculates positions from elapsed time rather than counting frames. During the first three seconds the particles gather into JAVA; they hold for two seconds, morph for three, hold for one, then spend three seconds returning to the original cloud. The last destination is also the next cycle’s starting position, so no particle needs to jump when the clock wraps.

The window is created on Swing’s event-dispatch thread. The timer starts when the panel becomes displayable and stops when it is removed.

Customize the animation

Try replacing JAVA with your initials. The example reduces the font size to fit the available width, but short words generally leave more room for distinct particles. To compare straight and curved paths, set the arc multiplier from 46 to 0 and run the example again.

The full source code above is free to copy and run. Find more examples in the SwingLoops repository.

How do I create a generic method in Java?

A generic method uses a type parameter to work with different reference types. This lets you reuse the same method without writing a separate version for each type.

In this example, you will create a method that prints the elements of an array. The same method will handle arrays of strings and integers.

Source code

Save the following code as GenericMethodExample.java:

public class GenericMethodExample {
    void main() {
        String[] languages = {"Java", "Python", "JavaScript"};
        Integer[] numbers = {10, 20, 30};

        IO.println("Languages:");
        printElements(languages);

        IO.println();
        IO.println("Numbers:");
        printElements(numbers);
    }

    <T> void printElements(T[] elements) {
        for (T element : elements) {
            IO.println(element);
        }
    }
}

Explanation

The printElements() method declares a type parameter named T:

<T> void printElements(T[] elements) {
    for (T element : elements) {
        IO.println(element);
    }
}

The <T> declaration appears before the return type, void. It makes T available as a type within the method declaration and body.

Each part has a purpose:

Part Purpose
<T> Declares the type parameter
void Indicates that the method does not return a value
T[] elements Accepts an array whose element type is T
T element Holds each element during iteration

T is a conventional name. You can use a more descriptive name when it makes the code easier to understand.

Calling the method with different types

The first call passes an array of strings:

String[] languages = {"Java", "Python", "JavaScript"};
printElements(languages);

The compiler infers T as String for this call.

The second call passes an array of integers:

Integer[] numbers = {10, 20, 30};
printElements(numbers);

For this call, the compiler infers T as Integer.

You do not need to specify the type argument explicitly because the compiler can infer it from the array argument.

Using wrapper types

Java generic type arguments must be reference types. You cannot use a primitive type such as int as the type argument for T.

That is why this example uses Integer[] rather than int[]. Autoboxing converts the integer literals into Integer objects when the array is initialized.

Passing an int[] directly to this method would cause a compilation error.

Run the example

Compile and run the file:

javac GenericMethodExample.java
java GenericMethodExample

Output

Languages:
Java
Python
JavaScript

Numbers:
10
20
30

The type parameter belongs to the method, so the containing class does not need to be generic. Use a generic method when the same operation can work with multiple types while keeping compile-time type checking.

How do I create interactive fireworks using Java Swing?

You can create interactive fireworks in Java Swing by combining Graphics2D, mouse events, and a javax.swing.Timer. Rockets travel toward the position you click, then explode into colorful particles affected by gravity, damping, and fading.

The effect does not require an external graphics library, physics engine, or image assets.

In this SwingLoops example, fireworks launch into a dark night sky above a simple city skyline. Each explosion produces dozens of glowing particles with different colors and movement patterns. You can click several positions to create multiple explosions, and the program launches fireworks automatically when you leave it running.

Interactive Fireworks

Interactive Fireworks

Source code

Save the following code as InteractiveFireworks.java:

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

public class InteractiveFireworks extends JPanel {
    private static final int W = 540, H = 960;
    private final Random random = new Random();
    private final java.util.List<Rocket> rockets = new ArrayList<>();
    private final java.util.List<Particle> particles = new ArrayList<>();
    private final Timer timer = new Timer(16, e -> update());
    private long lastLaunch;

    public InteractiveFireworks() {
        setPreferredSize(new Dimension(W, H));
        setBackground(new Color(3, 5, 20));

        addMouseListener(new MouseAdapter() {
            @Override
            public void mousePressed(MouseEvent e) {
                double x = e.getX() * W / (double) getWidth();
                double y = e.getY() * H / (double) getHeight();
                launch(x, y);
            }
        });
    }

    @Override
    public void addNotify() {
        super.addNotify();
        lastLaunch = System.currentTimeMillis();
        timer.start();
    }

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

    private void launch(double tx, double ty) {
        double x = 40 + random.nextDouble() * (W - 80);
        double y = H;
        double dx = tx - x;
        double dy = ty - y;
        double distance = Math.max(1, Math.hypot(dx, dy));
        double speed = 9 + random.nextDouble() * 3;

        rockets.add(new Rocket(x, y,
                dx / distance * speed,
                dy / distance * speed,
                tx, ty, random.nextFloat()));
    }

    private void update() {
        long now = System.currentTimeMillis();

        if (now - lastLaunch >= 1700) {
            launch(80 + random.nextDouble() * (W - 160),
                    100 + random.nextDouble() * 350);
            lastLaunch = now;
        }

        for (Iterator<Rocket> it = rockets.iterator(); it.hasNext();) {
            Rocket r = it.next();
            double remaining = Point2D.distance(
                    r.x, r.y, r.tx, r.ty);

            if (remaining <= Math.hypot(r.vx, r.vy)) {
                r.x = r.tx;
                r.y = r.ty;
                explode(r);
                it.remove();
            } else {
                r.x += r.vx;
                r.y += r.vy;
            }
        }

        for (Iterator<Particle> it = particles.iterator();
             it.hasNext();) {
            Particle p = it.next();

            p.px = p.x;
            p.py = p.y;
            p.x += p.vx;
            p.y += p.vy;
            p.vx *= .988;
            p.vy = p.vy * .988 + .075;
            p.life -= .013f;

            if (p.life <= 0) it.remove();
        }

        repaint();
    }

    private void explode(Rocket r) {
        int count = 80 + random.nextInt(30);
        int pattern = random.nextInt(3);

        for (int i = 0; i < count; i++) {
            double angle = pattern == 2
                    ? random.nextDouble() * Math.PI * 2
                    : i * Math.PI * 2 / count;

            double speed = 2 + random.nextDouble() * 4;

            if (pattern == 1) {
                speed *= .5 + Math.abs(Math.sin(angle * 2));
            }

            particles.add(new Particle(r.x, r.y,
                    Math.cos(angle) * speed,
                    Math.sin(angle) * speed,
                    r.hue + random.nextFloat() * .08f));
        }
    }

    @Override
    protected void paintComponent(Graphics graphics) {
        super.paintComponent(graphics);
        Graphics2D g = (Graphics2D) graphics.create();
        g.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
                RenderingHints.VALUE_ANTIALIAS_ON);
        g.scale(getWidth() / (double) W,
                getHeight() / (double) H);

        g.setPaint(new GradientPaint(0, 0,
                new Color(3, 5, 20),
                0, H, new Color(20, 9, 44)));
        g.fillRect(0, 0, W, H);

        drawCity(g);

        for (Rocket r : rockets) {
            g.setColor(Color.WHITE);
            g.fill(new Ellipse2D.Double(r.x - 3, r.y - 3, 6, 6));
            g.setColor(new Color(255, 190, 100, 100));
            g.setStroke(new BasicStroke(3));
            g.draw(new Line2D.Double(
                    r.x, r.y + 18, r.x, r.y));
        }

        for (Particle p : particles) {
            Color c = Color.getHSBColor(p.hue % 1f, .8f, 1f);
            int alpha = (int) (p.life * 230);

            g.setColor(new Color(c.getRed(),
                    c.getGreen(), c.getBlue(), alpha / 4));
            g.fill(new Ellipse2D.Double(
                    p.x - 8, p.y - 8, 16, 16));

            g.setColor(new Color(c.getRed(),
                    c.getGreen(), c.getBlue(), alpha));
            g.setStroke(new BasicStroke(2,
                    BasicStroke.CAP_ROUND,
                    BasicStroke.JOIN_ROUND));
            g.draw(new Line2D.Double(p.px, p.py, p.x, p.y));
            g.fill(new Ellipse2D.Double(
                    p.x - 2, p.y - 2, 4, 4));
        }

        g.dispose();
    }

    private void drawCity(Graphics2D g) {
        for (int i = 0; i < 18; i++) {
            int x = i * 32;
            int height = 45 + (i * 37) % 110;

            g.setColor(new Color(8, 12, 28));
            g.fillRect(x, H - height, 35, height);

            g.setColor(new Color(255, 205, 90, 100));
            for (int y = H - height + 12; y < H - 8; y += 18) {
                g.fillRect(x + 8, y, 4, 6);
                g.fillRect(x + 22, y, 4, 6);
            }
        }
    }

    private static class Rocket {
        double x, y, vx, vy, tx, ty;
        float hue;

        Rocket(double x, double y, double vx, double vy,
               double tx, double ty, float hue) {
            this.x = x;
            this.y = y;
            this.vx = vx;
            this.vy = vy;
            this.tx = tx;
            this.ty = ty;
            this.hue = hue;
        }
    }

    private static class Particle {
        double x, y, px, py, vx, vy;
        float hue, life = 1f;

        Particle(double x, double y,
                 double vx, double vy, float hue) {
            this.x = this.px = x;
            this.y = this.py = y;
            this.vx = vx;
            this.vy = vy;
            this.hue = hue;
        }
    }

    public static void main(String[] args) {
        SwingUtilities.invokeLater(() -> {
            JFrame frame = new JFrame("SwingLoops - Fireworks");
            frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
            frame.setContentPane(new InteractiveFireworks());
            frame.pack();
            frame.setLocationRelativeTo(null);
            frame.setVisible(true);
        });
    }
}

Run the example

You need Java 17 or later and a desktop environment capable of running Swing applications.

Open a terminal in the directory containing InteractiveFireworks.java, then compile and run it:

javac InteractiveFireworks.java
java InteractiveFireworks

A window with a preferred drawing area of 540 × 960 pixels opens. Fireworks launch automatically every 1.7 seconds.

Click anywhere in the window to launch a rocket toward that position. Clicking several locations creates multiple simultaneous explosions.

You can change the preferred panel size if the window is too large for your screen.

How does the animation work?

Updating the animation with a Swing Timer

The application uses a javax.swing.Timer to update the animation approximately every 16 milliseconds:

private final Timer timer = new Timer(16, e -> update());

Each update moves the rockets, updates the particles, removes expired objects, and requests a repaint.

The timer starts in addNotify() and stops in removeNotify(), preventing the animation from continuing after the panel is removed.

Because the timer executes its callback on Swing’s Event Dispatch Thread, mouse events, animation updates, and painting share the same thread.

Moving rockets toward the target

Each rocket starts near the bottom of the screen and receives a velocity pointing toward its target.

The direction is calculated from the difference between the target and starting positions:

double dx = tx - x;
double dy = ty - y;
double distance = Math.max(1, Math.hypot(dx, dy));

Math.hypot() calculates the Euclidean distance between two points.

We divide the direction components by this distance and multiply them by the desired speed:

double vx = dx / distance * speed;
double vy = dy / distance * speed;

This ensures that the rocket moves at a consistent speed regardless of its destination.

During each animation update, its position changes according to its velocity:

r.x += r.vx;
r.y += r.vy;

Once the rocket is close enough to reach its target during the next movement, the program triggers an explosion.

Creating particles with circular motion

An explosion produces between 80 and 109 particles.

For a circular burst, each particle receives a different angle:

double angle = i * Math.PI * 2 / count;

The horizontal and vertical velocity components are calculated using:

double vx = Math.cos(angle) * speed;
double vy = Math.sin(angle) * speed;

Together, these calculations distribute particles around the explosion center.

The example also introduces variations in particle speed and direction, producing different explosion shapes.

Adding gravity and damping

After an explosion, particles move independently.

Each frame applies damping and gravity:

p.vx *= .988;
p.vy = p.vy * .988 + .075;

Multiplying velocity by .988 gradually slows the particles.

The additional .075 accelerates them downward because Java2D’s Y coordinate increases toward the bottom of the screen.

The result is a curved trajectory: particles spread outward, slow down, and eventually fall.

This is a simple visual physics simulation rather than a fully realistic model, but it works well for a fireworks animation.

Creating fading particles

Each particle begins with a lifetime of 1.0:

float life = 1f;

Its lifetime decreases with every update:

p.life -= .013f;

The remaining lifetime determines the particle’s transparency:

int alpha = (int) (p.life * 230);

As life approaches zero, the particle becomes less visible.

Expired particles are removed using the iterator, which also prevents the particle collection from growing indefinitely.

Creating a glow effect

The glow is produced by drawing each particle twice.

First, a larger circle is drawn with low opacity:

g.setColor(new Color(
        c.getRed(), c.getGreen(), c.getBlue(), alpha / 4));

g.fill(new Ellipse2D.Double(
        p.x - 8, p.y - 8, 16, 16));

A small, brighter particle is then drawn on top.

The application also draws a short line between each particle’s previous and current positions, creating a glowing motion trail.

This approximates a neon effect using ordinary Java2D shapes, without shaders or blur filters.

Generating firework colors

The example uses Color.getHSBColor() to generate colors:

Color c = Color.getHSBColor(p.hue % 1f, .8f, 1f);

Each rocket receives a random hue, and its particles use nearby hue values.

This gives every explosion a recognizable color theme while still allowing subtle variation between individual particles.

Drawing the night sky

The background uses a GradientPaint that transitions from dark navy to deep purple.

A simple city skyline is drawn using rectangles, with smaller yellow rectangles representing illuminated windows.

These elements provide context for the fireworks without requiring image assets.

Customize the animation

Try changing one parameter at a time to see how it affects the result.

Parameter Effect
16 in the timer Animation update interval in milliseconds
1700 Time between automatic launches
80 + random.nextInt(30) Number of particles per explosion
9 + random.nextDouble() * 3 Rocket speed
.988 Particle damping; smaller values slow particles faster
.075 Gravity; larger values make particles fall faster
.013f Lifetime reduction; larger values make particles disappear sooner
new Color(3, 5, 20) Background color

For example, increasing the particle count to 150 makes each firework look fuller, but also increases the amount of rendering work.

Reducing gravity creates a floating, almost magical explosion, while increasing gravity makes particles fall more quickly.

Experimenting with these values is an easy way to create your own animation style.

The complete example uses only Java Swing and Java2D. No external libraries, physics engines, or image assets are required.

Happy coding!