String Search
Searching a string means finding whether some text appears in it, and usually where. Java's String class provides methods for every common case: checking for presence (contains), checking the beginning or end (startsWith, endsWith), finding positions (indexOf, lastIndexOf), and testing patterns (matches). Combined with substring, these methods are the basic tools for parsing log lines, file paths, URLs, and any text with a known structure.
Checking Whether Text Is Present
String line = "2026-11-11 10:05:42 ERROR Payment failed: timeout";
line.contains("ERROR"); // true
line.contains("error"); // false: searches are case-sensitive
line.startsWith("2026-11-11"); // true
line.endsWith("timeout"); // true
line.startsWith("ERROR", 20); // true: check for a prefix at a given index
| Method | Returns true when |
|---|---|
contains(s) | s appears anywhere |
startsWith(prefix) | The string begins with prefix |
startsWith(prefix, offset) | prefix appears starting exactly at offset |
endsWith(suffix) | The string ends with suffix |
isEmpty() / isBlank() | Nothing to search |
Every string contains the empty string, so s.contains("") is always true. Passing null to these methods throws NullPointerException.
Case-insensitive presence checks
There is no containsIgnoreCase. Convert both sides to the same case first:
boolean found = text.toLowerCase(Locale.ROOT).contains(term.toLowerCase(Locale.ROOT));
Locale.ROOT makes the conversion behave the same on every machine. For prefix or suffix checks, regionMatches(true, ...) compares without creating new strings:
String file = "REPORT.PDF";
boolean isPdf = file.regionMatches(true, file.length() - 4, ".pdf", 0, 4); // true
Finding Positions: indexOf and lastIndexOf
indexOf returns the index where text first appears, or -1 if it does not appear:
String s = "banana";
s.indexOf("an"); // 1
s.indexOf("an", 2); // 3: start searching from index 2
s.lastIndexOf("an"); // 3: last occurrence
s.indexOf("x"); // -1: not found
s.indexOf('n'); // 2: a char works too
| Method | Searches |
|---|---|
indexOf(str) / indexOf(ch) | From the start |
indexOf(str, fromIndex) | From fromIndex forward |
lastIndexOf(str) | From the end backward |
lastIndexOf(str, fromIndex) | Backward, starting at fromIndex |
Since Java 21, indexOf(str, beginIndex, endIndex) limits the search to a range.
Always check for -1 before using the result. Passing -1 to substring or charAt throws StringIndexOutOfBoundsException:
int at = email.indexOf('@');
if (at == -1) {
throw new IllegalArgumentException("Not an email: " + email);
}
String domain = email.substring(at + 1);
Finding every occurrence
Call indexOf repeatedly, starting each search just after the previous match:
static int countOccurrences(String text, String word) {
int count = 0;
int from = 0;
while ((from = text.indexOf(word, from)) != -1) {
count++;
from += word.length(); // move past this match
}
return count;
}
Advancing by word.length() counts non-overlapping matches. Advancing by 1 instead counts overlapping ones: in "aaa", "aa" occurs once without overlap but twice with overlap ("aaa".indexOf("aa", 1) is 1).
Extracting Text Around a Match
Search methods are most useful combined with substring. A common pattern extracts the text between two markers:
static String between(String text, String start, String end) {
int s = text.indexOf(start);
if (s == -1) return null;
s += start.length(); // skip past the start marker
int e = text.indexOf(end, s); // search for the end marker after it
return e == -1 ? null : text.substring(s, e);
}
between("[user=ada] Login", "[user=", "]"); // "ada"
Splitting a path or file name at its last separator:
String path = "/var/log/app/server.log";
String fileName = path.substring(path.lastIndexOf('/') + 1); // "server.log"
String dir = path.substring(0, path.lastIndexOf('/')); // "/var/log/app"
int dot = fileName.lastIndexOf('.');
String ext = dot == -1 ? "" : fileName.substring(dot + 1); // "log"
lastIndexOf is the right choice for extensions, because names like archive.tar.gz contain more than one dot. (For real file paths, java.nio.file.Path provides getFileName() and getParent().)
Pattern Searches: matches and Regular Expressions
matches(regex) tests whether the entire string matches a regular expression:
"ERROR".matches("ERR.*"); // true
"600042".matches("\\d{6}"); // true: exactly six digits
"order 42".matches("\\d+"); // false: the whole string must match
To find a pattern anywhere in a string, or to extract matched parts, use java.util.regex.Pattern and Matcher:
Pattern p = Pattern.compile("(\\d+)ms");
Matcher m = p.matcher("WARN Slow query 2310ms on /api/orders");
if (m.find()) {
int millis = Integer.parseInt(m.group(1)); // 2310
}
Regular expressions have their own lesson. For fixed text, prefer contains and indexOf: they are simpler and faster.
Searching Character by Character
For conditions that are about kinds of characters rather than specific text, loop over the characters or use chars():
boolean hasDigit = password.chars().anyMatch(Character::isDigit);
long vowels = word.toLowerCase().chars().filter(c -> "aeiou".indexOf(c) >= 0).count();
int firstSpace = text.indexOf(' ');
Performance Notes
contains,indexOf,startsWith, andendsWithscan the string; their cost grows with its length. For a few searches on ordinary text this is negligible.- Searching the same large text for many different words is faster with a different structure, such as splitting it into words and putting them in a
HashSet. String.matchescompiles its regular expression on every call. Inside loops, compile aPatternonce and reuse it.
Practical Example: Scanning Application Logs
import java.util.*;
public class LogScan {
static int countOccurrences(String text, String word) {
int count = 0;
int from = 0;
while ((from = text.indexOf(word, from)) != -1) {
count++;
from += word.length();
}
return count;
}
static String between(String text, String start, String end) {
int s = text.indexOf(start);
if (s == -1) return null;
s += start.length();
int e = text.indexOf(end, s);
return e == -1 ? null : text.substring(s, e);
}
public static void main(String[] args) {
List<String> log = List.of(
"2026-11-11 10:02:15 INFO [user=ada] Login success",
"2026-11-11 10:05:42 ERROR [user=linus] Payment failed: timeout",
"2026-11-11 10:06:03 WARN [user=grace] Slow query 2310ms",
"2026-11-11 10:07:19 ERROR [user=ada] Payment failed: card declined",
"2026-11-11 10:09:55 INFO [user=linus] Logout"
);
for (String line : log) {
if (line.contains(" ERROR ")) {
String user = between(line, "[user=", "]");
String reason = line.substring(line.lastIndexOf(':') + 1).strip();
String time = line.substring(11, 19);
System.out.println(time + " " + user + " -> " + reason);
}
}
String all = String.join("\n", log);
System.out.println("'Payment' appears " + countOccurrences(all, "Payment") + " times");
System.out.println("Lines for ada: " + log.stream().filter(l -> l.contains("[user=ada]")).count());
System.out.println("Starts with today: " + log.stream().allMatch(l -> l.startsWith("2026-11-11")));
}
}
Output:
10:05:42 linus -> timeout
10:07:19 ada -> card declined
'Payment' appears 2 times
Lines for ada: 2
Starts with today: true
Each search method extracts one piece of information:
contains(" ERROR ")selects error lines. The surrounding spaces stop it from matching a word likeERRORS.betweenuses twoindexOfcalls to pull the user name out of the[user=...]tag; the second search starts after the first marker.lastIndexOf(':')finds the colon before the failure reason. It must be the last colon, because the timestamp also contains colons.substring(11, 19)extracts the time from its fixed position in the line format.countOccurrencesloops withindexOf(word, from).startsWithverifies that every line is from the expected date.
This combination of searching for markers and cutting with substring is enough for many lightweight parsing tasks. For more complex or variable formats, regular expressions are the next step.