String Basics and Immutability

Understand what a Java String is, why it can never change, and the null, concatenation and length traps that follow from that.

SyntaxString s = "text"; // literal String t = s.strip(); // methods return a new String s = s.toLowerCase(Locale.ROOT); // reassign to keep the result

A String is an object of class java.lang.String, not a primitive. A literal such as "orders" creates one, and so does almost everything a back-end service touches: request paths, header values, JSON fields, database columns and environment variables.

Immutable means no method changes it

Once a String exists, its characters never change. Methods such as strip(), replace() and toUpperCase() build and return a new String and leave the original alone. The most common beginner bug follows directly from this: calling name.strip(); on a line of its own does nothing useful, because the new String is thrown away. Assign the result instead.

name.strip();          // result discarded, name unchanged
name = name.strip();   // variable now points at the new String

Reassigning a variable is not the same as changing the object. The variable now points at a different String; the old one is untouched, and any other code holding it still sees the old text.

Why the design is worth it

  • Safe to share. A web server handles many requests on many threads. A String can be cached, logged or passed between threads without locking, because nobody can alter it.
  • Safe as a map key. String caches its hash code, and since the content cannot change, a key never ends up in the wrong bucket of a HashMap.
  • Safe after validation. Once you have checked a file name or redirect URL, no other code can change it before you use it.

Literals, the pool and null

Identical literals in your code share one object from the string pool. That is a memory optimisation, not something to build on: compare text with equals, never ==. A String variable can also be null, meaning no object at all. Calling a method on it throws NullPointerException, and concatenating it produces the four letters null, which is how emails that start "Hello null" get sent. Check for missing values where data enters your service.

Concatenation and length

+ joins strings and converts other values to text, evaluating left to right, so "Total: " + 1 + 2 is Total: 12. Use brackets when you mean arithmetic.

length() counts UTF-16 code units (char values), not what a person sees. Most characters take one unit, but emoji and other characters outside the Basic Multilingual Plane take two, and an accented letter may be stored as a letter plus a combining mark. That matters when you enforce a 50 character limit to match a database column: decide which count you mean, and use codePointCount when you want Unicode code points.

Example

Try it yourself
Loading editor…
Press Run to execute the code.

When to use it

  • Cache configuration values such as a base URL in a static field and read them from many request threads without locking.
  • Use customer emails as HashMap keys after normalising them once, knowing the key text can never change afterwards.
  • Validate a username length against a VARCHAR(50) column, counting code points rather than chars when emoji are allowed.

More examples

A method cannot change the caller's String

Reassigning the parameter inside normalise points that local variable at a new String; the caller's variable still refers to the original. To get the cleaned value back, the method has to return it.

Example · java
String normalise(String email) {
    email = email.strip().toLowerCase(Locale.ROOT);  // rebinds the parameter only
    return email;
}

void main() {
    String input = " [email protected] ";
    String clean = normalise(input);
    IO.println("[" + input + "]");
    IO.println("[" + clean + "]");
}
// Output:
// [ [email protected] ]
// [[email protected]]

Concatenation order and null

Once the left operand is a String, every following + is string concatenation, so 1 and 2 are appended one after the other. A null reference is printed as the text null rather than throwing, which hides missing data instead of reporting it.

Example · java
void main() {
    String name = null;               // e.g. a missing JSON field
    IO.println("Hello " + name);
    IO.println("Total: " + 1 + 2);
    IO.println(1 + 2 + " items");
    IO.println("Total: " + (1 + 2));
}
// Output:
// Hello null
// Total: 12
// 3 items
// Total: 3

length() counts UTF-16 code units

The two spellings of cafe look identical on screen but have different lengths, and the emoji counts as two chars but one code point. Normalise text with java.text.Normalizer before comparing or measuring input that may come in either form.

Example · java
void main() {
    String plain = "cafe";
    String composed = "caf\u00e9";       // e with acute accent as one character
    String decomposed = "cafe\u0301";    // e followed by a combining accent
    String rocket = "go \uD83D\uDE80";   // an emoji stored as two chars

    IO.println(plain.length());
    IO.println(composed.length());
    IO.println(decomposed.length());
    IO.println(rocket.length());
    IO.println(rocket.codePointCount(0, rocket.length()));
}
// Output:
// 4
// 4
// 5
// 5
// 4

Discussion

  • Be the first to comment on this lesson.