Strings #

String is the most frequently used data type in almost every Java program, yet also the most misunderstood. Unlike primitive types, String in Java is an object — and not just any object. Strings are immutable: once created, their value can’t be changed. Every operation that appears to “modify” a String actually creates a new String object and discards the old one. Understanding this nature — along with the String pool concept, the difference between == and equals(), and when to use StringBuilder — is the foundation for writing correct and efficient Java code. In this article we’ll explore the entire String API from the Java standard library, from basic operations to formatting, parsing, regular expressions, and modern features like Text Blocks introduced in Java 15.

Immutability and the String Pool #

Before touching the API, understand the two fundamental properties of String that determine all its behavior.

Immutability #

When you write str = str + " world", Java doesn’t modify the existing String. It creates a new String with the combined value, points the str variable to the new object, and leaves the old String for garbage collection.

String a = "hello";
String b = a;           // b and a point to the SAME object

a = a + " world";       // a now points to a NEW object "hello world"
                        // b still points to "hello" — unchanged

System.out.println(a);  // "hello world"
System.out.println(b);  // "hello" — unaffected

// Immutability makes String automatically thread-safe
// No race conditions because there's no shared mutable state

The String Pool #

Java stores String literals in a special memory area called the String pool (part of the metaspace since Java 8). Two identical literals refer to the same object in the pool.

String s1 = "hello";         // goes into the String pool
String s2 = "hello";         // uses the SAME object from the pool
String s3 = new String("hello"); // creates a NEW object on the heap, outside the pool

// ✗ ANTI-PATTERN: comparing references with ==
System.out.println(s1 == s2);   // true  — coincidentally the same because of the pool
System.out.println(s1 == s3);   // false — different objects on the heap!

// ✓ CORRECT: always use equals() to compare String VALUES
System.out.println(s1.equals(s2));  // true
System.out.println(s1.equals(s3));  // true — same value

// intern() — force the String into the pool
String s4 = s3.intern();
System.out.println(s1 == s4);   // true — s4 now points to the pool object

// ✗ ANTI-PATTERN: new String("literal") — always wastes time and memory
String wasteful = new String("hello"); // avoid this
flowchart TD
    subgraph POOL["String Pool - Metaspace"]
        SP["hello"]
    end

    subgraph HEAP["Heap"]
        HO[""hello" - new object"]
    end

    S1["s1"] --> SP
    S2["s2"] --> SP
    S3["s3"] --> HO
    S4["s4 = s3.intern()"] --> SP

Creating Strings #

// From a literal — the most common way, goes into the String pool
String s1 = "Hello World";

// From a char array
char[] chars = {'H', 'e', 'l', 'l', 'o'};
String s2 = new String(chars);
String s3 = String.valueOf(chars); // equivalent, more idiomatic

// From a byte array (important: always specify the charset!)
byte[] bytes = "Hello".getBytes(java.nio.charset.StandardCharsets.UTF_8);
String s4 = new String(bytes, java.nio.charset.StandardCharsets.UTF_8);

// ✗ ANTI-PATTERN: not specifying a charset — depends on the platform default
String bad = new String(bytes); // charset depends on the OS!

// Joining multiple Strings
String joined = String.join(", ", "apple", "orange", "mango");
// "apple, orange, mango"

String joinedList = String.join(" | ", java.util.List.of("A", "B", "C"));
// "A | B | C"

// Repeating a String (Java 11+)
String repeated = "ha".repeat(3); // "hahaha"

// Empty and blank strings
String empty = "";
String spaces = "   ";
System.out.println(empty.isEmpty());  // true — length 0
System.out.println(spaces.isEmpty());   // false — has space characters
System.out.println(spaces.isBlank());   // true (Java 11+) — whitespace only

Basic Operations #

Length and Character Access #

String text = "Java Programming";

// String length
int length = text.length();       // 16

// Access a character by index (0-based)
char first = text.charAt(0);     // 'J'
char last = text.charAt(text.length() - 1); // 'g'

// Convert to a char array
char[] chars = text.toCharArray();

// Convert to a byte array
byte[] bytes = text.getBytes(java.nio.charset.StandardCharsets.UTF_8);

// Codepoints — for Unicode characters outside the Basic Multilingual Plane
// Emojis and some Asian characters need two chars (a surrogate pair)
String emoji = "Java ☕";
System.out.println(emoji.length());           // 7 (char count)
System.out.println(emoji.codePointCount(0, emoji.length())); // 6 (codepoint count)

Searching and Checking #

String sentence = "Learning Java is very fun";

// indexOf — first position of a substring (-1 if not found)
int position = sentence.indexOf("Java");        // 9
int positionChar = sentence.indexOf('a');       // 2
int fromPosition = sentence.indexOf("a", 10);  // search starting from index 10

// lastIndexOf — last position
int last = sentence.lastIndexOf("a");    // 30

// contains — does it contain a substring
boolean has = sentence.contains("Java");     // true

// startsWith / endsWith
boolean prefix = sentence.startsWith("Learning"); // true
boolean suffix = sentence.endsWith("fun");       // true

// matches — check with a regex
boolean digits = "12345".matches("\\d+");    // true
boolean email = "[email protected]".matches("[\\w.]+@[\\w.]+\\.[a-z]{2,}"); // true

// regionMatches — compare parts of two Strings
String s1 = "Hello World";
String s2 = "hello everyone";
boolean match = s1.regionMatches(
    true,  // ignoreCase
    0,     // offset in s1
    s2,    // the other string
    0,     // offset in s2
    5      // length to compare
); // true — "Hello" == "hello" (case insensitive)

Transformations #

String text = "  Hello World  ";

// Trim and strip
String trimmed = text.trim();    // "Hello World" — removes ASCII whitespace
String stripped = text.strip();  // "Hello World" (Java 11+) — removes Unicode whitespace
String stripLeft = text.stripLeading();  // "Hello World  "
String stripRight = text.stripTrailing(); // "  Hello World"

// Change case
String upper = "hello".toUpperCase();       // "HELLO"
String lower = "HELLO".toLowerCase();       // "hello"
// Use a Locale for specific languages
String upperLocale = "istanbul".toUpperCase(java.util.Locale.forLanguageTag("tr"));

// Substring
String sub1 = "Hello World".substring(6);     // "World" — from index 6 to the end
String sub2 = "Hello World".substring(0, 5);  // "Hello" — from 0 up to before 5

// Replace — replace characters or substrings
String r1 = "aababc".replace('a', 'x');         // "xxbxbc" — replace a char
String r2 = "hello hello".replace("hello", "hi"); // "hi hi" — replace all substrings
String r3 = "a1b2c3".replaceAll("[0-9]", "#");  // "a#b#c#" — replace with a regex
String r4 = "a1b2c3".replaceFirst("[0-9]", "#");// "a#b2c3" — replace only the first

// Split — split a String into an array
String[] words = "apple,orange,mango".split(",");
// ["apple", "orange", "mango"]

String[] delimited = "a  b   c".split("\\s+");
// ["a", "b", "c"] — split by one or more whitespaces

// Split with a maximum number of parts
String[] limited = "a:b:c:d".split(":", 2);
// ["a", "b:c:d"] — maximum 2 parts

// Concat — avoid for many operations, use StringBuilder
String combined = "Hello".concat(" World"); // "Hello World"

// Comparison
int cmp = "apple".compareTo("orange");         // negative — "apple" < "orange" lexicographically
int cmpIgnore = "APPLE".compareToIgnoreCase("apple"); // 0 — equal
boolean eq = "hello".equalsIgnoreCase("HELLO"); // true

StringBuilder and StringBuffer #

Because String is immutable, doing many String concatenations in a loop is very inefficient — every + creates a new String object. Use StringBuilder for intensive string operations.

// ✗ ANTI-PATTERN: concatenation in a loop — O(n²) operations
String result = "";
for (int i = 0; i < 10000; i++) {
    result += i + ","; // creates 10,000 new Strings that are immediately discarded!
}

// ✓ CORRECT: StringBuilder — O(n) operations, far more efficient
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 10000; i++) {
    sb.append(i).append(",");
}
String efficientResult = sb.toString();

// StringBuilder operations
StringBuilder builder = new StringBuilder("Hello");

builder.append(" World");          // "Hello World"
builder.append('!');               // "Hello World!"
builder.insert(5, ",");            // "Hello, World!" — insert at index 5
builder.delete(5, 6);              // "Hello World!" — delete from 5 up to before 6
builder.deleteCharAt(11);          // "Hello World" — delete the character at index 11
builder.replace(6, 11, "Java");    // "Hello Java" — replace a range with a new string
builder.reverse();                 // "avaJ olleH"
builder.setCharAt(0, 'J');         // change the character at index 0

// Size and capacity
int length = builder.length();    // number of characters present
int capacity = builder.capacity(); // internal buffer size (can be larger than length)

// StringBuilder vs StringBuffer
// StringBuilder — not thread-safe, but faster (use this almost always)
// StringBuffer  — thread-safe (synchronized), use only when concurrency is truly needed

// Performance tip: give an initial capacity if you know the approximate length
StringBuilder preAlloc = new StringBuilder(10000); // avoids internal buffer resizes
for (int i = 0; i < 10000; i++) {
    preAlloc.append(i).append(",");
}

String Formatting #

String.format() and printf #

// String.format — create a formatted string
String name = "Budi";
int age = 30;
double salary = 8500000.50;

String info = String.format("Name: %s, Age: %d years", name, age);
// "Name: Budi, Age: 30 years"

// Number formatting
String decimal = String.format("%.2f", salary);           // "8500000.50"
String scientific = String.format("%e", salary);              // "8.500001e+06"
String hex = String.format("%x", 255);                   // "ff"
String octal = String.format("%o", 8);                   // "10"

// Width and alignment
String right = String.format("|%10s|", "right");         // "|     right|" — right-aligned
String left = String.format("|%-10s|", "left");          // "|left      |" — left-aligned
String zero = String.format("|%05d|", 42);                // "|00042|" — zero padding

// Common format characters:
// %s  → String
// %d  → integer (decimal)
// %f  → float/double
// %.Nf → float with N decimals
// %e  → scientific notation
// %b  → boolean
// %c  → character
// %n  → newline (portable, differs on Windows/Unix)
// %x  → hex lowercase, %X uppercase
// %t  → date/time (many sub-formats)

// Date formatting
java.time.LocalDateTime now = java.time.LocalDateTime.now();
String date = String.format("%tF", now);              // "2024-05-10"
String time = String.format("%tT", now);                // "14:30:00"
String full = String.format("%1$tF %1$tT", now);      // use the same argument twice

// printf to System.out
System.out.printf("Hello, %s! You are %d years old.%n", name, age);

Formatted (Java 15+) and Text Blocks (Java 15+) #

// String.formatted() — the instance method version of String.format
String message = "Welcome, %s! Balance: Rp %.2f".formatted("Sari", 1500000.0);
// "Welcome, Sari! Balance: Rp 1500000.00"

// Text Blocks — multi-line strings without escape hell (Java 15+)
// ✗ ANTI-PATTERN: the old way — full of escapes and hard to read
String oldJson = "{\n" +
    "    \"name\": \"Laptop\",\n" +
    "    \"price\": 15000000\n" +
    "}";

// ✓ CORRECT: Text Block — clean and easy to read
String newJson = """
        {
            "name": "Laptop",
            "price": 15000000
        }
        """;

// Text Blocks automatically remove consistent indentation
// (indentation is relative to the position of the closing quotes)

// Text Block with formatting
String query = """
        SELECT p.name, p.price
        FROM products p
        WHERE p.category = '%s'
        ORDER BY p.price %s
        LIMIT %d
        """.formatted("Electronics", "ASC", 10);

// Text Block for HTML
String html = """
        <!DOCTYPE html>
        <html>
            <head><title>%s</title></head>
            <body>
                <h1>%s</h1>
            </body>
        </html>
        """.formatted("Page Title", "Welcome");

// Escapes inside a Text Block
String withQuotes = """
        Single quotes ' don't need escaping.
        Double quotes " don't need escaping.
        Three quotes \"\"\" need one escape.
        Backslash \\\\ needs escaping.
        """;

Parsing and Conversion #

String to Primitive Types #

// Parsing Strings to numeric types
int intNumber = Integer.parseInt("42");
long longNumber = Long.parseLong("9876543210");
double doubleNumber = Double.parseDouble("3.14");
float floatNumber = Float.parseFloat("2.71");
boolean boolTrue = Boolean.parseBoolean("true");  // case insensitive
boolean boolFalse = Boolean.parseBoolean("TRUE"); // true

// Parsing with a radix (number base)
int hex = Integer.parseInt("FF", 16);       // 255
int binary = Integer.parseInt("1010", 2);    // 10
int octal = Integer.parseInt("17", 8);      // 15

// Error handling — always catch NumberFormatException
try {
    int result = Integer.parseInt("not a number");
} catch (NumberFormatException e) {
    System.out.println("Not a valid number: " + e.getMessage());
}

// A safe way without exceptions — convert to Optional
java.util.OptionalInt safe = java.util.OptionalInt.empty();
try {
    safe = java.util.OptionalInt.of(Integer.parseInt("42"));
} catch (NumberFormatException ignored) {}

// Primitive types to String
String fromInt = String.valueOf(42);          // "42"
String fromDouble = String.valueOf(3.14);     // "3.14"
String fromBoolean = String.valueOf(true);    // "true"
String fromChar = String.valueOf('A');        // "A"

// Or use the wrapper class methods
String hex2 = Integer.toHexString(255);       // "ff"
String binary2 = Integer.toBinaryString(10);   // "1010"
String octal2 = Integer.toOctalString(8);     // "10"

// toString() — an alternative, but note the autoboxing overhead
String s = Integer.toString(42);              // "42"
String s2 = "" + 42;                          // "42" — but creates more objects

Character and Encoding Conversion #

import java.nio.charset.StandardCharsets;

// String to bytes with a specific encoding
byte[] utf8 = "Java Programming".getBytes(StandardCharsets.UTF_8);
byte[] latin1 = "Hello".getBytes(StandardCharsets.ISO_8859_1);

// Bytes to String with a specific encoding
String back = new String(utf8, StandardCharsets.UTF_8);

// Base64 encoding/decoding (Java 8+)
String original = "Secret: password123";
String encoded = java.util.Base64.getEncoder()
    .encodeToString(original.getBytes(StandardCharsets.UTF_8));
// "U2VjcmV0OiBwYXNzd29yZDEyMw=="

byte[] decoded = java.util.Base64.getDecoder().decode(encoded);
String backAgain = new String(decoded, StandardCharsets.UTF_8);
// "Secret: password123"

// URL-safe Base64 (for URLs and filenames)
String urlSafe = java.util.Base64.getUrlEncoder()
    .encodeToString(original.getBytes(StandardCharsets.UTF_8));

Regular Expressions with String #

Java has a complete regex API in java.util.regex, but many regex operations can be done directly from String methods.

import java.util.regex.Pattern;
import java.util.regex.Matcher;

// Validation with matches() — checks whether the ENTIRE string matches the pattern
boolean emailValid = "[email protected]".matches("[\\w.]+@[\\w.]+\\.[a-z]{2,}");
boolean phoneNumber = "08123456789".matches("0[89][0-9]{8,10}");
boolean onlyDigits = "12345".matches("\\d+");

// ✗ ANTI-PATTERN: compiling a Pattern in a loop — very slow
for (String item : emailList) {
    if (item.matches("[\\w.]+@[\\w.]+\\.[a-z]{2,}")) { ... }
    // Pattern.compile() is called again on every iteration!
}

// ✓ CORRECT: compile the Pattern once, reuse it many times
Pattern emailPattern = Pattern.compile("[\\w.]+@[\\w.]+\\.[a-z]{2,}");
for (String item : emailList) {
    if (emailPattern.matcher(item).matches()) { ... }
}

// Extract groups from a string
Pattern datePattern = Pattern.compile("(\\d{4})-(\\d{2})-(\\d{2})");
Matcher matcher = datePattern.matcher("Date: 2024-05-10 is today");

if (matcher.find()) {
    String year = matcher.group(1);  // "2024"
    String month = matcher.group(2);  // "05"
    String day = matcher.group(3);   // "10"
    System.out.println("Year: " + year + ", Month: " + month + ", Day: " + day);
}

// Find ALL matches
Pattern numberPattern = Pattern.compile("\\d+");
Matcher m = numberPattern.matcher("There are 3 cats and 12 dogs in 2 houses");
while (m.find()) {
    System.out.println("Number: " + m.group() + " at position " + m.start());
}
// Number: 3 at position 11
// Number: 12 at position 23
// Number: 2 at position 33

// Named groups (Java 7+) — more expressive than numbered groups
Pattern named = Pattern.compile("(?<year>\\d{4})-(?<month>\\d{2})-(?<day>\\d{2})");
Matcher nm = named.matcher("2024-05-10");
if (nm.matches()) {
    System.out.println("Year: " + nm.group("year"));
    System.out.println("Month: " + nm.group("month"));
}

// replaceAll with a regex — already covered above
String clean = "Hello    World   !!!".replaceAll("\\s+", " ").trim();
// "Hello World !!!"

// split with a regex
String[] tokens = "one, two ,three , four".split("\\s*,\\s*");
// ["one", "two", "three", "four"]

String in Modern Java #

String API Java 9–21 #

// chars() — an IntStream of each character's codepoint (Java 9+)
"Hello".chars()
    .forEach(c -> System.out.print((char) c + " "));
// H e l l o

// lines() — a Stream<String> line by line (Java 11+)
String multiline = "line one\nline two\nline three";
long lineCount = multiline.lines().count(); // 3
multiline.lines()
    .filter(line -> line.contains("two"))
    .forEach(System.out::println);
// "line two"

// strip(), stripLeading(), stripTrailing() (Java 11+)
// Unicode-whitespace aware, better than trim()
"  \u2000hello\u2000  ".strip(); // "hello" — removes Unicode whitespace

// repeat() (Java 11+)
String line = "-".repeat(40); // "----------------------------------------"

// isBlank() (Java 11+) — true if empty or whitespace only
"".isBlank();    // true
"   ".isBlank(); // true
" a ".isBlank(); // false

// indent() (Java 12+) — add or remove indentation on every line
String code = "int x = 1;\nint y = 2;";
String indented = code.indent(4);
// "    int x = 1;\n    int y = 2;\n"

// transform() (Java 12+) — apply a function to the String
String result = "  hello world  "
    .transform(String::strip)
    .transform(s -> s.substring(0, 1).toUpperCase() + s.substring(1));
// "Hello world"

// formatted() (Java 15+) — already covered in the formatting section

// String.valueOf() for null-safe conversion
Object obj = null;
String safe = String.valueOf(obj); // "null" — doesn't throw a NullPointerException
// (String) obj → NullPointerException!

Common Patterns and Anti-Patterns #

// ✗ ANTI-PATTERN: comparing Strings with ==
String a = new String("hello");
String b = new String("hello");
if (a == b) { }  // ALWAYS false! Compares references, not values

// ✓ CORRECT: use equals()
if (a.equals(b)) { }  // true

// ✓ CORRECT: if one side can be null, put the literal on the left
if ("hello".equals(maybeNullVariable)) { }  // doesn't throw an NPE
// ✗: if (maybeNullVariable.equals("hello")) → NPE if null!

// ✗ ANTI-PATTERN: concatenation in a loop (already covered)
String s = "";
for (String item : list) {
    s += item + ","; // creates a new object every iteration
}

// ✓ CORRECT: StringBuilder
StringBuilder sb = new StringBuilder();
for (String item : list) {
    sb.append(item).append(",");
}

// ✓ EVEN BETTER: String.join() for simple cases
String joined = String.join(",", list);

// ✓ OR: Collectors.joining() for Streams
String fromStream = list.stream()
    .collect(java.util.stream.Collectors.joining(", ", "[", "]"));
// "[item1, item2, item3]"

// ✗ ANTI-PATTERN: not specifying a charset when converting bytes
byte[] bytes = text.getBytes();              // platform default charset!
String fromBytes = new String(bytes);        // platform default charset!

// ✓ CORRECT: always specify the charset explicitly
byte[] bytesUTF8 = text.getBytes(StandardCharsets.UTF_8);
String fromBytesUTF8 = new String(bytesUTF8, StandardCharsets.UTF_8);

// ✗ ANTI-PATTERN: compiling a Pattern inside a frequently called method
public boolean isEmail(String s) {
    return s.matches("[\\w.]+@[\\w.]+\\.[a-z]{2,}"); // compiles the Pattern every time!
}

// ✓ CORRECT: Pattern as a static final field
private static final Pattern EMAIL_PATTERN =
    Pattern.compile("[\\w.]+@[\\w.]+\\.[a-z]{2,}");

public boolean isEmail(String s) {
    return EMAIL_PATTERN.matcher(s).matches();
}

String Comparison and Sorting #

The “correct” alphabetical ordering for a specific language can’t rely on Unicode ordering alone. compareTo() sorts by Unicode values — capital letters are always smaller than lowercase letters, and accented characters aren’t ordered per language conventions.

import java.text.Collator;
import java.util.Comparator;
import java.util.List;
import java.util.Locale;

// compareTo() — lexicographic order based on Unicode values
List<String> fruits = new java.util.ArrayList<>(
    List.of("mango", "Apple", "orange", "Banana")
);
fruits.sort(String::compareTo);
System.out.println(fruits);
// [Apple, Banana, mango, orange] — capitals come BEFORE lowercase in Unicode!

// compareToIgnoreCase() — more natural, ignores case
fruits.sort(String::compareToIgnoreCase);
System.out.println(fruits);
// [Apple, Banana, mango, orange]

// Chained comparators for complex sorting
List<String> data = List.of("Budi", "ani", "Citra", "budi", "ANI");
List<String> sorted = data.stream()
    .sorted(Comparator.comparingInt(String::length)   // sort by length first
        .thenComparing(String.CASE_INSENSITIVE_ORDER)) // then alphabetically
    .toList();

// ✓ CORRECT: Collator for proper linguistic ordering per language
Collator collator = Collator.getInstance(new Locale("id", "ID")); // Indonesian
collator.setStrength(Collator.SECONDARY); // ignore minor case differences
fruits.sort(collator::compare); // linguistically correct ordering

String and Security #

Passwords — Don’t Use String #

// ✗ ANTI-PATTERN: storing a password as a String
String password = "secret123";
// Problems:
// 1. Strings are interned in the String pool — stay in memory until GC runs
// 2. Strings can't be zeroed out — they can appear in heap dumps

// ✓ CORRECT: use char[] for sensitive data
char[] passwordChar = "secret123".toCharArray();
try {
    authenticate(passwordChar);
} finally {
    // Zero out after use — explicitly remove from memory
    java.util.Arrays.fill(passwordChar, '\0');
}

// Standard Java APIs follow this practice:
// javax.crypto.SecretKey, java.security.KeyStore, and SSL contexts
// all use char[] or byte[] for sensitive credentials

Log Injection #

// ✗ ANTI-PATTERN: logging user input directly
String userInput = request.getParameter("name");
logger.info("User login: " + userInput);
// If input = "admin\nINFO: Fake admin login successful"
// The log will be contaminated with a fake line!

// ✓ CORRECT: sanitize newlines from input before logging
String sanitized = userInput.replaceAll("[\r\n]", "_");
logger.info("User login: {}", sanitized); // parameterized logging — safer and faster

Summary #

  • Strings are immutable — every “modification” operation produces a new object. This is automatically thread-safe, but memory-wasteful if done repeatedly in a loop.
  • Always use equals() to compare String values, not ==. Put the literal on the left side ("hello".equals(variable)) to avoid NullPointerException.
  • Use StringBuilder for String concatenation in loops or repeated operations. For simple cases, String.join() or Collectors.joining() is more expressive than a manual loop.
  • Text Blocks (Java 15+) eliminate the need for escapes and concatenation for multi-line strings like JSON, SQL, and HTML. Use them for much better readability.
  • Compile Patterns once as static final fields, not inside frequently called methods. Regex compilation is expensive — don’t repeat it every time.
  • Always specify an explicit charset (StandardCharsets.UTF_8) when converting between String and byte[]. Relying on the platform default charset causes bugs that are very hard to reproduce.
  • strip() (Java 11+) is better than trim() because it’s Unicode-whitespace aware. isBlank() is more expressive than isEmpty() for checking whitespace-only strings.
  • String.valueOf(obj) is the null-safe way to convert an object to a String — it returns "null" if the object is null, not a NullPointerException.

← Previous: Articles & Resources   Next: IO →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact