String is the type you will use more than any other, and it has one property that
explains nearly all of its behaviour: a String is immutable. Once created, its
characters can never change. Every method that appears to modify a string actually returns a new
one.
Immutability, and what it means in practice
String name = "folau";
name.toUpperCase(); // returns "FOLAU" — and throws it away
System.out.println(name); // folau — unchanged
name = name.toUpperCase(); // assign the result, or nothing happened
System.out.println(name); // FOLAU
Forgetting to assign the result is the most common beginner mistake with strings, and it is
silent — no error, the code just does not do anything. If a String method's return value
is not being used, the line is dead.
Immutability is not an inconvenience the designers overlooked. It is what makes strings safe to
share between threads, safe to use as HashMap keys (the hash can never go stale), and
poolable — which is the next section.
Creating strings, and why == lies
String a = "hello"; // literal — goes in the string pool
String b = "hello"; // same pool entry, same object
String c = new String("hello"); // explicitly a new object
System.out.println(a == b); // true
System.out.println(a == c); // false
System.out.println(a.equals(c)); // true
Java keeps a pool of string literals and reuses them, so two identical literals really are
the same object. That is why == appears to work — until one of your strings came from a
file, a database, user input or string concatenation, at which point it is a different object and
== silently returns false.
Always compare with equals. Never use new String("...");
it only creates a redundant object.
String input = "Hello";
System.out.println(input.equals("hello")); // false — case matters
System.out.println(input.equalsIgnoreCase("hello")); // true
// Guard against null by putting the literal first, or use Objects.equals
System.out.println("hello".equals(input)); // safe even if input is null
System.out.println(Objects.equals(input, "hello")); // safe both ways
The methods worth knowing
String s = " Hello, World ";
System.out.println(s.length()); // 17
System.out.println(s.strip()); // "Hello, World" (Java 11+; prefer over trim)
System.out.println(s.strip().toLowerCase()); // "hello, world"
System.out.println(s.contains("World")); // true
System.out.println(s.strip().startsWith("H"));// true
System.out.println(s.indexOf("World")); // 9 (-1 if absent)
System.out.println(s.replace("World", "Java"));// " Hello, Java "
System.out.println(s.isBlank()); // false (true for "" and whitespace-only)
Two on that list are worth singling out. strip() replaced trim() in Java
11 because trim() only removes characters below U+0020 and misses real Unicode
whitespace. And isBlank() is what you almost always mean when you reach for
isEmpty() — the latter is false for a string of three spaces.
Splitting and joining come up constantly:
String csv = "red,green,blue";
String[] parts = csv.split(","); // ["red", "green", "blue"]
String joined = String.join(" | ", parts); // "red | green | blue"
String fromList = String.join(", ", List.of("a", "b")); // "a, b"
System.out.println("ab".repeat(3)); // "ababab"
System.out.println("a,b,,c".split(",").length); // 4 — empty fields are kept in the middle
split takes a regular expression, not a plain string. Splitting on
"." or "|" will not do what you expect, because both are regex
metacharacters — escape them as "\\." and "\\|".
Concatenation in a loop
Because strings are immutable, + creates a brand-new string every time. In a loop
that is quadratic work:
// Fine — the compiler turns a few + into one efficient operation
String greeting = "Hello, " + "Folau" + "!";
// Bad — 10,000 intermediate strings, each copying the last
String slow = "";
for (int i = 0; i < 10_000; i++) {
slow += i;
}
// Good — one buffer, appended to
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 10_000; i++) {
sb.append(i);
}
String fast = sb.toString();
The rule is narrow, so do not over-apply it: + is perfectly good for joining a handful
of values on one line, and clearer. Reach for StringBuilder only when you are building a
string across iterations.
Text blocks
Since Java 15, triple quotes give you multi-line strings without escaping — a large improvement for embedded JSON, SQL or HTML:
String json = """
{
"name": "Folau",
"role": "engineer"
}
""";
String sql = """
SELECT id, name
FROM customers
WHERE active = true
""";
Indentation is handled sensibly: Java strips the common leading whitespace, using the closing
""" to decide where the left margin is. Move that closing line and you change the
indentation of the whole block.
Formatting
String name = "Folau";
int orders = 3;
double total = 59.97;
System.out.println("%s placed %d orders totalling $%.2f".formatted(name, orders, total));
// Folau placed 3 orders totalling $59.97
String padded = "%-10s|".formatted("left"); // "left |"
%s takes anything, %d a whole number, %.2f a decimal to two
places. formatted() is the instance form of String.format() and reads
better at the end of a text block.
Width and alignment are what make columns line up. From the console bank app this site uses for examples, building one line of a statement:
public String toStatementLine() {
return "%-16s %-14s %12s %14s %s"
.formatted(
timestamp.format(DISPLAY),
type.label(),
Money.format(signedAmount()),
Money.format(balanceAfter),
description);
}
%-16s is left-aligned in 16 characters; %12s is right-aligned in 12,
which is what you want for money so the decimal points align down the column.
Checking for empty input
A small habit that prevents a lot of noise. These three questions are different:
String value = " ";
System.out.println(value == null); // false — no string at all
System.out.println(value.isEmpty()); // false — zero characters
System.out.println(value.isBlank()); // true — nothing but whitespace
// The usual check you actually want, null-safe:
boolean hasContent = value != null && !value.isBlank();
Iterating over characters
A string is a sequence, and you will occasionally need it one character at a time:
String word = "Java";
for (int i = 0; i < word.length(); i++) {
System.out.print(word.charAt(i) + " "); // J a v a
}
for (char c : word.toCharArray()) { // often cleaner
System.out.print(c);
}
System.out.println(new StringBuilder(word).reverse()); // avaJ
One caveat for later: char is 16 bits, so characters outside the basic multilingual
plane — emoji, most notably — occupy two char values. If that matters to your input,
iterate with word.codePoints() instead.
Next
You can now hold and manipulate values. Conditional statements is next: making the program choose between them.