Road to JDK 25 — Over-Engineering Tic-Tac-Toe (Java 23)!
Java (and the JVM) reaches the next long-term-support version v25 in September 2025 (it will be 30 years old!) and that warrants an exploration of its modern features. Despite the availability of newer technologies and languages like Kotlin, Scala, Clojure, and others like Go and Rust, Java still dominates many large codebases and sits 4th on the TIOBE index of programming languages. Rumors of the death of Java may be unfounded. What better way to discover and explore what’s new in a hands-on way than to over-engineer and overcomplicate the age-old game of tic-tac-toe!
This article was originally self-published in “The Life of Brian” the digital garden and is being serialized on Medium. All references and citations can be found on the original article.
This is Part 5 of the Multi-Part Series: Road to JDK 25 — Over-Engineering Tic-Tac-Toe. For a discussion of features introduced in JDK 22 written previously, visit:
Tic-Tac-Toe
Tic-tac-toe is a simple game usually played by two players who take turns marking the spaces in a three-by-three grid with X or O. The player who successfully places three of their marks in either a horizontal, vertical, or diagonal row is the winner.
Our task is to continue to progressively over-engineer tic-tac-toe (repo: here) focused primarily on finalized features from JEPs rather than those still in preview prior to v25 and introduce best practices as the code base grows.
Introduction to JDK 23
JDK 23 makes perhaps more news for what didn’t make the cut as for what did. We get improved documentation with markdown support, ZGC becomes generational by default, and the loved and loathed internal API sun.misc.Unsafe
(that we shouldn't have been using in the first place) heads for the exit.
However, anyone who was hoping for the finalization of String Templates — that combine literal text with embedded expressions to safely, easily, and dynamically construct strings containing values — would be disappointed as it was yoinked from the JDK after two previews. It’s not even available with --enable-preview
in JDK 23. This proves my point about features not being ready for professional production use until they exit preview; by all means test them out to help the community and process but JEP 12 (which announced the preview process) always left provision for features in preview to be removed if they didn't meet community standards and became a permanent fixture in the JDK.
In the aftermath of JDK 22’s blazing sprint, the road ahead seemed ripe with promise. Our seasoned developer, perched atop his well-worn JDK steed, squints into the distance, sensing the subtle hum of JDK 23 on the horizon.
“By the threads of concurrency, what might lies within?” he muses.
A whisper floats through the air, carried by the winds of optimization: “JDK 23 rides on the wings of speed! Its garbage collectors, honed and sharpened, dance through memory with grace unmatched.”
The developer feels the surge beneath him, the heartbeat of a machine that knows no pause. “Performance,” he breathes, “is no longer a goal, but a state of being.”
Yet, as he peers closer, he notices a hitch, a sudden halt. The promise of string templates, once so bright, now fades into the mist, halted in its tracks before it could shine.
He nods, “The road to perfection is a winding one, but we ride it well. For now, the race continues… without the song of strings.”
Markdown Documentation Comments
For those who diligently write comments across their Java codebase, it’s never been the best experience doing so with HTML tags to get the right formatting, especially if you’re subsequently generating javadocs. The JEP for markdown comments introduced in JDK 23 aims at solving that problem by using a markup format that people actually enjoy using, supporting comments in markdown which are denoted by three forward slashes (///
).
NB: Other forms of markdown documentation indicators were tried, such as
/**md
which extends javadoc comments but were unpopular during prototyping.
The main goal here is to make code documentation both easier to read and write which should be celebrated. Other modern languages including Rust support markdown comments through similar means so the consistency is beneficial. The javadoc tags like @snippet
, @implSpec
, etc. remain unchanged.
Example
Enhancing our Player
interface which included @snippet
for javadoc, is one area of enhancement where this can be illustrated.
Before:
/**
* Tic-tac-toe player interface for all players
* {@snippet :
* // Create a human player
* Player player = new HumanPlayer("X"); // @highlight region="player" substring="player"
*
* // Choose the next valid move on the game board
* int validBoardLocation = player.nextMove(gameBoard); // @end
* }
*/
public sealed interface Player permits HumanPlayer, BotPlayer, RemoteBotPlayer {
String getPlayerMarker();
int nextMove(GameBoard board);
}
After:
/// Tic-tac-toe player interface for all players
/// {@snippet :
/// // Create a human player
/// Player player = new HumanPlayer("X"); // @highlight region="player" substring="player"
///
/// // Choose the next valid move on the game board
/// int validBoardLocation = player.nextMove(gameBoard); // @end
/// }
public sealed interface Player permits HumanPlayer, BotPlayer, RemoteBotPlayer {
/// Returns the marker (e.g. "X" or "O") used by this player.
/// @return the player's marker
String getPlayerMarker();
/// Chooses the next valid move on the game board.
/// @param board the current state of the game board
/// @return the index of the next valid move on the board
int nextMove(GameBoard board);
}
which correctly outputs Javadoc as before (running javadoc
or gradle javadoc
):
Documenting code snippets of other languages with backticks also works as expected. e.g. in GameBoard.java
we can add a code block for javascript:
/// Converts the game board to a JSON string representation for serialization. Format
/// corresponds to the following JSON schema with content as a 1D array of strings of size
/// dimension x dimension.
///
/// ```javascript
/// { "dimension": int, "content": [ string, string, ..., string ] } }
/// ```
/// @return the game board as a JSON string
/// @see JsonSerializable
String asJsonString();
which renders the following Javadoc:
ZGC: Generational Mode by Default
As we discussed in a previous post the ZGC, a “scalable low-latency garbage collector” was introduced in JDK11 capable of supporting massive terabyte sized heaps, concurrent class loading, NUMA (non-uniform-memory-access) awareness, GC pause times not exceeding 1ms and not increasing with the size of the heap.
In JDK21 it was made multi-generational - enabled with a command-line option, taking advantage of the common use case that most created objects are short-lived and those can be more frequently collected. This means less frequent full GCs and overall better performance. In JDK 23 the generational ZGC is now the default mode when using it and non-generational ZGC is deprecated for removal.
It’s now enabled with the ZGC flag -XX:+UseZGC
. It can currently be disabled with -XX:+UseZGC -ZZ:-ZGenerational
but that functionality will be removed in future.
Example
In our tic-tac-toe game we have a few VSCode launch configurations, applying the setting is as simple as updating the vmArgs
:
From:
{
"type": "java",
"name": "GameServer (on ZGC)",
"request": "launch",
"vmArgs": "-XX:+UseZGC -XX:+ZGenerational",
"mainClass": "org.example.GameServer",
"projectName": "app",
"env": {
"LIB_PATH": "${workspaceFolder}/app/build/cargo/debug",
"PATH": "${workspaceFolder}/app/build/cargo/debug", // For Windows
"LD_LIBRARY_PATH": "${workspaceFolder}/app/build/cargo/debug", // For Linux
"DYLD_LIBRARY_PATH": "${workspaceFolder}/app/build/cargo/debug" // For macOS
},
}
to:
{
"type": "java",
"name": "GameServer (on ZGC)",
"request": "launch",
"vmArgs": "-XX:+UseZGC",
"mainClass": "org.example.GameServer",
"projectName": "app",
"env": {
"LIB_PATH": "${workspaceFolder}/app/build/cargo/debug",
"PATH": "${workspaceFolder}/app/build/cargo/debug", // For Windows
"LD_LIBRARY_PATH": "${workspaceFolder}/app/build/cargo/debug", // For Linux
"DYLD_LIBRARY_PATH": "${workspaceFolder}/app/build/cargo/debug" // For macOS
},
}
Deprecate the Memory-Access Methods in sun.misc.Unsafe for Removal
JEP 471 represents an important step in the evolution of the Java platform. By deprecating the memory-access methods in sun.misc.Unsafe
, we're being encouraged to adopt safer, more robust APIs that align with the language's overall goals of security, maintainability, and forward compatibility.
The sun.misc.Unsafe
API is an internal API that provides low-level operations, including direct memory access, allowing developers to bypass Java’s memory safety guarantees for performance, but was never meant for public consumption. With VarHandle
and the FFM API its functionality has been superseded with standard APIs in the SDK that allow developers to access performance and perform memory access in a way that aligns with the Java safety and integrity-first ethos.
Java’s direction with the Foreign Memory Access API shows a move towards balancing control and safety, similar to modern languages like Rust, though Java’s runtime safety mechanisms as a GC language differ in fundamental ways from Rust’s compile-time guarantees.
Example
Previously, we already made use of the FFM API, integrating with Rust to have a native representation of the GameBoard
. When we did so, we created a player id, which used an AtomicInteger
to map an increasing identifier to the player marker (typically 'X' or 'O'), meaning the first player to move would get ID = 1, the next ID = 2, etc.
We could, similarly, create an only-increasing number fountain of our own using the lower level but safer APIs provided by VarHandle
. It's over-engineering for sure, since AtomicInteger is highly optimized but that's why we're here; also we have the benefit of only using exposing the functionality we need.
To do that we can use one of the bimodal memory-access methods VarHandle.getAndAdd
:
public class PlayerIds {
private volatile int nextId;
private static final VarHandle NEXT_ID_VH;
static {
try {
NEXT_ID_VH = MethodHandles.lookup().findVarHandle(PlayerIds.class, "nextId", int.class);
} catch (NoSuchFieldException | IllegalAccessException e) {
throw new ExceptionInInitializerError(e);
}
}
public PlayerIds(int initialValue) {
this.nextId = initialValue;
}
public int getNextId() {
return nextId;
}
public int getNextIdAndIncrement() {
return (int) NEXT_ID_VH.getAndAdd(this, 1);
}
}
In the code above, the VarHandle NEXT_ID_VH
grabs the nextId
field and uses atomic operations to provide simple id generator functionality with thread-safety guarantees.
Benchmarking the new PlayerIds
class (using JMH - the Java Microbenchmark Harness) against AtomicInteger
and a naive control implementation that uses a ReentrantLock
to surround our atomic operations shows that our new implementation is marginally the fastest but at the very least comparable to an AtomicInteger
implementation:
Benchmark Mode Cnt Score Error Units
PlayerIdsBenchmark.testAtomicIntegerGetAndIncrement avgt 25 7352.476 ± 267.315 ns/op
PlayerIdsBenchmark.testAtomicIntegerGetId avgt 25 26.847 ± 0.626 ns/op
PlayerIdsBenchmark.testLockGetAndIncrement avgt 25 13114.880 ± 458.892 ns/op
PlayerIdsBenchmark.testLockGetId avgt 25 12996.146 ± 312.691 ns/op
PlayerIdsBenchmark.testPlayerIdsGetAndIncrementId avgt 25 7305.610 ± 247.107 ns/op
PlayerIdsBenchmark.testPlayerIdsGetId avgt 25 26.858 ± 0.625 ns/op
That’s all folks — that’s a wrap (for now) for JDK 23! Feel free to deep dive into the docs or codebase to discover more.
Disclaimer: The views and opinions expressed in this blog are based on my personal experiences and knowledge acquired throughout my career. They do not necessarily reflect the views of or experiences at my current or past employers.
Next Steps
- Are you a Java developer? Comment & share your own opinions, favorite learning resources, or book recommendations.
- Follow my blog (or digital garden) for future updates on my engineering exploits and professional / personal development tips.
- Connect with @briancorbinxyz on social media channels.
- Subscribe!