Guide contents

Lesson 2 of 71

Strings and Text

This lesson covers how Java stores and processes text. After it, a reader can compare strings correctly, build them without wasted allocation, handle characters outside the basic Unicode range, and convert between text and bytes without depending on the platform. It builds on Types, Variables, and Expressions.

Topic Release
Compact strings (Latin-1 storage) Java 9
strip, isBlank, lines, repeat Java 11
indent, transform Java 12
Text blocks, formatted, stripIndent, translateEscapes Java 15
HexFormat Java 17
UTF-8 as the default charset Java 18
splitWithDelimiters, StringBuilder.repeat Java 21

Example 1: immutability, the pool, and equality

A String is immutable: no method changes its characters, and every "modifying" method returns a new string. The string pool is a JVM-wide table of strings; all string literals with the same content share one pooled object.

Java
public class StringEquality {
    public static void main(String[] args) {
        String a = "order";
        String b = "order";
        String c = new String("order");
        String d = "ord" + "er";             // constant expression, folded by javac
        String part = "ord";
        String e = part + "er";              // computed at run time

        System.out.println("a == b: " + (a == b));
        System.out.println("a == c: " + (a == c));
        System.out.println("a == d: " + (a == d));
        System.out.println("a == e: " + (a == e));
        System.out.println("a.equals(e): " + a.equals(e));
        System.out.println("a == e.intern(): " + (a == e.intern()));

        String upper = a.toUpperCase();
        System.out.println(a + " " + upper);
        System.out.println("equalsIgnoreCase: " + a.equalsIgnoreCase(upper));
    }
}
Output
a == b: true
a == c: false
a == d: true
a == e: false
a.equals(e): true
a == e.intern(): true
order ORDER
equalsIgnoreCase: true

== compares references, as for any object, so its result depends on where each string came from. Literals and constant expressions are pooled; new String and run-time concatenation create new objects. intern() returns the pooled instance with the same content, adding it if missing. Content comparison always uses equals. Immutability makes strings safe to share between threads and to use as HashMap keys, and lets String cache its hash code after the first hashCode() call.

Since Java 9, compact strings (JEP 254) store a string as a byte[] plus a coder flag. If every character fits in Latin-1 (U+0000 to U+00FF), each takes one byte; otherwise the whole string uses UTF-16 with two bytes per character. One non-Latin-1 character doubles the storage of the whole string. The JVM flag -XX:-CompactStrings turns this off.

Example 2: concatenation and StringBuilder

StringBuilder is a mutable sequence of characters with an internal array that grows as needed. A + inside a loop creates a new String on every iteration and copies all previous characters, so the total work grows with the square of the length.

Java
public class Building {
    public static void main(String[] args) {
        String slow = "";
        for (int i = 0; i < 5; i++) {
            slow += i + ",";                 // new String each iteration
        }
        System.out.println(slow);

        StringBuilder sb = new StringBuilder(32);
        for (int i = 0; i < 5; i++) {
            if (sb.length() > 0) sb.append(',');
            sb.append(i);
        }
        System.out.println(sb);

        sb.setLength(0);                     // reuse the same buffer
        sb.append("BUY ").append(100).append(" @ ").append(99.5);
        sb.insert(0, '[').append(']');
        System.out.println(sb + " length=" + sb.length());
        System.out.println(sb.reverse());

        String single = "qty=" + 7 + " px=" + 99.5;   // one expression: fine
        System.out.println(single);
    }
}
Output
0,1,2,3,4,
0,1,2,3,4
[BUY 100 @ 99.5] length=16
]5.99 @ 001 YUB[
qty=7 px=99.5

A single concatenation expression is compiled efficiently: since Java 9 (JEP 280) javac emits one invokedynamic call that sizes the result once, so + outside loops needs no StringBuilder. The constructor argument 32 sets the initial capacity, which avoids array growth when the final size is known. setLength(0) clears the builder and keeps its array, a common pattern in code that formats many messages. StringBuffer is the older synchronized variant; StringBuilder is used when one thread owns the builder.

Example 3: text blocks and formatted

A text block (Java 15) is a multi-line string literal between """ delimiters. The compiler removes the common leading indentation, normalizes line endings to \n, and strips trailing spaces.

Java
public class TextBlocks {
    public static void main(String[] args) {
        String json = """
            {
              "symbol": "%s",
              "qty": %d
            }
            """.formatted("ABC", 100);
        System.out.print(json);

        String oneLine = """
            SELECT id, name \
            FROM users \
            WHERE active""";
        System.out.println(oneLine);

        String spaces = """
            a\s
            b   \s
            "quoted" and \"""triple\"""
            """;
        for (String line : spaces.split("\n")) {
            System.out.println("[" + line + "]");
        }

        System.out.println(String.format("%-6s|%6.2f|%05d", "px", 3.14159, 42));
    }
}
Output
{
  "symbol": "ABC",
  "qty": 100
}
SELECT id, name FROM users WHERE active
[a ]
[b    ]
["quoted" and """triple"""]
px    |  3.14|00042

The closing """ on its own line ends the block with a newline; placing it after the last word omits that newline. A \ at the end of a line joins the next line without a newline, and \s is a single space that is not stripped. Indentation is measured from the leftmost line, including the closing delimiter. formatted(args) is the instance form of String.format; both take %s, %d, %f, width, flags, and precision.

Example 4: String methods added since Java 11

String gained methods in several releases. The table shows the ones common in production code.

Method Release
isBlank, strip, stripLeading, stripTrailing, lines, repeat 11
indent, transform 12
formatted, stripIndent, translateEscapes 15
splitWithDelimiters, indexOf(ch, from, to); StringBuilder.repeat 21
Java
import java.util.Arrays;

public class StringAdditions {
    public static void main(String[] args) {
        System.out.println("   ".isBlank() + " " + "   ".isEmpty());
        String padded = " trade ";        // em spaces around the word
        System.out.println("[" + padded.strip() + "]");
        System.out.println("trim kept em spaces: " + (padded.trim().length() == padded.length()));
        System.out.println("a\nb\r\nc".lines().count());
        System.out.println("-".repeat(10));
        System.out.print("line1\nline2".indent(4));
        System.out.println("42".transform(Integer::parseInt) + 1);
        System.out.println(Arrays.toString("a,b;c".splitWithDelimiters("[,;]", 0)));
        System.out.println(new StringBuilder().repeat("ab", 3));
    }
}
Output
true false
[trade]
trim kept em spaces: true
3
----------
    line1
    line2
43
[a, ,, b, ;, c]
ababab

strip removes all Unicode whitespace, while trim removes only characters up to U+0020, so the em space U+2003 survives trim. lines treats \n, \r, and \r\n as terminators and returns a stream of lines. indent(n) adds n spaces to every line and ends the result with a newline. transform applies a function to the string, which keeps a chain of calls readable; Integer::parseInt is a method reference, a short way to pass a method as a function, taught in Lambdas and Functional Interfaces. lines and codePoints return streams, taught in Streams and Collectors; here only count() and toArray() are used. splitWithDelimiters returns the delimiters as elements between the parts.

Example 5: chars, code points, and Character

chars() returns the UTF-16 code units of a string as an IntStream, and codePoints() returns whole Unicode code points. Text that can contain emoji or rare scripts is processed by code point.

Java
public class CodePoints {
    public static void main(String[] args) {
        String s = "Go🚀!";               // "Go", U+1F680 (rocket), "!"
        System.out.println("chars: " + s.chars().count() + ", code points: " + s.codePoints().count());

        StringBuilder reversedChars = new StringBuilder(s).reverse();
        System.out.println("reverse keeps pair: " + reversedChars.toString().contains("🚀"));

        String truncated = s.substring(0, 3);       // cuts the surrogate pair
        System.out.println("broken: " + Character.isHighSurrogate(truncated.charAt(2)));

        int end = s.offsetByCodePoints(0, 3);       // 3 code points
        System.out.println("safe prefix length: " + end);

        for (int cp : s.codePoints().toArray()) {
            System.out.println("U+" + Integer.toHexString(cp).toUpperCase()
                    + " letter=" + Character.isLetter(cp)
                    + " digit=" + Character.isDigit(cp)
                    + " name=" + Character.getName(cp));
        }

        System.out.println(Character.toChars(0x1F680).length);
        System.out.println(Character.isWhitespace(' ') + " " + Character.isDigit('٣'));
        System.out.println(Character.getNumericValue('٣'));
    }
}
Output
chars: 5, code points: 4
reverse keeps pair: true
broken: true
safe prefix length: 4
U+47 letter=true digit=false name=LATIN CAPITAL LETTER G
U+6F letter=true digit=false name=LATIN SMALL LETTER O
U+1F680 letter=false digit=false name=ROCKET
U+21 letter=false digit=false name=EXCLAMATION MARK
2
true true
3

substring and charAt index code units, so cutting at an arbitrary index can split a surrogate pair and produce an invalid string. offsetByCodePoints finds a safe cut. StringBuilder.reverse keeps surrogate pairs in order. Character methods take an int code point, so they work for every character; the char overloads cannot see characters above U+FFFF. Character.isDigit accepts digits from all scripts, such as the Arabic-Indic three U+0663, which matters when validating input that must be ASCII digits.

Example 6: HexFormat and StringJoiner

java.util.HexFormat (Java 17) converts between bytes and hexadecimal text. StringJoiner builds a delimited string with an optional prefix and suffix.

Java
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.HexFormat;
import java.util.StringJoiner;

public class HexAndJoin {
    public static void main(String[] args) {
        byte[] frame = {0x01, (byte) 0xAB, 0x7F, 0x00};

        HexFormat hex = HexFormat.of();
        System.out.println(hex.formatHex(frame));
        System.out.println(HexFormat.ofDelimiter(":").withUpperCase().formatHex(frame));
        System.out.println(Arrays.toString(hex.parseHex("cafe")));
        System.out.println(hex.toHexDigits((byte) 10) + " " + hex.toHexDigits(255));
        System.out.println(HexFormat.fromHexDigits("ff"));

        byte[] text = "FIX".getBytes(StandardCharsets.US_ASCII);
        System.out.println(HexFormat.ofDelimiter(" ").withPrefix("0x").formatHex(text));

        StringJoiner joiner = new StringJoiner(", ", "[", "]");
        joiner.setEmptyValue("none");
        System.out.println(joiner);
        joiner.add("AAPL").add("MSFT").add("GOOG");
        System.out.println(joiner + " (" + joiner.length() + " chars)");
        System.out.println(String.join("|", "a", "b", "c"));
    }
}
Output
01ab7f00
01:AB:7F:00
[-54, -2]
0a 000000ff
255
0x46 0x49 0x58
none
[AAPL, MSFT, GOOG] (18 chars)
a|b|c

HexFormat instances are immutable and thread-safe, so one instance can be shared as a constant. toHexDigits(int) always prints 8 digits; the byte overload prints 2. parseHex returns signed bytes, so 0xca prints as -54. String.join is a shortcut over StringJoiner for a fixed delimiter with no prefix or suffix.

Example 7: regular expressions

A Pattern is a compiled regular expression, and a Matcher applies it to one input. Compiling is the expensive step, so a pattern used repeatedly is compiled once and stored in a static final field.

Java
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class Regex {
    private static final Pattern ORDER =
            Pattern.compile("(?<side>BUY|SELL) (?<qty>\\d+) (?<sym>[A-Z]{1,5}) @ (?<px>\\d+(?:\\.\\d+)?)");

    public static void main(String[] args) {
        String[] lines = {"BUY 100 AAPL @ 189.5", "SELL 20 MSFT @ 410", "HOLD 5 X @ 1"};
        for (String line : lines) {
            Matcher m = ORDER.matcher(line);
            if (m.matches()) {
                System.out.println(m.group("side") + " " + Integer.parseInt(m.group("qty"))
                        + " " + m.group("sym") + " " + m.group("px"));
            } else {
                System.out.println("no match: " + line);
            }
        }

        Matcher numbers = Pattern.compile("\\d+").matcher("a1b22c333");
        int sum = 0;
        while (numbers.find()) {
            sum += Integer.parseInt(numbers.group());
        }
        System.out.println("sum = " + sum);

        System.out.println("2026-10-03".replaceAll("(\\d+)-(\\d+)-(\\d+)", "$3/$2/$1"));
        System.out.println(String.join("|", "a, b,,c".split(",\\s*")));
        System.out.println("a.b".split(".").length + " " + "a.b".split(Pattern.quote(".")).length);
    }
}
Output
BUY 100 AAPL 189.5
SELL 20 MSFT 410
no match: HOLD 5 X @ 1
sum = 356
03/10/2026
a|b||c
0 2

matches requires the whole input to match; find searches for the next match and can be called in a loop. Named groups (?<name>...) make extraction readable. String.matches, replaceAll, and split compile the pattern on every call, so in a hot loop they cost more than a stored Pattern. split(".") returns an empty array because . matches every character and trailing empty strings are removed; Pattern.quote treats the argument literally.

Example 8: charsets and the UTF-8 default

A charset maps characters to bytes. Since Java 18 (JEP 400) the default charset is UTF-8 on every platform; before that it depended on the operating system locale, so the same program could write different bytes on Windows and Linux.

Java
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;

public class Charsets {
    public static void main(String[] args) {
        System.out.println("default: " + Charset.defaultCharset());

        String word = "café";                   // "café"
        byte[] utf8 = word.getBytes(StandardCharsets.UTF_8);
        byte[] latin1 = word.getBytes(StandardCharsets.ISO_8859_1);
        byte[] utf16 = word.getBytes(StandardCharsets.UTF_16BE);
        System.out.println("UTF-8 " + utf8.length + " " + Arrays.toString(utf8));
        System.out.println("Latin-1 " + latin1.length + " " + Arrays.toString(latin1));
        System.out.println("UTF-16BE " + utf16.length);

        String wrong = new String(utf8, StandardCharsets.ISO_8859_1);
        System.out.println("mojibake length: " + wrong.length());
        System.out.println("round trip: " + new String(utf8, StandardCharsets.UTF_8).equals(word));

        byte[] ascii = "é".getBytes(StandardCharsets.US_ASCII);
        System.out.println("unmappable becomes: " + (char) ascii[0]);
    }
}
Output
default: UTF-8
UTF-8 5 [99, 97, 102, -61, -87]
Latin-1 4 [99, 97, 102, -23]
UTF-16BE 8
mojibake length: 5
round trip: true
unmappable becomes: ?

é takes two bytes in UTF-8 and one in Latin-1. Decoding UTF-8 bytes as Latin-1 gives one wrong character per byte (mojibake), with no exception. getBytes replaces characters the charset cannot represent with ?. The StandardCharsets constants avoid the checked UnsupportedEncodingException of the name-based overloads. Code that reads files or sockets passes the charset explicitly, so behavior does not depend on the JDK version or on the file.encoding property.

Interview points

  • Compare strings with equals; == depends on pooling and is true only for the same object.
  • Literals and compile-time constants are interned; new String and run-time concatenation are not.
  • Immutability gives thread safety, safe map keys, and a cached hash code.
  • Compact strings store Latin-1 text in one byte per character; any wider character switches the string to UTF-16.
  • + in a loop is quadratic; a single + expression is compiled with invokedynamic and is fine.
  • length() counts UTF-16 code units; use codePoints() and offsetByCodePoints for user-visible characters.
  • strip is Unicode-aware, trim is not.
  • Compile a Pattern once; String.split and replaceAll compile on each call.
  • Default charset is UTF-8 since Java 18; pass charsets explicitly anyway.

Summary

  • String is immutable and caches its hash.
  • intern() returns the pooled instance.
  • StringBuilder is mutable, unsynchronized, and reusable with setLength(0).
  • Text blocks strip common indentation; \ joins lines, \s keeps a space.
  • HexFormat (Java 17) formats and parses hex; instances are thread-safe.
  • StringJoiner and String.join build delimited text.
  • Matcher.matches checks the whole input, find searches.
  • Unmappable characters become ? in getBytes; wrong decoding gives mojibake without an error.