Command Palette

Search for a command to run...

PHASE 1Beginner ~31 min· topic 10 of 14

Topic 1.10

Strings: The Basics

In one line

A String is an object that holds a fixed sequence of characters. You create one with double quotes, join Strings with +, read characters by position starting at 0, compare contents with equals (never ==), and every method that seems to change a String actually returns a new one.

Think of it like this

A necklace of letter beads. Each bead has a position number, starting at 0 for the first bead. You can count the beads, look at bead number 3, or copy a part of the necklace to make a shorter one. But this necklace is glued shut: you can never swap a bead. To get a different word you make a new necklace. A Java String is that glued necklace of characters.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

String
A piece of text: an object holding characters in order, such as "hello".
String literal
Text written directly in your code between double quotes, like "Asha".
Index
The position number of a character in a String. The first character is at index 0.
Concatenation
Joining pieces of text end to end. In Java the + operator does it when one side is a String.
Immutable
Can't be changed after it is created. Every String method that 'changes' text returns a new String instead.
Reference
The arrow (address) stored in a variable that points to an object living on the heap.
String pool
A shared store where the JVM keeps one copy of each String literal, so equal literals reuse the same object.
Method
A named action an object can do, called with a dot and brackets: name.length().
null
A special value meaning 'this reference points at no object at all'.

Step by step

01Make a String and ask it questions

Write text between double quotes and store it in a String variable. Because a String is an object, you ask it questions by calling methods with a dot: city.length() asks 'how many characters do you have?'.

Methods always need brackets, even with nothing inside. city.length without brackets is a compile error for Strings (arrays use .length with no brackets, Topic 3.8; mixing these up is a classic slip).

Main.javawhole filejava
String city = "Pune";
int n = city.length();        // 4
char first = city.charAt(0);  // 'P'
String loud = city.toUpperCase();  // "PUNE", city is still "Pune"

02Positions start at 0

Index 0 is the first character, so the last character is at length() - 1. In "Pune" the characters are P(0) u(1) n(2) e(3); there is no index 4.

substring(begin, end) takes a half-open range: it includes begin and stops just before end. The nice side effect is that end - begin is exactly the length of the result, and s.substring(0, k) + s.substring(k) always rebuilds s. With one argument, substring(begin) runs to the end.

Positions start at 0diagram
Rendering diagram…

03Where a String lives in memory

The variable city sits in the stack frame like any local (Topic 1.1), but it holds only a reference. The String object lives on the heap. Since Java 9 it stores its characters in a byte[]: one byte per character when every character fits in Latin-1 (this is called compact strings), two bytes per character otherwise.

Literals are special. When a class loads, each distinct literal is placed once in the string pool. Two variables initialised with the same literal "java" therefore point at one shared object. That sharing is safe only because Strings are immutable: nobody can change the shared object under somebody else's feet.

Where a String lives in memorydiagram
Rendering diagram…

04Immutability: methods return a new String

Call greeting.toUpperCase() and Java creates a brand-new String "HI". The old object "hi" is untouched. If you don't store the new reference somewhere, it is simply garbage-collected later.

So the rule is: assign the result. greeting = greeting.toUpperCase(); points the variable at the new object. The variable changed; the String objects never did.

Main.javawhole filejava
String greeting = "hi";
greeting.toUpperCase();              // result discarded, greeting is still "hi"
greeting = greeting.toUpperCase();   // greeting now refers to a new String "HI"

05+ reads left to right

Java evaluates + from left to right, one pair at a time. 1 + 2 + "3" is (1 + 2) + "3", which is 3 + "3", which is "33". "1" + 2 + 3 is ("1" + 2) + 3, which is "12" + 3, which is "123".

Watch out for char: 'a' + 'b' adds the character codes (97 + 98 = 195) because neither side is a String. Start with "" or use String.valueOf to force text: "" + 'a' + 'b' is "ab".

When every piece is a constant (literals or final variables holding literals), the compiler joins them at compile time: "ja" + "va" is stored in the class file as the single literal "java". Otherwise the joining happens at run time and produces a new object.

+ reads left to rightdiagram
Rendering diagram…

06Comparing: equals, not ==

== on references asks 'same object?'. equals asks 'same characters?'. For text you almost always mean the second.

The trap is that == sometimes gives the right answer by luck: two identical literals share the pooled object, so "java" == "java" is true. But a String made at run time (new String(...), joining a non-constant, reading input, substring) is a different object, and == returns false even though the text is identical.

If one side might be null, put the literal first: "yes".equals(answer) returns false for a null answer instead of throwing.

07Empty, blank and null

"" is a String with zero characters. " " has three space characters. null is not a String at all. isEmpty() is true only for ""; isBlank() (Java 11) is true for both "" and whitespace-only text.

Calling a method on null throws NullPointerException at run time. Joining null with + does not throw; it produces the four letters null, which then quietly ends up in your output. Phase 4.12 covers null in depth.

Try it yourself

  1. 1

    Slice your own name

    In the first example, change city to your own name. Before running, write down what length(), charAt(0) and substring(0, 2) will print. Then run and check.

    Now print city.substring(1, city.length() - 1): it should be your name without its first and last letters.

  2. 2

    Predict the + puzzles

    Add these lines to the second example and predict each result before running: System.out.println(10 + 20 + "x" + 10 + 20); and System.out.println('x' + 1);.

    You should get 30x1020 (maths until the String appears, joining after) and 121 (the code of x is 120, plus 1).

  3. 3

    Catch == lying

    In the third example, add String e = "ja" + "va"; and print a == e. It prints true, unlike a == d. Both sides of this + are constants, so the compiler joined them into the literal "java", which comes from the pool. Now make half final and check a == d again: it also becomes true. This is exactly why == is unreliable for text.

Code & diagrams

Length, positions and pieces New tab
Sign in to run this example in your browser.

Expected output

text:      Bengaluru
length:    9
first:     B
last:      u
indexOf u: 6
indexOf z: -1
0 to 4:    Beng
from 4:    aluru
upper:     BENGALURU
unchanged: Bengaluru
+ joins left to right New tab
Sign in to run this example in your browser.

Expected output

33
123
15
195!
ab
score: 9.5, passed: true
value: null
equals compares text, == compares objects New tab

`compareTo` returns the difference of the first differing characters: 'a' (97) minus 'b' (98) is -1. Only its sign is guaranteed to mean anything.

Sign in to run this example in your browser.

Expected output

a == b       true
a == c       false
a == d       false
a.equals(c)  true
a.equals(d)  true
ignore case  true
compareTo    -1
Immutability and tidy-up methods Java 11+ New tab

`strip`, `isBlank` and `repeat` arrived in Java 11. Older code uses `trim()`, which removes only characters with codes up to 32 (space and control characters).

Sign in to run this example in your browser.

Expected output

ignored:  hi
assigned: HI
[  Hello World  ]
[Hello World]
contains World: true
replace:  HeLLo WorLd
repeat:   ababab
"" isEmpty:    true
"  " isEmpty:  false
"  " isBlank:  true
Null-safe comparison (fragment)java
String answer = null;
// answer.equals("yes")      // NullPointerException: answer points at nothing
"yes".equals(answer);        // false, no exception: the literal is never null
java.util.Objects.equals(answer, "yes");   // false: handles null on either side

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Use single quotes for text

Write String name = 'Asha';.

Main.javawhole filejava
public class Main {
    public static void main(String[] args) {
        String name = 'Asha';
        System.out.println(name);
    }
}
terminal
$ javac Main.java
── what you'll see ──
Main.java:3: error: unclosed character literal
String name = 'Asha';
^
Main.java:3: error: unclosed character literal
String name = 'Asha';
^
Main.java:3: error: not a statement
String name = 'Asha';
^
3 errors

Break #2

Read one past the end

Print word.charAt(3) when word is "cat".

Main.javawhole filejava
public class Main {
    public static void main(String[] args) {
        String word = "cat";
        System.out.println(word.charAt(3));
    }
}
terminal
$ javac Main.java
java Main
── what you'll see ──
Exception in thread "main" java.lang.StringIndexOutOfBoundsException: Index 3 out of bounds for length 3
at java.base/jdk.internal.util.Preconditions$1.apply(Preconditions.java:55)
at java.base/jdk.internal.util.Preconditions$1.apply(Preconditions.java:52)
at java.base/jdk.internal.util.Preconditions$4.apply(Preconditions.java:213)
at java.base/jdk.internal.util.Preconditions$4.apply(Preconditions.java:210)
at java.base/jdk.internal.util.Preconditions.outOfBounds(Preconditions.java:98)
at java.base/jdk.internal.util.Preconditions.outOfBoundsCheckIndex(Preconditions.java:106)
at java.base/jdk.internal.util.Preconditions.checkIndex(Preconditions.java:302)
at java.base/java.lang.String.checkIndex(String.java:4832)
at java.base/java.lang.StringLatin1.charAt(StringLatin1.java:46)
at java.base/java.lang.String.charAt(String.java:1555)
at Main.main(Main.java:4)
This is JDK 21. Older JDKs print a shorter trace and the message `String index out of range: 3`. The last line, pointing at your code, is the one to read.

Break #3

Compare text with ==

Build a String at run time and compare it to a literal with ==.

Main.javawhole filejava
public class Main {
    public static void main(String[] args) {
        String typed = "ye";
        typed = typed + "s";          // made at run time, like real input
        if (typed == "yes") {
            System.out.println("confirmed");
        } else {
            System.out.println("not confirmed?!");
        }
    }
}
terminal
$ javac Main.java
java Main
── what you'll see ──
not confirmed?!

Myth vs fact

Myth

String is a primitive type like int.

Fact

It is a class in java.lang. Its variables hold references, it can be null, and it has methods. Java just gives it literal syntax and the + operator.

Myth

s.toUpperCase() changes s.

Fact

Strings are immutable. The method returns a new String; you must assign it (s = s.toUpperCase();) to keep it.

Myth

== works for Strings because it worked in my test.

Fact

It worked because both sides were pooled literals. Strings built at run time are different objects, and == then returns false for identical text. Always use equals.

Myth

length() counts the letters a person sees.

Fact

It counts UTF-16 code units. Most characters are one unit, but an emoji such as a smiling face is two, so length() returns 2 for it (Topic 6.8).

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    Compact strings (JEP 254, Java 9): a String holds a byte[] value plus a coder byte (LATIN1 or UTF16). Latin-1-only text uses one byte per character, roughly halving memory for typical English text; charAt checks the coder and reads one or two bytes.

  • ▸

    Since Java 9 (JEP 280), javac compiles non-constant concatenation into one invokedynamic call bootstrapped by StringConcatFactory, which sizes the result once. Before that, it generated a StringBuilder chain. Concatenation inside a loop still creates a new String on every pass; use StringBuilder there (Topic 6.3).

  • ▸

    Compile-time constant expressions (JLS 15.29) are folded by javac, and constant Strings are always interned. That is why "ja" + "va" == "java" is true while the same join with a non-final variable is false. s.intern() returns the pooled copy at run time.

  • ▸

    hashCode() is computed once and cached in a field (the String is immutable, so it can't go stale). That, plus immutability, makes String the most common HashMap key; Topic 9.4 shows why mutable keys break maps.

Remember this

  1. 1

    String is not one of the eight primitive types (Topic 1.2). It is a class from java.lang, so a String variable holds a reference to an object on the heap. Java gives it special treatment: you can create one with a literal in double quotes ("Asha"), and the + operator joins Strings. Single quotes are only for one char (Topic 1.5).

  2. 2

    Characters are numbered by index from 0 to length() - 1. s.length() counts them, s.charAt(i) returns the char at index i, s.indexOf("x") finds the first position of some text (or -1 if it isn't there), and s.substring(begin, end) copies the characters from begin up to but not including end. Asking for an index outside the range throws StringIndexOutOfBoundsException.

  3. 3

    Strings are immutable: once created, their characters never change. toUpperCase(), trim(), replace(...) and friends build and return a new String; the original is untouched. Writing name.toUpperCase(); on its own line does nothing useful, because the result is thrown away. Write name = name.toUpperCase(); to keep it.

  4. 4

    + works left to right. As long as both sides are numbers it adds; as soon as one side is a String it concatenates (joins), turning the other side into text. So 1 + 2 + "3" is "33" but "1" + 2 + 3 is "123". Use brackets to force the maths first: "1" + (2 + 3) is "15". Any value can be joined: numbers, char, boolean, and even null, which becomes the text null.

  5. 5

    To compare text use a.equals(b) (same characters, same case) or a.equalsIgnoreCase(b). The == operator compares references: it asks whether both variables point at the very same object. Identical literals happen to share one object from the string pool, so == sometimes looks like it works, then fails for Strings built at run time. a.compareTo(b) orders Strings alphabetically by character code: negative if a comes first, 0 if equal, positive if after.

  6. 6

    An empty String "" has length 0 and is a real object; null means there is no String at all, and calling any method on it throws NullPointerException. isEmpty() tests for length 0; isBlank() (Java 11) is also true for whitespace-only text like " ". Phase 6 goes much deeper: the pool and immutability (6.1), == vs equals (6.2), StringBuilder (6.3) and every everyday method (6.4).

Explain it without notes

01

Why is String immutable, and what does that let the JVM do?

02

Explain the difference between == and equals for Strings, and why == sometimes returns true for equal text.

03

What do 1 + 2 + "3" and "1" + 2 + 3 evaluate to, and why?

04

What is the difference between "", " " and null, and how do you test for each?

Practice

01

Given String word = "programming";, print its length, its first and last characters, and the middle part without the first and last characters.

02

Given String email = "asha.k@example.com";, use indexOf and substring to print the user part (before @) and the domain (after @).

03

Given String a = "Level";, print whether it reads the same forwards and backwards ignoring case, by comparing its lower-case form with the reverse built from charAt in a simple sequence (the word has 5 letters).

Trade-offs

  • ↔

    Immutability makes Strings safe to share, cache and use as map keys, but every change allocates a new object; for text built piece by piece in a loop, StringBuilder is the better tool.

  • ↔

    + concatenation is the most readable way to build a short message; for aligned columns and number formats, printf/formatted (Topic 1.12) reads better than long + chains.

  • ↔

    "literal".equals(x) is null-safe but reads backwards to some people; Objects.equals(a, b) is null-safe on both sides at the cost of a static call.

Done when you can

  • I can create Strings and use length, charAt, indexOf and substring with correct 0-based indexes.

  • I can predict the result of mixed + expressions with numbers, chars and Strings.

  • I always compare text with equals and can explain why == sometimes seems to work.

  • I remember to assign the result of toUpperCase, strip and replace.

  • I can tell empty, blank and null apart and test each one safely.