String Class
String is one of the most fundamental classes in Java — used in virtually every program, yet deeply misunderstood by many developers. A String represents an immutable sequence of Unicode characters. It is not a primitive type but a full class in java.lang, automatically imported into every Java file. Understanding String means understanding how it is stored in memory, why it is immutable, how the string pool works, what the difference between == and equals() means for strings, and how to use the class efficiently. This entry covers String's nature as a class, its internal representation, the critical distinction between reference equality and value equality, String's place in the type hierarchy, and the design decisions that make String behave the way it does.
String as a Class — Not a Primitive
// ── String is a class, not a primitive ───────────────────────────────
String s = "Hello"; // s holds a REFERENCE to a String object on the heap
// ── Reference semantics ───────────────────────────────────────────────
String a = "Hello";
String b = a; // b and a hold the SAME reference
// ── == compares references, not content ──────────────────────────────
String x = new String("Hello");
String y = new String("Hello");
System.out.println(x == y); // false — different objects
System.out.println(x.equals(y)); // true — same content
// ── String is final — cannot subclass ────────────────────────────────
// public class MyString extends String { } // COMPILE ERROR
// ── String implements CharSequence ───────────────────────────────────
CharSequence cs = "Hello"; // String is-a CharSequence
CharSequence sb = new StringBuilder("Hello"); // StringBuilder is-a CharSequence
// Methods accepting CharSequence work with both:
public static int charCount(CharSequence seq) {
return seq.length();
}
System.out.println(charCount("Hello")); // 5
System.out.println(charCount(new StringBuilder("Hello"))); // 5
// ── String implements Comparable<String> ─────────────────────────────
String[] words = {"banana", "apple", "cherry"};
Arrays.sort(words); // uses compareTo() — lexicographic natural order
System.out.println(Arrays.toString(words)); // [apple, banana, cherry]
// ── Type hierarchy ────────────────────────────────────────────────────
String str = "Hello";
System.out.println(str instanceof String); // true
System.out.println(str instanceof Object); // true — everything is
System.out.println(str instanceof CharSequence); // true
System.out.println(str instanceof Comparable); // true
System.out.println(str instanceof Serializable); // trueInternal Representation and Memory Layout
// ── String length vs code point count ───────────────────────────────
String ascii = "Hello";
String emoji = "Hello 👋"; // thumbs wave = U+1F44B, a supplementary char
String chinese = "你好"; // BMP characters, 1 char each
System.out.println(ascii.length()); // 5
System.out.println(emoji.length()); // 8 (5 + space + 2 surrogates)
System.out.println(emoji.codePointCount(0, emoji.length())); // 7 (5 + space + 1 emoji)
System.out.println(chinese.length()); // 2
// ── Iterating code points correctly ──────────────────────────────────
String text = "Hi 👋";
// WRONG for supplementary chars:
for (int i = 0; i < text.length(); i++) {
char c = text.charAt(i); // may get half a surrogate pair
}
// CORRECT — iterate by code point:
text.codePoints().forEach(cp ->
System.out.printf("U+%04X %s%n",
cp, Character.getName(cp)));
// ── Hash code caching ─────────────────────────────────────────────────
String s = "hello world";
System.out.println(s.hashCode()); // computes and caches
System.out.println(s.hashCode()); // returns cached value — O(1)
// ── Memory comparison: String vs char[] ──────────────────────────────
// String object overhead (approximate):
// - Object header: 16 bytes
// - char[]/byte[] ref: 8 bytes
// - hash field: 4 bytes
// - coder field: 1 byte (Java 9+)
// - char[] object: 16 bytes header + 2 bytes per char
//
// "Hello" (5 chars): ~16 + 8 + 4 + 1 + 16 + 10 = ~55 bytes
// vs char[] alone: ~16 + 10 = 26 bytes
// The String wrapper adds ~30 bytes overhead per string object
// ── Compact strings (Java 9+) ─────────────────────────────────────────
// Latin-1 strings (all chars <= U+00FF) use 1 byte per char:
// "Hello" → byte[5] = {72, 101, 108, 108, 111}
//
// Non-Latin-1 strings use 2 bytes per char (UTF-16):
// "Héllo" → byte[10] (because é = U+00E9 > U+00FF)
//
// This halves memory for ASCII-heavy applicationsequals() vs == — The Critical Distinction
// ── == vs equals() — the fundamental difference ──────────────────────
String a = "Hello";
String b = "Hello";
String c = new String("Hello");
System.out.println(a == b); // true — same pool object
System.out.println(a == c); // false — c is a new heap object
System.out.println(a.equals(b)); // true — same content
System.out.println(a.equals(c)); // true — same content
// ── Why == sometimes "works" and sometimes fails ──────────────────────
String s1 = "Hello";
String s2 = "Hel" + "lo"; // compile-time constant, interned
System.out.println(s1 == s2); // true — compiler optimised
String part = "Hel";
String s3 = part + "lo"; // runtime concatenation, not interned
System.out.println(s1 == s3); // false — s3 is a new heap object
System.out.println(s1.equals(s3)); // true — same content
// ── Correct comparison patterns ───────────────────────────────────────
// Standard comparison
if (s1.equals(s3)) { System.out.println("same content"); }
// Null-safe comparison (avoids NullPointerException)
String maybeNull = null;
System.out.println(Objects.equals(s1, maybeNull)); // false, no NPE
// Literal on left — protects against NPE if variable is null
if ("Hello".equals(maybeNull)) { } // safe — no NPE
// Case-insensitive comparison
System.out.println("Hello".equalsIgnoreCase("HELLO")); // true
// ── Pitfall: == in switch before Java 7 (now safe with equals) ────────
// Never do:
// if (status == "active") { ... } // only works by accident sometimes
// Always do:
// if ("active".equals(status)) { ... }
// ── intern() — force pool entry ──────────────────────────────────────
String c2 = c.intern(); // return pool entry for c's content
System.out.println(a == c2); // true — now same pool objectString Concatenation — Performance and Mechanics
// ── Compiler translation of + ────────────────────────────────────────
String first = "Hello";
String last = "World";
// What you write:
String full = first + ", " + last + "!";
// What the compiler generates (conceptually):
String full2 = new StringBuilder()
.append(first)
.append(", ")
.append(last)
.append("!")
.toString();
// ── The loop problem — O(n²) without StringBuilder ────────────────────
// WRONG — creates a new StringBuilder on every iteration:
String result = "";
for (int i = 0; i < 10_000; i++) {
result += i + ","; // new StringBuilder each time — O(n²)
}
// CORRECT — one StringBuilder for the whole loop — O(n):
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 10_000; i++) {
sb.append(i).append(",");
}
String efficient = sb.toString();
// ── Performance demonstration ─────────────────────────────────────────
int N = 50_000;
// String += accumulation
long start = System.currentTimeMillis();
String s = "";
for (int i = 0; i < N; i++) s += "x";
System.out.println("String +=: " + (System.currentTimeMillis() - start) + "ms");
// e.g. ~2000ms for N=50,000
// StringBuilder
start = System.currentTimeMillis();
StringBuilder sb2 = new StringBuilder();
for (int i = 0; i < N; i++) sb2.append("x");
String s2 = sb2.toString();
System.out.println("StringBuilder: " + (System.currentTimeMillis() - start) + "ms");
// e.g. ~1ms for N=50,000
// ── When + is fine: non-loop expressions ─────────────────────────────
// These compile to efficient single StringBuilder operations:
String msg = "User " + userId + " logged in at " + timestamp;
String err = "Error: " + ex.getMessage() + " in " + getClass().getName();
// ── String.join() — joining with delimiter ────────────────────────────
String csv = String.join(", ", "Alice", "Bob", "Carol");
System.out.println(csv); // Alice, Bob, Carol
List<String> items = List.of("apple", "banana", "cherry");
String joined = String.join(" | ", items);
System.out.println(joined); // apple | banana | cherry