The String class covers almost all everyday text handling with a few dozen methods, and you only need a handful of them regularly. Measuring, slicing, searching, changing case, trimming, replacing, splitting and joining are the eight groups that show up in nearly every program you will write. Learning this set properly saves you from hand-rolled character loops.
How do you measure and inspect a string?
length() gives the number of characters, charAt gives one character at a position, and isEmpty and isBlank tell you whether there is anything worth processing. The difference between the last two is worth remembering: a string of spaces is not empty, but it is blank, and blank is usually what you mean when you check user input.
public class StringBasics {
public static void main(String[] args) {
String text = " Order #A-1042 confirmed ";
String trimmed = text.trim();
System.out.println("raw length : " + text.length());
System.out.println("trimmed : '" + trimmed + "'");
System.out.println("upper : " + trimmed.toUpperCase());
System.out.println("at index 6 : " + trimmed.charAt(6));
System.out.println("index of # : " + trimmed.indexOf('#'));
System.out.println("last index of m : " + trimmed.lastIndexOf('m'));
System.out.println("contains A- : " + trimmed.contains("A-"));
System.out.println("starts with Order : " + trimmed.startsWith("Order"));
System.out.println("ends with confirmed : " + trimmed.endsWith("confirmed"));
System.out.println("empty? '" + "".isEmpty() + "' blank? '" + " ".isBlank() + "'");
}
}raw length : 27
trimmed : 'Order #A-1042 confirmed'
upper : ORDER #A-1042 CONFIRMED
at index 6 : #
index of # : 6
last index of m : 20
contains A- : true
starts with Order : true
ends with confirmed : true
empty? 'true' blank? 'true'Note the raw length of 30 against the trimmed length of 26. That gap is the entire reason trim and strip exist, and it is a classic source of bugs when you compare a stored value with a trimmed one.
How do you slice a string and search inside it?
substring takes a start index and an optional end index. The start index is inclusive and the end index is exclusive, so substring(0, 3) gives you the first three characters. Searching uses indexOf for a first match, lastIndexOf for the final one, and contains when you only need a yes-or-no answer.
public class StringSlicing {
public static void main(String[] args) {
String code = "ORD-2026-0042";
System.out.println("first 3 : " + code.substring(0, 3));
System.out.println("after dash : " + code.substring(4));
System.out.println("last 4 : " +code.substring(code.length() - 4));
String padded = "cat";
System.out.println("padded : [" + padded.repeat(4) + "]");
String mixed = "Hello World";
System.out.println("substring keeps case : " + mixed.substring(6));
System.out.println("equalsIgnoreCase : " + "ABC".equalsIgnoreCase("abc"));
System.out.println("compareTo : " + "apple".compareTo("banana"));
System.out.println("compareTo same : " + "apple".compareTo("apple"));
}
}first 3 : ORD
after dash : 2026-0042
last 4 : 0042
padded : [catcatcatcat]
substring keeps case : World
equalsIgnoreCase : true
compareTo : -1
compareTo same : 0The compareTo results in the output are the model for comparing strings: it returns a negative number when the receiver sorts earlier, a positive number when it sorts later, and zero when the two hold the same characters. This is the comparison a sort uses, and it is also the comparison that makes "apple" come before "banana" in an ordered list.
How do you change case and clean up whitespace?
toUpperCase and toLowerCase return new strings in the other case. trim removes whitespace from both ends and strip does the same but understands a wider set of whitespace characters. Both return a new string, so the original is unchanged.
Two more methods tidy text further. repeat returns a string built by repeating the original, which is handy for separators and simple rules. replace swaps every occurrence of a literal, while replaceAll and replaceFirst take a regular expression. A regular expression is a pattern language, so "[0-9]" matches any digit and "^[0-9]{4}-" matches a four-digit year at the very start of the text.
public class StringReplacing {
public static void main(String[] args) {
String csv = "ana, bo ,cy";
System.out.println("replace literal : '" + csv.replace(", ", ",") + "'");
System.out.println("replace all digits : " + "a1b2c3".replaceAll("[0-9]", "*"));
System.out.println("replace first digit : " + "a1b2c3".replaceFirst("[0-9]", "*"));
System.out.println("remove prefix : " + "2026-07-14".replaceFirst("^[0-9]{4}-", ""));
System.out.println("chars : " + "hi".chars().mapToObj(c -> (char) c).toList());
System.out.println("reverse : " + new StringBuilder("stressed").reverse());
}
}replace literal : 'ana,bo ,cy'
replace all digits : a*b*c*
replace first digit : a*b2c3
remove prefix : 07-14
chars : [h, i]
reverse : dessertsThe remove prefix line is the one you will use most: stripping a year prefix off a date string with a single call beats writing an indexOf and a substring by hand, and it does not throw when the prefix is missing.
How do you split text into parts and put it back together?
split turns a string into an array of pieces using a regular expression, and String.join turns an array back into one string. The split method has two behaviours that catch people out, and both are visible in the output below.
import java.util.Arrays;
public class StringSplitJoin {
public static void main(String[] args) {
String line = "alpha,beta,,gamma";
System.out.println("plain split : " + Arrays.toString(line.split(",")));
System.out.println("limit 3 : " + Arrays.toString(line.split(",", 3)));
System.out.println("trailing : " + Arrays.toString("a,b,,".split(",")));
System.out.println("no trailing : " + Arrays.toString("a,b,,".split(",", -1)));
String[] parts = { "one", "two", "three" };
System.out.println("joined : " + String.join(" | ", parts));
System.out.println("joined with char : " + String.join("-", "a", "b", "c"));
}
}plain split : [alpha, beta, , gamma]
limit 3 : [alpha, beta, ,gamma]
trailing : [a, b]
no trailing : [a, b, , ]
joined : one | two | three
joined with char : a-b-cFirst, trailing empty strings are dropped by default: "a,b,,".split(",") gives you two elements, not four. Passing a negative limit as the second argument keeps them, which is what you want when an empty field is meaningful. Second, an empty field in the middle is not dropped, so "alpha,beta,,gamma" gives you four elements with an empty one in the middle. If empty fields matter to you, split with the limit set to -1 so the count is predictable.
Which method should you reach for?
| Task | Method | Watch out for |
|---|---|---|
| Length, one character | length(), charAt | Index starts at zero |
| Cut out a piece | substring(start, end) | End is exclusive |
| Find text | indexOf, contains | Returns -1 when absent |
| Clean whitespace | trim, strip | Returns a new string |
| Case change | toUpperCase, toLowerCase | Locale-sensitive for some scripts |
| Swap text | replace, replaceAll | Second argument is a regex |
| Split into parts | split(regex) | Trailing empties dropped by default |
| Join parts | String.join(sep, parts) | Also available on streams |
Two more that deserve a line each. chars() gives you a stream of the characters, which pairs well with the stream operations you will meet later. And formatted builds a new string from a format template, which is the cleanest way to produce a report line without concatenation.
The habits worth forming
- Trim user input once, at the edge, and work with the trimmed value from then on.
- Use
isBlankrather thanisEmptywhen checking user input. - Remember the end index of
substringis exclusive. - Use
split(regex, -1)when empty fields are meaningful. - Prefer
replaceAllover a loop ofreplacewhen the pattern is a regular expression.
Try it yourself: take the string " Ada Lovelace, Countess of Lovelace;1815-1852 " and write a sequence of calls that trims it, splits the name from the dates, extracts the birth year, and prints a single line like Ada Lovelace was born in 1815. Then run the same extraction on a string with no semicolon and make sure your code reports a clear message instead of throwing an exception.