Java Strings

BEGINNER ~8 min read Tutorial

A String is a sequence of characters. In Java, strings are objects — instances of the java.lang.String class — but the language gives them special treatment: you write literals in double quotes, and the + operator concatenates them. Strings are immutable: once created, their contents never change.

This tutorial covers creation, the most useful methods, immutability and why it matters, the String pool, StringBuilder for efficient concatenation, and modern features like text blocks (multi-line string literals).

1. Creating Strings

java
class=class="tok-str">"tok-cmt">// literal - reused from the string pool
String a = "Hello";

class=class="tok-str">"tok-cmt">// new - always a fresh object
String b = new String("Hello");

class=class="tok-str">"tok-cmt">// from a char array
char[] chars = {'J', 'a', 'v', 'a'};
String c = new String(chars);

class=class="tok-str">"tok-cmt">// from a byte array (with a charset)
byte[] bytes = "Java".getBytes();
String d = new String(bytes);

class=class="tok-str">"tok-cmt">// from a StringBuilder
String e = new StringBuilder().append("Hi").append("!").toString();

class=class="tok-str">"tok-cmt">// empty string
String empty = "";
String alsoEmpty = String.Empty;   class=class="tok-str">"tok-cmt">// does not exist - use ""

Strings created with new always allocate a fresh object. String literals are taken from the JVM's string pool: when the same literal appears multiple times, the JVM reuses one String instance. This is an optimisation, and it is why == on literals sometimes appears to work — but you must never rely on it.

2. Equality: .equals() vs ==

The single most common string bug in Java is using == to compare contents:

java
String a = "hello";
String b = "hello";
String c = new String("hello");

System.out.println(a == b);          class=class="tok-str">"tok-cmt">// true (pool reuse) - DON'T rely on this!
System.out.println(a == c);          class=class="tok-str">"tok-cmt">// false (c is a fresh object)
System.out.println(a.equals(c));     class=class="tok-str">"tok-cmt">// true  - correct content comparison
System.out.println(a.equalsIgnoreCase("HELLO"));   class=class="tok-str">"tok-cmt">// true

class=class="tok-str">"tok-cmt">// null-safe comparison
import java.util.Objects;
String maybeNull = null;
System.out.println(Objects.equals(a, maybeNull));   class=class="tok-str">"tok-cmt">// false, no NPE
Always use .equals() for content comparison

Use s1.equals(s2) or, for null safety, Objects.equals(s1, s2). The method equalsIgnoreCase compares ignoring case.

3. Useful String Methods

Here is a tour of the methods you will use most often:

java
String s = "Hello, Java!";

s.length();              class=class="tok-str">"tok-cmt">// class="tok-num">12
s.charAt(class="tok-num">0);             class=class="tok-str">"tok-cmt">// 'H'
s.indexOf("Java");       class=class="tok-str">"tok-cmt">// class="tok-num">7
s.substring(class="tok-num">7);          class=class="tok-str">"tok-cmt">// "Java!"
s.substring(class="tok-num">0, class="tok-num">5);       class=class="tok-str">"tok-cmt">// "Hello"
s.toUpperCase();         class=class="tok-str">"tok-cmt">// "HELLO, JAVA!"
s.toLowerCase();         class=class="tok-str">"tok-cmt">// "hello, java!"
s.trim();                class=class="tok-str">"tok-cmt">// remove leading/trailing whitespace
s.replace('l', 'L');     class=class="tok-str">"tok-cmt">// "HeLLo, Java!"
s.replaceAll("[aeiou]", "*");   class=class="tok-str">"tok-cmt">// regex - "H*ll*, J*v*!"
s.split(", ");           class=class="tok-str">"tok-cmt">// ["Hello", "Java!"]
s.contains("Java");      class=class="tok-str">"tok-cmt">// true
s.startsWith("Hello");   class=class="tok-str">"tok-cmt">// true
s.endsWith("!");         class=class="tok-str">"tok-cmt">// true
s.equals("Hello, Java!"); class=class="tok-str">"tok-cmt">// true
s.isEmpty();             class=class="tok-str">"tok-cmt">// false
String.format("%s has %d chars", s, s.length());  class=class="tok-str">"tok-cmt">// formatted

4. Immutability and Why It Matters

Once a String object is created, its character sequence cannot change. Operations that appear to modify a String actually return a new String:

java
String s = "Hello";
s.concat(" World");      class=class="tok-str">"tok-cmt">// returns "Hello World" but s is still "Hello"
s.toUpperCase();          class=class="tok-str">"tok-cmt">// returns "HELLO" but s is still "Hello"

class=class="tok-str">"tok-cmt">// to keep the change, reassign:
s = s.concat(" World");   class=class="tok-str">"tok-cmt">// now s is "Hello World"

class=class="tok-str">"tok-cmt">// strings as HashMap keys - safe because hashcode never changes
Map<String, Integer> counts = new HashMap<>();
counts.put("apple", class="tok-num">3);
class=class="tok-str">"tok-cmt">// there is no way to mutate "apple" and break the map

Immutability gives four concrete benefits. Strings are thread-safe without synchronisation. The JVM can pool literals safely. Strings can be used as HashMap keys without fear of their hashcode changing. Sensitive data such as file paths or SQL is safe from being mutated after construction.

5. StringBuilder for Efficient Concatenation

In a tight loop, + on strings is wasteful: each + creates a new String object and copies the characters. StringBuilder holds an internal mutable buffer and grows it as needed:

java
class=class="tok-str">"tok-cmt">// SLOW: each + creates a new String
String csv = "";
for (int i = class="tok-num">0; i < class="tok-num">1000; i++) {
    csv += i + ",";     class=class="tok-str">"tok-cmt">// O(n^class="tok-num">2) - class="tok-num">1000 allocations
}

class=class="tok-str">"tok-cmt">// FAST: StringBuilder mutates a single buffer
StringBuilder sb = new StringBuilder();
for (int i = class="tok-num">0; i < class="tok-num">1000; i++) {
    sb.append(i).append(",");
}
String csv2 = sb.toString();   class=class="tok-str">"tok-cmt">// O(n)

class=class="tok-str">"tok-cmt">// Java class="tok-num">8+ Stream approach
String csv3 = java.util.stream.IntStream.range(class="tok-num">0, class="tok-num">1000)
    .mapToObj(String::valueOf)
    .collect(java.util.stream.Collectors.joining(","));

For single-line concatenations of two or three strings, the JVM optimises + into a StringBuilder call for you. The concern is only with loops or repeated concatenation across statements.

6. Text Blocks (Java 15+)

Multi-line string literals are written with three double quotes:

java
String json = """
        {
          "name": "Alice",
          "age": class="tok-num">30,
          "roles": ["admin", "user"]
        }
        """;

class=class="tok-str">"tok-cmt">// strip incidental whitespace with indent()
String sql = """
    SELECT id, name, email
    FROM   users
    WHERE  active = true
    ORDER  BY name
    """;

Text blocks preserve the line structure. Leading whitespace is stripped intelligently (the JVM aligns to the least-indented line). They are perfect for embedding JSON, SQL, HTML or YAML in Java source.

7. String Formatting

Use String.format or System.out.printf for formatted output. The format string uses C-style specifiers:

java
String name = "Alice";
int age = class="tok-num">30;
double salary = class="tok-num">95_000.5;

String s = String.format("%s is %d years old and earns $%,.class="tok-num">2f",
                         name, age, salary);
class=class="tok-str">"tok-cmt">// "Alice is class="tok-num">30 years old and earns $class="tok-num">95,class="tok-num">000.50"

System.out.printf("%-10s | %class="tok-num">5d | %class="tok-num">8.2f%n", name, age, salary);
class=class="tok-str">"tok-cmt">// "Alice      |    class="tok-num">30 | class="tok-num">95000.50"

class=class="tok-str">"tok-cmt">// %s string, %d int, %f double, %b boolean, %c char, %n newline
class=class="tok-str">"tok-cmt">// %-10s left-aligned in class="tok-num">10 chars, %,d groups thousands, %class="tok-num">5d pad to class="tok-num">5

8. Modern Alternatives

Java 8 added String.join, String.join with a delimiter, and Collectors.joining for streams:

java
class=class="tok-str">"tok-cmt">// String.join with a delimiter
String csv = String.join(",", "a", "b", "c");   class=class="tok-str">"tok-cmt">// "a,b,c"

class=class="tok-str">"tok-cmt">// joining a collection
List<String> names = List.of("Alice", "Bob", "Carol");
String joined = String.join(", ", names);   class=class="tok-str">"tok-cmt">// "Alice, Bob, Carol"

class=class="tok-str">"tok-cmt">// joining via stream collector
String result = names.stream()
    .collect(Collectors.joining(", ", "[", "]"));
class=class="tok-str">"tok-cmt">// "[Alice, Bob, Carol]"

class=class="tok-str">"tok-cmt">// Java class="tok-num">11+
"abc".repeat(class="tok-num">3);   class=class="tok-str">"tok-cmt">// "abcabcabc"
"".isBlank();      class=class="tok-str">"tok-cmt">// true
"  hi  ".strip();  class=class="tok-str">"tok-cmt">// "hi" (Unicode-aware)
"a\nb\nc".lines();   class=class="tok-str">"tok-cmt">// Stream of "a","b","c"

Java 11 added String.repeat, String.isBlank, String.strip (Unicode-aware trim) and String.lines for splitting by line terminators.

Exercises

  1. Reverse a string with new StringBuilder(s).reverse().
  2. Count the vowels in a string by iterating with a for loop and switch.
  3. Replace all spaces in a string with underscores without using replaceAll.
  4. Build a 1000-line CSV string with StringBuilder and print its length.
  5. Write a multi-line SQL query using a text block and print it.