String is an object, not a primitive
Almost everything real programs handle is text: usernames, file paths, URLs, JSON from an API. In Java all of it lives in String, with a capital S and double quotes.
Unlike the primitives from lesson 2-1, a String is an object, meaning a bundle of data plus methods you call with a dot, exactly like Python's "abc".upper().
String lang = "Java"; lang.length() // 4 lang.toUpperCase() // "JAVA" lang.charAt(0) // 'J', a char lang.substring(1) // "ava", from index 1 to the end lang.contains("av") // true
Strings are immutable, the same as in Python. Every method returns a new String and none of them changes the original, so lang.toUpperCase() on its own accomplishes nothing unless you keep the result.
Being an object also has a consequence for comparison, which is the subject of the next section and the most common beginner bug in the language.
A tour of the core String methods
Five calls on one String, with the original printed last.
public class Main { public static void main(String[] args) { String lang = "Java"; System.out.println(lang.length()); System.out.println(lang.toUpperCase()); System.out.println(lang.charAt(0)); System.out.println(lang.substring(1)); System.out.println(lang); } }
Output
4
JAVA
J
ava
JavaThe last line is the important one: lang is still Java after four method calls, which is immutability in action.
Note that length() has parentheses because it is a method, unlike an array's length field in a later lesson. Indexes start at 0, so charAt(0) gives the first character and substring(1) skips it.
Comparing objects, and the == trap
For primitives, == compares values and behaves as expected. For objects, == asks whether these are the same object in memory, not whether they look equal. To compare String contents, always use .equals(...):
String a = "hi"; String b = new String("hi"); a == b // false, different objects a.equals(b) // true, same characters
This is the single most common beginner bug in Java. The rule is short: == for primitives, .equals() for objects.
The behavior follows from where Java stores things. Local variables live on the stack, a small fast memory region tied to the method currently running. Objects live on the heap, a larger region for data that can outlive any single method.
A variable of object type holds only a reference, which is the heap location of the object, so == on two object variables compares the two locations rather than the characters stored at them. The diagram below shows exactly that.
The comparison that reads the characters
For two String variables a and b built separately but holding the same characters, a.equals(b) is the reliable test.
equals compares the characters, while == compares heap locations. Both variables are references, each holding the address of a String object, so == asks whether they point at the very same object, and two separately built Strings do not.
| Expression | Asks |
|---|---|
a == b | are these the same object |
a.equals(b) | do these hold the same characters |
a = b | assignment, not a comparison at all |
The trap is that == sometimes returns true anyway. Java reuses identical string literals from a shared pool, so "hi" == "hi" is often true, which lets the bug pass every quick test and fail on text that arrived from a file or a user.
Four facts about one word
Length, first character, last character, and uppercase, all from methods on the same String.
public class Main { public static void main(String[] args) { String word = "polymorphism"; System.out.println(word.length()); System.out.println(word.charAt(0)); System.out.println(word.charAt(word.length() - 1)); System.out.println(word.toUpperCase()); } }
Output
12
p
m
POLYMORPHISMlength() is a method call with parentheses, unlike Python's len(word). Indexes start at 0, so the last character sits at word.length() - 1, and charAt(word.length()) would throw at runtime.
That off-by-one is worth internalizing now, because the same reasoning governs arrays and lists later. A collection of 12 items has valid indexes 0 through 11.