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).