☕ Java

String Improvements

Across Java 11–21, the String class received a steady stream of small, targeted additions — isBlank(), strip()/stripLeading()/stripTrailing(), repeat(int), lines(), and String Templates as a preview feature in Java 21 — each addressing a specific, recurring pain point that previously required either importing a third-party library (Apache Commons Lang, Guava) or writing manual, easy-to-get-subtly-wrong helper code, particularly around Unicode-aware whitespace handling and Unicode-aware blank detection, neither of which the much older trim() method actually does correctly. This entry covers exactly why trim() and isEmpty() were insufficient, the new whitespace and blank-detection methods and their Unicode-correctness improvements, repeat() and lines() and the common manual idioms they replace, and the Java 21 String Templates preview feature for safer, more readable string interpolation.

Motivation — trim() and isEmpty() Were Not Unicode-Aware or Sufficient

String.trim(), present since Java 1.0, removes leading and trailing characters whose code point is less than or equal to U+0020 (the space character) — meaning it only correctly strips the small set of ASCII control characters and the plain ASCII space, but fails to strip many legitimate Unicode whitespace characters with code points above U+0020, such as the non-breaking space (U+00A0), various Unicode space separators, and other characters that Java's own Character.isWhitespace(...) correctly classifies as whitespace but that fall outside trim()'s narrow "less than or equal to U+0020" definition. This made trim() subtly incorrect for any text that could plausibly contain non-ASCII whitespace, which is increasingly common given how much modern text originates from word processors, web forms, or non-English locales that use different whitespace conventories. Similarly, String.isEmpty() (added in Java 6) only checks whether a string has zero length, so a string consisting purely of whitespace (" ") is not considered empty by isEmpty(), even though for most practical "does this field actually contain anything meaningful" checks, a whitespace-only string should be treated the same as a genuinely empty one. Before Java 11, checking for this "blank" condition required manually combining isEmpty() with trim() (str.trim().isEmpty()), which not only required remembering to write that combination every time but also inherited trim()'s Unicode-whitespace blind spots described above. Java 11 directly addressed both gaps: isBlank() correctly determines whether a string is empty or contains only whitespace, using the same Unicode-aware whitespace definition as Character.isWhitespace(...) (not trim()'s narrower one), and strip()/stripLeading()/stripTrailing() provide Unicode-correct replacements for trim(), removing whitespace from both ends, only the leading end, or only the trailing end respectively, again using the proper Unicode whitespace definition rather than trim()'s legacy ASCII-only one.
Java
// ── trim() — NOT Unicode-aware, misses non-ASCII whitespace ────────────
String nbsp = "\u00A0Hello\u00A0";   // non-breaking space on both sides
System.out.println(nbsp.trim().length());     // length unchanged — trim() did NOT strip U+00A0!
System.out.println(nbsp.strip().length());    // correctly stripped — strip() IS Unicode-aware

// ── isEmpty() doesn't account for whitespace-only strings ──────────────
String whitespaceOnly = "   \t  ";
System.out.println(whitespaceOnly.isEmpty());   // false — has length, even though it's "blank"

// Old manual idiom — works, but easy to forget and inherits trim()'s Unicode gaps:
boolean oldBlankCheck = whitespaceOnly.trim().isEmpty();

// ── Java 11+ — correct, direct, Unicode-aware ───────────────────────────
System.out.println(whitespaceOnly.isBlank());        // true
System.out.println("  Hello  ".strip());              // "Hello" — Unicode whitespace stripped
System.out.println("  Hello  ".stripLeading());       // "Hello  "
System.out.println("  Hello  ".stripTrailing());      // "  Hello"

repeat() and lines() — Replacing Manual Loop Idioms

Before Java 11, repeating a string a fixed number of times required either a manual loop appending to a StringBuilder, or a commonly-seen but somewhat opaque one-liner trick using String.join("", new String[n]).replace("", str) or similar Collections-based hacks, none of which were particularly readable or discoverable. String.repeat(int count), added in Java 11, does exactly what its name says — returns a new string consisting of the original string concatenated with itself count times — covering a surprisingly common need (building separator lines, indentation, padding, simple ASCII art, repeated delimiters in test fixtures) with a single direct, self-explanatory method call. repeat(0) returns an empty string, and repeat() throws IllegalArgumentException for a negative count. String.lines(), also added in Java 11, splits a string into a Stream<String> of its constituent lines, correctly handling \n, \r, and \r\n as line terminators (the mixed-line-ending case that plain String.split("\n") handles incorrectly on Windows-originated text containing \r\n sequences, since split("\n") would leave a trailing \r on each line). Returning a Stream rather than an array or List also allows lines() to be composed directly into a stream pipeline — filtering, mapping, or counting lines — without a separate conversion step, which is a natural fit given lines() arrived as a String-side complement to the Streams API introduced in Java 8.
Java
// ── Before Java 11 — manual loop or opaque one-liner for repetition ────
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 20; i++) sb.append("-");
String separatorOld = sb.toString();

// ── Java 11+ — direct, self-explanatory ─────────────────────────────────
String separator = "-".repeat(20);
System.out.println(separator);          // --------------------
System.out.println("ab".repeat(3));     // "ababab"
System.out.println("x".repeat(0));      // "" — empty string
// "x".repeat(-1);                       // IllegalArgumentException

// Practical use — indentation, padding:
String indent = " ".repeat(4) + "nested content";

// ── lines() — correctly handles \n, \r, and \r\n, returns a Stream ──
String multilineText = "first line\nsecond line\r\nthird line\rfourth line";
long lineCount = multilineText.lines().count();
System.out.println(lineCount);   // 4 — correctly split despite mixed line terminators

multilineText.lines()
    .filter(line -> line.contains("second"))
    .forEach(System.out::println);   // "second line"

// Contrast — plain split("\n") mishandles \r\n-terminated lines:
String[] splitOld = multilineText.split("\n");
System.out.println(splitOld[1].endsWith("\r"));   // true — trailing \r leaked through, a common bug

String Templates (Java 21 Preview) — Safer Interpolation

String Templates, delivered as a preview feature in Java 21 (JEP 430, finalized later under continued preview refinement), introduce a new syntax for embedding expressions directly inside string literals using a template processor, addressing the verbosity and error-proneness of building dynamic strings via repeated + concatenation or String.format(...) with positional placeholders that must be kept manually in sync with the argument list. A string template is written with a processor name (such as the built-in STR) immediately preceding a string literal that contains \{expression} placeholders, and the processor evaluates each embedded expression and splices its value into the resulting string at that position — keeping the variable reference visually inline with the text it populates, rather than separated into a distant argument list that the reader must mentally re-correlate with each %s or + concatenation point. Beyond pure convenience, template processors are a deliberately pluggable abstraction: STR is the simple built-in processor that behaves like straightforward interpolation, but a custom processor can validate or transform the final result before it's produced — the JEP's own motivating example is a hypothetical processor for constructing SQL queries that automatically and correctly escapes embedded values, structurally preventing SQL injection in a way that naive string concatenation into a query cannot, because the processor controls how each embedded expression's value is incorporated rather than the developer manually remembering to escape it every time. Because String Templates remained a preview feature through Java 21 (requiring the --enable-preview compiler and runtime flag, and subject to change in subsequent releases before final standardization, including being withdrawn from preview pending redesign in a later release), production code targeting Java 21 should treat this feature as experimental and confirm its exact status and syntax against the specific JDK feature-release version in use rather than assuming permanence.
Java
// ── Before templates — concatenation or positional format strings ──────
String name = "Alice";
int age = 30;
String messageOld = "Name: " + name + ", Age: " + age;       // verbose, easy to mis-concatenate
String messageFormat = String.format("Name: %s, Age: %d", name, age);   // must keep args in sync
// with %s/%d order — a reordering bug here is easy to introduce and easy to miss

// ── Java 21 preview — String Templates with the built-in STR processor ─
// (requires --enable-preview on javac and java)
String message = STR."Name: \{name}, Age: \{age}";
// Variable references are visually inline with the text — no separate argument list to
// keep mentally synchronized with placeholder order

// Expressions, not just simple variables, are allowed inside \{...}:
String summary = STR."\{name} will be \{age + 1} next year";

// ── Custom template processors can validate/transform output ───────────
// Conceptual example from the JEP motivation — a SQL-safe processor that
// escapes embedded values automatically, rather than relying on the
// developer to remember to escape every interpolated value manually:
//
// PreparedStatement stmt = SQL."SELECT * FROM users WHERE name = \{userInput}";
// The processor controls how userInput is incorporated — structurally
// preventing the kind of injection that naive "..." + userInput + "..."
// concatenation into a query string would be vulnerable to.

// NOTE: preview feature — exact syntax/availability may change across
// JDK releases; confirm current status before depending on it in production.

Related Topics in Java 9 to 21 Features

Module System (JPMS)
The Java Platform Module System (JPMS), introduced in Java 9 under Project Jigsaw, adds a module as a new, higher-level unit of code organization above the package, allowing a JAR-like artifact to explicitly declare which packages it exports for use by other modules, which modules it depends on, and which packages it keeps entirely internal and inaccessible from outside — closing a long-standing gap in the classpath model where every public class in every JAR on the classpath was accessible to every other JAR, regardless of whether that access was intended. JPMS was also the mechanism used to decompose the JDK itself, which had grown into one monolithic rt.jar, into roughly 90 separate modules, enabling smaller custom runtime images and explicit internal-API encapsulation (notably restricting access to sun.* and other internal packages that many libraries had relied on despite warnings). This entry covers the classpath problems JPMS solves, module-info.java syntax and the requires/exports/opens directives, the distinction between the classpath and module path including automatic and unnamed modules, and jlink custom runtime images.
JShell
JShell, introduced in Java 9, is an interactive Read-Eval-Print Loop (REPL) bundled directly with the JDK, allowing Java statements, expressions, method, class, and variable declarations to be entered and evaluated one at a time at an interactive prompt, with immediate feedback — without requiring a class wrapper, a main method, explicit compilation, or a build tool, all of which Java had always required for even the smallest snippet of executable code before Java 9. This solved a long-standing friction point for learning, quick experimentation, and API exploration, where languages like Python or Ruby offered an immediate interactive prompt but Java required the full edit-compile-run cycle even to test a single line. This entry covers what problem JShell solves and how it differs from a script, core REPL usage including implicit variable creation and forward references, snippet management commands, and how to feed JShell external classes and files.
Collection Factory Methods
Collection factory methods — List.of(...), Set.of(...), and Map.of(...)/Map.ofEntries(...) — introduced in Java 9, provide a concise, standard way to create small, fixed-content, genuinely immutable collections in a single expression, replacing the previously common but verbose combination of Arrays.asList(...), Collections.unmodifiableList(...), and manual add() calls, none of which produced a collection that was both concise to write and fully immutable. This entry covers exactly what problem these factories solve compared to the older idioms, the specific immutability and null-rejection guarantees they provide (which are stricter than Collections.unmodifiableXxx), their behavior around duplicate keys/elements, and important caveats around mutability expectations and performance for very small collections.
Private Interface Methods
Private interface methods, introduced in Java 9, allow an interface to declare methods marked private — with a full method body — that are accessible only from within that same interface (from its default methods, its other private methods, and its private static methods), but completely invisible to implementing classes and to any code outside the interface. They close a code-duplication gap left after Java 8's default methods: when an interface has multiple default methods that share common logic, that shared logic previously either had to be duplicated in each default method or exposed as a public default method purely to enable reuse, polluting the interface's actual API surface with a method meant only for internal reuse. This entry covers the specific problem they solve, the distinction between private instance and private static interface methods, the rules governing what they can call and be called from, and how they interact with the broader default-method design Java established in version 8.