Strings and Text
Working with text in Java: immutable strings, comparing with equals instead of ==, common String methods, StringBuilder, text blocks and formatting numbers and dates.
String literals and escapes
A string literal is text between double quotes. Single quotes are
for a single char, which is a different type (a primitive). Special characters are written with a backslash
escape sequence: \n (new line), \t (tab),
\" (a double quote), \\ (a backslash) and
\u20AC (the Unicode character U+20AC, the euro sign).
void main() {
String customer = "Amara Okafor";
String path = "C:\\kestrel\\meters";
String quote = "She said \"the meter is broken\"";
char grade = 'A';
IO.println(customer);
IO.println(path);
IO.println(quote);
IO.println("Line one\nLine two\tafter a tab");
IO.println("Grade: " + grade + ", euro sign: \u20AC");
}
Amara Okafor C:\kestrel\meters She said "the meter is broken" Line one Line two after a tab Grade: A, euro sign: €
Java has no single-quoted strings, no raw strings and no string
interpolation such as Python's f-strings, JavaScript's template
literals or C#'s $"…". Text is joined with
+ or formatted with String.format, both
covered below. For multi-line text there are
text blocks. The examples on this page
are compact source files that print with IO.println.
Strings are immutable
A String object never changes after it is created; it
is immutable.
Every method that seems to change a string returns a new one.
Forgetting to use the result is a common mistake that the compiler
does not catch:
void main() {
String status = "open";
status.toUpperCase(); // result thrown away
IO.println(status);
status = status.toUpperCase(); // keep the new string
IO.println(status);
}
open OPEN
Immutability is what makes strings safe to share: a string passed to a method, stored in a map or used by another thread can never be changed behind your back.
Comparing strings
String is a class, so a String variable
holds a reference to an object. For references, == asks
"do these two variables point to the same object?". Whether two
strings with the same text are the same object depends on how they
were made. String literals with the same text are stored once, in
the string pool, a table of
shared string objects the JVM keeps; strings built at run time, read
from a file or typed by a user are new objects.
void main() {
String literal = "RESOLVED";
String fromFile = new String("RESOLVED"); // as if read from a file
String part = "RESOL";
String built = part + "VED"; // built at run time
IO.println(literal == "RESOLVED");
IO.println(literal == fromFile);
IO.println(literal == built);
IO.println(literal.equals(fromFile));
IO.println(literal.equals(built));
IO.println("resolved".equalsIgnoreCase(fromFile));
}
true false false true true true
== gave true for two literals and
false for the same text from another source. That is
exactly what makes it dangerous: code that compares with
== passes a quick test with literals and fails with
real data. The method
equals() compares the content: two
strings are equal when they contain the same characters in the same
order. equalsIgnoreCase ignores the difference between
capitals and small letters.
equals and null
Calling a method on null throws a
NullPointerException. When a string may be missing, put
the known value first, or use Objects.equals, which
handles null on either side:
void main() {
String status = null; // e.g. a missing field
IO.println("RESOLVED".equals(status));
IO.println(Objects.equals(status, "RESOLVED"));
IO.println(status.equals("RESOLVED"));
}
false false Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.equals(Object)" because "<local1>" is null
The message names the variable as
"<local1>" instead of "status":
these examples are run in source-file mode, which compiles without
the table of local variable names, so the JVM can only give the
variable's slot number. Code compiled with javac -g, as
build tools and IDEs usually do for debugging, shows the real name.
More in
Null, Optional and NullPointerException.
Ordering strings
compareTo puts strings in order. It returns a negative
number when the first string comes before the second, zero when they
are equal and a positive number when it comes after; only the sign
matters. The order is by UTF-16 code unit, so all capitals come
before all small letters:
void main() {
IO.println("Bakker".compareTo("Okafor"));
IO.println("Okafor".compareTo("Bakker"));
IO.println("Mei".compareTo("Mei"));
IO.println("apple".compareTo("Banana"));
IO.println("apple".compareToIgnoreCase("Banana"));
}
-13 13 0 31 -1
For names in a user interface, alphabetical order in a language
(with accents, ij and the like) needs a
java.text.Collator for that language. Sorting lists of
strings is covered in
Arrays and Collections.
Common String methods
Characters are numbered from 0.
substring(begin, end) includes begin but
not end, like slicing in Python;
indexOf returns -1 when the text is not
found.
void main() {
String ticket = "INC-20417";
IO.println(ticket.length());
IO.println(ticket.charAt(0));
IO.println(ticket.substring(4));
IO.println(ticket.substring(0, 3));
IO.println(ticket.indexOf("-"));
IO.println(ticket.indexOf("X"));
IO.println(ticket.startsWith("INC-") + " " + ticket.endsWith("17"));
IO.println(ticket.contains("204"));
IO.println(ticket.toLowerCase());
IO.println(ticket.replace("INC", "PRB"));
}
9 I 20417 INC 3 -1 true true true inc-20417 PRB-20417
Java has no negative indexes: the last character is
charAt(ticket.length() - 1). An index outside the
string throws an exception:
void main() {
IO.println("INC-20417".charAt(9));
}
Exception in thread "main" java.lang.StringIndexOutOfBoundsException: Index 9 out of bounds for length 9
Methods for cleaning up input and building text:
void main() {
String typed = " E-001 ";
IO.println("[" + typed.strip() + "]");
IO.println("[" + typed.stripLeading() + "]");
IO.println(" ".isBlank() + " " + " ".isEmpty() + " " + "".isEmpty());
IO.println("=".repeat(12));
IO.println(String.join(", ", List.of("Amara", "Mei", "Hamid")));
IO.println("INC-20417 INC-20418".replaceAll("[0-9]+", "#####"));
}
[E-001] [E-001 ] true false true ============ Amara, Mei, Hamid INC-##### INC-#####
strip() removes white space at both ends (Java 11),
including Unicode spaces; the older trim() only removes
spaces and control characters up to U+0020.
isBlank() is true for an empty string or one with only
white space, isEmpty() only for length 0.
replace replaces plain text;
replaceAll takes a
regular expression.
Splitting
split cuts a string into an array of parts. It has two
surprises: its argument is a regular expression, and it drops empty
parts at the end.
void main() {
String line = "E-001,2026-10-01,350,,";
String[] fields = line.split(",");
IO.println(fields.length + " " + Arrays.toString(fields));
String[] all = line.split(",", -1);
IO.println(all.length + " " + Arrays.toString(all));
String version = "25.0.4";
IO.println(Arrays.toString(version.split(".")));
IO.println(Arrays.toString(version.split("\\.")));
}
3 [E-001, 2026-10-01, 350] 5 [E-001, 2026-10-01, 350, , ] [] [25, 0, 4]
A CSV line with two empty columns at the end came back with three
fields instead of five; a limit of -1 keeps them, which
is what you want when columns are counted. And "." is
the regular expression for "any character", so
split(".") cut at every character and left nothing;
escape it as "\\." (a backslash for the regular
expression, doubled for the Java string). Real CSV, with quoted
fields that contain commas, needs a CSV library.
Joining text and StringBuilder
The + operator joins strings and converts any other
value to text, including null. It works from left to
right, so parentheses matter as soon as numbers are involved:
void main() {
String customer = null;
IO.println("Customer: " + customer);
IO.println(1 + 2 + " kWh");
IO.println("kWh: " + 1 + 2);
IO.println("kWh: " + (1 + 2));
IO.println(String.valueOf(350) + Integer.toString(12));
}
Customer: null 3 kWh kWh: 12 kWh: 3 35012
Because strings are immutable, every + creates a new
string. In a single expression that does not matter: the compiler
optimizes it. In a loop that adds to the same string thousands of
times, it creates thousands of discarded strings. A
StringBuilder is a
mutable sequence of characters for exactly this: you append to it,
and call toString() (or just print it) at the end.
void main() {
List<String> tickets = List.of("INC-20417", "INC-20418", "INC-20421");
StringBuilder report = new StringBuilder();
for (String ticket : tickets) {
if (!report.isEmpty()) {
report.append("; ");
}
report.append(ticket);
}
report.insert(0, "Open: ");
IO.println(report);
IO.println(report.length());
IO.println(new StringBuilder("R1;R2;R3").reverse());
}
Open: INC-20417; INC-20418; INC-20421 37 3R;2R;1R
append returns the builder itself, so calls can be
chained: sb.append("INC-").append(20417). For the
common case of joining with a separator, String.join or
a stream with Collectors.joining(", ") is shorter (see
Lambdas, Functional Interfaces and Streams). Old code also uses StringBuffer, a synchronized and
slower version of the same class; prefer StringBuilder.
Text blocks
A text block (since Java 15) is a
multi-line string literal between triple double quotes. The opening
""" must be followed by a line break; the text starts
on the next line. Indentation that all lines share is removed,
measured against the least indented line, which includes the closing
""". Quotes inside need no escaping, which makes text
blocks ideal for letters, SQL and JSON.
void main() {
String customer = "Amara Okafor";
BigDecimal amount = new BigDecimal("127.05");
String letter = """
Dear %s,
Your bill for September is EUR %s.
Questions? Call us on 0800-1234.
Kestrel Energy
""".formatted(customer, amount);
IO.print(letter);
IO.println("--- end of letter");
}
Dear Amara Okafor,
Your bill for September is EUR 127.05.
Questions? Call us on 0800-1234.
Kestrel Energy
--- end of letter
The eight spaces in front of every line disappeared, while the four
extra spaces before "Kestrel Energy" stayed. Because the closing
""" is on its own line, the text ends with a line
break; that is why the example uses IO.print. A text
block is an ordinary String, and
.formatted(…) fills in its placeholders, as described
in the next section.
void main() {
String json = """
{"meter": "E-001", "kwh": 350}""";
IO.println(json);
String oneLine = """
Kestrel Energy \
customer service""";
IO.println(oneLine);
}
{"meter": "E-001", "kwh": 350}
Kestrel Energy customer service
With the closing """ right after the text there is no
final line break. A backslash at the end of a line joins it with the
next one, for long lines that you want to wrap in the source code
only.
Formatting with String.format
String.format(pattern, values…) builds a string from a
pattern with placeholders, called format specifiers. Since Java 15,
pattern.formatted(values…) does the same and often
reads better. System.out.printf prints directly.
void main() {
String name = "Amara";
int kwh = 342;
double cost = 92.3412;
IO.println(String.format("%s used %d kWh, cost %.2f", name, kwh, cost));
IO.println("%s used %d kWh".formatted(name, kwh));
IO.println("INC-%05d".formatted(42));
IO.println("[%8.2f] [%-8s] [%8s]".formatted(cost, name, name));
IO.println("%,d Wh".formatted(8_760_000_000L));
IO.println("%x %e %b %c %%".formatted(255, 12345.678, true, 'Z'));
}
Amara used 342 kWh, cost 92.34 Amara used 342 kWh INC-00042 [ 92.34] [Amara ] [ Amara] 8,760,000,000 Wh ff 1.234568e+04 true Z %
| Specifier | Meaning |
|---|---|
%s |
Any value, as text |
%d |
A whole number |
%.2f |
A floating-point number or BigDecimal with 2 decimals |
%05d |
Width 5, padded with zeros |
%8s, %-8s |
Width 8, right-aligned or left-aligned (-)
|
%,d |
With grouping separators |
%x, %e, %b,
%c
|
Hexadecimal, scientific notation, boolean, character |
%n, %% |
A line break, a percent sign |
A wrong specifier is not caught by the compiler:
%d with a double throws an
IllegalFormatConversionException when the line runs.
The rounding of %.2f also differs from what Python
users expect. It takes the shortest decimal form of the
double (the digits that Double.toString
prints, here 2.675) and rounds that half up:
void main() {
IO.println(String.format("%.2f", 0.125));
IO.println(String.format("%.2f", 2.675));
IO.println(String.format("%.1f", 0.25));
IO.println(Math.round(0.125 * 100) / 100.0);
}
0.13 2.68 0.3 0.13
Python prints 2.67 for f"{2.675:.2f}",
because it rounds the binary value that is stored, which is just
below 2.675. Java gives 2.68. For amounts, round a
BigDecimal with an explicit RoundingMode
before formatting (see
BigDecimal for money), so the rule is in your code, not in a formatting detail.
Locale-aware formatting
The comma in 8,760,000,000 above is not universal: in
the Netherlands it is 8.760.000.000. A
Locale is a Java object that
identifies a language and usually a region, such as
nl-NL (Dutch, the Netherlands) or en-US;
formatting methods use it to choose decimal and grouping separators,
currency symbols, and the names of months and days.
void main() {
double amount = 1234.5;
Locale dutch = Locale.forLanguageTag("nl-NL");
IO.println(String.format("%,.2f", amount));
IO.println(String.format(dutch, "%,.2f", amount));
IO.println(NumberFormat.getCurrencyInstance(dutch).format(amount));
IO.println(NumberFormat.getCurrencyInstance(Locale.US).format(amount));
IO.println(NumberFormat.getCurrencyInstance().format(amount));
}
1,234.50 1.234,50 € 1.234,50 $1,234.50 ¤1,234.50
Without a locale argument, Java uses the default locale of
the machine it runs on. The last line shows why that is risky: the
server that checks these examples has the default locale "English,
no country", so Java does not know which currency to use and prints
the generic currency sign ¤. On a Dutch laptop the same
line prints euros, on an American server dollars. The Dutch format
also puts a non-breaking space between the euro sign and the amount,
so a test that compares with a normal space fails.
Dates work the same way. LocalDate and
DateTimeFormatter come from
java.time (covered in
Files, I/O and JSON); patterns
use letters such as dd (day), MM (month),
yyyy (year), MMMM (month name) and
EEEE (day name):
void main() {
LocalDate due = LocalDate.of(2026, 10, 5);
Locale dutch = Locale.forLanguageTag("nl-NL");
IO.println(due);
IO.println(due.format(DateTimeFormatter.ofPattern("dd-MM-yyyy")));
IO.println(due.format(DateTimeFormatter.ofPattern("EEEE d MMMM yyyy", dutch)));
IO.println(due.format(DateTimeFormatter.ofPattern("EEEE d MMMM yyyy", Locale.UK)));
}
2026-10-05 05-10-2026 maandag 5 oktober 2026 Monday 5 October 2026
Unicode and characters
Java strings are sequences of
UTF-16
code units, and a char is one code unit. Most
characters, including accented letters, take one; characters outside
the Basic Multilingual Plane, such as emoji, take two (a
surrogate pair). length() counts code units,
not the characters a person sees:
void main() {
IO.println("Ångström".length());
IO.println("😀".length() + " " + "😀".codePointCount(0, "😀".length()));
IO.println("ß".toUpperCase());
IO.println("title".toUpperCase(Locale.forLanguageTag("tr")));
IO.println("TITLE".toLowerCase(Locale.ROOT));
}
8 2 1 SS TİTLE title
The emoji counts as 2 for length() but is one code
point. Changing case is language-dependent too: German
ß becomes two letters, and in Turkish the capital of
i is İ with a dot. Code that compares
keywords after toUpperCase() can therefore break on a
server with a Turkish default locale; pass
Locale.ROOT for such technical text. Since Java 18 the
default character encoding for files is UTF-8 on every
platform, which removed a long-standing source of garbled text (see
Character Encoding).