Skip to main content
Java · lesson 9 of 66
Open the Java page →
Java beginner Lesson 9 of 66

Strings in Java

Learn Java strings by running them: immutability, == vs equals, the string pool, core methods, formatting, text blocks, and StringBuilder.

By SkillByExample · Updated

String is the most-used class in Java. Two facts explain most string bugs: strings are immutable, and == compares objects, not text. Every example is a single file run with the JDK 21 source launcher (java File.java).

Immutability

public class ImmutableDemo {
    public static void main(String[] args) {
        String s = "hello";
        s.toUpperCase();                 // result thrown away
        System.out.println(s);

        s = s.toUpperCase();             // capture the new String
        System.out.println(s);

        String original = "data";
        String alias = original;
        original = original + "-changed";
        System.out.println(alias + " / " + original);
    }
}
$ java ImmutableDemo.java
hello
HELLO
data / data-changed

toUpperCase returns a new string and leaves s alone, so the first call does nothing useful. Concatenation also builds a new object: alias still points at the old "data", which no code can change. That is why strings are safe to share and to use as map keys.

== versus equals and the string pool

public class EqualityDemo {
    public static void main(String[] args) {
        String a = "java";
        String b = "java";
        String c = new String("java");
        String d = "ja" + "va";                      // constant, folded at compile time
        String part = "ja";
        String e = part + "va";                      // built at runtime

        System.out.println((a == b) + " " + (a == c) + " " + (a == d) + " " + (a == e));
        System.out.println(a.equals(c) + " " + a.equals(e));
        System.out.println(a == e.intern());
        System.out.println(a.equalsIgnoreCase("JAVA"));
    }
}
$ java EqualityDemo.java
true false true false
true true
true
true

Identical literals — including "ja" + "va", which the compiler folds into one constant — share a single pooled object, so == happens to be true. new String and strings built at runtime are separate objects, so == is false even though the text matches. intern() returns the pooled copy. Compare content with equals, every time.

Core methods

import java.util.Arrays;

public class MethodsDemo {
    public static void main(String[] args) {
        String s = "  Hello, Java World!  ";
        System.out.println(s.length() + " " + s.strip().length());
        System.out.println("charAt(7)='" + s.charAt(7) + "' indexOf(Java)=" + s.indexOf("Java") + " lastIndexOf(o)=" + s.lastIndexOf("o"));
        System.out.println("[" + s.substring(9, 13) + "]");
        System.out.println(s.contains("Java") + " " + s.strip().startsWith("Hello") + " " + s.isBlank() + " " + "   ".isBlank());
        System.out.println("[" + s.replace("Java", "Python") + "]");
        System.out.println(s.strip().toLowerCase().replace(" ", "_"));
        System.out.println(Arrays.toString("a,b,,c".split(",")));
        System.out.println(String.join(" | ", "Alice", "Bob", "Charlie"));
        System.out.println("ab".repeat(3) + " " + "x".compareTo("y") + " " + "apple".compareTo("apple"));
    }
}
$ java MethodsDemo.java
22 18
charAt(7)=',' indexOf(Java)=9 lastIndexOf(o)=15
[Java]
true true false true
[  Hello, Python World!  ]
hello,_java_world!
[a, b, , c]
Alice | Bob | Charlie
ababab -1 0

Indexes count the two leading spaces, so charAt(7) is the comma and "Java" starts at index 9; substring(9, 13) takes indexes 9–12 (the end is exclusive). isBlank is true for whitespace-only strings, unlike isEmpty. compareTo returns a negative, zero or positive number — only the sign is meaningful — and is what sorting uses.

Formatting and text blocks

public class FormatDemo {
    public static void main(String[] args) {
        String msg = String.format("Name: %s, Age: %d, Score: %.2f", "Alice", 25, 98.456);
        System.out.println(msg);

        System.out.printf("[%-8s][%5d][%05d][%,d][%x][%b]%n", "Bob", 42, 42, 1234567, 255, true);
        System.out.println("%s scored %d".formatted("Cara", 88));

        String json = """
            {
              "name": "Alice",
              "active": true
            }""";
        System.out.println(json);
        System.out.println(json.lines().count() + " lines");
    }
}
$ java FormatDemo.java
Name: Alice, Age: 25, Score: 98.46
[Bob     ][   42][00042][1,234,567][ff][true]
Cara scored 88
{
  "name": "Alice",
  "active": true
}
4 lines

%-8s left-aligns in an 8-wide field, %5d right-aligns, %05d pads with zeros, and %,d adds grouping separators. A text block (""", Java 15+) strips the common indentation of its lines, so the JSON starts at column 0 even though the source is indented; closing the quotes on the last line avoids a trailing newline.

StringBuilder

public class StringBuilderDemo {
    public static void main(String[] args) {
        StringBuilder sb = new StringBuilder("Hello");
        sb.append(", World");
        System.out.println(sb);
        sb.insert(5, " there");
        System.out.println(sb);
        sb.delete(5, 11);
        System.out.println(sb);
        sb.replace(7, 12, "Java");
        System.out.println(sb + " length=" + sb.length());
        System.out.println(sb.reverse());
        System.out.println(sb);

        String csv = new StringBuilder().append("Alice").append(',').append(30).append(',').append(true).toString();
        System.out.println(csv);
    }
}
$ java StringBuilderDemo.java
Hello, World
Hello there, World
Hello, World
Hello, Java length=11
avaJ ,olleH
avaJ ,olleH
Alice,30,true

Unlike String, a StringBuilder changes in place: every mutating method edits the same buffer and returns the builder itself, which is what makes chaining work. Note that reverse() mutated sb — the line after it prints the reversed text again, not the original.

Why StringBuilder in loops

public class LoopConcat {
    public static void main(String[] args) {
        int n = 20_000;

        long start = System.nanoTime();
        String s = "";
        for (int i = 0; i < n; i++) s += i % 10;
        long plusMs = (System.nanoTime() - start) / 1_000_000;

        start = System.nanoTime();
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < n; i++) sb.append(i % 10);
        String built = sb.toString();
        long builderMs = (System.nanoTime() - start) / 1_000_000;

        System.out.println("same result: " + s.equals(built) + ", length " + built.length());
        System.out.println("+= at least 5x slower: " + (plusMs > 5 * Math.max(builderMs, 1)));
    }
}
$ java LoopConcat.java
same result: true, length 20000
+= at least 5x slower: true

Each s += ... copies the whole string so far into a new object, so the loop does roughly n²/2 character copies; the builder appends into one growing buffer. Exact timings depend on the machine, so the program prints the comparison rather than milliseconds. Outside loops, plain + is fine — the compiler already optimises a single expression.

Putting it together: a text analyzer

import java.util.Comparator;
import java.util.Map;
import java.util.TreeMap;

public class TextAnalyzer {
    public static void main(String[] args) {
        String text = "The quick brown fox jumps over the lazy dog. Pack my box with five dozen liquor jugs.";
        String[] words = text.toLowerCase().split("[^a-z]+");
        System.out.println("words:     " + words.length);
        System.out.println("letters:   " + text.chars().filter(Character::isLetter).count());
        System.out.println("sentences: " + text.split("[.!?]\\s*").length);
        System.out.println("vowels:    " + text.chars().filter(c -> "aeiouAEIOU".indexOf(c) >= 0).count());

        Map<String, Integer> freq = new TreeMap<>();
        for (String w : words) freq.merge(w, 1, Integer::sum);
        Map.Entry<String, Integer> top = freq.entrySet().stream()
            .max(Map.Entry.<String, Integer>comparingByValue()
                 .thenComparing(Map.Entry.comparingByKey(Comparator.reverseOrder())))
            .orElseThrow();
        System.out.println("top word:  " + top.getKey() + " (" + top.getValue() + "x)");
    }
}
$ java TextAnalyzer.java
words:     17
letters:   67
sentences: 2
vowels:    22
top word:  the (2x)

Splitting on [^a-z]+ treats any run of non-letters as one separator, so "dog. Pack" yields two clean words. Here no empty strings appear because the text starts with a letter — a leading separator would produce an empty first element. The tie-breaker makes the top word deterministic: highest count, then alphabetically first.

Practice

  1. Write isPalindrome(String) that ignores case, spaces and punctuation, using StringBuilder.reverse(), and test one palindrome and one non-palindrome.
Solution
public class Palindrome {
    static boolean isPalindrome(String s) {
        String clean = s.toLowerCase().replaceAll("[^a-z0-9]", "");
        return clean.equals(new StringBuilder(clean).reverse().toString());
    }

    public static void main(String[] args) {
        System.out.println(isPalindrome("A man, a plan, a canal: Panama"));
        System.out.println(isPalindrome("Hello, World"));
    }
}
$ java Palindrome.java
true
false
  1. Capitalise the first letter of every word in "the quick brown fox" while keeping the double space.
Solution
public class TitleCase {
    public static void main(String[] args) {
        String input = "the quick  brown fox";
        StringBuilder sb = new StringBuilder(input.length());
        boolean startOfWord = true;
        for (char c : input.toCharArray()) {
            sb.append(startOfWord ? Character.toUpperCase(c) : c);
            startOfWord = c == ' ';
        }
        System.out.println("[" + sb + "]");
    }
}
$ java TitleCase.java
[The Quick  Brown Fox]

Walking characters keeps the original spacing; split(" ") followed by join would also work but needs care with the empty token between the two spaces.

  1. Count the vowels and consonants in a sentence, ignoring digits, spaces and punctuation.
Solution
public class VowelCount {
    public static void main(String[] args) {
        String s = "Java 21 is here!";
        long vowels = s.toLowerCase().chars().filter(c -> "aeiou".indexOf(c) >= 0).count();
        long letters = s.chars().filter(Character::isLetter).count();
        System.out.println("vowels=" + vowels + " consonants=" + (letters - vowels));
    }
}
$ java VowelCount.java
vowels=5 consonants=5
  1. Show the difference between trim() and strip() on a string padded with the Unicode em space  .
Solution
public class StripVsTrim {
    public static void main(String[] args) {
        String s = "  padded  ";
        System.out.println("trim:  [" + s.trim() + "] length " + s.trim().length());
        System.out.println("strip: [" + s.strip() + "] length " + s.strip().length());
    }
}
$ java StripVsTrim.java
trim:  [  padded  ] length 10
strip: [padded] length 6

trim() only removes characters up to U+0020, so the em spaces survive; strip() understands Unicode whitespace.

Frequently Asked Questions

Why is String immutable in Java?
Immutability lets the JVM share one String object between many references, which is what makes the string pool safe. It also means strings can be passed to other methods, shared across threads and used as HashMap keys without anyone changing them underneath you. Methods that look like modifications return a new String instead.
Why does == sometimes work for comparing strings in Java?
== compares object identity, not content. Identical string literals are interned in the string pool, so two literals with the same text are the same object and == returns true. Strings built at runtime, read from input or created with new String are separate objects, so == returns false even when the text matches. Always compare content with equals.
When should I use StringBuilder instead of + in Java?
Use StringBuilder when building a string in a loop. Each + inside a loop creates a new String and copies everything so far, which is quadratic work for long results. For a single expression like "Hello, " + name, the compiler already generates efficient code, so plain + is fine and easier to read.
What is the difference between StringBuilder and StringBuffer?
They have the same API, but every StringBuffer method is synchronized so one instance can be shared between threads. StringBuilder has no synchronization and is the right default, because string building almost always happens inside a single method on one thread. StringBuffer is mostly found in older code.
What is the difference between trim() and strip()?
trim() removes characters with code points up to U+0020, which covers ASCII spaces and control characters. strip(), added in Java 11, removes all Unicode whitespace, including characters such as the em space U+2003 that trim() leaves in place. Prefer strip(), stripLeading() and stripTrailing() in new code.