JAVA / STRINGS AND TEXT HANDLING
Slicing intent with substring and splitting text
Slice Java strings at exact positions with substring and split records into fields, controlling regex delimiters and the limit that drops empty fields.
What you will learn
- Read substring(a, b) as the characters from a to b-1, so its length is b - a
- Cut on a delimiter with substring(0, i) and substring(i + delimiter.length())
- Escape regex metacharacters like . | + ( in split, or use Pattern.quote
- Pass -1 to split to keep trailing empty fields and n to keep the last field whole
Understanding Slicing intent with substring and splitting text
An index in Java does not point at a character, it points at the gap before one. That is why substring(begin, end) returns what lies between those two gaps, with end excluded, and why the result always has exactly end - begin characters. So begin == end is a legal request for zero characters, and begin == s.length() is legal too because the gap after the last character exists; only going past that gap, or asking for end < begin, throws StringIndexOutOfBoundsException. The payoff of this half-open convention is that s.substring(0, k) + s.substring(k) rebuilds s for every valid k, with no overlap and no dropped character.
Real slicing starts by locating the cut, and indexOf or lastIndexOf give you the index of the delimiter's first character. That index is already the exclusive end of the left field, so substring(0, i) needs no adjustment, while the right field has to step over the whole delimiter with substring(i + delimiter.length()). Two failures follow from this directly: indexOf returns -1 when the delimiter is absent, and that -1 either blows up as substring(0, -1) or, worse, becomes substring(0) and silently hands back the entire string. Check for -1 before slicing, and reach for lastIndexOf when the delimiter can repeat but only the final one marks the boundary, as with a file extension.
split covers the many-fields case but switches languages on you: its first argument is a regular expression, not a literal, so "." matches any character and "|" matches the empty position between characters. Escape metacharacters as "\\." or "\\|", or wrap the delimiter in Pattern.quote when it comes from data you did not write. The second argument, the limit, decides what happens at the edges: 0 (the default) discards trailing empty fields, a negative number keeps every field including the empty ones, and a positive n stops after n - 1 cuts so the last element keeps the remainder with its delimiters intact. That last form is how you split a log line into date, level and a message that itself contains spaces.
import java.util.Arrays;
public class Slicing {
public static void main(String[] args) {
String line = "timeout=30";
int eq = line.indexOf('=');
String key = line.substring(0, eq);
String value = line.substring(eq + 1);
System.out.println("key=[" + key + "] value=[" + value + "]");
System.out.println("end " + eq + " gave " + key.length() + " chars");
System.out.println("tail=[" + line.substring(line.length()) + "]");
String csv = "a,b,,c,,";
System.out.println("default: " + Arrays.toString(csv.split(",")));
System.out.println("keep all: " + Arrays.toString(csv.split(",", -1)));
System.out.println("limit 2: " + Arrays.toString(csv.split(",", 2)));
}
}A slice is described by the gaps between characters, so the end index is exclusive and every boundary you compute must account for the length of the delimiter you cut on.
Worked examples
The delimiter is a pattern
Shows why splitting a filename on "." returns nothing and how lastIndexOf slices off an extension instead.
import java.util.Arrays;
public class DotSplit {
public static void main(String[] args) {
String file = "report.2026.csv";
System.out.println(file.split(".").length);
System.out.println(Arrays.toString(file.split("\\.")));
int dot = file.lastIndexOf('.');
System.out.println(file.substring(0, dot) + " | " + file.substring(dot + 1));
}
}Example explained
Line 1split(".") reads the dot as the regex for any character, so every position is a delimiter, every field is empty, and the default limit strips them all away, leaving a zero-length array.
Line 2"\\." is a Java string holding backslash plus dot, which the regex engine reads as a literal period, producing the three fields you expected.
Line 3lastIndexOf('.') returns 11, the final dot, so substring(0, 11) keeps the internal dot in "report.2026" and substring(12) starts just past the delimiter.
Legal edges and illegal ranges
Demonstrates which substring boundaries produce an empty string and which ones throw.
public class Bounds {
public static void main(String[] args) {
String word = "stream";
System.out.println("[" + word.substring(2, 2) + "]");
System.out.println("[" + word.substring(6) + "]");
try {
System.out.println(word.substring(3, 2));
} catch (RuntimeException e) {
System.out.println("threw " + e.getClass().getSimpleName());
}
int max = 4;
System.out.println(word.substring(0, Math.min(max, word.length())));
}
}Example explained
Line 1substring(2, 2) asks for the characters between one gap and itself, which is zero characters, so it returns "" rather than failing.
Line 2substring(6) on a six-character string starts at the gap after the last character, the last valid position, so it also yields "".
Line 3substring(3, 2) has end before begin, which describes no range at all, so the bounds check throws StringIndexOutOfBoundsException.
Line 4Math.min clamps the exclusive end to the length, which is the standard way to take at most n characters without checking the length yourself.
Keeping the last field whole
Uses a positive limit to protect a message that contains the delimiter, and contrasts a single space with a whitespace run.
import java.util.Arrays;
public class LogSplit {
public static void main(String[] args) {
String log = "2026-09-03 WARN disk usage: 91% at /var, retry: yes";
System.out.println(log.split(" ").length);
String[] parts = log.split(" ", 3);
System.out.println(parts.length + " " + parts[0] + " | " + parts[1] + " | " + parts[2]);
String spaced = "a b";
System.out.println(Arrays.toString(spaced.split(" ")));
System.out.println(Arrays.toString(spaced.split("\\s+")));
}
}Example explained
Line 1split(" ") cuts at all eight spaces, including the ones inside the message, giving nine fields you would then have to glue back together.
Line 2split(" ", 3) applies the pattern at most twice, so parts[2] is the untouched remainder of the line with its spaces intact.
Line 3In "a b" the pattern matches twice in a row, and the text between those two matches is an empty field, which is why three elements come back.
Line 4"\\s+" treats a run of whitespace as one delimiter, which is what human-spaced or column-aligned input needs.
Important notes
substring copies the characters into a fresh String, so slicing repeatedly while scanning a long string costs time proportional to each slice; carry indices around and cut once when you are done.
Indices count UTF-16 code units, so cutting at an arbitrary index in text containing an emoji or other supplementary character can split a surrogate pair and leave half a character; use codePointAt and offsetByCodePoints when such input is possible.
Common mistakes
Reading the second argument as a count or as an inclusive index: "timeout".substring(0, 3) is "tim", not "time", and substring(0, s.length() - 1) quietly loses the final character, so the bug only shows up at the edges of the data.
Passing a literal delimiter straight to split: split(".") returns a zero-length array because the dot matches everything, and split("|") cuts between every character, so parts[0] is one letter instead of the first field and the damage appears later as wrong values or an ArrayIndexOutOfBoundsException.
Trusting the default limit on fixed-shape records: "id,name,,".split(",") returns 2 elements because trailing empty fields are dropped, so reading parts[3] throws ArrayIndexOutOfBoundsException; pass -1 when a missing value at the end still counts as a field.
Try it yourself
Change, predict, then run
Given String path = "/usr/local/share/doc.txt", use only indexOf, lastIndexOf and substring to print the directory, the file name, the base name and the extension, each wrapped in square brackets, and check that directory + "/" + fileName equals path. Then print Arrays.toString(path.split("/", -1)) and work out why the first element is empty.
Open the Java workspaceCheck your understanding
A record uses the two-character delimiter "::" and you have computed int i = rec.indexOf("::"). Which pair of slices gives the left and right field with no stray colons?
- rec.substring(0, i) and rec.substring(i + 2)
- rec.substring(0, i) and rec.substring(i + 1)
- rec.substring(0, i - 1) and rec.substring(i + 2)
- rec.substring(0, i + 1) and rec.substring(i + 2)
Show answer
indexOf returns the position of the delimiter's first character, and substring's end index is exclusive, so substring(0, i) already stops before both colons; the right field must start after the whole delimiter, at i + "::".length(), which is i + 2. The i + 1 option is tempting because +1 is correct for a single-character delimiter, but here it leaves a leading ':' on the second field, and i - 1 would also chop the last character of the first field.