Common String Methods
Use the String methods you reach for every day to clean, inspect and slice request data, and avoid their index and locale traps.
int i = s.indexOf("@"); // -1 when absent
String part = s.substring(start, end); // end is exclusive
String clean = s.strip().toLowerCase(Locale.ROOT);Most string handling in a service is cleaning and inspecting input: trimming a form field, reading a header, taking the extension off a file name. String has a method for each of these, and all of them return new values without changing the original.
| Method | Returns |
|---|---|
length(), isEmpty(), isBlank() | Size, zero length, or only whitespace (Java 11) |
charAt(i) | The char at index i |
substring(start, end) | Characters from start up to, not including, end |
indexOf(s), lastIndexOf(s) | Position of the first or last match, or -1 |
contains, startsWith, endsWith | boolean |
replace(a, b) | Every literal a replaced by b |
strip(), trim() | Copy without leading and trailing whitespace |
toLowerCase(locale), toUpperCase(locale) | Case-converted copy |
Indexes
Indexes start at 0, and the end index of substring is exclusive, so s.substring(0, 3) is the first three characters and end - start is always the length of the result. An index outside the String throws StringIndexOutOfBoundsException, which in a request handler becomes a 500 error caused by short or malformed input. Check the result of indexOf for -1 before using it as an index, and check length before slicing a fixed prefix.
strip versus trim
trim() dates from Java 1.0 and removes only characters with codes up to the space character. strip(), added in Java 11, removes every character that Character.isWhitespace accepts, including the em spaces that arrive in text pasted from documents. Neither removes the no-break space (U+00A0), which is common in pasted text too; replace it explicitly if your input can contain it. Prefer strip(), stripLeading() and stripTrailing() in new code, and isBlank() for "the user typed nothing useful".
Case conversion needs a locale
toLowerCase() with no argument uses the JVM's default locale. On a server whose default is Turkish, "TITLE".toLowerCase() produces a dotless i, so a comparison against "title" quietly fails. For identifiers, header names, enum names and emails, pass Locale.ROOT. Keep the default locale only for text you show to a person in their own language.
Matching without copying
regionMatches(true, ...) compares part of a String ignoring case without building a lower-cased copy. It suits checks like the scheme of an Authorization header, which is case-insensitive.
Example
When to use it
- Extract the token from an Authorization: Bearer header without failing on short or oddly cased values.
- Normalise an email address with strip and toLowerCase(Locale.ROOT) before looking up the user row.
- Reject a signup form whose name field is blank or only whitespace with a 400 response.
More examples
File extension with lastIndexOf
lastIndexOf finds the final dot, and the guard handles the edge cases before substring can throw. Use this to pick a Content-Type for an upload, but never trust the extension alone to decide whether a file is safe.
String extension(String fileName) {
int dot = fileName.lastIndexOf('.');
if (dot <= 0 || dot == fileName.length() - 1) {
return ""; // no dot, a hidden file like .env, or a trailing dot
}
return fileName.substring(dot + 1).toLowerCase(Locale.ROOT);
}
void main() {
for (String name : List.of("report.PDF", "archive.tar.gz", ".env", "README", "notes.")) {
IO.println(name + " -> [" + extension(name) + "]");
}
}
// Output:
// report.PDF -> [pdf]
// archive.tar.gz -> [gz]
// .env -> []
// README -> []
// notes. -> []strip versus trim, isBlank versus isEmpty
trim leaves the em spaces in place because they are above U+0020, while strip removes them. isEmpty only checks for zero length, so use isBlank to reject a form field that contains nothing but spaces.
void main() {
String padded = "\u2003name\u2003"; // em spaces, common in pasted text
IO.println(padded.length());
IO.println(padded.trim().length());
IO.println(padded.strip().length());
IO.println(" ".isEmpty());
IO.println(" ".isBlank());
}
// Output:
// 6
// 6
// 4
// false
// trueWhy case conversion takes a locale
Turkish lower-cases I to a dotless i (U+0131), so code that relies on the default locale breaks on a server configured for Turkey. Locale.ROOT gives the same answer everywhere, which is what protocol values and keys need.
void main() {
String header = "TITLE";
String root = header.toLowerCase(Locale.ROOT);
String turkish = header.toLowerCase(Locale.forLanguageTag("tr"));
IO.println(root.equals("title"));
IO.println(turkish.equals("title"));
IO.println((int) turkish.charAt(1)); // 305 is the dotless i
}
// Output:
// true
// false
// 305
Discussion