SaveMyCert
Log in
5 of 5 free questions left today·for 30 a day
Handling Date, Time, Text, Numeric and Boolean Values

Java Strings, StringBuilder and Text Blocks (OCP Java SE 25)

16 min read1Z0-831 · Handling Date, Time, Text, Numeric and Boolean ValuesUpdated

Manipulating text in Java means working with two classes: <code>String</code>, an immutable sequence of characters whose methods always return new values, and <code>StringBuilder</code>, a mutable buffer whose methods change it in place and return the same object. Objective 1.2 of the Java SE 25 Developer exam also covers text blocks, the multi-line String literals delimited by three double quotes. This lesson covers how each class behaves: string-pool identity, zero-based indexes and their exceptions, how <code>+</code> mixes numbers and text, the white-space, comparison, formatting and split methods, StringBuilder's index rules and equality, and exactly how the compiler turns a text block into a String.

What you’ll learn
  • Explain why String methods return new objects and predict what a caller sees after String and StringBuilder arguments are changed or reassigned
  • Decide when two Strings are the same object, using the string pool, compile-time constant folding and intern()
  • Predict the results and exceptions of charAt, indexOf, lastIndexOf, substring and the + operator with numbers, chars and null
  • Use strip/trim, isBlank/isEmpty, compareTo, format/formatted, repeat, join and split and state their exact output
  • Trace chained StringBuilder mutations, aliasing and index rules, and compare builders correctly
  • Write text blocks that compile and predict their content: incidental white space, the closing delimiter, \ and \s escapes
  • Apply lines(), indent(), stripIndent() and translateEscapes() to multi-line text

Why String methods never change the original String

A String is immutable: once created, its characters can never change, so every method that seems to modify text — toUpperCase(), concat(), replace(), strip(), substring() — returns a new String (or the same one when nothing changes) and leaves the receiver untouched. If you don't assign the result, it is simply lost.

String t = "duke";
t.toUpperCase();              // result discarded
String u = t.toUpperCase();   // result kept
System.out.println(t + " " + u);   // duke DUKE

Chained calls each work on the String returned by the previous call, not on the original. That matters when an index is computed from one String and used on another: an indexOf() taken on the untrimmed original is a position in that text, and applying it to a stripped or lower-cased copy points at a different character.

Immutability also explains how Strings behave as method arguments. Java passes every argument by value — for objects, a copy of the reference. Inside a method, name = name.concat("?") builds a new String and points only the local parameter at it; the caller's variable still refers to the old one. Contrast this with a StringBuilder argument: calling buf.insert(0, "#") mutates the one shared object, so the caller sees the change — but buf = new StringBuilder(...) inside the method again only moves the local copy of the reference.

Inside the methodVisible to the caller?
name = name.concat("?") on a String parameterNo — a new String, local reference only
buf.insert(0, "#") on a StringBuilder parameterYes — the shared object was mutated
buf = new StringBuilder("y")No — only the local reference moved; later calls through buf change the new object

String identity: the pool, constant expressions, intern() and == versus equals()

equals() compares the characters; == compares references — it asks whether two variables point at the same object. Two Strings can be equal in content and still fail ==.

The JVM keeps a string pool of interned Strings. Every String literal is interned, so identical literals anywhere in the program share one object. The compiler also folds compile-time constant expressions into a single literal: "Du" + "ke" becomes "Duke" before the program runs, so it is the pooled object too. For this topic, a constant expression is built from literals, operators and final variables of primitive or String type that are initialized with constant expressions (constant variables); a plain, non-final variable is never part of one.

Concatenation that involves a non-constant value is evaluated at run time and produces a new String object that is not pooled. new String("Duke") always creates a new object. Calling intern() returns the pooled instance with the same content (adding it to the pool first if needed), so the result is == to the matching literal.

String a = "Duke";
String b = "Du" + "ke";        // constant expression: folded
String prefix = "Du";
final String P = "Du";         // constant variable
String c = prefix + "ke";      // run time: new object
String d = P + "ke";           // constant expression: folded
String e = new String("Duke"); // always a new object
System.out.println((a == b) + " " + (a == c) + " " + (a == d)
        + " " + (a == e) + " " + (a == e.intern()));
// true false true false true

Text blocks are String literals too, so a text block whose processed content equals another literal is the same pooled object. The safe rule for real code: compare text with equals() (or equalsIgnoreCase()), and treat == results as a question about object identity.

Indexing: charAt, indexOf, lastIndexOf and substring

String positions are zero-based, and ranges are start-inclusive, end-exclusive: substring(b, e) returns the characters from index b up to but not including e, so its length is e - b. substring(b) runs to the end.

  • Valid ranges: 0 <= b <= e <= length(). b == e gives the empty String, and substring(length()) is legal and returns "". A start greater than the end, a negative index, or an end past length() throws StringIndexOutOfBoundsException at run time — the compiler never checks these values.
  • charAt(i) needs 0 <= i < length(); charAt(length()) throws.
  • indexOf(x) returns the first position of a char or String, or -1 if absent. indexOf(x, fromIndex) starts searching at fromIndex (inclusive).
  • lastIndexOf(x) searches backwards from the end; lastIndexOf(x, fromIndex) searches backwards starting at fromIndex. The search methods never throw for an out-of-range fromIndex: a negative start is treated as 0 for indexOf, a start past the end is treated as the last index for lastIndexOf, and a search that starts beyond the text (indexOf from length() or more, lastIndexOf from a negative index) returns -1.
String w = "parallel";
System.out.println(w.indexOf('l') + " " + w.indexOf('l', 5) + " "
        + w.lastIndexOf('l') + " " + w.lastIndexOf('l', 6) + " "
        + w.indexOf('z'));                     // 4 5 7 5 -1
System.out.println(w.indexOf('a', -3) + " "
        + w.lastIndexOf('a', 50) + " "
        + w.indexOf('a', 20) + " "
        + w.lastIndexOf('a', -1));             // 1 3 -1 -1
System.out.println("[" + w.substring(8) + "] " + w.substring(2, 5)
        + " " + w.substring(5));               // [] ral lel
w.substring(5, 1);   // StringIndexOutOfBoundsException

How the + operator mixes numbers, chars and Strings

The + operator is evaluated left to right, one pair at a time, and each pair decides its own meaning: if either operand is a String, the pair is concatenation; otherwise it is numeric addition. So numbers before the first String are added, and everything after it is appended as text.

System.out.println(4 + 5 + "-" + 4 + 5);   // 9-45
System.out.println('x' + 'y' + "!");       // 241!  (120 + 121, then "!")
System.out.println("!" + 'x' + 'y');       // !xy
char ch = 'x';
System.out.println(ch + 1);                // 121   (char + int is int)

A char is a numeric type: char + char and char + int produce an int, not a character. Only once a String is on one side does a char get appended as a character.

null in concatenation becomes the text "null" — no exception. That includes the compound form: if t is a null String, t += "?" gives "null?". With +=, the right-hand side is evaluated first as its own expression, so in t += m + 2 with char m = 'm', m + 2 is computed as the int 111 and the digits 111 are appended, not the letter o.

Everyday String methods: white space, comparison, formatting and split

Beyond indexing, the exam expects you to predict the exact result of the common String methods.

White space: strip() versus trim(), isBlank() versus isEmpty()

MethodWhat it treats as white space / tests
trim()Removes leading and trailing characters with code point <= U+0020 (space and ASCII control characters) only
strip(), stripLeading(), stripTrailing()Unicode-aware: remove characters for which Character.isWhitespace is true, such as U+2003 EM SPACE; the leading/trailing variants work on one end only
isEmpty()True only when length() == 0
isBlank()True when the String is empty or contains only white space (Unicode-aware, like strip())

So "\n ".isEmpty() is false while "\n ".isBlank() is true, and for "\u2003ok\u2003" trim() leaves all 4 characters while strip() leaves "ok".

Comparing: equals, equalsIgnoreCase, compareTo

compareTo() orders Strings lexicographically by UTF-16 char values. It returns the difference of the first differing chars; if one String is a prefix of the other, it returns the difference in lengths. Negative means the receiver sorts first, zero means equal content. Because every uppercase ASCII letter (65–90) has a smaller value than every lowercase one (97–122), "Mango".compareTo("kiwi") is negative even though m follows k in the alphabet. compareToIgnoreCase() and equalsIgnoreCase() ignore case differences.

System.out.println("cat".compareTo("Cat") + " "        // 32
        + "cat".compareTo("catalog") + " "             // -4
        + "Mango".compareTo("kiwi") + " "              // -30
        + "Mango".compareToIgnoreCase("kiwi"));        // 2

Building text: format, formatted, repeat, join

String.format(fmt, args...) and the instance form fmt.formatted(args...) are equivalent. Common conversions: %s (any value, via toString), %d (integral types only), %f (floating point; %.2f sets precision), %b (boolean: null is false, a non-null non-Boolean argument is true), %c (a char, or an int code point — 97 prints a), %x (hexadecimal) and %n (platform line separator). Width and flags: a width (%5s right-justifies in five columns), the - flag (left-justify) and the 0 flag (zero-pad, numeric conversions only).

Format strings are checked at run time, not by the compiler: String.format("%d", 7.0) throws IllegalFormatConversionException, and the 0 flag with %s throws FormatFlagsConversionMismatchException.

"xy".repeat(n) returns n copies in total (repeat(0) is ""). String.join(delim, parts) puts the delimiter only between elements — never before the first or after the last.

System.out.println(String.format("%04d|%6s|%-4s|%.1f|%b|%c|%x",
        42, "ok", "ok", 2.75, "no", 'Q', 26));
// 0042|    ok|ok  |2.8|true|Q|1a
System.out.println(String.join("+", "1", "2", "3") + "!".repeat(2));
// 1+2+3!!

split(): a regex split that drops trailing empty Strings

split(regex) treats its argument as a regular expression and is the same as split(regex, 0): empty Strings between delimiters are kept, but trailing empty Strings are removed. A negative limit keeps them all; a positive limit n splits at most n - 1 times and leaves the rest of the input, delimiters included, in the last element. Because . is a regex metacharacter, "1.2.3.4".split(".") produces only empty Strings, all trailing, so the array is empty; escape it as "\\.".

String row = "x;y;;z;;;";
System.out.println(row.split(";").length + " "      // 4  [x, y, , z]
        + row.split(";", -1).length + " "           // 7
        + row.split(";", 2)[1]);                    // y;;z;;;
System.out.println("1.2.3.4".split(".").length + " "
        + "1.2.3.4".split("\\.").length);         // 0 4

StringBuilder: methods that mutate in place

StringBuilder is the mutable counterpart of String: its methods change the builder's own character sequence and return this, the same object, which is why calls can be chained. Every call in a chain sees the result of the previous one.

MethodEffect
append(x)Adds the String form of x at the end (any type; a null String or object reference appends "null")
insert(offset, x)Inserts before position offset; offset may equal length() (append at the end)
delete(start, end)Removes [start, end)
deleteCharAt(i)Removes the one char at i
replace(start, end, str)Replaces [start, end) with str — note the range, unlike String.replace(old, new)
reverse()Reverses the sequence
setCharAt(i, c)Overwrites one char
setLength(n)Truncates to n, or pads with '\u0000' chars up to n — it really changes the content and length()
StringBuilder sb = new StringBuilder("planet");
sb.insert(0, "#").delete(3, 5).reverse();
System.out.println(sb);           // telp#
StringBuilder r = new StringBuilder("compile");
r.replace(0, 3, "X");             // Xpile
r.setLength(3);
System.out.println(r + ":" + r.length());   // Xpi:3

Aliasing: the return value is the same object

Because these methods return this, StringBuilder q = p.append("2"); does not copy anything — p and q now name one object, p == q is true, and a later mutation through either variable is visible through both. The distinction to keep straight is mutation versus reassignment: mutating follows the object, while assigning a new builder to one variable (p = new StringBuilder("9")) moves only that variable and leaves every other alias on the old object.

StringBuilder p = new StringBuilder("1");
StringBuilder q = p.append("2");
q.insert(0, "0");
System.out.println(p + " " + (p == q));   // 012 true

StringBuilder index rules: capacity versus length

A builder's capacity is the size of its internal storage; its length is how many characters it actually holds. new StringBuilder(16) has capacity 16 but length 0, and capacity grows automatically as needed. Index checks are always made against length(), never capacity.

MethodIndex rule
charAt(i), setCharAt(i, c), deleteCharAt(i)0 <= i < length(), otherwise StringIndexOutOfBoundsException
insert(offset, x)0 <= offset <= length()
delete(start, end), replace(start, end, s)start must be valid and not greater than end; an end past length() is clamped to length() instead of throwing
substring(start, end)Strict, like String: end must not exceed length()
StringBuilder sb = new StringBuilder(16);   // capacity 16, length 0
sb.append("dog");
sb.insert(3, "s");      // ok: offset == length()  -> dogs
sb.delete(2, 40);       // end clamped             -> do
// sb.charAt(2);          StringIndexOutOfBoundsException
// sb.setCharAt(4, '!');  StringIndexOutOfBoundsException

As with String, none of these limits are compile-time errors: bad indexes compile and fail when the statement runs.

Comparing StringBuilders and Strings

StringBuilder does not override equals(), so sb1.equals(sb2) is inherited from Object and is true only for the same object. It does implement Comparable<StringBuilder>: compareTo() compares content lexicographically and returns 0 for equal content.

Comparing across the two types needs care. "node".equals(sb) is always false, because String.equals() returns true only for another String. To compare content, use String.contentEquals(CharSequence) or convert first with sb.toString().

var m = new StringBuilder("node");
var n = new StringBuilder("node");
System.out.println((m == n) + " "                     // false
        + m.compareTo(n) + " "                        // 0
        + "node".equals(m) + " "                      // false
        + "node".contentEquals(n) + " "               // true
        + m.toString().equals(n.toString()));         // true
ComparisonWhat it checks
sb1.equals(sb2), sb1 == sb2Identity
sb1.compareTo(sb2) == 0Content
str.contentEquals(sb)Content
str.equals(sb)Always false (different type)
sb.toString().equals(str)Content

Text blocks: syntax, escapes and incidental white space

A text block is a multi-line String literal delimited by three double quotes. It produces an ordinary String — same type, interned like any literal — and the compiler turns its source lines into content in three steps: normalize line terminators to \n, strip incidental white space, then interpret escape sequences.

Rules that decide whether it compiles

  • The opening """ must be followed by optional white space and a line terminator; content cannot start on the same line. """Start is a compile error ("illegal text block open delimiter sequence, missing line terminator").
  • The closing """ may sit on the last content line or on a line of its own.
  • Single and double quotes may appear unescaped. Three quotes in a row must be broken up, for example \""".
  • Escape sequences are processed as in ordinary literals, so an invalid one such as \w in D:\work is a compile error ("illegal escape character"); write \\ for a backslash.

Incidental white space and the closing delimiter

The compiler removes the indentation shared by all content lines and the closing-delimiter line (when it is on its own line), and it always strips trailing spaces from each line. That makes the closing delimiter a control: placed level with the content it removes all indentation; moved left of the content it keeps the extra indentation as real spaces. A closing delimiter on its own line gives the content a trailing \n; putting it at the end of the last content line omits it.

static String show(String t) {
    return t.replace(' ', '.').replace("\n", "|");
}

String a = """
    SELECT *
      FROM t
    """;
String b = """
        SELECT *
          FROM t
    """;
String c = """
    SELECT *
      FROM t""";
// show(a): SELECT.*|..FROM.t|
// show(b): ....SELECT.*|......FROM.t|
// show(c): SELECT.*|..FROM.t

The two text-block-only escapes

  • \<line-terminator> — a backslash at the very end of a line suppresses that newline, joining the line to the next one (any spaces before the backslash stay).
  • \s — a single space. Because escapes are interpreted after trailing white space is stripped, \s at a line's end keeps a trailing space that would otherwise vanish.
String d = """
    tab\s
    wrap \
    me "now" \"""
    """;
// show(d): tab.|wrap.me."now"."""|

Multi-line helpers: lines(), indent(), stripIndent(), translateEscapes()

Several String methods are designed for multi-line text and pair naturally with text blocks.

  • lines() returns a Stream<String> of lines split on \n, \r or \r\n. Empty lines in the middle count; a final line terminator does not create an extra empty line. "one\n\ntwo\n".lines().count() is 3: one, an empty line and two.
  • indent(n) adds n spaces to the start of every line (positive n) or removes up to n leading white-space characters (negative n; lines with fewer simply lose all of theirs). It also normalizes line terminators to \n and always ends the result with \n, even if the input had none. indent(0) only normalizes.
  • stripIndent() applies the text-block incidental-white-space algorithm at run time: it removes common leading indentation and trailing spaces, but keeps the final line as it was — it does not add a trailing line terminator.
  • translateEscapes() interprets escape sequences that exist as literal characters in the String at run time. The source literal "1\\t2\\t3" is seven characters (each \\t is a backslash and a t); translateEscapes() turns it into five (1, tab, 2, tab, 3).
  • chars() returns an IntStream of the char values — numbers, not characters ("abc".chars().sum() is 294).
System.out.println(show("p\n  q".indent(1)));       // .p|...q|
System.out.println(show("   m\n n".indent(-2)));    // .m|n|
System.out.println(show("   m\n n".stripIndent())); // ..m|n
String raw = "1\\t2\\t3";
System.out.println(raw.length() + " "
        + raw.translateEscapes().length());       // 7 5

Tip. Expect short, complete programs that you must trace exactly: what is printed, whether the code compiles, or whether an exception is thrown. Items combine several calls on one String or StringBuilder, so you need to track whether each call returns a new String or mutates a shared builder, and which variable refers to which object after a reassignment or method call. Identity questions mix literals, constant and non-constant concatenation, new String() and intern(); index questions probe inclusive/exclusive bounds, fromIndex searches and the run-time exceptions. Text block items show several declarations and ask which compile, or render the content with spaces and newlines made visible so the closing delimiter, \ and \s escapes decide the answer. Multiple-answer items compare near-twin methods side by side: strip versus trim, isBlank versus isEmpty, compareTo versus compareToIgnoreCase, indent versus stripIndent, and split with different limits.

Key takeaways
  • String is immutable: methods return a new String, and an unassigned result is lost; StringBuilder methods mutate the builder and return this.
  • == on Strings tests identity. Literals, compile-time constant expressions (including final constant variables), text blocks and intern() results share the pooled object; runtime concatenation and new String() do not.
  • Ranges are start-inclusive, end-exclusive. substring needs 0 <= begin <= end <= length(); violations throw StringIndexOutOfBoundsException at run time.
  • + is evaluated left to right pair by pair: numbers before the first String are added, everything after is appended; char + int is an int, and null appends as "null".
  • strip() and isBlank() are Unicode-aware; trim() only removes characters up to U+0020; isEmpty() means length 0.
  • compareTo returns the first char difference or the length difference; uppercase letters sort before lowercase ones.
  • split() takes a regex and, with no limit, drops trailing empty Strings; format conversion mismatches fail at run time.
  • StringBuilder index checks use length(), not capacity; delete/replace clamp an end past the length, while setCharAt/deleteCharAt/charAt throw.
  • StringBuilder.equals() is identity; compare content with compareTo(), contentEquals() or toString().equals().
  • A text block starts on the line after """; the closing delimiter's position controls indentation and the trailing newline; \ joins lines and \s keeps a trailing space.

Frequently asked questions

Why does calling toUpperCase() on a Java String not change it?

Java Strings are immutable, so toUpperCase(), replace(), concat(), strip() and every other String method return a new String instead of changing the original. The result must be assigned, for example s = s.toUpperCase(); otherwise it is discarded and the variable still refers to the unchanged text.

When does == return true for two Java Strings?

== returns true only when both variables refer to the same object. That happens for identical String literals and text blocks, for compile-time constant expressions such as "Du" + "ke" or concatenations of final constant variables, and for the results of intern(). Concatenation with a non-final variable and new String(...) create separate objects, so use equals() to compare text.

What is the difference between strip() and trim() in Java?

trim() removes leading and trailing characters whose code point is U+0020 or lower, which covers the ASCII space and control characters only. strip() removes any Unicode white space as defined by Character.isWhitespace, such as U+2003 EM SPACE. stripLeading() and stripTrailing() do the same at one end, and isBlank() uses the same Unicode-aware definition.

Does StringBuilder override equals() in Java?

No. StringBuilder inherits equals() from Object, so two builders with the same characters are equal only if they are the same object. To compare content, use compareTo() (StringBuilder implements Comparable since Java 11), String.contentEquals(builder), or compare the toString() results with equals().

Why does Java's split() return fewer elements than expected?

split(regex) is split(regex, 0), which removes trailing empty Strings from the result; empty Strings between delimiters are kept. Pass a negative limit, such as split(",", -1), to keep the trailing empties. The argument is also a regular expression, so a delimiter like a dot must be escaped as "\\.".

How does the closing delimiter affect a Java text block?

When the closing three quotes are on their own line, that line counts toward the shared indentation that is removed, and the content ends with a newline. Moving the delimiter further left than the content keeps the extra indentation as spaces; placing it at the end of the last content line removes the trailing newline.

What do the \s and backslash-newline escapes do in a text block?

\s is a single space that is not stripped, so it preserves a trailing space at the end of a line, which the compiler would otherwise remove. A backslash at the very end of a line suppresses the line terminator and joins that line to the next one, which lets long text be wrapped in source without adding newlines.

Which StringBuilder methods throw for an index past the length?

charAt, setCharAt and deleteCharAt require an index below length(), and insert requires an offset no greater than length(); otherwise they throw StringIndexOutOfBoundsException. delete(start, end) and replace(start, end, str) clamp an end beyond length() to length(), though their start must still be valid. Capacity never affects these checks.

Source

This lesson covers the "Handling Date, Time, Text, Numeric and Boolean Values" domain of the official 1Z0-831 exam guide. Vendors revise their guides — check the source for the current version.

Sign up free to mark lessons complete, bookmark topics and track your exam readiness.

Spotted a mistake in this lesson?