Java String contains - Case-Sensitive Payment Filter Fail
Payments marked 'failed' not retried because contains() expected 'Failed' after upgrade.
20+ years shipping production Java in banking & fintech. Drawn from code that ran under real load.
- String.contains() returns true if the exact substring appears anywhere in the target string, case-sensitively.
- For case-insensitive, normalise both strings to Locale.ROOT before comparing.
- Calling contains() on a null string or passing null both throw NullPointerException.
- Performance: O(n*m) worst case; repeated calls on the same large text should use indexOf with offsets or Aho-Corasick.
- Production trap: A single NPE from .contains() can crash the entire request if not guarded.
String.contains() answers one question: does this string have this other string somewhere inside it? It's a substring check — not an equality check, not a starts-with check. The method is straightforward; the edge cases around null, case-sensitivity, and performance on large strings are where developers trip up.
contains() is in the top 20 Java String methods by frequency of use. It's also in the top 10 sources of NullPointerException in production Java code. Understanding the null contract and the case-sensitivity behaviour up front prevents the bugs I see repeatedly in code review.
How Java String.contains() Really Works — And Why It's Case-Sensitive
Java's String.contains(CharSequence s) returns true if and only if this string contains the specified sequence of char values. Under the hood, it delegates to String.indexOf(String) — a naive O(n*m) search in the worst case, though the JVM may intrinsify it for performance. There is no locale, no pattern, no flag: it's a straight character-by-character match.
Key properties: it's case-sensitive, null-hostile (throws NullPointerException if the argument is null), and operates on char values, not code points. That means supplementary characters (e.g., emoji) are represented as two surrogate chars — contains() will match them, but only if you pass the exact surrogate pair. The method does not normalize or fold case; 'A' and 'a' are distinct.
Use contains() when you need a simple, fast substring check and you control both the source and the search term — e.g., verifying an order ID prefix, checking a log level tag, or filtering a known set of tokens. Do not use it for user-facing search, natural language processing, or any scenario where case-insensitivity or locale-aware matching is required. In production payment systems, a case-sensitive filter on transaction codes has directly caused failed captures and duplicate charges.
contains() for business logic that involves user input or external system codes.contains() for user-facing search or locale-sensitive matching without explicit normalization.contains() Usage, Case-Insensitive Variant, and Null Safety
String.contains() takes a CharSequence (which String implements) and returns true if the argument appears anywhere within the string. The check is case-sensitive. For case-insensitive contains, convert both strings to the same case first — but use Locale.ROOT or Locale.ENGLISH to avoid locale-specific case conversion bugs (the Turkish 'i' problem).
contains() calls with a null check unless you're absolutely certain the reference is non-null.contains() with a null check.Performance of contains() and Alternatives When Speed Matters
contains() internally calls indexOf(), which uses a naive O(nm) algorithm. For most one-off checks on small to medium strings, it's fast enough. But in tight loops over large strings (e.g., processing a 100K-character log line a thousand times), that O(nm) adds up. If you're checking for multiple patterns repeatedly, consider using Aho-Corasick or building a trie. For a single pattern, you can use indexOf() with a starting offset to search incrementally.
If you need to check the same large string for many substrings, cache the result after the first check or convert to a hash-based approach. Never assume contains() is cheap on a gigabyte-sized string.
contains() was called on every line in a 1GB file for filtering.contains() vs matches() vs indexOf() – Choosing the Right Tool
contains() treats its argument as a literal substring. If you need regex pattern matching, use String.matches() or Pattern.compile(). The matches() method checks if the entire string matches the pattern, not just a substring — so you'll need .* around the pattern for a containment check. indexOf() returns the index of the first occurrence, or -1 if not found; use it when you need the position.
Remember: contains() cannot handle regex metacharacters. If you pass "\\d+" to contains(), it looks for the literal backslash-d-plus, not a digit pattern. That's a common trap.
contains() on a phone number field expecting regex validation, and it always returned false for valid numbers.contains() was looking for literal dots and hyphens.contains() only for exact literal substrings; for patterns, use Pattern.find().Pattern.find() or matches() with .*.contains() just a boolean.Using contains() with CharSequence and StringBuilder
The parameter of contains() is CharSequence, not String. This means you can pass StringBuilder, StringBuffer, or any other CharSequence implementation directly – no need to call toString(). That saves an allocation and speeds things up when you're working with mutable strings.
However, note that the object on which you call contains() must be a CharSequence as well (usually a String). If you have a StringBuilder and want to check if it contains something, you must call contains() on the StringBuilder? Actually StringBuilder does not have a contains() method – it's only on String. So you'd need to convert the StringBuilder to a String first, or use indexOf() on the StringBuilder (StringBuilder has indexOf). But if you have a String and want to check if it contains a substring from a StringBuilder, you can pass the StringBuilder directly.
contains() – use indexOf() on it instead.contains().contains() in Stream and Lambda Pipelines
contains() is commonly used in stream filters to keep only elements that contain a certain substring. It works naturally with lambdas: list.stream().filter(s -> s.contains("keyword")). But watch out — if any element in the stream is null, you'll get an NPE before the filter even processes the keyword. Always check for nulls first with filter(Objects::nonNull).
Also, remember that contains() is case-sensitive in streams too. If you need case-insensitive, chain toLowerCase(Locale.ROOT) on the element before calling contains().
contains() caused a silent spike in NPE logs during a data migration.contains() in streams.Syntax of contains() – The Signature That Hides a Trap
The method signature is dead simple – and that simplicity has shipped more NullPointerExceptions into production than most devs want to admit. public boolean contains(CharSequence sequence). It takes a CharSequence, not a String. That's the first clue this method was designed for interfaces, not just strings. The return is boolean. True if the sequence exists, false if not. No index, no position, no context.
Why does the parameter type matter? Because you can pass a StringBuilder, StringBuffer, or any custom CharSequence implementation. But under the hood, calls contains()indexOf(sequence.toString()). That .toString() call is your hidden cost. If you're passing a massive StringBuilder that hasn't been trimmed, you're paying for a full string conversion before the search even starts.
And yes – passing null throws a NullPointerException. Not silently returns false. The Javadoc is clear, but every week someone refactors a method to return null and their contains() check explodes. Defensive check your input or use a utility that handles null gracefully.
contains() on a string that might be null. A two-line null check is cheaper than a 2 AM pager alert and a rollback.contains(), or wrap it in a safe utility method.Implementation of contains() – What the JDK Actually Does
Let's look under the hood. Open String.java in your JDK, and you'll find this: return indexOf(sequence.toString()) > -1;. That's it. is syntactic sugar over contains()indexOf(). Knowing this changes how you read and write search logic.
indexOf() uses a naive brute-force search for small patterns or falls back to an optimized loop for longer texts. No Boyer-Moore, no KMP – you're getting O(n*m) worst-case complexity. For most strings under 10K characters, this is fine. For high-throughput parsing or large logs, it's a bottleneck.
Why should you care? Because creates a temporary string from the contains()CharSequence parameter. If you're calling in a hot loop, you're generating garbage. Every. Single. Time. A contains()StringBuilder that gets passed to is fully converted to a contains()String before the search. That allocation pressure hits GC, and your latency graph shows it.
If you're writing performance-sensitive code – batch processing, stream parsing, real-time systems – skip and use contains()indexOf() directly on the raw string. Or better yet, write a custom search that works on CharSequence without conversion.
The Case-Sensitive Payment Filter That Lost Us Customers
contains() expecting 'Failed'.contains() check.contains().- Never assume data case from external systems.
- Always normalise when checking substrings from outside your control.
- Add a unit test with both capitalised and lowercase input strings.
contains()contains(). Add a null guard: str != null && str.contains(sub).System.out.println(). Check if one side has leading/trailing whitespace. Use trim() if needed.if (str != null && str.contains("keyword")) { ... }Objects.toString(str, "").contains("keyword")Key takeaways
String.contains() is case-sensitive. For case-insensitive contains, use str.toLowerCase(Locale.ROOT).contains(query.toLowerCase(Locale.ROOT)).contains() on any string that might be null.contains() chained with || for simple cases or a stream with anyMatch() for a list of patterns.contains()Common mistakes to avoid
4 patternsCalling contains() on a potentially null string
contains(): str != null && str.contains(sub).Passing null to contains()
contains() cannot be null.Using contains() with case-sensitive expectation on user input
contains().Using contains() in a tight loop on large strings
contains() if the string doesn't change, or use indexOf() with a starting position to search incrementally. For multiple patterns, use Aho-Corasick.Interview Questions on This Topic
How would you implement a case-insensitive substring check in Java?
contains(). The standard approach is to convert both the string and the substring to the same case using toLowerCase(Locale.ROOT) or toUpperCase(Locale.ROOT) before calling contains(). Always use Locale.ROOT to avoid locale-specific case conversion issues such as the Turkish 'i' problem. Example: str.toLowerCase(Locale.ROOT).contains(sub.toLowerCase(Locale.ROOT)).Frequently Asked Questions
20+ years shipping production Java in banking & fintech. Drawn from code that ran under real load.
That's Strings. Mark it forged?
5 min read · try the examples if you haven't