Back to Blog
Java

Java String Contains: Checking Substrings Safely

Check for substrings in Java with String.contains: understand null and empty arguments, case sensitivity, and when to use indexOf or matches instead.

String.containssubringindexOfcase-insensitiveJava
Illustration of a magnifying glass over a a Java string highlighting a substring match, representing the contains method.

The String.contains(CharSequence) method returns true when the sequence appears in the string, and false otherwise. It is a literal, case-sensitive search; it does not interpret regular expressions.

The contains Method and Its Contract

contains is defined on String and accepts any CharSequence, including String, StringBuilder, and StringBuffer. The method returns a boolean, so it is often used directly in if statements.

String message = "The quick brown fox"; if (message.contains("quick")) { System.out.println("Found"); }

The search is for the exact sequence of characters. There is no pattern matching, no wildcard support, and no locale-aware comparison. The standard JDK implementation delegates to indexOf, so the behavior is consistent with a plain substring search.

Because contains returns a boolean, it is not useful when you need the position of the match. For that, use indexOf, which returns the starting index or -1.

Case Sensitivity and Locale Behavior

contains is case-sensitive. "Hello".contains("hello") is false. There is no built-in containsIgnoreCase method, so you must normalize the strings yourself.

The simplest approach is to convert both sides to the same case:

String text = "Hello World"; boolean found = text.toLowerCase().contains("hello".toLowerCase());

This works, but toLowerCase() without a locale can produce unexpected results in Turkish and other locales where I and i have special mappings. For consistent behavior, specify a locale:

boolean found = text.toLowerCase(Locale.ROOT).contains("hello".toLowerCase(Locale.ROOT));

Alternatively, use regionMatches with the ignoreCase flag to avoid creating new strings. This is covered later.

Null and Empty Arguments

contains throws NullPointerException if the argument is null. The method has no overload that accepts a nullable value, so you must check the argument before calling it.

String text = "abc"; if (text.contains(null)) { // throws NullPointerException }

An empty string is always contained in any string. "anything".contains("") returns true. This follows the same rule as indexOf(""), which returns 0. If your business logic requires that an empty search term be treated as invalid, you need an explicit check:

boolean isValidSearch = term != null && !term.isEmpty() && text.contains(term);

The null check matters because calling contains on a null string also throws:

String text = null; text.contains("a"); // NullPointerException

contains vs indexOf vs matches

The three methods serve different purposes. contains is the clearest when you only need a boolean. indexOf gives you the position and also allows you to search from a specific offset. matches checks whether the entire string matches a regular expression.

MethodReturnsSearch typeUse case
containsbooleanLiteral substringSimple presence check
indexOfintLiteral substringNeed position or multiple searches
matchesbooleanFull regex matchPattern validation on the whole string

contains delegates to indexOf, so for a single presence check the behavior and cost are the same as a substring search with indexOf. matches is much heavier because it compiles and executes a regular expression. Do not use matches just to check for a literal substring.

Performance and Runtime Cost

For typical inputs, a single contains call is fast; the cost grows with the length of the text and the search term. The main performance concern is repeated calls. If you are checking many different substrings against the same long text, repeated calls can do multiple passes. Depending on your data, you may be able to reduce those passes by tokenizing the text and checking tokens against a Set, or by building one Pattern with alternation if a regex fits the task. If you need the position of a match or want to continue searching after a match, use indexOf with an offset.

Another subtle cost is that contains accepts CharSequence. If you pass a StringBuilder, the implementation may need to convert it to a String via toString(). This allocation is usually negligible, but in a tight loop it can add garbage. If you are repeatedly checking a StringBuilder, convert it to a String once and reuse that reference.

Case-Insensitive Search Without Regex

For a case-insensitive check, regionMatches is often a better choice than converting both strings to lowercase. It avoids creating new strings and lets you specify the region directly.

String text = "Hello World"; String search = "hello"; boolean found = text.regionMatches(true, 0, search, 0, search.length());

The first argument enables case-insensitive comparison. The second is the starting offset in text, the third is the search term, the fourth is the offset in the search term, and the last is the number of characters to compare. This method is locale-independent and does not allocate.

If you prefer a more readable expression, you can use toLowerCase(Locale.ROOT) on both sides. The tradeoff is that it creates two new strings. For one-off checks that is fine; in a loop, regionMatches is more efficient.

Common Edge Cases and Pitfalls

One common mistake is to use contains with a regular expression. contains does not interpret patterns, so "price: 12".contains("\\d+") will not match. Use matches or a Pattern if you need pattern matching.

Another edge case is the empty string. As mentioned, contains("") always returns true. This can cause subtle bugs in validation code where an empty search field is treated as a match.

Also remember that contains is not null-safe. If the string you are searching can be null, guard it first:

if (text != null && text.contains("needle")) { // ... }

Finally, be careful with StringBuilder and other CharSequence implementations. contains will call toString() on the argument if it is not already a String. That conversion may have side effects or cost, depending on the implementation. For most use cases, passing a String is the safest and clearest choice.

Java String.contains: Practical Usage and Code Examples | RYUSLOG DEV