The previous lesson introduced what a String is and showed the handful of methods that answer "how big is this?" or "what character sits at this position?". This lesson goes wider. The String class ships with dozens of methods for searching inside text, slicing it, swapping characters out, splitting it on a delimiter, changing its case, and trimming whitespace. We'll work through the most useful ones with runnable examples drawn from a normal online store, and call out the spots where the method's behavior surprises beginners.
One thing to keep in mind. Every method on this page returns a new String. None of them changes the original. Treat the return value as the thing that holds your result.
The most basic question you can ask a string is "where does this substring start?". String has two methods for that. indexOf scans from left to right and returns the index of the first match. lastIndexOf scans from right to left and returns the index of the last match. Both return -1 when the target isn't present.
The -1 is the contract for "not found". Code that uses indexOf should always check for that before doing arithmetic on the result, because feeding -1 into substring or array indexing leads to confusing failures further down.
Both methods come in four overloads. The two we've used so far accept either a char or a String. The other two add a fromIndex parameter, which tells the search where to start.
The fromIndex form is useful when you want to walk through every occurrence of a character or substring. Start at 0, find the first one, then call the method again starting one position past where the previous match landed.
lastIndexOf takes a fromIndex too, but the meaning flips. It treats fromIndex as the rightmost position the search is allowed to look at and scans backward from there.
A common use of lastIndexOf is pulling out a file extension or the trailing segment of any string.
substringsubstring carves a piece out of a string and hands you back a new String containing just that piece. It comes in two forms.
substring(beginIndex) returns everything from beginIndex to the end.substring(beginIndex, endIndex) returns everything from beginIndex (inclusive) up to endIndex (exclusive).The half-open range is a common source of off-by-one bugs. The character at endIndex isn't in the result. The result's length is endIndex - beginIndex.
For the year, 5 is the position of the digit 2 and 9 is the position of the dash that follows the year. The character at 9 isn't included, which is what we want when "year" means four characters.
substring pairs well with indexOf and lastIndexOf when you need to pick a piece out of a string whose exact layout you don't know in advance. Pulling the file extension off a name is the classic case.
The dot + 1 skips the dot itself, which is what most callers want. Off-by-one mistakes around substring come in two flavors: forgetting that endIndex is exclusive (and slicing one character short), and forgetting to add 1 after a delimiter (and including the delimiter in the result). Reading the boundaries carefully each time is the only fix.
If you ask for indices that don't make sense, substring throws StringIndexOutOfBoundsException immediately.
The valid range is 0 <= beginIndex <= endIndex <= length(). Calling s.substring(0, s.length()) is legal and returns a string equal to s. Calling s.substring(s.length()) is also legal and returns an empty string. Going past length() isn't.
substring allocates a new String and copies the selected characters into it. Slicing the same region many times in a loop creates many short-lived objects. If you're carving up the same source string repeatedly, slice once and reuse the result.
String has a small family of replacement methods, and the differences between them matter. The two safe ones do straight literal replacement. The two regex-aware ones can fail unexpectedly if you don't know they treat their first argument as a regular expression.
The literal versions are replace(char, char) and replace(CharSequence, CharSequence). Both swap every occurrence of the first argument with the second, with no regex interpretation. CharSequence is a supertype that String implements, so passing two strings works as expected.
Both calls produce new strings. address itself is unchanged. If you want to replace several patterns, chain the calls or assign the intermediate results to a variable.
The regex-aware versions are replaceAll(String, String) and replaceFirst(String, String). Both treat the first argument as a regular expression pattern. replaceAll replaces every match. replaceFirst replaces only the leftmost one.
Because the first argument is a regex, characters with special regex meaning (., *, +, ?, (, ), [, ], {, }, |, ^, $, \) don't match themselves literally. The . is the most common source of confusion.
What's wrong with this code?
The caller wanted 29,99. They got ,,,,, instead. In a regex, . matches any character, so every position in the string is a match and gets replaced. The fix is to either escape the dot or use the literal replace method.
Fix:
For now, the rule of thumb is: if you're replacing a fixed string, use replace first. Only use replaceAll or replaceFirst when a pattern is required.
split(String regex) takes a delimiter and breaks the string into an array of pieces. Wherever the delimiter matches, the string is cut, and the parts between cuts become elements of the result.
The argument to split is a regex, the same as with replaceAll. A single literal character like , happens to have no special regex meaning, so it works as written. If you want to split on a character that does have regex meaning, like . or |, you've to escape it in the pattern.
The pattern "\\." is a two-character Java string that contains the two characters backslash and dot. In regex, \. means "a literal dot". The double backslash is how Java spells one backslash inside a string literal.
split can also drop trailing empty strings from the result, which is a common gotcha.
The trailing empty fields after bob are removed without warning. If you need them, the two-argument form split(regex, limit) with a negative limit keeps every empty field.
Every call to split compiles the regex pattern from scratch. If you split the same delimiter many times in a hot loop, pre-compile a Pattern once and reuse it.
toUpperCase() and toLowerCase() return new strings with every letter converted to the matching case. Non-letter characters pass through unchanged.
The third line prints the original because, again, neither method changes the source. Both have a variant that takes a Locale, which controls how the case rules are applied for languages where uppercase is more than a simple character swap. The no-argument versions use the JVM's default locale. For e-commerce search and comparison, normalizing to lowercase before comparing is a common pattern, and a locale-aware call (toLowerCase(Locale.ROOT)) avoids problems on systems with unexpected defaults.
When text comes from a form, a CSV file, or a copy-paste, it usually arrives with stray whitespace at the ends. trim and strip both clean that up. They differ in what they consider whitespace.
trim() is the old method. It removes any leading or trailing character whose code point is at or below U+0020, which covers the ASCII space and the usual control characters but not Unicode whitespace.
strip(), added in Java 11, removes any character that Character.isWhitespace considers whitespace. That includes a wide range of Unicode whitespace characters that trim would leave alone.
trim left the EM SPACE characters in place because their code point (0x2003) is above 0x20. strip removed them because they count as Unicode whitespace.
For plain ASCII input, the two methods behave the same. For anything that might include Unicode whitespace, prefer strip. New code should use strip by default.
Java 11 also added stripLeading() and stripTrailing() for one-sided trims.
These are useful when you care about whitespace on one side but not the other, like keeping indentation on the left but cleaning trailing spaces on the right.
Often you don't need the exact position of a substring. You want to know whether the string includes it, or starts or ends with it. Four boolean methods cover those cases.
contains(CharSequence target) returns true if target appears anywhere in the string.startsWith(String prefix) returns true if the string begins with prefix.startsWith(String prefix, int offset) returns true if the string has prefix at position offset.endsWith(String suffix) returns true if the string finishes with suffix.contains is the easiest way to test for membership. It's roughly the same as indexOf(target) != -1, but reads more clearly when position is irrelevant.
The two-argument startsWith is helpful when checking for a prefix at a position other than the beginning.
You could write the same check with substring(4, 8).equals("2024"), but that allocates a temporary string just to throw it away. The startsWith(prefix, offset) form does the comparison in place without the extra allocation.
startsWith(prefix, offset) avoids the temporary String that substring(...).equals(...) would create. Tiny gain on a single call, but it shows up when filtering thousands of strings.
All three of these methods are case-sensitive. If you want a case-insensitive check, normalize both sides first.
There's no containsIgnoreCase in the String API. Lowercasing both sides is the standard workaround.
Java 11 added repeat(int count), which returns a string consisting of the original repeated count times. It's the simplest method on this page.
repeat(0) returns the empty string. A negative count throws IllegalArgumentException. Before Java 11, the same result took a loop with StringBuilder, which we cover in lesson 05.
This is useful any time you need a divider or a column of padding without typing out the characters by hand.
The reverse of split is String.join. It takes a delimiter and a sequence of pieces, then concatenates the pieces with the delimiter between each pair. There are two forms. One takes a varargs of CharSequence (so any number of strings as arguments), and the other takes an Iterable<? extends CharSequence> (so a List, a Set, or anything else you can loop over).
The iterable form is the more common choice, because in practice the pieces are usually already in a collection.
The delimiter goes between elements only, never at the start or the end. An empty iterable produces an empty string. A one-element iterable produces just that element with no delimiter.
String.join is cleaner than building the same result with a loop and +=, and it's faster too, because it allocates one final string instead of one per concatenation.
chars()chars() returns an IntStream of the char values (each cast to int) in the string. We haven't covered streams yet, so we won't go deep, but one quick use is counting characters that match a condition.
For each character in orderRef, chars() produces its int value. filter(Character::isDigit) keeps only the ones that are digits. count() returns how many made it through. The result is the number of digits in the string.
For now, treat chars() as a way to look at every character in the string with a one-line stream pipeline. You can also walk a string with a for loop over length() and charAt(i).
The methods we covered split into a small number of categories based on what kind of work they do.
The split between "searching" methods that return positions or booleans and "modifying" methods that return new strings is the main distinction. When the return type of a method is unclear, the next table answers that question for the ones we covered.
| Method | Return type | What it does |
|---|---|---|
indexOf(int / String) | int | Position of first match, or -1 |
indexOf(int / String, int) | int | First match at or after the given index |
lastIndexOf(int / String) | int | Position of last match, or -1 |
lastIndexOf(int / String, int) | int | Last match at or before the given index |
substring(int) | String | From begin to the end |
substring(int, int) | String | From begin (inclusive) to end (exclusive) |
replace(char, char) | String | Replace every matching char, literal |
replace(CharSequence, CharSequence) | String | Replace every matching substring, literal |
replaceAll(String, String) | String | Replace every regex match |
replaceFirst(String, String) | String | Replace the first regex match |
split(String) | String[] | Split on a regex delimiter |
toUpperCase() | String | All letters to upper case |
toLowerCase() | String | All letters to lower case |
trim() | String | Strip ASCII whitespace from both ends |
strip() | String | Strip Unicode whitespace from both ends |
stripLeading() | String | Strip Unicode whitespace from the left |
stripTrailing() | String | Strip Unicode whitespace from the right |
contains(CharSequence) | boolean | Whether the substring appears |
startsWith(String) | boolean | Whether the string starts with the prefix |
startsWith(String, int) | boolean | Whether the prefix appears at the offset |
endsWith(String) | boolean | Whether the string ends with the suffix |
repeat(int) | String | Repeat the string the given number of times |
chars() | IntStream | Stream of character code points |
String.join(CharSequence, ...) | String | Concatenate pieces with a delimiter |
The static method String.join is at the bottom because it's not called on an instance like the others.
10 quizzes