Abstract
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 index1 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-toe2!
Java: Loved and Hated
Quote
”There are only two kinds of languages: the ones people complain about and the ones nobody uses” - Bjarne Stroustrup (Creator of C++)
For all of the complaints, there’s still a lot to love about Java. It has earned a place on the the Mount Rushmore of programming languages and when its time comes to pass the torch to a truly worthy general purpose language successor it will but that time is not now, nor will it be for some time. If you’re an experienced Java developer who had to code through years of stagnant features and are were enviously at other languages or even a new engineer wondering if there’s life in the old beast yet, come and take a sip as we explore the new features and enhancements that have come and are still coming thick and fast over recent years.
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 tic-tac-toe repository started off with a simple JDK 16-compatible implementation and is available here: https://github.com/briancorbinxyz/overengineering-tictactoe/tree/jdk16 with minimal support for and X by X game of tic-tac-toe using a one-dimensional array with sequential checks for a winner between a human vs. unintelligent (read: it picks a spot at random) bot.
As we update our code we (at least mostly) use the typical OpenJDK reference implementation from jdk.java.net via SDKMan3 though we may dip into some performance implementations like Azul Builds for comparison. We’ll also focus primarily on finalized features rather than those still in preview prior to v25. We will also introduce best practices as the code base grows.
Introduction to JDK 17
JDK 17 introduced a number of interesting features4, including Sealed Classes, kicking off improvements to Switch pattern matching. It also removed/deprecated a few that I can’t imagine anyone will miss like Applet API support5, the Security Manager6 the latter of which was a pain to use and for all the effort failed to provide protection against 80% of the top security threats7, as well as RMI8 (remote-method-invocation) which has effectively been replaced with web-technologies and micro-services.
Features
Sealed Classes
Sealed Classes9 offer more control over the class hierarchy, enabling you to specify which classes or interfaces can extend or implement them. This allows you to maintain a controlled and predictable class hierarchy which is especially useful for exhaustive pattern matching and reflection.
Example
In our game of tic-tac-toe we had a simple interface Player
which could be one of HumanPlayer
or BotPlayer
. We can seal the hierarchy by using the permits
clause, and therefore disallowing any new extensions like AlienPlayer
, or JoePlayer
10 making it possible to reason on our fixed set of known Player
types.
This means that the classes themselves also need to be sealed by making them final
:
The class HumanPlayer with a sealed direct superclass or a sealed direct superinterface Player should be declared either final, sealed, or non-sealed
Now, if someone tries to extend Player
(directly) with a new type not included in our permits
clause there will be an error:
The type AlienPlayer that implements a sealed interface Player should be a permitted subtype of Player
Altogether, this method of sealing the hierarchy helps us to abide by the principle “design for extension, or else prohibit it.”11 which, in practice, is more pragmatic than the open-closed principle12, certainly at the class-level.
Enhanced Pseudo-Random Number Generators
Essentially, prior to JDK 17 the random number generators were not easily swappable and didn’t take advantage of newer features and programming models in java like stream processing. Various random number generators are available that provide enhanced statistical levels of random distribution, better quality, scalability, or the ability to hop/skip/jump over generated numbers, all of these now implement the RandomGenerator
interface and if we desire we can create our own.
Example
In our game of tic-tac-toe our bot (BotPlayer
) currently selects a random available location for its next move. Swapping out the field:
for:
allows us to code to the new interface. It wouldn’t be over-engineered, though, if we didn’t go the lengths of making it cryptographically secure by default and specifiable in its constructor.
This is less over-engineered for this case than grabbing a stream of random.ints()
upfront and taking the first valid move (int
) it spits out (don’t tempt me - I’ll do it!).
AOT Compilation Removal
The experimental AOT (ahead-of-time) compiler GRAAL was removed13 from the standard build because it was a lot of a work to maintain vs. the usage it was getting. We may want to still be able to use this feature to be able to start our applications more quickly or take advantage of other benefits of ahead-of-time compilation.
Example
Per the JEP:
Developers who wish to use the Graal compiler for either AOT or JIT compilation can use GraalVM
This is easily done by setting up our Gradle build to use it, which will give us the task gradlew nativeCompile
which will build an executable (which itself leverages the native-image
tool from the distribution).
build.gradle.kts
:
Executed:
Context-Specific Deserialization Filters
Now this isn’t the most sexy sounding feature but if you’ve lost vacation dealing with zero-day vulnerabilities and exploits then you will at least understand the motivation which is to provide a
level of protection against deserialization vulnerabilities. This feature is used by implementing an ObjectInputFilter
Example
Even writing the line implements Serializable
feels like a relic of the past since any sane engineer would be using something like json, avro, protocol-buffers, or some other object serialization flavor of their choice to serialize or persist data, but this is over-engineering java after all so let’s go!
First of all we make all the game objects Serializable
and introduce a new class that can load the game state (that is now persisted after each turn) at startup.
So when we deserialize i.e. convert from a file via an ObjectInputStream
into our Game
object we can choose to reject deserialization any game objects with a contrived number (1000
) of object references or any that use a LegacyPlayer
(added for this purpose to our sealed Player
hierarchy) which we no longer support.
Introduction to JDK 18
Java 18 introduces several notable features14 aimed at enhancing developer productivity, performance, robustness and modernity. Among these are the introduction of UTF-8 as the default charset, which improves consistency across different environments, a new Simple Web Server was added for testing and development purposes. The deprecation of the finalization mechanism marked a significant step towards improving resource management in Java which had multiple critical flaws15. Additionally, Java 18 included enhancements in the foreign function and memory API, allowing for safer and more efficient interactions with non-Java code and memory.
Also of note are some changes to internals, notably core reflection now using java.lang.invoke
method handles, the reflection API remains the same but there are some marginal performance differences16.
Features
UTF-8 by Default
In prior versions of Java the default charset could be different on different operating systems17 and was determined on JVM startup, which could lead to files becoming corrupted on read. There was also a lack of consistency about which character set would be used (the default) or specifically the most common on the world-wide-web: UTF-8
. In general, the added benefit is less verbosity with character reading/writing and the ability to use default method references (::
) rather than overloads. Python made a similar (more painful) transition when it went from v2 to v3.
The potential issue is that running with JDK 18+ without some flags may lead to files saved with a prior JDK under a different default character set not being loaded properly. In this case the JVM can be run with -Dfile.enconding=COMPAT
. To retrieve and use the native encoding of the system the property System.getProperty("native.encoding")
can be submitted to Charset::forName
.
Simple Web Server
This is a simple command-line tool jwebserver
which starts up a minimal web server which serves static files18. If used via the API it can also be enhanced to use custom handlers.
As a long-time engineer this is great - especially when accessing a server/box to retrieve logs/files is a pain in the neck. The Java developer who wanted to do this previously would have to dip into their Python bag of tricks e.g. python -m SimpleHttpServer 9090 (python 2)
or python -m http.server 9090 (python 3)
to open a file server at port 9090. Now this is possible with just Java tools. (In fact under the hood it does something similar: java -m jdk.httpserver
)
Example
Our tic-tac-toe game persists our game state to a number of files in a specific directory, if we wanted to pull them locally for inspection we could simply start a web server on the host in that directory and access it locally or via curl
.
E.g. jwebserver -p 9090
:
Directory listing at http:///localhost:9090
:
Code Snippets in Java API Documentation
Including code examples in API documentation has generally been a bit of a pain in the past, especially so if those are more complex code examples. This introduces the ability to add both inline and external code snippets as examples in Javadocs, as well as highlight specific regions and substrings of those snippets.
Example
Our tic-tac-toe game previously had almost no documentation because I knew this was coming. I kid, it had no documentation because of lazy developer syndrome: Although, in practice and in my defense, IRL I generally tend to at least document interfaces and entry points. Seriously though, this is the type of work that AI is for!
Adding in some documentation including the tag @snippet
which is throughly documented in JEP 41319 will give nice snippets in generated java docs - which will please the library creators and their clients.
Player.java
:
Which looks like the below in the JavaDocs:
Internet-Address Resolution SPI
Previously, if you wanted to do an internet address lookup using the standard InetAddress
static functions then you were stuck waiting on a blocking call to the operating system to use the local DNS and internet lookup call. This new feature20 allows us to specify / override the default with our own setup and configuration by using our own service provider and implementation. This is great for testing.
Example
Given we didn’t really have a use for InetAddress
in the current implementation I contrived and over-engineered one - printing a user agent-style string at the start of the game identifying both players:
This leverages InetAddress
functions as follows:
The SPI (service provider interface) comes in handy to override the default behavior via the implementation class InetAddressResolver
and the factory class InetAddressResolverProvider
, which is registered in META-INF/services
as a java.net.spi.InetAddressResolverProvider
. The implementation overrides the default address resolution resolving every ip to 127.0.0.1
and every address to www.example.org
masking the true ones:
ExampleOrgInetAddressResolver
:
ExampleOrgInetAddressResolverProvider
:
This results in:
Deprecation of Finalization for Removal
The Java finally
clause was never a catch all for ensuring resource cleanup for everything you might be holding onto after you were done with it. If you had multiple resources to clean up there was still a chance it wouldn’t happen properly or in a timely way or there would be some error/exception in between resource closing calls leaving one or more unclosed. So we’d develop a finalize
method on an object but unfortunately without any real control over when it would be called.
Per the JEP15 :
Maintainers of libraries and applications that rely upon finalization should consider migrating to other resource management techniques such as the try-with-resources statement and cleaners.
Example
The deprecation of finalization calls for the use of existing features like try-with-resources which will auto close AutoCloseable
classes (which we already use in the tic-tac-toe codebase):
In the above, the FileOutputStream
and ObjectOutputStream
are automatically closed when they go out of context without any explicit calls to close()
.
More, interesting is using Cleaner
. This sits atop of java phantom references which allow you to take action just before an object is garbage collected. At present in the HumanPlayer
character we’re holding a transient field to accept user input private transient Scanner io = new Scanner(System.in)
. The IDE doesn’t love that we never close it though:
So we resolved that by tweaking how the app runs making the Game
and the HumanPlayer
implement AutoCloseable
and running the game in a try-with-resources block to deterministically ensure the resources are cleaned up.
HumanPlayer.java
snippet:
So, now we are really sure io
gets cleaned up both with the explicit use of try-with-resources and should someone happen to forget to use that or the close()
method explicitly then our cleanup will occur for sure just before garbage collection. It would, of course, have more value if we were holding onto a native/foreign resource outside of the JVM memory model which is typically what we would have if we were previously using finalize()
.
Introduction to JDK 19
Java 19 was not a particularly exciting release21 (unless you include incubating/preview features). So instead I asked an AI to come up with a joke about the lack of new features instead. It was actually pretty good:
Quote
A Java developer walks into a bar and orders a JDK 19. The bartender hands them a glass with just a few drops in it.
”What’s this?” asks the developer. “I ordered a full JDK 19!”
The bartender replies, “Sorry, most of it is still in preview. But don’t worry, it’s a RISC-V you’re willing to take!”
As an enterprise guy, it’s not a “risk” I’m going to take on new features just yet (and I’m already using a RISC-compatible AARCH JDK variant), so stay tuned for JDK 21 where most of the quite exciting preview/incubating features get finalized.
Introduction to JDK 20
Ah, Java 2022… the release full of, um, nothing to see (yet). I’m starting to see now that the primary purpose is AI is not to take our jobs or to write our code. It’s to simply tell a few jokes to keep us sane:
Quote
A Java developer excitedly upgrades to JDK 20 from JDK 19, hoping for groundbreaking features. They call their colleague and say, “Hey, I just installed JDK 20! Ask me anything it can do!”
The colleague asks, “Okay, what can it do?”
The developer replies, “Anything JDK 19 can do!”
The colleague sighs, “So… not much then?”
The developer chuckles, “Hey, at least it’s a short learning curve!”
Introduction to JDK 21
JDK 2123. After a few barren releases now we ride! Features galore and long-term support. This is where most of us should be sat in enterprise before we reach JDK 25. Features include Pattern Matching for switch, which allows for more complex and readable data handling; Record Patterns, enabling concise matching against record values; Sequenced collections, and a key encapsulation mechanism API. Additionally, Virtual Threads provide a lightweight and scalable approach to concurrency, making it easier to write concurrent applications.
Quote
In the wasteland of the Java apocalypse, a grizzled developer revs up his rusty JDK 19 machine, sputtering across the barren landscape. Suddenly, a chrome-plated behemoth roars past him, leaving him in a cloud of dust.
”What in the null pointer exception is that?” he cries.
A voice booms from the passing juggernaut: “Witness JDK 21, shiny and chrome! We ride eternal to Valhalla, all features and glory!”
The old dev gapes as he watches the JDK 21 war rig disappear over the horizon, trailing streams of virtual threads and pattern matching switch expressions.
He mutters to himself, “So that’s where all the features went. JDK 19 and 20 were just… guzzoline for this beast!”
Warning
In honor of hitting a LTS version, we’re removing the previously deprecated
LegacyPlayer
that we introduced for Context-Specific Deserialization Filters (It will remain in the previous branches of the repository).
Features
It’s worth a mention here that Windows 32-bit x86-32 JVM is deprecated from JDK 21 onwards. That typically means if you’re still using JNI and wrapping Windows 32-bit libraries then you’ll have to wrap and expose the functionality to your APIs some other way24. JDK also strengthens integrity by disallowing the dynamic loading of agents25 which could be use to inject code / behavior without an explicit startup javaagent
or agentlib
being explicitly selected.
Sequenced Collections
The Java collections framework had several collections with a defined encounter order (e.g. List, LinkedHashset, etc.), but lacked a unified type to represent them. Therefore, certain operations on these collections could be inconsistent or absent, and processing elements in reverse order is often inconvenient or even impossible. This is where SequencedCollection
(and SequencedSet
, SequencedMap
) comes in26.
Example
The game currently maintains a list of players which can be one of HumanPlayer
or BotPlayer
. Typically, in tic-tac-toe they can only be one of O
or X
(noughts or crosses) but this is over-engineered to one day allow more than those. However, as it’s implemented with a list, technically I could have two BotPlayer
s with O
and one HumanPlayer
with X
in the player list and that would give the Bot two turns, to the human player’s one turn - a more specialized collection is needed to resolve issues like that.
The new class PlayerList
captures the players and their selection and maintains both a List
for iterating through by index and a SequenedSet
for validating players with set semantics in the order they were added. (This could also have been a SequencedMap
)
The above method tryAddPlayer
can now reject sets of players that have duplicate markers for tic-tac-toe.
Record Patterns and Pattern Matching for Switch
Many languages provide great pattern matching capabilities these days, e.g. Scala, Gleam - it’s generally in vogue and is particularly useful for deconstructing records, avoiding branching and extracting fields we care about. JDK 21 builds upon prior work in JDK 16 to allow nested pattern matching capabilities2728 that lead to cleaner code.
Example
In our tic-tac-toe game we added a close
method on Game
and PlayerList
the latter calling close
if the Player
instance had implemented AutoCloseable
. Since, JDK 16 this can be rewritten more succinctly from:
to:
What’s far more interesting than any of that, though, is doing a bit of a refactor getting rid of all of that and turning all the Player
classes we built in our Sealed Classes hierarchy into record
types so that we can both simplify our classes which had mostly final
fields anyway, as well as take advantage of pattern matching. Conveniently, the PlayerPrinter
class has a method ripe for a rewrite here. This allows us to go from:
to:
which is both shorter, safer, and exhaustive. With an extra tweak we can go one step further to deconstruct the Player
classes to extract the playerMarker
.
which yields the following to the console:
We’re not yet done, though! As the switch pattern matching has become much more advanced we can replace methods with a bunch of if
statements into something more readable. E.g. From our persistence Context-Specific Deserialization Filters:
becomes:
Generational ZGC
The ZGC was introduced as far back as JDK11 as a “scalable low-latency garbage collector” 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, minimized impact to application throughput, etc.
If high-performance computing environments if you’re not using ZGC it’s likely because you’re still stuck on Java 8, or just using G1, or you’re using an alternative JDK entirely like Azul which has the C4 (Continuously Concurrent Compacting Collector)29. If you are not then you might get some extra performance and consistency for free by switching.
Until JDK21 the ZGC was single-generational, so all allocated objects were treated the same regardless of age meaning more objects to inspect for liveness ergo potentially longer pauses. Multi-generational garbage collection takes advantage of the common use case: most created objects are short-lived. This means more frequent efficient collection of short-lived objects, less frequent full GCs, all with no need to configure space sizes30.
Example
Our small tic-tac-toe game doesn’t have a big reason at present to tune the garbage collector. Starting it up with -XX:+UseZGC -XX:+ZGenerational
is enough to benefit from the ZGC, though.
Virtual Threads
Remember when reactive programming was going to replace the old way of doing things and we went ahead and signed the reactive manifesto for creating applications that were responsive, resilient, elastic and message driven31? I do, I signed it 10 years ago. The principles remain relevant until this day but increasingly it’s becoming possible to follow those principles without stepping too far out of your programming comfort zone of sequential, synchronous-style coding.
Virtual threads are lightweight threads managed by the JVM rather than the operating system. They enable highly scalable and efficient concurrency by allowing millions of threads to be created and managed with minimal overhead. They’re designed to be extremely fast to create, start, and context-switch, making them ideal for handling massive numbers of concurrent tasks in Java applications. Similar design goals have benefited go (with goroutines), erlang (with processes) and others.
The JEP32 describes the problem we’re trying to solve well:
Quote
The scalability of server applications is governed by Little’s Law, which relates latency, concurrency, and throughput: For a given request-processing duration (i.e., latency), the number of requests an application handles at the same time (i.e., concurrency) must grow in proportion to the rate of arrival (i.e., throughput).
In order to do this Java steps away from binding Java threads 1-to-1 with OS threads and allows these more plentiful virtual threads to be suspended with non-blocking OS calls transparently to the user, enabling high throughput without a change in programming paradigm, or even pooling. They bring benefits when:
- The number of concurrent tasks is high (i.e. more than a few thousand)
- The workload is not CPU-bound - more threads than processor cores cannot improve throughput for this use case
Example
Until now, our game of tic-tac-toe would run on a single machine within a single JVM. In order to utilize a threading model, after a bit of a refactor to configure persistence on/off, we introduce a GameServer
and GameClient
which simply use TCP (Java Sockets) to communicate with the server holding the game state. The server spawns virtual threads for each connecting client to simulate a game and the client spawns virtual threads that connect to the server and respond to the server which prompts it for the next move in the game. A specialized Player
(RemoteBotPlayer
) was added to the hierarchy to support the remote connection.
The virtual threads are spawned in the server by submitting tasks to an ExecutorService
:
This allows us to scale with unlimited virtual threads to levels of concurrency that are harder to achieve by setting fixed numbers of platform threads with NIO.
Hopefully when we visit StructuredTaskScope
in future, this code will look less convoluted, and we can handle errors in a cleaner, more structured way.
Key Encapsulation Mechanism API
A key encapsulation mechanism (KEM) is a modern public-key encryption system designed to succeed traditional public-private key encryption systems by providing enhanced security against eavesdropping and interception by malicious actors. Prior to JDK 21, there was no standardized Java API to support KEMs25.
In a standard (or pre-quantum) cryptographic public-key encryption system, anyone who has the public key can encrypt a message, yielding a ciphertext. Only those who know the corresponding private key can decrypt the ciphertext to recover the original message. The problem with this is that algorithms and brute-force attacks enabled by quantum technology will compromise these systems. Once compromised, all past encrypted communications that have used the same public-private key pair could also be compromised.
KEMs address this vulnerability by allowing a sender who knows a public key to simultaneously generate a short random secret key (or shared session key) and an encapsulation of the secret key using the KEM’s encapsulation algorithm. The receiver, who knows the private key corresponding to the public key, can recover the same secret key from the encapsulation using the KEM’s decapsulation algorithm. This process can utilize post-quantum cryptographic algorithms, making KEMs resistant to quantum attacks. The securely exchanged session key can then be used for encrypting the actual message with symmetric encryption, which is not susceptible to quantum attacks.
Example
In JDK 21, KEM key providers implement the KEMSpi
interface, which provides the necessary methods for encapsulation and decapsulation processes. This interface is used alongside the KeyPairGenerator
API which facilitates the generation and management of the key pairs needed for KEMs, integrating seamlessly into the existing Java cryptographic architecture.
Nothing says over-engineering more than using post-quantum cryptographic techniques to secure a game of tic-tac-toe. SSL/TLS is simply not strong enough to protect our game of tic-tac-toe in post-quantum computing era.
The journey to integrating this in the game took me on a side quest through Star Wars (Kyber33), Star Trek (Dilithium34) and a bunch of other videos and papers to understand post-quantum cryptographic techniques. I highly recommend the YouTube series from “Chalk Talk”35 - the topic probably deserves a post of its own.
We introduce a class SecureMessageHandler
which uses Kyber post-quantum cryptography to securely exchange a SecretKey
which then used with an AES/CBC
cipher for symmetric encryption of messages sent between the client and server.
Note: as of the time of writing there were no third-party implementations of PQC that used the
KEMSpi
from JDK21 so I had to implement it myself using a JDK 18 version of bouncy castle cryptographic APIs1 in order to implement the code in the style of the API in the JEP.
The main methods of the new KyberKEMSpi
introduced are for the generation, encapsulation and decapsulation of the shared key:
The IO of of the client/server is then refactored to use a MessageHandler
which uses a secure implementation SecureMessageHandler
that handles the public key exchange, shared key generation and exchange, and message encryption/decryption. The main methods related to to the key encapsulation mechanism are the server side, and client side key generation and exchange:
That's a wrap for JDK 21!
Ahead of adding features for the next JDK release I took the extra liberty of applying some code styling to the codebase at this point due to its growth using Spotless36 and the AOSP (android open source project) styling, which is my personal preference for Java projects.
Also, since we introduced using third-party APIs as implementations for JDK facades and service providers with the KEM Spi, I also introduced JDK 9 Platform Logging (from JEP 264)37 over an SLF4J to Logback bridge, to avoid all of those unwieldy
System.out.println
calls in a way that still allows us to change the logging implementation at will. I’m of the opinion, though, that theSystem.Logger
API needs to change sincelog.log(Level.INFO,...)
, is just not as simple and clean as simply writinglog.info(...)
and developers shouldn’t have to wrap that functionality to get it. The other issue is that
Introduction to JDK 22
JDK 22 keeps up the momentum gained in JDK 21. Unnamed Patterns and Variables, helping to ignore unneeded parts of data structures for cleaner code (Scala, Python had this for ages), the ability to launch multi-file source code programs, and finally delivering an interoperability API with the foreign function and memory API to replace JNI as the defacto way to perform interop, that allows us to interoperate with code and data outside of the runtime and heap.
Features
Launch Multi-File Source-Code Programs
One fair criticism with Java development over the years is that it’s not beginner friendly and slow to get started with, or slow to ideate with, or RAD (referring to rapid-application-development) with. Even when kicking off this app, the first tools I used before calling java
were Homebrew, SDKMan, and Gradle.
In previous feature updates, the JDK answered the call with jshell
where we have a REPL (read-evaluate-print-loop) that simplifies experimentation with language features, debugging, and learning by providing instant feedback on code execution; the java
command being able to directly run .java
files without precompiling38 or in shell scripts with #!
shebangs, and other niceties us older heads did not have when starting out.
Those last features are enhanced in JDK 22 by allowing java to run and execute multiple source code programs directly with the java
command39 and it will compile only those necessary for your execution or those explicitly requested. Additionally you can pull in jars by specifying the class path so it’s possible to start more significant development without having to reach for a build tool.
Example
e.g. starting our tic-tac-toe app without using gradle or even javac..
before when using JDK 21: java org/example/App.java
:
after switching to JDK 22: java org/example/App.java
:
It’s still failing. This time because when we introduced Key Encapsulation Mechanism API we did so using a 3rd party post-quantum crypto implementation which is a compile-time dependency on bcprov-jdk18on-1.78.1
. For it to run we would have to ensure the 3rd-party libraries are in the class path.
This works after switching to JDK 22 and providing the class path of the jar dependency:
java --class-path '/var/tmp/bcprov-jdk18on-1.78.1.jar' org/example/App.java
:
That feature is pretty great and simplifies the early development feedback loop. The keen-eyed will notice that although it ran fine, it did so without using the Logback implementation and fell back on java.util.logging
. Ensuring that’s included alongside other dependencies in the class path using a wildcard remedies the issue:
java --class-path '/var/tmp/libs/*' org/example/App.java
:
Unnamed Variables & Patterns
JDK 22 introduces unnamed variables and nested patterns40 which can be used when variable declarations or nested patterns are required but never used. They are denoted by the underscore character, _
and their use reduces the verbosity/boilerplate of writing code and reduces the cognitive load whilst reading code since only the relevant details need to be declared.
Example
When we introduced Record Patterns and Pattern Matching for Switch to the game we implemented a case statement that had to unnecessarily declare the RandomGenerator
that we wasn’t going to refer to since it was in the record constructor for BotPlayer
:
Since JDK 22, however, we no longer have that problem, we can replace anything that we will not use or refer to with an underscore i.e. case BotPlayer(String playerMarker, _)
:
Similarly in the same PlayerPrinter
class, we have a case where we are using a try/catch block with an exception we do not use, since it’s an expected (or handled) exception:
In the past typical convention may have been to name the UnknownHostException
expected
41 or UnknownHostException ignored
. In JDK 22, however we can simply use the underscore UnknownHostException _
(as long as it’s clearly understood why the exception is not being used by the code).
Region Pinning for G1
Java is part of the family of languages recognized as “Memory Safe”42 since as it uses automatic garbage collection it provides guarantees against certain type of programming errors (and related exploits and security vulnerabilities) related to memory management. These type of issues could lead to undefined behavior, such as accessing memory out of bounds, using uninitialized memory, or freeing memory incorrectly. There are whole category of techniques used in order to avoid these types of issues in non-memory safe languages.43
However, even a memory safe language like Java allows some unsafe operations when performance is critical. For a long time JNI (the Java Native Interface) has been the primary means for interoperating with memory non-safe languages like C and C++.
In certain such use-cases, Java objects on the heap may need to be pinned (i.e. forced to ensure their address remains constant) when being referenced by native code which require a stable address to reference them in memory. The G1 garbage collector - the default garbage collector, however, is designed to compact the heap and reduce fragmentation which may mean migrating objects around. This activity would be problematic for pinned objects so previously, the G1 collector would prevent collection whilst objects were pinned which would lead to both performance degradation due to fragmented memory or long thread pauses. The more pinned objects, the more likely there was to be issues and increased latency.
Region pinning in JDK 2244 improves efficiency as it isolates pinned regions allowing the garbage collector to better manage the rest of the heap, improving overall memory utilization and reducing the negative impact of pinned objects.
Foreign Function & Memory API
The goal of the foreign function and memory API45 finally delivered in this JDK was to introduce an API that allowed Java programs to interoperate with ‘foreign’ code (i.e. outside the JVM) and ‘foreign’ data (i.e. not managed by the JVM) in a way that is safer, more efficient, and more natural than previous methods, such as JNI, or others introduced later such as JNA and other custom libraries.
It allows us to efficiently invoke foreign functions and to safely access foreign memory on the same machine but outside of the Java runtime by calling native libraries and processes native heap data not subject to the memory management (garbage collection) of the runtime.
As I previously mentioned, even a memory safe language like Java sometimes need to invoke some unsafe operations when performance is critical; this includes high performance computing, trading, scientific, or AI contexts, or perhaps if an important library is only available in another language.
The FFM API lives in the java.lang.foreign
package46 and has key constructs that allow us to:
- Directly call foreign functions or submit Java functions as callbacks (
Linker
,SymbolLookup
,FunctionDescriptor
, andMethodHandle
). - Reference and control the lifecycle of foreign memory
(MemorySegment
,Arena
, andSegmentAllocator
), - Manipulate and access structured foreign memory
(MemoryLayout
andVarHandle
)
Example
This one was fun diversion to look at since it was an excuse to take a modern look at interop in memory safe languages by bridging between Java and Rust, since my past integrations for performance were typically either in C or C++.
The FFM API builds upon Java constructs that may be familiar to developers who use reflection or who have written code that programmatically generates other Java code to invoke functions or reference variables. These constructs are MethodHandle
47 and VarHandle
48, which were used to re-implement reflection way back in JDK 1816 and provide direct references to method-like and variable-like entities.
As a related example of using MethodHandle
, when we set up a logger we typically did this in the TicTacToe code:
It simply looks up a logger based on the class name. It’s the same type of pattern you would use if you’re logging with JDK platform logging, SLF4J, or directly with Logback or Log4J.
Alternatively, though, you could use a one-liner that works in the same way across source files and is, therefore, copy-paste safe:
MethodHandles.lookup().lookupClass().getName()
uses the MethodHandles
factory to return the fully qualified name of the class from which it is called by creating a MethodHandles.Lookup
object that references the current class and then invoking getName()
on that class object.
To introduce a native library to Tic Tac Toe, we first abstracted the area that would benefit the most - the GameBoard
which holds the most data. We had previously made it immutable specifically to increase our memory footprint to make garbage collection more impactful but also allow for future undo or game history DVR. This gives us an interface GameBoard
which now has two implementations GameBoardDefaultImpl
which worked as before and the new GameBoardNativeImpl
which uses a native library/java wrapper pair to implement the functions required by the interface.
The GameBoardNativeImpl
calls the Java wrapper TicTacToeLibrary
for the Rust FFI library libtictactoe
, and the wrapper TicTacToeGameBoard
for the game board. We also use the Cleaner
and Cleanable
we discussed in Deprecation of Finalization for Removal to ensure resources are freed appropriately.
The full description of the features used, the gotchas, and surprises are worthy of a single post of their own but in summary (“trust-me-bro”), here’s a sample of a section of the library class, which makes use of most of the features from the FFM API.
When the TicTacToeLibrary
object is created, it initializes the library by loading it and setting up method handles for two functions: version
and version_string
(the former of which is shown above, version_string
is removed for brevity). These method handles allow Java to call functions in our native library.
The code goes through several important steps:
- It loads the native library using the platform-specific library name.
- It sets up method handles for the
version
andversion_string
functions from the native library. - It calls these functions to retrieve and log the version information of the native library.
- It provides a simple interface for creating native game boards.
For the version
function, in the logVersion
method it first calls it with a null pointer to determine the required buffer size, then allocates a buffer of that size and calls the function again to get the actual version string. It’s a typical FFI “double-call” pattern allocating sufficient memory for variable length buffers (which could be avoided by allocating a certain amount with a “good guess” upfront).
For completeness, in Rust, the function being called for version
is simple enough. Describing it in depth is out of scope for this particular blog post since the focus is on Java. In short, though, it publishes an external function with a “C” binary interface to the dynamic library. This allows the caller to retrieve the version of the library when it was built, and the function ensures the foreign caller allocates enough native memory for it to write back a c-style null-terminated string:
Dealing with variable length arrays does tend to be non-trivial with FFI in any language since there’s not always a simple way to know the length of a pointer (MemorySegment
) at the foreign call-site without passing that information along. So this was also an opportunity to swap out the string representation of player markers at the data layer with integer ones in the native implementation of TicTacToeGameBoard
and worry about the string representation at the presentation layer. A sample of this in action is below in the withMove
method:
withMove
takes two inputs: a playerMarker
(the string representing the player making the move e.g. ‘X’, or ‘O’) and a location
(an integer indicating where the player wants to place their mark).
The method first checks if the playerMarker
is new to the game. If it is, it assigns a unique ID to this player and stores this information in two maps: playerMarkerToId
and idToPlayerMarker
. This allows the game to keep track of different players.
Next, the method makes a move on the game board by calling a native function (represented by the withMove
MethodHandle) from libtictactoe
. This function takes the current board state, the chosen location, and the player’s ID as inputs, and returns a new board state with the move applied.
The supporting method in Rust is below, which uses Rust’s Box
to take control of the raw pointer which is passed onto the JVM which will manage its lifecycle as a MemorySegment
.
That's a wrap for JDK 22!
In the aftermath of the JDK 21’s triumphant passage, the desolate expanse still echoes with whispers of innovation. Our weathered developer, now aboard his patched-up JDK buggy, surveys the horizon for signs of the next marvel.
“By the garbage collector’s mercy, what sorcery is this?” he gasps.
A spectral voice resonates from the chariot’s core: “Behold JDK 22, forged in the crucible of progress! It storms ahead, unyielding and unstoppable!”
The veteran dev stands in awe, watching the JDK 22 phantom streak through the skies, leaving a trail of high-performance data processing and seamless native integration.
He murmurs, “The journey to Valhalla continues… JDK 21 was a war rig, but this, this is the streamlined juggernaut of our future.”
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 previews49. 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)50 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.
Features
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 2351 aimw 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 (///
).
Note
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 means52 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:
After:
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:
which renders the following Javadoc:
ZGC: Generational Mode by Default
As we discussed in Generational ZGC 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 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 it52 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:
to:
Deprecate the Memory-Access Methods in sun.misc.Unsafe for Removal
JEP 47153 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
:
In the code about 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 JMH54 - 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:
Algorithmic Interlude
Over-Engineering Tic-Tac-Toe — An Algorithmic Interlude
Abstract
Exploring intelligent AI in Tic-Tac-Toe with Minimax, MaxN, Paranoid, Alpha-Beta, and Monte Carlo Tree Search.
This is the first of a series of expected interludes for the overall Multi-Part Series: Road to JDK 25 — Over-Engineering Tic-Tac-Toe.
Previously: Introduction to JDK 23
So far our primary task has been to progressively over-engineer tic-tac-toe (available for your pleasure at the overengineering-tictactoe GitHub repository: here) focused primarily on using finalized features from Java Enhancement Proposals).
In this article, though, we’ll take a break — or rather, a massive side quest from JDK enhancements to zoom in improve upon and explore the primary algorithms used in the game and look at a few used in game theory in general. It’s an interesting (even if a bit niche) diversion but worth the time especially with the rather long wait to JDK 24.
If you’re not familiar with Java, no need to worry — the code is intentionally written in a way that reads language neutral. All you have to do is wrap your head around all of the recursion!
Quick Note:
If you’ve been following the series through GitHub, you’ll note the classes have had a significant cleanup/refactor since the last update for JDK 23. The new
GameState
class captures the current player, current board, last move and has methods to understand if moves are available in the game or not which proxy some of those previously found inGameBoard
.
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.
It is what’s known as a zero-sum game. In a zero-sum game, whatever one player gains, the other player loses. There’s a fixed amount of “win” to go around, so if you win, your opponent loses an equal amount — and vice versa. In other words, the sum of gains and losses between players is always zero.
Here’s why this matters: in a zero-sum game, your goal isn’t just to win — it’s to make sure your opponent doesn’t win. Every move you make either increases your chances of victory or limits your opponent’s.
Algorithm Level 1
Random Strategy
Imagine you’re playing tic-tac-toe, but instead of thinking about strategy, you decide to make your moves randomly — just picking any available square without a plan. Sometimes you might win by chance, but other times, you’ll lose because you didn’t consider what your opponent might do next. It’s like flipping a coin, or casting die in a game where strategy is everything.
That’s exactly what our first bot implementation did; it naively picked moves from those available at random in the vein hope that it would win, mimicking the strategy of a tic-tac-toe newbie:
How does it work? Rather simply. Given a player and a board (part of the game state), it simply picks a random move from the list of
board.availableMoves()
.Tic-Tac-Toe using a “Random” strategy for choosing the next best move
More often than not, though, picking moves at random proves to be a losing strategy against any experienced opponent.
But what if you could improve on that pure randomness with a strategy so powerful it’s like having a genius guiding your every move? Enter the minimax algorithm — a zero-sum method that transforms your gameplay from random guessing to calculated genius.
Algorithm Level 2
Minimax Strategy
Minimax is designed specifically for zero-sum games like tic-tac-toe. It’s a strategy that helps you navigate the game by maximizing your minimum gains and minimizing your maximum losses.
In a zero-sum context, this means you’re always trying to increase your share of the “win” while reducing your opponent’s.
Here’s how it works:
- Planning Ahead: Instead of making random moves, minimax considers every possible move you can make and imagines what your opponent might do in response. It looks several steps ahead to predict the outcome of each potential path.
- Scoring Outcomes: Minimax assigns a score to each possible outcome — e.g.
+100
if you win (maximum gain),-100
if you lose (maximum loss), and0
if it’s a tie. In a zero-sum game, these scores reflect the perfect balance of victory and defeat between you and your opponent (a maximum loss for you is a maximum gain for your opponent and vice-versa).- Choosing the Best Move: With all possible outcomes in mind, minimax picks the move that guarantees you the best possible result, even if your opponent plays perfectly. It’s like finding the safest path through a battlefield, ensuring that you always come out on top or, at the very least, avoid defeat.
In a random strategy, you might accidentally give your opponent the upper hand without realizing it. But with minimax, every move is carefully calculated to maximize your advantage while minimizing your opponent’s chances of success.
In a zero-sum game like tic-tac-toe, this kind of strategic thinking is crucial if you want to win. There’s no room for mistakes because every loss you suffer is a direct gain for your opponent. Minimax helps you navigate this balance, ensuring that you’re always making the smartest possible move.
How does it work? The genius is encapsulated in the
Minimax
class. This class is like a super-intelligent brain for a game-playing AI. It’s constantly asking, “What’s the absolute best move I, themaximizer
, can make right now?” And it doesn’t just think one step ahead — it’s thinking ALL the steps ahead — It’s like a time machine for games!The class takes the current game board and like Dr. Strange plays out EVERY possible future — every move, every countermove, all the way to the end of the game. It’s like having an all-seeing eye that shows all possible futures!
But wait, there’s more! As it’s exploring all these possible futures, it’s keeping score. Winning moves get high scores, whilst losing moves get low scores. And here’s the clever bit: it assumes both players are playing their absolute best. So it’s always prepared for the toughest competition!
Now, the key part is the
bestMove()
method. This is where all that future-gazing pays off. It looks at all the possible next moves determined byminimax()
, figures out which one leads to the best possible future, and says, “That’s the one! That’s the best move!”Also get this — it even has a built-in preference for quick wins! (by tracking the
depth
) If it sees two paths to victory, it’ll choose the faster one. i.e. a win now (lesser depth) is preferred over a win later (greater depth), and a loss later (greater depth) is preferred over a loss now (lesser depth). How cool is that?Tic-Tac-Toe using a minimax strategy to pick the next best move
Seen in full:
So that’s it, right? Not quite. This is over-engineered tic-tac-toe, so although that traditionally means a two-player, zero-sum game on a 3x3 board, our over-engineered game is supposed to support n-player, any x any grid games which makes minimax limiting. Furthermore, as the board size increases, the number of possible moves increase exponentially making generating the
bestMove
using minimax computationally impractical without also introducing a limit on thedepth
the algorithm will search for future moves (which we limit usingconfig.exceedsMaxDepth()
)
Minimax Strategy (w. Alpha-Beta Pruning)
A variation on pure minimax is to use alpha-beta pruning to only consider possibilities that are stronger that those already considered. The algorithm is implemented in the
alphabeta()
method of theAlphaBeta
class. As with minimax this method recursively explores possible future game states, alternating between maximizing the score on the current player’s turn and minimizing on the opponent’s turn. The algorithm uses two values, alpha and beta, to prune (ignore) branches of the game tree that are guaranteed to be worse than already explored options. This pruning significantly reduces the number of game states that need to be evaluated, making the algorithm more efficient than vanilla minimax.The earlier you are in the game tree — with either only a few moves made or a larger than standard board, the more the benefits of this becomes apparent. A worked example is below:
Minimax algorithm using Alpha-Beta pruning to reduce the size of the game tree to analyze
Unfortunately, even with all this new algorithmic magic we’re still limited to just two players. If only there was an algorithm like minimax that supported an arbitrary number of players instead of just 2. As it turns out there is: Enter Maxn — a generalization of the minimax algorithm for n-player games.
Algorithm Level 3
Maxn Strategy
Maxn is built to deal with games that involve three or more players, where the interactions are more complicated. While Minimax is designed for two-player games where one player’s gain is exactly the other player’s loss, what happens when there are 3 or more players?
In these games, the relationships between players aren’t strictly zero-sum; one player’s gain doesn’t necessarily mean the same amount of loss for another player.
Maxn extends minimax to handle these multi-player situations. It considers each player’s potential moves and strategies, not just focusing on a simple win-loss scenario, but rather on how each player’s actions affect all others. This allows Maxn to evaluate the move that maximizes the score for the current player.
To achieve its purpose, the
MaxN
class uses a recursive algorithm. ThebestMove()
method starts by considering all the available moves for the current player. For each move, it simulates making that move and then calls themaxn()
method to evaluate the resulting game state.The
maxn()
method then recursively explores all possible future moves for all players, up to a certain depth or until the game ends, producing a vector of best scores.MaxN strategy applied to tic-tac-toe, which converges to Minimax in a two-player game
Paranoid Strategy
An alternative approach to
MaxN
which requires less computational resources for those larger multiplayer games is to assume everyone is out to get you.Paranoid
is an AI so cautious, so on-edge, that it thinks everyone else in the game is out to get it. The Paranoid algorithm takes the complexity of multiplayer games and simplifies it in the most pessimistic way possible, effectively reducing the game to a two-player “me vs. the world” scenario.By simplifying the game to a two-player scenario, we can potentially search deeper in the game tree with the same computational resources — essentially reducing the algorithm to a minimax search: the current player (
maximizer
) vs everyone else.The
bestMove()
method is similar to the one used by minimax:A worked example shows interesting differences between how some of the other algorithms reach the goal, certainly with less computational resources.
Paranoid Strategy for Tic-Tac-Toe reduces the game to “Me vs. The World”
The issue with most of the approaches so far is that we typically have to limit the depth the algorithm analyzes the game tree, or else wait days for it to complete its analysis when the board size grows — this may leave these algorithms blind to winning (or losing) strategies. Similarly in multiplayer games, predicting what everyone will do gets exponentially harder with each added player. We could, of course, as we did with minimax, improve upon the base algorithms with alpha-beta pruning, but that still faces similar limitations.
Can we solve for this issue in a way that is also easier to tune and configure? There is a class of powerful algorithms which improve with iterations or time. Enter Monte Carlo, named after the Monte Carlo Casino in Monaco, where the main developer of the method was inspired by his uncle’s gambling habits! Monte Carlo methods are ones I’m very familiar with having worked so long in financial risk management.
We come full-circle as “random” comes back, but this time in a useful way. It uses random sampling for deterministic problems which are difficult, uncertain, or impossible to solve using other approaches, is used across Science, Engineering, Finance, and a range of other disciplines.
Monte-Carlo methods mimic the trial-and-error learning process that we humans often use to achieve our goals — a process known as reinforcement learning
Algorithm Level 4
Monte Carlo Tree Search Strategy
This Monte Carlo approach to tic-tac-toe demonstrates how reinforcement learning concepts can be applied to game playing. The algorithm learns from experience (simulations) and gradually improves its decision-making, balancing between exploring new possibilities and exploiting known good strategies.
Here’s how it works: First, it takes the current game state and starts simulating possible moves. Then, for each of these moves, it plays out the rest of the game — not just once, but many, many times. It’s like speed-running through thousands of parallel universes of the game!
But wait, there’s more! As it’s zipping through these game simulations, it’s keeping score. Which moves led to more wins? Which ones were total busts? It’s building this incredible tree of knowledge, figuring out which branches are worth exploring further and which ones are dead ends, always ready to give its best move at any time as it’s learning.
And the best part? It doesn’t need to know all the strategic ins and outs of the game like other algorithms. It just needs to know the rules and then it learns the rest through sheer trial and error.
This is why Monte Carlo Tree Search is so powerful. It can tackle complex games that other algorithms struggle with, and it does it with style. It’s not just playing the game — it’s writing a playbook with each and every single move.
MonteCarloTreeSearch
is our key class that implements a strategy for playing the game using the Monte Carlo Tree Search (MCTS) algorithm. The purpose of this code is to determine the best move for a player in a given game state using a configurable maximum time allowed for calculation. TheMCTSNode
class represents a node in the game tree. Each node keeps track of the game state, its parent node, child nodes, the number of times it has been visited, and the scores for each player.The algorithm works a little differently to those we’ve built to date — by building a tree of possible future game states and repeatedly simulating games from these states. It starts with the current game state as the root of the tree. Then, it follows these steps:
- Selection: It chooses a promising node in the tree to explore further. As it simulates, it balances exploration of new possibilities with exploitation of known good strategies, using the Algos, UCT, Upper Confidence Bound for Trees formula allowing it to make intelligent decisions even in complex game situations.
2. Expansion: If the chosen node isn’t fully explored, it adds a new child node representing a possible next move.
3. Simulation: It plays out a random game from the new node to its conclusion.
- The
treePolicy()
method decides which node to explore next, balancing between exploring new possibilities and exploiting known good moves.- The
expand()
method adds new child nodes to the tree, representing unexplored moves.- The
defaultPolicy()
method simulates a random game from a given state to its conclusion, using our reward functiondefaultReward()
to quantitatively reinforce promising paths through terminal states.4. Back-propagation: It updates the statistics of all nodes in the path from the new node back to the root based on the game’s outcome. Here, transforms the abstract idea of “good moves” into concrete statistics by repeatedly simulating games and aggregating their results up the game tree.
These steps are repeated many times within the allowed time limit. The more iterations, the more improved our algorithm’s decision-making becomes.
Finally, the
bestChild()
method selects the most promising move at the end of all simulations.Visualizing a few worked scenarios we see how impressive this technique is especially with the UCT formula ensuring we have the right balance between exploration and exploitation:
Monte Carlo Tree Search with a low number of iterations is about as good as our level 1 Algorithm: Random
With more iterations MCTS becomes more confident of the right path to choose.
Monte-Carlo Tree Search applied to Tic-Tac-Toe makes efficient use of computational resources to beat out our other algos
And there we have it. A game as simple as Tic-Tac-Toe, is transformed from a naive newbie to an all-powerful game AI. So the next time you’re playing a multiplayer game and the AI seems suspiciously good at surviving against all odds, remember: you might be up against one of these algorithms!
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
References
Link to original
- M. P. D. Schadd and M. H. M. Winands, “Best-Reply Search for Multi-Player Games”.
- N. Sturtevant, M. Zinkevich, and M. Bowling, “Prob-Maxn: Playing N-Player Games with Opponent Models”.
- A. M. Nijssen and M. H. M. Winands, “An Overview of Search Techniques in Multi-Player Games”.
- M. Świechowski, K. Godlewski, B. Sawicki, and J. Mańdziuk, “Monte Carlo Tree Search: a review of recent modifications and applications,” Artif Intell Rev, vol. 56, no. 3, pp. 2497–2562, Mar. 2023, doi: 10.1007/s10462-022-10228-y
- S. J. Russell and P. Norvig, Artificial intelligence : a modern approach. Pearson, 2016. Available: https://thuvienso.hoasen.edu.vn/handle/123456789/8967. [Accessed: Aug. 27, 2024]
Introduction to JDK 24
The journey will continue to JDK 24…
Introduction to JDK 25
The journey will continue to JDK 25…
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
Footnotes
-
“TIOBE Index,” TIOBE. Available: https://www.tiobe.com/tiobe-index/. [Accessed: Jul. 16, 2024] ↩
-
When in Rome (I currently live in the US) I speak as the Romans do but I grew up in the UK calling this “Noughts and crosses”! ↩
-
SDKMAN! “JDK Distributions - SDKMAN! The Software Development Kit Manager.” Accessed July 9, 2024. https://sdkman.io/jdks. ↩
-
“JDK 17.” Available: https://openjdk.org/projects/jdk/17/. [Accessed: Jul. 09, 2024] ↩
-
“JEP 398: Deprecate the Applet API for Removal.” Accessed July 10, 2024. https://openjdk.org/jeps/398. ↩
-
“JEP 411: Deprecate the Security Manager for Removal.” Accessed July 10, 2024. https://openjdk.org/jeps/411. ↩
-
“CWE - 2020 CWE Top 25 Most Dangerous Software Weaknesses.” Accessed July 10, 2024. https://cwe.mitre.org/top25/archive/2020/2020_cwe_top25.html. ↩
-
“JEP 385: Deprecate RMI Activation for Removal.” Available: https://openjdk.org/jeps/385. [Accessed: Jul. 16, 2024] ↩
-
“JEP 409: Sealed Classes.” Accessed July 10, 2024. https://openjdk.org/jeps/409. ↩
-
“Don’t Wanna Be a Player.” In Wikipedia, January 25, 2024. https://en.wikipedia.org/w/index.php?title=Don%27t_Wanna_Be_a_Player&oldid=1198753099. ↩
-
Bloch, Joshua. Effective java. Addison-Wesley Professional, 2017. ↩
-
Meyer, Bertrand. Object-oriented software construction. Vol. 2. Englewood Cliffs: Prentice hall, 1997. ↩
-
“JEP 410: Remove the Experimental AOT and JIT Compiler.” Available: https://openjdk.org/jeps/410. [Accessed: Jul. 16, 2024] ↩
-
“JDK 18.” Available: https://openjdk.org/projects/jdk/18/. [Accessed: Jul. 09, 2024] ↩
-
“JEP 421: Deprecate Finalization for Removal.” Available: https://openjdk.org/jeps/421. [Accessed: Jul. 17, 2024] ↩ ↩2
-
“JEP 416: Reimplement Core Reflection with Method Handles.” Available: https://openjdk.org/jeps/416. [Accessed: Jul. 16, 2024] ↩ ↩2
-
“JEP 400: UTF-8 by Default.” Available: https://openjdk.org/jeps/400. [Accessed: Jul. 16, 2024] ↩
-
“JEP 408: Simple Web Server.” Available: https://openjdk.org/jeps/408. [Accessed: Jul. 17, 2024] ↩
-
“JEP 413: Code Snippets in Java API Documentation.” Available: https://openjdk.org/jeps/413. [Accessed: Jul. 16, 2024] ↩
-
“JEP 418: Internet-Address Resolution SPI.” Available: https://openjdk.org/jeps/418. [Accessed: Jul. 17, 2024] ↩
-
JDK 19.” Available: https://openjdk.org/projects/jdk/19/. [Accessed: Jul. 09, 2024] ↩
-
“JDK 20.” Available: https://openjdk.org/projects/jdk/20/. [Accessed: Jul. 09, 2024] ↩
-
“JDK 21.” Available: https://openjdk.org/projects/jdk/21/. [Accessed: Jul. 09, 2024] ↩
-
“JEP 449: Deprecate the Windows 32-Bit X86 Port for Removal.” Accessed July 21, 2024. https://openjdk.org/jeps/449. ↩
-
“JEP 452: Key Encapsulation Mechanism API.” Accessed July 22, 2024. https://openjdk.org/jeps/452. ↩ ↩2
-
“JEP 431: Sequenced Collections.” Accessed July 17, 2024. https://openjdk.org/jeps/431. ↩
-
“JEP 440: Record Patterns.” Accessed July 17, 2024. https://openjdk.org/jeps/440. ↩
-
“JEP 441: Pattern Matching for Switch.” Accessed July 18, 2024. https://openjdk.org/jeps/441. ↩
-
Azul | Better Java Performance, Superior Java Support. “Azul C4 Garbage Collector.” Accessed July 18, 2024. https://www.azul.com/products/components/pgc/. ↩
-
“JEP 439: Generational ZGC.” Accessed July 17, 2024. https://openjdk.org/jeps/439. ↩
-
“The Reactive Manifesto.” Accessed July 19, 2024. https://www.reactivemanifesto.org/. ↩
-
“JEP 444: Virtual Threads.” Accessed July 19, 2024. https://openjdk.org/jeps/444. ↩
-
Schwabe, Peter. “Kyber.” Text. Accessed July 22, 2024. https://pq-crystals.org/kyber/index.shtml. ↩
-
Schwabe, Peter. “Dilithium.” Text. Accessed July 24, 2024. https://pq-crystals.org/dilithium/. ↩
-
“(776) Chalk Talk - YouTube.” Accessed July 24, 2024. https://www.youtube.com/@chalktalkmath. ↩
-
“diffplug/spotless.” DiffPlug, Jul. 25, 2024. Available: https://github.com/diffplug/spotless. [Accessed: Jul. 25, 2024] ↩
-
“JEP 264: Platform Logging API and Service.” Available: https://openjdk.org/jeps/264. [Accessed: Jul. 25, 2024] ↩
-
“JEP 330: Launch Single-File Source-Code Programs.” Accessed July 26, 2024. https://openjdk.org/jeps/330. ↩
-
“JEP 458: Launch Multi-File Source-Code Programs.” Accessed July 26, 2024. https://openjdk.org/jeps/458. ↩
-
“JEP 456: Unnamed Variables & Patterns.” Available: https://openjdk.org/jeps/456. [Accessed: Jul. 27, 2024] ↩
-
“Google Java Style Guide.” Accessed July 27, 2024. https://google.github.io/styleguide/javaguide.html#s6.2-caught-exceptions. ↩
-
National Security Agency, “Software Memory Safety”, Nov 2022, https://media.defense.gov/2022/Nov/10/2003112742/-1/-1/0/CSISOFTWAREMEMORYSAFETY.PDF [Accessed: Jul. 27, 2024] ↩
-
V. E. Moghadam, G. Serra, F. Aromolo, G. Buttazzo, and P. Prinetto, “Memory Integrity Techniques for Memory-Unsafe Languages: A Survey,” IEEE Access, vol. 12, pp. 43201–43221, 2024, doi: 10.1109/ACCESS.2024.3380478 ↩
-
“JEP 423: Region Pinning for G1.” Available: https://openjdk.org/jeps/423. [Accessed: Jul. 27, 2024] ↩
-
“JEP 454: Foreign Function & Memory API.” Accessed August 6, 2024. https://openjdk.org/jeps/454. ↩
-
“Java Development Kit Version 22 API Specification - java.lang.foreign.” Accessed August 6, 2024. https://docs.oracle.com/en/java/javase/22/docs/api/java.base/java/lang/foreign/package-summary.html. ↩
-
“Java Development Kit Version 22 API Specification - MethodHandle.” Accessed August 6, 2024. https://docs.oracle.com/en/java/javase/22/docs/api/java.base/java/lang/invoke/MethodHandle.html. ↩
-
“Java Development Kit Version 22 API Specification - VarHandle.” Accessed August 6, 2024. https://docs.oracle.com/en/java/javase/22/docs/api/java.base/java/lang/invoke/VarHandle.html. ↩
-
“[JDK-8329949] Remove the String Templates preview feature - Java Bug System.” Available: https://bugs.openjdk.org/browse/JDK-8329949. [Accessed: Aug. 09, 2024] ↩
-
“JEP 12: Preview Features.” Available: https://openjdk.org/jeps/12. [Accessed: Aug. 09, 2024] ↩
-
“JEP 467: Markdown Documentation Comments.” Accessed August 7, 2024. https://openjdk.org/jeps/467. ↩
-
“Documentation - Rust By Example.” Accessed August 7, 2024. https://doc.rust-lang.org/rust-by-example/meta/doc.html. ↩ ↩2
-
“JEP 471: Deprecate the Memory-Access Methods in sun.misc.Unsafe for Removal.” Available: https://openjdk.org/jeps/471. [Accessed: Aug. 08, 2024] ↩
-
“OpenJDK: jmh.” Available: https://openjdk.org/projects/code-tools/jmh/. [Accessed: Aug. 08, 2024] ↩