If you have worked with Java for any length of time, you know that String is the single most used class in the language. Every validation check, every log message, every API response, every database query — they all involve strings. Despite this, Java’s String class went largely unchanged from Java 1.0 through Java 8. Developers had to reach for Apache Commons Lang, Guava, or hand-rolled utility methods just to do basic things like check if a string was blank or repeat a character.
Java 11 finally addressed these long-standing gaps by adding six new methods to the String class. These methods are small individually, but collectively they eliminate a surprising amount of boilerplate code and third-party dependencies. If you have ever written str != null && !str.trim().isEmpty(), you will appreciate what Java 11 brings to the table.
Here is what Java 11 added to String:
| Method | What It Does | What It Replaces |
|---|---|---|
isBlank() |
Returns true if the string is empty or contains only whitespace |
str.trim().isEmpty() or Apache Commons StringUtils.isBlank() |
strip() |
Removes leading and trailing whitespace (Unicode-aware) | trim() (ASCII-only) |
stripLeading() |
Removes leading whitespace only | Manual regex or substring logic |
stripTrailing() |
Removes trailing whitespace only | Manual regex or substring logic |
lines() |
Splits string into a Stream<String> by line terminators |
split("\\n") or BufferedReader.lines() |
repeat(int) |
Repeats the string n times | String.join() hacks or StringUtils.repeat() |
In this tutorial, we will explore each method in depth with practical, compilable examples that show exactly when and why you would use them in real-world Java applications.
Prerequisites: Java 11 or later. All code examples compile and run on Java 11+.
The isBlank() method returns true if the string is empty ("") or contains only whitespace characters. This is the method Java developers have been asking for since approximately forever.
Before Java 11, checking whether a string was “blank” (empty or whitespace-only) required either a multi-step check or a third-party library:
// Before Java 11: verbose and error-prone
if (name != null && !name.trim().isEmpty()) {
// name has actual content
}
// Or using Apache Commons Lang
if (StringUtils.isNotBlank(name)) {
// name has actual content
}
// Java 11: clean and built-in
if (!name.isBlank()) {
// name has actual content
}
This is the most common question developers ask. The difference is simple but critical:
| Method | Returns true when |
"" |
" " |
"\t\n" |
"hello" |
|---|---|---|---|---|---|
isEmpty() |
Length is 0 | true |
false |
false |
false |
isBlank() |
Length is 0 or all characters are whitespace | true |
true |
true |
false |
Think of it this way: isEmpty() checks if the box is empty. isBlank() checks if the box is empty or filled with packing peanuts (whitespace) — either way, there is nothing useful inside.
public class IsBlankDemo {
public static void main(String[] args) {
// Basic comparisons
String empty = "";
String spaces = " ";
String tab = "\t";
String newline = "\n";
String mixed = " \t \n ";
String content = " hello ";
System.out.println("String | isEmpty | isBlank");
System.out.println("------------- | ------- | -------");
printComparison("\"\"", empty); // true | true
printComparison("\" \"", spaces); // false | true
printComparison("\"\\t\"", tab); // false | true
printComparison("\"\\n\"", newline); // false | true
printComparison("\" \\t \\n \"", mixed); // false | true
printComparison("\" hello \"", content);// false | false
}
static void printComparison(String label, String value) {
System.out.printf("%-14s| %-8s| %s%n",
label, value.isEmpty(), value.isBlank());
}
}
// Output:
// String | isEmpty | isBlank
// ------------- | ------- | -------
// "" | true | true
// " " | false | true
// "\t" | false | true
// "\n" | false | true
// " \t \n " | false | true
// " hello " | false | false
isBlank() shines in form validation where users might submit fields filled with spaces:
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
public class FormValidator {
public static List validate(Map formData) {
List errors = new ArrayList<>();
String username = formData.getOrDefault("username", "");
String email = formData.getOrDefault("email", "");
String password = formData.getOrDefault("password", "");
String bio = formData.getOrDefault("bio", "");
// isBlank() catches spaces-only submissions that isEmpty() would miss
if (username.isBlank()) {
errors.add("Username is required");
} else if (username.length() < 3) {
errors.add("Username must be at least 3 characters");
}
if (email.isBlank()) {
errors.add("Email is required");
} else if (!email.contains("@")) {
errors.add("Email must contain @");
}
if (password.isBlank()) {
errors.add("Password is required");
}
// Bio is optional -- only validate length if user typed something
if (!bio.isBlank() && bio.strip().length() > 500) {
errors.add("Bio must be 500 characters or less");
}
return errors;
}
public static void main(String[] args) {
// Simulating a form where a user typed only spaces in the username field
var formData = Map.of(
"username", " ", // spaces only -- isBlank() catches this!
"email", "john@example.com",
"password", "secret123",
"bio", ""
);
List errors = validate(formData);
if (errors.isEmpty()) {
System.out.println("Form is valid!");
} else {
errors.forEach(e -> System.out.println("ERROR: " + e));
}
}
}
// Output:
// ERROR: Username is required
isBlank() recognizes all Unicode whitespace characters, not just ASCII space (\u0020). This matters when processing text from international sources:
public class IsBlankUnicode {
public static void main(String[] args) {
// Various Unicode whitespace characters
String noBreakSpace = "\u00A0"; // Non-breaking space
String emSpace = "\u2003"; // Em space (typography)
String ideographicSpace = "\u3000"; // CJK ideographic space (Chinese/Japanese)
String zeroWidthSpace = "\u200B"; // Zero-width space
System.out.println("Non-breaking space isBlank: " + noBreakSpace.isBlank());
System.out.println("Em space isBlank: " + emSpace.isBlank());
System.out.println("Ideographic space isBlank: " + ideographicSpace.isBlank());
System.out.println("Zero-width space isBlank: " + zeroWidthSpace.isBlank());
}
}
// Output:
// Non-breaking space isBlank: true
// Em space isBlank: true
// Ideographic space isBlank: true
// Zero-width space isBlank: false <-- not classified as whitespace by Character.isWhitespace()
Key takeaway: isBlank() uses Character.isWhitespace() internally, which covers most Unicode whitespace categories but not all code points (zero-width space is excluded). For the vast majority of real-world applications, this is exactly what you want.
Java has had trim() since version 1.0, so why did Java 11 add strip()? The answer is Unicode awareness. The trim() method only removes characters with a code point less than or equal to U+0020 (the ASCII space). It completely ignores Unicode whitespace characters like non-breaking spaces, em spaces, and ideographic spaces.
| Feature | trim() |
strip() |
|---|---|---|
| Available since | Java 1.0 | Java 11 |
| Whitespace definition | Characters <= '\u0020' |
Character.isWhitespace() |
| Handles Unicode whitespace | No | Yes |
Non-breaking space (\u00A0) |
Not removed | Removed |
Em space (\u2003) |
Not removed | Removed |
Ideographic space (\u3000) |
Not removed | Removed |
public class StripVsTrim {
public static void main(String[] args) {
// ASCII whitespace -- both trim() and strip() handle this
String asciiSpaces = " hello ";
System.out.println("trim(): [" + asciiSpaces.trim() + "]"); // [hello]
System.out.println("strip(): [" + asciiSpaces.strip() + "]"); // [hello]
// Unicode whitespace -- only strip() handles this
String unicodeSpaces = "\u2003 hello \u2003"; // Em spaces
System.out.println("\ntrim(): [" + unicodeSpaces.trim() + "]"); // [ hello ] -- em spaces remain!
System.out.println("strip(): [" + unicodeSpaces.strip() + "]"); // [hello]
// Non-breaking space from HTML
String nbspString = "\u00A0\u00A0data\u00A0\u00A0";
System.out.println("\ntrim(): [" + nbspString.trim() + "]"); // [ data ] -- nbsp not removed!
System.out.println("strip(): [" + nbspString.strip() + "]"); // [data]
// CJK ideographic space (common in Chinese/Japanese text)
String cjkString = "\u3000Tokyo\u3000";
System.out.println("\ntrim(): [" + cjkString.trim() + "]"); // [ Tokyo ] -- not removed!
System.out.println("strip(): [" + cjkString.strip() + "]"); // [Tokyo]
}
}
Before Java 11, removing whitespace from only one side of a string required regex or manual character counting. Now it is a single method call:
public class StripDirectional {
public static void main(String[] args) {
String text = " Hello, World! ";
System.out.println("Original: [" + text + "]");
System.out.println("strip(): [" + text.strip() + "]");
System.out.println("stripLeading():[" + text.stripLeading() + "]");
System.out.println("stripTrailing():[" + text.stripTrailing() + "]");
}
}
// Output:
// Original: [ Hello, World! ]
// strip(): [Hello, World!]
// stripLeading():[Hello, World! ]
// stripTrailing():[ Hello, World!]
Here is a real-world scenario where strip() and its variants clean up messy input from different sources:
import java.util.List;
public class InputCleaner {
/**
* Cleans a CSV value that may have been copied from a spreadsheet.
* Spreadsheets often add non-breaking spaces or other Unicode whitespace.
*/
public static String cleanCsvField(String raw) {
if (raw == null) return "";
return raw.strip(); // Unicode-aware -- handles nbsp from Excel copy-paste
}
/**
* Formats log lines by preserving leading indentation structure
* but removing trailing whitespace (common in log files).
*/
public static String cleanLogLine(String line) {
return line.stripTrailing();
}
/**
* Normalizes code indentation by removing leading whitespace
* for a code formatter.
*/
public static String removeIndentation(String codeLine) {
return codeLine.stripLeading();
}
public static void main(String[] args) {
// CSV field with non-breaking spaces from Excel
String excelField = "\u00A0\u00A0John Smith\u00A0";
System.out.println("Cleaned CSV: [" + cleanCsvField(excelField) + "]");
// Log lines with trailing whitespace
List logLines = List.of(
"2024-01-15 INFO Starting server... ",
" 2024-01-15 DEBUG Connection pool initialized ",
" 2024-01-15 ERROR Failed to bind port "
);
System.out.println("\nCleaned log lines:");
logLines.stream()
.map(InputCleaner::cleanLogLine)
.forEach(line -> System.out.println("[" + line + "]"));
// Code with mixed indentation
String code = " return result; ";
System.out.println("\nStripped leading: [" + removeIndentation(code) + "]");
}
}
// Output:
// Cleaned CSV: [John Smith]
//
// Cleaned log lines:
// [2024-01-15 INFO Starting server...]
// [ 2024-01-15 DEBUG Connection pool initialized]
// [ 2024-01-15 ERROR Failed to bind port]
//
// Stripped leading: [return result; ]
Rule of thumb: Always prefer strip() over trim() in new code. There is no performance penalty, and it handles edge cases that trim() silently ignores. The only reason to keep using trim() is backward compatibility with code that explicitly depends on its ASCII-only behavior.
The lines() method splits a string into a Stream<String> using line terminators (\n, \r, or \r\n). This is a huge improvement over the old approaches of splitting by regex or wrapping strings in a BufferedReader.
There are three good reasons to prefer lines() over split("\\n"):
lines() handles \n (Unix), \r\n (Windows), and \r (old Mac) automatically. With split(), you need split("\\r?\\n|\\r").lines() returns a Stream, so it processes lines on demand. If you only need the first 10 lines of a 10,000-line string, it does not split the entire string.lines() does not produce a trailing empty string if the text ends with a newline. With split(), you need the -1 limit parameter to control this behavior, and even then the behavior is different.public class LinesDemo {
public static void main(String[] args) {
String multiline = "Line 1\nLine 2\nLine 3\n";
// Old way: split -- note the trailing empty string issue
String[] splitResult = multiline.split("\n");
System.out.println("split() count: " + splitResult.length); // 3
// split with -1 limit to include trailing empties
String[] splitAll = multiline.split("\n", -1);
System.out.println("split(-1) count: " + splitAll.length); // 4 (includes trailing "")
// Java 11: lines() -- clean, no trailing empty string
long linesCount = multiline.lines().count();
System.out.println("lines() count: " + linesCount); // 3
// lines() handles all line ending styles
String mixedEndings = "Windows\r\nUnix\nOld Mac\rEnd";
mixedEndings.lines().forEach(System.out::println);
}
}
// Output:
// split() count: 3
// split(-1) count: 4
// lines() count: 3
// Windows
// Unix
// Old Mac
// End
One of the best use cases for lines() is processing multiline text like log entries, CSV data, or configuration files:
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
public class LogProcessor {
public static void main(String[] args) {
String logContent = """
2024-01-15 08:30:00 INFO UserService - User login: john@example.com
2024-01-15 08:30:05 DEBUG ConnectionPool - Acquired connection #42
2024-01-15 08:30:10 ERROR PaymentService - Payment failed: insufficient funds
2024-01-15 08:30:15 WARN SecurityFilter - Rate limit approached: 192.168.1.100
2024-01-15 08:30:20 INFO UserService - User logout: john@example.com
2024-01-15 08:30:25 ERROR PaymentService - Payment timeout after 30s
2024-01-15 08:31:00 INFO HealthCheck - System status: OK
""";
// Count lines by log level
Map levelCounts = logContent.lines()
.filter(line -> !line.isBlank())
.collect(Collectors.groupingBy(
line -> line.split("\\s+")[2], // Extract log level (3rd token)
Collectors.counting()
));
System.out.println("Log level counts: " + levelCounts);
// Extract only ERROR lines
System.out.println("\nError lines:");
logContent.lines()
.filter(line -> line.contains("ERROR"))
.forEach(line -> System.out.println(" " + line.strip()));
// Get unique services mentioned
List services = logContent.lines()
.filter(line -> !line.isBlank())
.map(line -> line.split("\\s+")[3]) // 4th token is the service name
.distinct()
.collect(Collectors.toList());
System.out.println("\nServices: " + services);
// Get first 3 lines (lazy -- does not process the rest)
System.out.println("\nFirst 3 lines:");
logContent.lines()
.limit(3)
.forEach(System.out::println);
}
}
// Output:
// Log level counts: {WARN=1, ERROR=2, DEBUG=1, INFO=3}
//
// Error lines:
// 2024-01-15 08:30:10 ERROR PaymentService - Payment failed: insufficient funds
// 2024-01-15 08:30:25 ERROR PaymentService - Payment timeout after 30s
//
// Services: [UserService, ConnectionPool, PaymentService, SecurityFilter, HealthCheck]
//
// First 3 lines:
// 2024-01-15 08:30:00 INFO UserService - User login: john@example.com
// 2024-01-15 08:30:05 DEBUG ConnectionPool - Acquired connection #42
// 2024-01-15 08:30:10 ERROR PaymentService - Payment failed: insufficient funds
The lines() method combined with stream operations makes parsing structured text (like CSV or configuration) clean and expressive:
import java.util.List;
import java.util.stream.Collectors;
public class CsvParser {
record Employee(String name, String department, double salary) {}
public static void main(String[] args) {
String csvData = """
Name,Department,Salary
Alice Johnson,Engineering,95000
Bob Smith,Marketing,72000
Carol Williams,Engineering,105000
David Brown,Marketing,68000
Eve Davis,Engineering,110000
""";
// Parse CSV into Employee records (skip header)
List employees = csvData.lines()
.skip(1) // Skip header row
.filter(line -> !line.isBlank()) // Skip blank lines
.map(line -> line.split(","))
.map(parts -> new Employee(
parts[0].strip(),
parts[1].strip(),
Double.parseDouble(parts[2].strip())
))
.collect(Collectors.toList());
// Average salary by department
employees.stream()
.collect(Collectors.groupingBy(
Employee::department,
Collectors.averagingDouble(Employee::salary)
))
.forEach((dept, avg) ->
System.out.printf("%s: $%,.0f average%n", dept, avg));
// Top earner
employees.stream()
.max((a, b) -> Double.compare(a.salary(), b.salary()))
.ifPresent(e ->
System.out.printf("\nTop earner: %s ($%,.0f)%n", e.name(), e.salary()));
}
}
// Output:
// Engineering: $103,333 average
// Marketing: $70,000 average
//
// Top earner: Eve Davis ($110,000)
The repeat(int count) method returns a string whose value is the concatenation of the original string repeated count times. If count is 0, it returns an empty string. If count is 1, it returns the original string. If count is negative, it throws IllegalArgumentException.
Repeating a string was surprisingly awkward before Java 11. Here are the workarounds developers used:
// Before Java 11: various hacky approaches
// Approach 1: StringBuilder loop
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 5; i++) {
sb.append("ha");
}
String laughOld = sb.toString(); // "hahahahaha"
// Approach 2: String.join hack (Java 8)
String stars = String.join("", java.util.Collections.nCopies(5, "*"));
// Approach 3: char array fill (only works for single characters)
char[] dashes = new char[50];
java.util.Arrays.fill(dashes, '-');
String line = new String(dashes);
// Java 11: simple, readable, one line
String laugh = "ha".repeat(5); // "hahahahaha"
String border = "-".repeat(50); // "--------------------------------------------------"
String indent = " ".repeat(3); // " " (6 spaces)
repeat() is incredibly useful for building formatted console output, ASCII art, and text reports:
import java.util.List;
public class ReportFormatter {
record Product(String name, int quantity, double price) {
double total() { return quantity * price; }
}
public static void main(String[] args) {
List products = List.of(
new Product("Laptop", 3, 999.99),
new Product("Mouse", 15, 29.99),
new Product("Keyboard", 10, 79.99),
new Product("Monitor", 5, 449.99)
);
int width = 60;
String border = "=".repeat(width);
String thinBorder = "-".repeat(width);
// Header
System.out.println(border);
System.out.println(centerText("INVENTORY REPORT", width));
System.out.println(border);
// Column headers
System.out.printf("%-20s %8s %10s %12s%n",
"Product", "Qty", "Price", "Total");
System.out.println(thinBorder);
// Data rows
double grandTotal = 0;
for (Product p : products) {
System.out.printf("%-20s %8d %10.2f %12.2f%n",
p.name(), p.quantity(), p.price(), p.total());
grandTotal += p.total();
}
// Footer
System.out.println(thinBorder);
System.out.printf("%-20s %8s %10s %12.2f%n",
"GRAND TOTAL", "", "", grandTotal);
System.out.println(border);
}
static String centerText(String text, int width) {
int padding = (width - text.length()) / 2;
return " ".repeat(Math.max(0, padding)) + text;
}
}
// Output:
// ============================================================
// INVENTORY REPORT
// ============================================================
// Product Qty Price Total
// ------------------------------------------------------------
// Laptop 3 999.99 2999.97
// Mouse 15 29.99 449.85
// Keyboard 10 79.99 799.90
// Monitor 5 449.99 2249.95
// ------------------------------------------------------------
// GRAND TOTAL 6499.67
// ============================================================
repeat() combined with depth tracking makes it easy to display hierarchical data:
import java.util.List;
public class TreePrinter {
record TreeNode(String name, List children) {
TreeNode(String name) { this(name, List.of()); }
}
public static void printTree(TreeNode node, int depth) {
String indent = " ".repeat(depth);
String prefix = depth == 0 ? "" : "|" + "--".repeat(1) + " ";
System.out.println(indent + prefix + node.name());
for (TreeNode child : node.children()) {
printTree(child, depth + 1);
}
}
public static void main(String[] args) {
TreeNode root = new TreeNode("src", List.of(
new TreeNode("main", List.of(
new TreeNode("java", List.of(
new TreeNode("com.example", List.of(
new TreeNode("Application.java"),
new TreeNode("config", List.of(
new TreeNode("AppConfig.java"),
new TreeNode("SecurityConfig.java")
)),
new TreeNode("service", List.of(
new TreeNode("UserService.java"),
new TreeNode("OrderService.java")
))
))
)),
new TreeNode("resources", List.of(
new TreeNode("application.properties")
))
)),
new TreeNode("test", List.of(
new TreeNode("java")
))
));
printTree(root, 0);
}
}
// Output:
// src
// |-- main
// |-- java
// |-- com.example
// |-- Application.java
// |-- config
// |-- AppConfig.java
// |-- SecurityConfig.java
// |-- service
// |-- UserService.java
// |-- OrderService.java
// |-- resources
// |-- application.properties
// |-- test
// |-- java
public class RepeatEdgeCases {
public static void main(String[] args) {
// repeat(0) returns empty string
System.out.println("[" + "hello".repeat(0) + "]"); // []
// repeat(1) returns the original string
System.out.println("[" + "hello".repeat(1) + "]"); // [hello]
// Empty string repeated is still empty
System.out.println("[" + "".repeat(100) + "]"); // []
// Negative count throws IllegalArgumentException
try {
"hello".repeat(-1);
} catch (IllegalArgumentException e) {
System.out.println("Negative count: " + e.getMessage());
}
// Very large repeat can cause OutOfMemoryError
// "x".repeat(Integer.MAX_VALUE); // OutOfMemoryError
}
}
While not new methods on the String class itself, Java 11 also improved how strings work with null values in the broader ecosystem. Understanding these patterns helps you write safer string-handling code:
import java.util.Objects;
public class NullSafeStrings {
public static void main(String[] args) {
String nullStr = null;
String emptyStr = "";
String blankStr = " ";
String validStr = "Hello";
// Pattern 1: Null-safe blank check
System.out.println("--- Null-safe blank check ---");
System.out.println(isNullOrBlank(nullStr)); // true
System.out.println(isNullOrBlank(emptyStr)); // true
System.out.println(isNullOrBlank(blankStr)); // true
System.out.println(isNullOrBlank(validStr)); // false
// Pattern 2: Objects.toString() with default
System.out.println("\n--- Objects.toString() ---");
System.out.println(Objects.toString(nullStr, "N/A")); // N/A
System.out.println(Objects.toString(validStr, "N/A")); // Hello
// Pattern 3: String.valueOf() -- never returns null
System.out.println("\n--- String.valueOf() ---");
System.out.println(String.valueOf(nullStr)); // "null" (the string literal)
System.out.println(String.valueOf(12345)); // "12345"
System.out.println(String.valueOf(true)); // "true"
// Pattern 4: Combining new methods with null checks
System.out.println("\n--- Combined pattern ---");
String input = " ";
String result = (input != null && !input.isBlank())
? input.strip()
: "default";
System.out.println("Result: " + result); // "default"
}
/**
* Utility method: null-safe blank check.
* Combines null check with Java 11's isBlank().
*/
static boolean isNullOrBlank(String str) {
return str == null || str.isBlank();
}
}
Here is a complete reference of every String method added in Java 11:
| Method | Signature | Return Type | Description | Example |
|---|---|---|---|---|
isBlank() |
isBlank() |
boolean |
Returns true if the string is empty or contains only whitespace (Unicode-aware) |
" ".isBlank() returns true |
strip() |
strip() |
String |
Removes leading and trailing Unicode whitespace | " hi ".strip() returns "hi" |
stripLeading() |
stripLeading() |
String |
Removes only leading Unicode whitespace | " hi ".stripLeading() returns "hi " |
stripTrailing() |
stripTrailing() |
String |
Removes only trailing Unicode whitespace | " hi ".stripTrailing() returns " hi" |
lines() |
lines() |
Stream<String> |
Splits by line terminators (\n, \r\n, \r) into a lazy stream |
"a\nb".lines().count() returns 2 |
repeat(int) |
repeat(int count) |
String |
Repeats the string count times |
"ha".repeat(3) returns "hahaha" |
Let us combine all the new methods in real-world scenarios that you will encounter in production Java applications.
This example parses a properties-style configuration, handling comments, blank lines, and messy whitespace:
import java.util.LinkedHashMap;
import java.util.Map;
public class ConfigParser {
public static Map parse(String configContent) {
Map config = new LinkedHashMap<>();
configContent.lines() // Split into stream of lines
.map(String::strip) // Remove leading/trailing whitespace
.filter(line -> !line.isBlank()) // Skip blank lines
.filter(line -> !line.startsWith("#")) // Skip comments
.filter(line -> line.contains("=")) // Must be key=value
.forEach(line -> {
int eq = line.indexOf('=');
String key = line.substring(0, eq).strip();
String value = line.substring(eq + 1).strip();
config.put(key, value);
});
return config;
}
public static String toConfigString(Map config) {
StringBuilder sb = new StringBuilder();
sb.append("# Generated Configuration\n");
sb.append("#").append("=".repeat(40)).append("\n\n");
config.forEach((key, value) ->
sb.append(key).append(" = ").append(value).append("\n"));
return sb.toString();
}
public static void main(String[] args) {
String configFile = """
# Database Configuration
# ======================
db.host = localhost
db.port = 5432
db.name = myapp
# Connection Pool
pool.size = 10
pool.timeout = 30000
# Feature Flags
feature.darkMode = true
feature.beta = false
""";
Map config = parse(configFile);
// Display parsed configuration
System.out.println("Parsed " + config.size() + " properties:");
String separator = "-".repeat(45);
System.out.println(separator);
config.forEach((key, value) ->
System.out.printf(" %-20s = %s%n", key, value));
System.out.println(separator);
// Regenerate config file
System.out.println("\nRegenerated config:");
System.out.println(toConfigString(config));
}
}
// Output:
// Parsed 7 properties:
// ---------------------------------------------
// db.host = localhost
// db.port = 5432
// db.name = myapp
// pool.size = 10
// pool.timeout = 30000
// feature.darkMode = true
// feature.beta = false
// ---------------------------------------------
//
// Regenerated config:
// # Generated Configuration
// #========================================
//
// db.host = localhost
// db.port = 5432
// db.name = myapp
// pool.size = 10
// pool.timeout = 30000
// feature.darkMode = true
// feature.beta = false
This example uses repeat(), strip(), and lines() together to generate a formatted text report:
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
public class SalesReport {
record Sale(String region, String product, double amount) {}
public static void main(String[] args) {
List sales = List.of(
new Sale("North", "Laptop", 2999.97),
new Sale("North", "Phone", 1599.98),
new Sale("South", "Laptop", 999.99),
new Sale("South", "Phone", 3199.96),
new Sale("South", "Tablet", 1499.97),
new Sale("East", "Laptop", 5999.94),
new Sale("East", "Phone", 799.99),
new Sale("West", "Tablet", 2999.94)
);
printReport(sales);
}
static void printReport(List sales) {
int width = 55;
String doubleLine = "=".repeat(width);
String singleLine = "-".repeat(width);
// Title
System.out.println("\n" + doubleLine);
System.out.println(center("QUARTERLY SALES REPORT", width));
System.out.println(center("Q4 2024", width));
System.out.println(doubleLine);
// Group by region
Map> byRegion = sales.stream()
.collect(Collectors.groupingBy(Sale::region));
double grandTotal = 0;
for (var entry : byRegion.entrySet()) {
String region = entry.getKey();
List regionSales = entry.getValue();
double regionTotal = regionSales.stream()
.mapToDouble(Sale::amount).sum();
grandTotal += regionTotal;
System.out.println("\n" + " ".repeat(2) + region + " Region");
System.out.println(" ".repeat(2) + "-".repeat(region.length() + 7));
for (Sale sale : regionSales) {
System.out.printf(" %-20s $%,10.2f%n",
sale.product(), sale.amount());
}
System.out.printf(" %-20s $%,10.2f%n",
"Subtotal:", regionTotal);
}
System.out.println("\n" + doubleLine);
System.out.printf(" %-20s $%,10.2f%n", "GRAND TOTAL:", grandTotal);
System.out.println(doubleLine + "\n");
}
static String center(String text, int width) {
int pad = (width - text.length()) / 2;
return " ".repeat(Math.max(0, pad)) + text;
}
}
This example demonstrates using the new String methods for a realistic web input sanitization pipeline:
import java.util.List;
import java.util.Map;
import java.util.LinkedHashMap;
import java.util.stream.Collectors;
public class InputSanitizer {
record ValidationResult(boolean valid, String cleanValue, List errors) {}
public static ValidationResult sanitizeAndValidate(String fieldName, String rawInput,
int minLength, int maxLength,
boolean required) {
List errors = new java.util.ArrayList<>();
// Step 1: Handle null
if (rawInput == null) {
if (required) {
errors.add(fieldName + " is required");
}
return new ValidationResult(!required, "", errors);
}
// Step 2: strip() removes Unicode whitespace (better than trim())
String cleaned = rawInput.strip();
// Step 3: isBlank() detects whitespace-only submissions
if (cleaned.isBlank()) {
if (required) {
errors.add(fieldName + " cannot be blank");
}
return new ValidationResult(!required, "", errors);
}
// Step 4: Normalize internal whitespace using lines() + joining
// This handles embedded newlines, tabs, and multiple spaces
cleaned = cleaned.lines()
.map(String::strip)
.filter(line -> !line.isBlank())
.collect(Collectors.joining(" "));
// Step 5: Length validation
if (cleaned.length() < minLength) {
errors.add(fieldName + " must be at least " + minLength + " characters");
}
if (cleaned.length() > maxLength) {
errors.add(fieldName + " must be at most " + maxLength + " characters");
cleaned = cleaned.substring(0, maxLength);
}
return new ValidationResult(errors.isEmpty(), cleaned, errors);
}
public static void main(String[] args) {
// Simulate messy form submissions
Map formInputs = new LinkedHashMap<>();
formInputs.put("username", " john_doe ");
formInputs.put("email", "\u00A0\u00A0user@example.com\u00A0"); // Non-breaking spaces
formInputs.put("bio", " Hello!\n\n I am a\n developer. \n\n ");
formInputs.put("name", " \t\n "); // Whitespace only
formInputs.put("website", null); // Missing field
System.out.println("=".repeat(60));
System.out.println("Input Sanitization Results");
System.out.println("=".repeat(60));
formInputs.forEach((field, raw) -> {
boolean required = !field.equals("website");
var result = sanitizeAndValidate(field, raw, 2, 200, required);
System.out.printf("\nField: %s%n", field);
System.out.printf(" Raw: [%s]%n", raw);
System.out.printf(" Clean: [%s]%n", result.cleanValue());
System.out.printf(" Valid: %s%n", result.valid());
if (!result.errors().isEmpty()) {
result.errors().forEach(e -> System.out.printf(" Error: %s%n", e));
}
});
}
}
// Output:
// ============================================================
// Input Sanitization Results
// ============================================================
//
// Field: username
// Raw: [ john_doe ]
// Clean: [john_doe]
// Valid: true
//
// Field: email
// Raw: [ user@example.com ]
// Clean: [user@example.com]
// Valid: true
//
// Field: bio
// Raw: [ Hello!
//
// I am a
// developer.
//
// ]
// Clean: [Hello! I am a developer.]
// Valid: true
//
// Field: name
// Raw: [
// ]
// Clean: []
// Valid: false
// Error: name cannot be blank
//
// Field: website
// Raw: [null]
// Clean: []
// Valid: true
Here are the key guidelines for using Java 11's new String methods effectively in production code:
In new code, always use strip() instead of trim(). It handles all Unicode whitespace, has no performance penalty, and prevents subtle bugs when processing text from international sources, web forms (where browsers may insert ), or files saved with different encodings.
Use isBlank() when you need to know whether a string has meaningful content (form validation, business logic). Use isEmpty() when you need to know whether a string is literally empty (protocol handling, serialization, exact-length checks).
Whenever you are splitting a string by line breaks, prefer lines() over split("\\n"). It handles all line ending formats, is lazy (better for large strings), and has cleaner semantics around trailing newlines.
The new methods work beautifully together in stream pipelines:
// Common pattern: parse multiline input, clean each line, skip blanks String rawInput = " line one \n\n line two \n \n line three \n"; ListcleanedLines = rawInput.lines() // Split into stream .map(String::strip) // Clean each line .filter(line -> !line.isBlank()) // Remove blank lines .collect(Collectors.toList()); // Result: ["line one", "line two", "line three"]
Replace any loop that builds a repeated string pattern with repeat(). It is more readable, and internally it uses an optimized algorithm (not simple concatenation).
| Do | Don't |
|---|---|
input.isBlank() |
input.trim().isEmpty() |
input.strip() |
input.trim() (in new code) |
text.lines().filter(...) |
text.split("\\n") (for multiline processing) |
"-".repeat(50) |
Loop with StringBuilder.append("-") |
line.stripTrailing() |
line.replaceAll("\\s+$", "") |
Null-check before calling isBlank() |
Calling isBlank() on a potentially null reference |
| Method | Signature | Returns | Key Behavior | Replaces |
|---|---|---|---|---|
isBlank() |
boolean isBlank() |
boolean |
True if empty or all whitespace (Unicode-aware) | trim().isEmpty(), StringUtils.isBlank() |
strip() |
String strip() |
String |
Removes leading + trailing Unicode whitespace | trim() |
stripLeading() |
String stripLeading() |
String |
Removes leading Unicode whitespace only | Regex replaceAll("^\\s+", "") |
stripTrailing() |
String stripTrailing() |
String |
Removes trailing Unicode whitespace only | Regex replaceAll("\\s+$", "") |
lines() |
Stream<String> lines() |
Stream<String> |
Lazy split by \n, \r\n, or \r |
split("\\r?\\n"), BufferedReader.lines() |
repeat(int) |
String repeat(int count) |
String |
Repeats string count times. 0 returns "". Negative throws IllegalArgumentException |
StringUtils.repeat(), StringBuilder loop |
These six methods may seem small individually, but they collectively eliminate a significant amount of boilerplate code. If you are still using Apache Commons Lang's StringUtils solely for isBlank() and repeat(), you can likely remove that dependency now. Java 11's built-in methods are well-tested, well-optimized, and available everywhere.