This is Part 2 of our Java 21 Interview Preparation series. We’ll explore modern String API enhancements introduced in Java 11-17, comparing them with Scala 3 and Kotlin approaches.
The Problem: Processing Multi-line Text
A common programming task involves processing multi-line text: stripping indentation, filtering blank lines, and formatting output. Let’s see how this task evolved from Java 8 to modern Java 21, and compare with idiomatic Scala 3 and Kotlin solutions.
Java 8 Style - Verbose and Manual
Before Java 11, string processing required manual operations:
public static String processJava8Style(String text) {
if (text == null || text.trim().isEmpty()) {
return "";
}
String[] lines = text.split("\n");
StringBuilder result = new StringBuilder();
for (String line : lines) {
String trimmed = line.trim();
if (!trimmed.isEmpty()) {
result.append(trimmed).append("\n");
}
}
// Remove trailing newline if present
if (!result.isEmpty() && result.charAt(result.length() - 1) == '\n') {
result.deleteCharAt(result.length() - 1);
}
return result.toString();
}
This approach requires:
- Manual splitting by newline character
- Manual trimming of each line
- Manual filtering of empty lines
- StringBuilder for output construction
Modern Java 11+ Style - Fluent and Expressive
Java 11+ introduced several String API enhancements that enable a fluent, functional approach:
public static String processModernStyle(String text) {
if (text == null || text.isBlank()) {
return "";
}
return text.lines()
.map(String::strip)
.filter(line -> !line.isBlank())
.collect(Collectors.joining("\n"));
}
Key APIs used:
lines()- Splits into a Stream of lines (Java 11)strip()- Removes leading/trailing whitespace, Unicode-aware (Java 11)isBlank()- Checks for empty or whitespace-only strings (Java 11)
Key String API Features (Java 11-17+)
1. String.isBlank() vs isEmpty() (Java 11)
String empty = "";
String whitespace = " \t\n ";
String text = "hello";
// isEmpty() - only checks length == 0
empty.isEmpty(); // true
whitespace.isEmpty(); // false
text.isEmpty(); // false
// isBlank() - checks empty OR only whitespace
empty.isBlank(); // true
whitespace.isBlank(); // true
text.isBlank(); // false
2. String.lines() (Java 11)
String multiline = """
First line
Second line
Third line
""";
multiline.lines()
.forEach(System.out::println);
// Output:
// First line
// Second line
// Third line
3. String.strip(), stripLeading(), stripTrailing() (Java 11)
Unlike trim() which only removes characters ≤ U+0020, strip() methods are Unicode-aware:
String text = "\u00A0 hello \u00A0"; // Non-breaking spaces
text.trim(); // " hello " (doesn't remove Unicode whitespace)
text.strip(); // "hello" (removes all Unicode whitespace)
4. String.indent() (Java 12)
String code = """
public void hello() {
System.out.println("Hello");
}
""";
// Add 4 spaces to each line
code.indent(4);
// Remove up to 2 spaces from each line
code.indent(-2);
5. String.transform() (Java 12)
Enables fluent chaining of arbitrary string operations:
String result = " hello world "
.transform(String::strip)
.transform(String::toUpperCase)
.transform(s -> "[" + s + "]");
// Result: "[HELLO WORLD]"
6. Text Blocks (Java 15)
Multi-line string literals with natural formatting:
String json = """
{
"name": "%s",
"email": "%s",
"active": %b
}
""";
System.out.println(json.formatted("Alice", "alice@example.com", true));
7. String.formatted() (Java 15)
Instance method alternative to String.format():
// Traditional
String.format("Name: %s, Age: %d", name, age);
// Modern (Java 15+)
"Name: %s, Age: %d".formatted(name, age);
Comparison: Java 21 vs Scala 3 vs Kotlin
Processing Multi-line Text
public static String processText(String text) {
if (text == null || text.isBlank()) {
return "";
}
return text.lines()
.map(String::strip)
.filter(line -> !line.isBlank())
.collect(Collectors.joining("\n"));
}
def processText(text: String): String =
Option(text)
.filter(_.trim.nonEmpty)
.map(_.linesIterator
.map(_.strip)
.filter(_.nonEmpty)
.mkString("\n"))
.getOrElse("")
fun processText(text: String?): String {
if (text.isNullOrBlank()) return ""
return text.lines()
.map { it.trim() }
.filter { it.isNotBlank() }
.joinToString("\n")
}
Multi-line Strings
| Feature | Java 21 | Scala 3 | Kotlin |
|---|---|---|---|
| Multi-line literal | Text blocks ("""...""") |
Triple quotes with stripMargin |
Triple quotes with trimIndent |
| String interpolation | formatted() method |
s"...", f"..." |
"$variable", "${expr}" |
| Margin handling | Automatic | \| with stripMargin |
Auto with trimIndent |
String Checking Methods
| Check | Java 21 | Scala 3 | Kotlin |
|---|---|---|---|
| Empty | isEmpty() |
isEmpty |
isEmpty() |
| Blank | isBlank() |
isBlank |
isBlank() |
| Null-safe blank | Manual | Option(s).exists(_.nonEmpty) |
isNullOrBlank() |
Creating Multi-line Strings
String json = """
{
"name": "%s",
"email": "%s"
}
""".formatted(name, email);
val json = s"""{
| "name": "$name",
| "email": "$email"
|}""".stripMargin
val json = """
{
"name": "$name",
"email": "$email"
}
""".trimIndent()
Complete Example: Text Processing Pipeline
Here’s a complete example combining multiple modern String APIs:
Java 21
public static String processCompletePipeline(String text) {
if (text == null || text.isBlank()) {
return "";
}
return text.lines()
.map(String::strip)
.filter(line -> !line.isBlank())
.map(line -> "• %s".formatted(line))
.collect(Collectors.joining("\n"))
.transform(result -> "Processed Content:\n" + result)
.transform(result -> result.indent(2).stripTrailing());
}
Scala 3
def processCompletePipeline(text: String): String =
Option(text)
.filter(_.trim.nonEmpty)
.map { t =>
val processed = t.linesIterator
.map(_.strip)
.filter(_.nonEmpty)
.map(line => s"• $line")
.mkString("\n")
s"Processed Content:\n$processed"
.linesIterator
.map(line => s" $line")
.mkString("\n")
}
.getOrElse("")
Kotlin
fun processCompletePipeline(text: String?): String {
if (text.isNullOrBlank()) return ""
val processed = text.lines()
.map { it.trim() }
.filter { it.isNotBlank() }
.joinToString("\n") { "• $it" }
return "Processed Content:\n$processed"
.lines()
.joinToString("\n") { " $it" }
}
Extension Methods: Scala and Kotlin Advantage
Both Scala and Kotlin allow extending String with custom methods:
Scala 3 Extension Methods
extension (s: String)
def isNullOrBlank: Boolean =
s == null || s.isBlank
def truncate(maxLength: Int): String =
if s.length <= maxLength then s
else s.take(maxLength - 3) + "..."
def wrapWith(delimiter: String): String =
s"$delimiter$s$delimiter"
// Usage
"Hello, World!".truncate(8) // "Hello..."
"Hello".wrapWith("***") // "***Hello***"
Kotlin Extension Functions
fun String.truncate(maxLength: Int): String =
if (length <= maxLength) this
else take(maxLength - 3) + "..."
fun String.wrapWith(delimiter: String): String =
"$delimiter$this$delimiter"
// Usage
"Hello, World!".truncate(8) // "Hello..."
"Hello".wrapWith("***") // "***Hello***"
Summary: Feature Comparison
| Feature | Java 8 | Java 21 | Scala 3 | Kotlin |
|---|---|---|---|---|
| Check blank | text.trim().isEmpty() |
text.isBlank() |
text.isBlank |
text.isBlank() |
| Split lines | text.split("\n") |
text.lines() |
text.linesIterator |
text.lines() |
| Strip whitespace | text.trim() |
text.strip() |
text.strip |
text.trim() |
| Multi-line strings | Concatenation | Text blocks | Triple quotes | Triple quotes |
| String formatting | String.format() |
"...".formatted() |
s"...", f"..." |
"$var" |
| Indentation | Manual | text.indent(n) |
Manual | trimIndent() |
| Functional transform | Manual | text.transform(f) |
Option(text).map(f) |
text.let { f(it) } |
Best Practices
- Prefer
isBlank()overisEmpty()when checking for meaningful content - Use
strip()instead oftrim()for proper Unicode whitespace handling - Use
lines()for stream processing multi-line text - Use text blocks for multi-line string literals (JSON, SQL, etc.)
- Chain operations with
transform()for readable pipelines - Consider null safety - Kotlin’s
isNullOrBlank()is convenient
Code Samples
See the complete implementations in our repository:
Conclusion
Java’s String API has evolved significantly from Java 8 to Java 21. The modern APIs provide:
- Cleaner code with fluent, functional operations
- Better Unicode support with
strip()andisBlank() - Natural multi-line strings with text blocks
- Fluent transformations with
transform()andformatted()
For Scala and Kotlin developers, the modern Java APIs feel more familiar and idiomatic. While Scala and Kotlin still offer advantages like extension methods and powerful string interpolation, Java 21 has closed much of the gap in string manipulation ergonomics.
This is Part 2 of our Java 21 Interview Preparation series. Check out Part 1: Immutable Data with Java Records and the full preparation plan.