Java String Methods: substring, split, replace and indexOf
The Java String methods that matter and their traps: substring's exclusive end, split dropping trailing empty strings, and replace versus regex replaceAll.
- Course: Java study plan
- Module: Strings
- Kind: Lesson
- Reading time: 16 min
- Runtime: OpenJDK 18
How does substring work in Java?
substring(begin, end) in Java returns a new string from index begin up to but not including index end, so its length is end - begin: "Hello, World".substring(0, 5) is "Hello". With one argument it runs to the end of the string. A negative index, begin greater than end, or end past the length throws StringIndexOutOfBoundsException.
Lesson
String has around seventy methods. Perhaps twenty carry ninety percent of real code, and each has a detail — an exclusive end index, a regex where you expected a literal, a trailing-empty-string rule — that separates "I know the method" from "I know what it does". This lesson is those twenty, with the details.
Substrings and searching
String s = "Hello, World";
s.substring(7) // "World" — from index 7 to the end
s.substring(0, 5) // "Hello" — begin inclusive, END EXCLUSIVE
s.substring(5, 5) // "" — legal: empty
s.substring(7, 5) // StringIndexOutOfBoundsException — begin > end
s.substring(0, 20) // exception — end past length
s.indexOf('o') // 4 — first occurrence
s.indexOf('o', 5) // 8 — search from index 5
s.indexOf("World") // 7
s.indexOf("xyz") // -1 — not found: -1, never an exception
s.lastIndexOf('o') // 8
s.contains("lo, W") // true
s.startsWith("Hell") // true
s.startsWith("World", 7) // true — with an offset
s.endsWith("ld") // true
s.charAt(s.length() - 1) // 'd' — the last character
The half-open interval [begin, end) is Java's convention everywhere (arrays, lists, streams), and it makes end - begin the length. Pair it with indexOf:
int at = s.indexOf(',');
String first = s.substring(0, at); // "Hello"
String rest = s.substring(at + 1).trim(); // "World"
Case, whitespace, and cleaning
" padded \t\n".trim() // "padded" — removes chars ≤ U+0020 at both ends
" padded ".strip() // "padded" — Unicode-aware (Java 11+); also stripLeading, stripTrailing
"Hello".toUpperCase() // "HELLO"
"Hello".toLowerCase() // "hello"
"hello".isBlank() // false; " ".isBlank() is true
"ab".repeat(3) // "ababab"
toUpperCase()/toLowerCase() use the default locale — in Turkish, "i".toUpperCase() is İ. For programmatic text (keywords, identifiers, file extensions) pass Locale.ROOT: s.toLowerCase(Locale.ROOT). Static analysers flag the no-argument form for this reason.
Replacing
"a.b.c".replace('.', '-') // "a-b-c" — char, all occurrences
"a.b.c".replace(".", "-") // "a-b-c" — LITERAL CharSequence, all occurrences
"a.b.c".replaceAll(".", "-") // "-----" — REGEX: . matches any character!
"a.b.c".replaceAll("\\.", "-") // "a-b-c" — escaped regex
"a.b.c".replaceFirst("\\.", "-") // "a-b.c"
The trap is in the names: replace is literal, replaceAll is a regular expression. Use replace unless you need a pattern. When you do need a pattern with a literal that has special characters, Pattern.quote(".") produces the escaped form.
Splitting and joining
"a,b,c".split(",") // ["a", "b", "c"]
"a,b,,c,,".split(",") // ["a", "b", "", "c"] — trailing empties DROPPED
"a,b,,c,,".split(",", -1) // ["a", "b", "", "c", "", ""] — limit -1 keeps them
"a,b,c".split(",", 2) // ["a", "b,c"] — at most 2 pieces
"one two three".split(" ") // ["one", "", "two", "", "", "three"] — every single space
"one two three".split("\\s+") // ["one", "two", "three"] — runs of whitespace
" lead".split("\\s+") // ["", "lead"] — a leading empty string IS kept
"a.b".split(".") // [] — "." is a regex matching everything; every piece is empty and dropped
"a.b".split("\\.") // ["a", "b"]
"".split(",") // [""] — one empty string, not an empty array
String.join("-", "a", "b", "c") // "a-b-c"
String.join(", ", List.of("x", "y")) // "x, y"
split takes a regex, drops trailing empty strings (unless the limit is negative), and keeps leading ones. The safe form for whitespace-separated input is line.trim().split("\\s+"). For a single literal character that is not a regex metacharacter (,, ;, | is a metacharacter!, -), plain split(",") is fine; for ., |, +, *, ?, (, [, \, ^, $ escape with \\.
Conversion to and from other types
String.valueOf(3.5) // "3.5" — overloaded for every primitive, and Object (null-safe → "null")
String.valueOf(chars) // from char[]
Integer.parseInt("42") // 42; NumberFormatException for "42 ", "4.2", "", null
Integer.parseInt("ff", 16) // 255
Double.parseDouble("1e3") // 1000.0
Boolean.parseBoolean("yes") // false — only "true" (any case) is true
s.toCharArray() // char[] copy
s.chars() // IntStream of UTF-16 code units (Module 15)
s.getBytes(StandardCharsets.UTF_8) // byte[] — always name the charset
Comparison helpers (recap)
equals, equalsIgnoreCase, compareTo, compareToIgnoreCase, contentEquals, regionMatches, matches. Covered in the previous lesson.
Newer additions worth knowing
| Method | Since | What |
|---|---|---|
isBlank(), strip(), stripLeading(), stripTrailing() | 11 | Unicode whitespace handling |
repeat(n) | 11 | Repetition |
lines() | 11 | Stream of lines, splitting on \n, \r, \r\n |
chars(), codePoints() | 9 | Streams of characters |
formatted(args…) | 15 | "%d items".formatted(3) — instance form of String.format |
indent(n) | 12 | Adds/removes leading spaces on every line |
translateEscapes() | 15 | Interprets \n, \t in the string's content |
Text blocks """…""" | 15 | Multi-line literals (next lessons) |
Performance notes
substringcopies (since Java 7u6).s.substring(i)in a loop overiis O(n²).indexOf/containsare linear; for many searches in a large text, consider one pass with aMatcheror your own scan.splitcompiles a regex on every call for patterns longer than one non-meta character; in a hot loop, precompile withPattern.compile(...).split(s).+in a single expression is fine;+=in a loop is not.
Common exceptions
StringIndexOutOfBoundsException—charAt/substringwith a bad index. Message:begin 0, end 20, length 12.NullPointerException— calling anything on anullreference.NumberFormatException— parsing non-numeric text. Message:For input string: "4.2".PatternSyntaxException— a broken regex insplit/replaceAll/matches, e.g.split("(").
Interview angle
- "
"a,b,,".split(",")length?" 2 — trailing empties are dropped. - "
replacevsreplaceAll?" Literal vs regex; both replace every occurrence. - "Is
substringO(1)?" Not since Java 7u6 — it copies. - "How do you reverse a string?"
new StringBuilder(s).reverse().toString()(next lesson), or a two-pointer loop over achar[].
Key takeaways
substring(begin, end)— end is exclusive;indexOfreturns −1, never throws.replaceis literal;replaceAll/split/matchestake regexes — escape.|+*?.splitdrops trailing empty strings;trim().split("\\s+")for whitespace.- Pass
Locale.ROOTto case conversions in programmatic text. - Every method returns a new string — capture the result.
Common questions
What is the difference between replace and replaceAll in Java?
Both replace every occurrence. replace treats its arguments as literal text, while replaceAll treats the first as a regular expression, so "a.b.c".replaceAll(".", "-") gives ----- because . matches any character. Use replace unless you need a pattern.
Why does split drop empty strings at the end in Java?
String.split removes trailing empty strings by default, so "a,b,,".split(",") has length 2. Pass a negative limit, split(",", -1), to keep them; leading empty strings are always kept.
Why does "a.b".split(".") return an empty array?
split takes a regular expression, and . matches every character, so every piece between the matches is empty and trailing empty strings are dropped. Escape the dot with a backslash in the regex, or pass Pattern.quote("."), to split on a literal dot.
What is the difference between trim and strip in Java?
trim() removes characters with codes up to U+0020, which covers ASCII spaces and control characters, from both ends. strip(), added in Java 11, is Unicode-aware and also removes other whitespace; stripLeading and stripTrailing work on one end only.
Is substring O(1) in Java?
No. Since Java 7 update 6, substring copies the characters into a new string, so it takes time proportional to the length of the result, and calling s.substring(i) in a loop over i is quadratic. Earlier versions shared the original's backing array.
Exercises
Email parts
Read an integer n, then n email addresses, one per line. For each, print user=<user> domain=<domain> tld=<tld> where user is everything before the @, domain everything after it, and tld the part of the domain after its last dot. If the line has no @, or the domain has no dot, print invalid.
Use indexOf, lastIndexOf and substring. Trim each line first.
Example: ada@mail.example.com → user=ada domain=mail.example.com tld=com
Slug maker
Read lines until end of input; each is a title. Turn it into a URL slug: trim, lower-case (Locale.ROOT), replace every run of characters that are not a-z or 0-9 with a single -, and strip any leading or trailing -. Print slug=<slug> per line, or slug=(empty) when nothing is left.
Example input
Hello, World!
Java 17: What's New?
→
slug=hello-world
slug=java-17-what-s-newIn this module: Strings
← Comparing strings — equals, compareTo and hashCode · StringBuilder — building strings efficiently →