Besides the “big” features that often aggregate under well-known project names, like “Amber”, “Loom”, or “Panama”, there are many little things in every release that are easy to miss. These API and tool improvements might not be as visible as other features, as they’re not represented by a JEP. That doesn’t mean we don’t need to know about them.

Let’s take a look at some of the “other” changes that Java 21 gave us!

Working with Strings and Characters

Java’s new String templates might have been the show-stealer of this release in regards to String, but there are a few more additions the String type and adjacent ones.


We already had 4 different indexOf methods available to find the index of an int or String argument, starting at the beginning or a given index.

Java 21 added another variant for both finding either an int or String by providing an end-index in addition to a beginning one:

int indexOf(int ch,
            int beginIndex,
            int endIndex)

int indexOf(String str,
            int beginIndex,
            int endIndex)

Be aware that the beginIndex is inclusive, but the endIndex is exclusive. In my opinion, they should’ve named them accordingly, as they did with IntStream.rangeClosed for example.


The two split methods got a new acquaintance that works a little differently, hence the more expressive name:

String[] splitWithDelimiters(String regex,
                             int limit)

The splitting itself is like you would expect it from knowing split(String, int), but the returned String[] contains the split parts AND the delimiters:

var str = "foo:::bar::hello:world";
var regex = ":+"; // at least one colon

var result = str.splitWithDelimiters(regex, -1);
// => String[7] { "foo", ":::", "bar", "::", "hello", ":", "world" }

As you can see in the actual source code and the API note in the documentation, besides from an optimization for one or two character non-regexes, the call is equivalent to Pattern.compile(regex).splitWithDelimiters(str, limit)

StringBuilder and StringBuffer

Both types received new convenience methods called repeat for both CharSequence and int arguments:

StringBuilder repeat(CharSequence cs,
                    int count)
StringBuilder repeat(int codePoint,
                     int count)

StringBuffer repeat(CharSequence cs,
                    int count)
StringBuffer repeat(int codePoint,
                    int count)

They all do what it says on the carton:

var santaBuilder = new StringBuilder();
santaBuilder.repeat("Ho! ", 3);

var str = santaBuilder.toString()
// => "Ho! Ho! Ho! "

Identifying Emojis

Over the years, Emojis gained more and more properties, like different color shades and combined presentations. To make identification easier, the Character type gained 6 new static methods:

static boolean isEmoji(int codePoint)
static boolean isEmojiPresentation(int codePoint)
static boolean isEmojiModifier(int codePoint)
static boolean isEmojiModifierBase(int codePoint)
static boolean isEmojiComponent(int codePoint)
static boolean isExtendedPictographic(int codePoint)

Complementing these identification methods, new predefined character groups are available in patterns, too:

  • \p{IsEmoji}
  • \p{IsEmoji_Presentation}
  • \p{IsEmoji_Modifier}
  • \p{IsEmoji_Modifier_Base}
  • \p{IsEmoji_Component}

What these properties actually mean is defined in the “Unicode Technical Standard #51.

Clamping, a common use case, was simplified by adding 4 static method clamp for number primitives to ensure a value is in between or equal min and max:

static int clamp(long value,
                 int min,
                 int max)

static long clamp(long value,
                  long min,
                  long max)

static float clamp(float value,
                   float min,
                   float max)

static double clamp(double value,
                    double min,
                    double max)

The first method returns an int to do double duty, providing a way to clamp long and a (casted) int into an int-based range.

All methods are also available on the StrictMath type.

HttpClient is an AutoClosable (and more)

The now implements AutoClosable, making it usable in a try-with-resources block.

There are also 5 new related methods:

// Initiates an orderly shutdown by completing previously submitted
// request but not accepting new ones.
void close()

// Initiates a graceful shutdown and returns immediately
void shutdown()

// Initiates an immediate shutdown and tries to interrupt any active
// operation. Returns immediately, too.
void shutdownNow()

// Waits for the client to terminate for the given duration.
// Returns true if the client was terminated in time.
boolean awaitTermination(Duration duration)

// Checks if a client is terminated.
boolean isTerminated()

Instances obtained via HttpClient.newHttpClient() or HttpClient.newBuilder() provide best-effort implementations of these methods.

To learn more about them, check out the “Implementation Note” in the official documentation.

Improved Sleep Quality

Thread.sleep(long millis, int nanos) can now perform sub-millisecond sleeps on POSIX platforms. Before, non-zero arguments for nanos were rounded up to a full millisecond before.

Be aware that the actual precision still depends on the underlying system!

JDK Tool Access in JShell

The JShell is one of my favorite tool added to the JDK in version 9, as it provides a quick and easy way to check out code snippets and verify behavior.

To set it up to your particular needs, you can use “start-up scripts”. Before the release of Java 21, there were 3 start-up scripts available:

  • DEFAULT: loaded if no start-up script is specified.
  • JAVASE: imports a lot more packages.
  • PRINTING: like default, but adds print convenience methods.

Now, there’s a new one available: TOOLING

This start-up script introduces various Java tools as methods right into JShell:

void jar(String... args)
void javac(String... args)args)
void javadoc(String... args)
void javap(String... args)
void jdeps(String... args)
void jlink(String... args)
void jmod(String... args)
void jpackage(String... args)

The also available method tools() lists all available Java tools.

For example, to introspect the Bytecode of a type, you can now do:

jshell> record Point(int x, int y) { }

jshell> javap(Point.class)

Classfile /tmp/TOOLING-11410713526801554514.class
  Last modified Oct 3, 2023; size 1281 bytes
  SHA-256 checksum 6f3f6c989dacd62919ce9330c7e1cc9a9511402d044198747d9a35c66a1160f4
  Compiled from "$JShell$"
public final class REPL.$JShell$22$Point extends java.lang.Record
  minor version: 0
  major version: 65
  flags: (0x0031) ACC_PUBLIC, ACC_FINAL, ACC_SUPER
  this_class: #8                          // REPL/$JShell$22$Point
  super_class: #2                         // java/lang/Record
  interfaces: 0, fields: 2, methods: 6, attributes: 5
Constant pool:
   #1 = Methodref          #2.#3          // java/lang/Record."<init>":()V
   #2 = Class              #4             // java/lang/Record
   #3 = NameAndType        #5:#6          // "<init>":()V
// ... snip ...

Be aware that starting JShell only with TOOLING will lack the common default imports, so I suggest always to load DEFAULT, too:

jshell --start DEFAULT TOOLING


These were just a few of the many changes flying under the radar as they don’t have a JEP attached to them. Nevertheless, it’s always a good idea to check out the “little things”, too, as they can affect such basic types like String and might contain a hidden gem.

To check out all changes of Java 21, regardless of whether it is a JEP or “just” a ticket, see the official release notes.

If you’re interested in a more technical comparison, I recommend the Java Version Almanac, which compares the API of different versions to highlight all changes, additions, and removals.

A Functional Approach to Java Cover Image
Interested in using functional concepts and techniques in your Java code?
Check out my book!


Looking at Java 21