Strings Are Objects, and They Never Change
A String in Java is not a primitive. It is an object of the class java.lang.String, which is why it gets a capital S and why it has methods you call with a dot. Java gives it two conveniences no other class has — you can create one with a literal in double quotes, and you can join two with + — but underneath it is an ordinary class.
The property that shapes everything else is immutability. Once a String object exists, its characters can never be modified. Every method that looks like it edits a string — toUpperCase(), replace(), trim(), substring() — actually leaves the original untouched and returns a brand-new String.
This produces the most common beginner bug with strings, and it is completely silent. You write name.toUpperCase(); on a line by itself, print name, and it is still lower case. Nothing errored; you simply threw the new string away. The fix is always to capture the return value: name = name.toUpperCase();.
Why did the designers make strings immutable? Because it makes them safe to share. Since a String can never change, two variables can point at the same one with no risk, strings can be used as keys in a HashMap without their hash code ever going stale, and a string passed into a method can never be altered behind your back. That safety is also what allows the string constant pool, where the compiler stores one copy of each distinct literal and reuses it — the mechanism behind the == trap from the previous lesson.
String name = "ananya";
name.toUpperCase(); // creates "ANANYA" and throws it away
System.out.println(name); // ananya <- unchanged, and no error
name = name.toUpperCase(); // capture the result
System.out.println(name); // ANANYA
// Every "modifying" method really returns a new String
String text = " Hello, World! ";
String a = text.trim(); // "Hello, World!"
String b = text.replace("World", "Java");
System.out.println(text); // " Hello, World! " — still original
// Chaining works because each call returns a String
String clean = " Ananya Sharma ".trim().toLowerCase().replace(" ", "_");
System.out.println(clean); // ananya_sharma - Because strings are objects, always compare them with
.equals()and never with==.==asks whether two variables point at the same object;.equals()compares the characters.
The Methods You Will Actually Use
The String class has dozens of methods. About a dozen cover almost everything you will write, and they fall into four groups: asking about the string, searching it, transforming it, and cutting it up.
For asking: length() gives the number of characters — note the brackets, because unlike an array a String's length is a method, and mixing up arr.length with str.length() is a standard beginner compile error. isEmpty() is true only when the length is zero, while isBlank() (Java 11 and later) is also true for a string containing nothing but spaces, which is what you usually want when validating a form field.
For searching: contains() answers yes or no; indexOf() gives the position of the first occurrence, or -1 when it is not there. That -1 matters — code that does str.substring(str.indexOf("@")) without checking will throw when the input has no @ in it. startsWith() and endsWith() are the readable way to test prefixes and suffixes.
For transforming: toUpperCase(), toLowerCase(), replace(), and strip(). Prefer strip() to the older trim() — trim() only removes characters up to the ASCII space, while strip() understands the full range of Unicode whitespace, which matters for text pasted from documents or copied from a web page.
String text = "Hello, World!";
// Asking
text.length(); // 13 — a METHOD, with brackets
text.isEmpty(); // false
" ".isBlank(); // true (Java 11+) — whitespace only
// Searching
text.contains("World"); // true
text.indexOf("World"); // 7
text.indexOf("Java"); // -1 <- not found, NOT an exception
text.lastIndexOf("l"); // 10
text.startsWith("Hello"); // true
text.endsWith("!"); // true
// Transforming (each returns a NEW String)
text.toUpperCase(); // "HELLO, WORLD!"
text.toLowerCase(); // "hello, world!"
text.replace("World", "Java"); // "Hello, Java!"
" padded ".strip(); // "padded" (Java 11+, Unicode-aware)
"ab".repeat(3); // "ababab" (Java 11+)
// Always check indexOf before using its result
String email = "ananya.example.com"; // note: no @
int at = email.indexOf('@');
if (at != -1) {
System.out.println(email.substring(at + 1));
} else {
System.out.println("Not a valid email");
} str.length()has brackets.arr.lengthdoes not. Strings use a method because the class chooses to expose one; arrays use a field built into the language. There is no logic to remember here beyond the fact that they differ.
Indexing and substring: Where the Off-by-One Errors Live
Characters in a String are numbered from 0, so in "Hello" the H is at index 0 and the last o is at index 4 — which is length() - 1. Asking for charAt(5) throws StringIndexOutOfBoundsException. Almost every loop bug over a string comes from writing i <= s.length() where you meant i < s.length().
substring() has two forms and one rule that surprises everybody: the start index is included and the end index is excluded. So "Hello, World!".substring(0, 5) gives "Hello" — indices 0, 1, 2, 3 and 4, but not 5. The mental shortcut that works is: the length of the result is simply end - start.
The single-argument form substring(7) takes everything from index 7 to the end, which is what you want when splitting a string at a marker you have just located with indexOf.
One consequence of the exclusive end that catches people: substring(3, 3) is legal and returns an empty string, and substring(s.length()) is also legal and returns an empty string. But substring(s.length() + 1) throws. When a substring call throws in real code, print the two indices before the call — the numbers almost always reveal the mistake immediately.
String s = "Hello, World!";
// 0123456789...
s.charAt(0); // 'H'
s.charAt(s.length() - 1); // '!' — the LAST valid index
// s.charAt(s.length()); // StringIndexOutOfBoundsException
// substring: start included, end EXCLUDED
s.substring(0, 5); // "Hello" length = 5 - 0
s.substring(7, 12); // "World" length = 12 - 7
s.substring(7); // "World!" to the end
// Walking a string one character at a time
for (int i = 0; i < s.length(); i++) { // note: < not <=
System.out.print(s.charAt(i) + " ");
}
System.out.println();
// Or convert to a char array and use the enhanced for loop
for (char c : s.toCharArray()) {
if (Character.isLetter(c)) {
System.out.print(c);
}
}
// HelloWorld
// Reversing a string — the interview warm-up question
String reversed = new StringBuilder(s).reverse().toString();
System.out.println(reversed); // !dlroW ,olleH Characteris a wrapper class with genuinely useful static helpers:isLetter,isDigit,isWhitespace,isUpperCase,toUpperCase. They are far clearer than comparing char codes by hand, and they handle characters beyond A–Z correctly.
StringBuilder: Why Concatenating in a Loop Is Slow
Because a String can never change, result = result + word; cannot append anything. It must allocate a brand-new String, copy every character of the old one into it, then copy the new word on the end, and finally leave the old string for the garbage collector.
Do that once and it costs nothing. Do it inside a loop that runs ten thousand times and the cost grows with the square of the number of iterations, because each pass copies everything accumulated so far. A loop building a report from ten thousand rows can take seconds instead of milliseconds, and the code looks perfectly innocent.
StringBuilder is the fix. It is a mutable character buffer: append() writes into the space it already has and only occasionally grows, so building a string of any length stays fast. When you are finished, toString() produces the immutable String once. Its append() returns the builder itself, which is why calls can be chained.
The rule in practice: use plain + for joining a handful of pieces in a single expression — the compiler handles that case efficiently and it reads better. Reach for StringBuilder the moment the joining happens inside a loop or across several statements. And if you are simply joining a list with a separator, String.join() is shorter than either.
// Slow: each pass copies everything built so far
String slow = "";
for (int i = 0; i < 10000; i++) {
slow = slow + i + ","; // a new String object every iteration
}
// Fast: one buffer, appended in place
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 10000; i++) {
sb.append(i).append(','); // append() returns the builder, so it chains
}
String fast = sb.toString();
// Other StringBuilder operations
StringBuilder b = new StringBuilder("Hello");
b.append(", World"); // Hello, World
b.insert(0, ">> "); // >> Hello, World
b.reverse(); // dlroW ,olleH >>
b.setLength(0); // empty it and reuse
// Joining a list — simplest of all
List<String> subjects = List.of("Maths", "Physics", "Chemistry");
String line = String.join(", ", subjects);
System.out.println(line); // Maths, Physics, Chemistry - You may also meet
StringBuffer. It does the same job but adds thread-safety locking on every method, which costs speed and which you do not need in single-threaded code. UseStringBuilderunless several threads genuinely share one buffer.
Splitting, Formatting and Text Blocks
split() cuts a string into an array using a separator, and it holds a genuine trap: the argument is not plain text, it is a regular expression. For ordinary separators like a comma this makes no difference. But ., |, *, +, ?, (, ), [, ], {, }, ^, $ and \ all have special meanings in a regular expression. "a.b.c".split(".") returns an empty array, because . in a regex means "any character", so everything is a separator. You must escape it as "\\." — two backslashes, because one of them is consumed by Java's own string escaping before the regex ever sees it.
For formatting, gluing values together with + becomes unreadable quickly and gives you no control over how numbers are printed. String.format() builds a formatted string and System.out.printf() prints one directly. The placeholders are %s for any value as text, %d for a whole number, %f for a decimal (with %.2f meaning two decimal places), and %n for a line break. Use %n rather than \n in formatted output, since it produces the correct line ending on whichever operating system is running.
Finally, text blocks (Java 15 and later) let you write multi-line strings between triple quotes without escaping every internal quote mark and without stringing \n through the middle. They are ideal for JSON, SQL, HTML and multi-line messages. The indentation is handled sensibly: Java strips the common leading whitespace, measured against the closing """, so you can indent the block to match your code without that indentation ending up in the value.
// ---- split() takes a REGEX ----
String csv = "Maths,Physics,Chemistry";
String[] subjects = csv.split(","); // works — comma is not special
String ip = "192.168.1.1";
String[] wrong = ip.split("."); // length 0 — '.' means "any character"
String[] right = ip.split("\\."); // [192, 168, 1, 1]
// Splitting on any run of whitespace
String[] words = "the quick brown fox".split("\\s+"); // 4 words
// ---- Formatting ----
String student = "Ananya";
int marks = 87;
double fee = 4500.5;
String msg = String.format("%s scored %d marks", student, marks);
System.out.println(msg); // Ananya scored 87 marks
System.out.printf("Fee due: Rs %.2f%n", fee); // Fee due: Rs 4500.50
System.out.printf("%-10s|%5d|%n", student, marks); // left-pad / right-pad
// ---- Text blocks (Java 15+) ----
String json = """
{
"name": "Ananya",
"marks": 87
}
""";
System.out.println(json); String.format("%d", 3.5)throwsIllegalFormatConversionExceptionat run time, not compile time — the placeholder and the argument type must match.%saccepts anything, so when you are unsure,%sis the safe choice.- Text blocks need a line break immediately after the opening
""". Writing content on that same line is a compile error.
