A class inside a class, and the string that never changes
A class only one other class needs can live inside it. And Java's String hides one property that quietly explains half the confusing behaviour you will meet with it.
After this lesson
You should be able to
- Explain when a nested class is a reasonable design choice.
- State that a Java String is immutable, and explain what that means in practice.
- Predict the output of code that appears to modify a String.
A class that only makes sense inside another
Sometimes a small helper class exists purely to support one other class, and nowhere else in the program has any reason to use it — a Node class that only a LinkedList uses internally is a common example, one you effectively built by hand in CS205ES using a struct and a separate typedef. Java lets you nest that helper class directly inside the class that uses it, making the relationship explicit instead of leaving two top-level classes that happen to be related only by convention.
A nested class declared static behaves like a regular class that simply lives inside another for organisational purposes — it does not need an instance of the outer class to exist. A non-static inner class is tied to a specific instance of the outer class and can access that instance's fields directly, useful when the inner class genuinely represents a piece of one particular outer object rather than a general-purpose helper.
public class LinkedList {
private static class Node { // only LinkedList needs this, so it lives here
int value;
Node next;
}
private Node head;
// insert, delete, and traverse methods go here, using Node internally
}String looks mutable and is not
Every String object in Java is immutable: once created, its characters can never change, ever. Every method on String that appears to modify it — concat, replace, toUpperCase, and the + operator itself — actually creates and returns a brand new String object, leaving the original completely untouched. This is genuinely new; nothing in CS105ES's char arrays worked this way, since you modified characters in a C string in place all the time.
This explains a classic beginner mistake: writing str.toUpperCase(); on its own line and expecting str itself to have changed. It has not — toUpperCase built a new, uppercase String and handed it back, and since nothing captured that return value, it was immediately discarded. The fix is str = str.toUpperCase();, reassigning str to point at the new object.
String name = "kavya";
name.toUpperCase(); // creates "KAVYA" and throws it away
System.out.println(name); // still prints "kavya" — unchanged
name = name.toUpperCase(); // now name is reassigned
System.out.println(name); // prints "KAVYA"Why immutability was chosen on purpose
Immutability is not an accident of implementation; it is a deliberate design choice with real payoffs. A String can be safely shared between two parts of a program, or two threads running at the same time in Unit 3, with no risk that one holder's changes silently affect another's copy — because no holder can change it at all. That safety property becomes especially valuable once your programs start doing more than one thing at once.
The trade-off is exactly what you saw above: code that looks like it modifies a string silently does nothing unless you capture the new object it returns. Once you know the reason — a guaranteed, sharable, thread-safe string — that trade reads as sensible rather than surprising, the same way understanding why int division truncates in C made the behaviour predictable instead of mysterious.
Try it yourself
Predict, without running it, what each line of the code below prints. Then explain in one sentence why the third line's output is what it is.
Need a hint?
Track which lines capture a return value with an assignment and which lines simply call a method and discard whatever it returns.
Check the worked solution
Line 2 prints "hyderabad" unchanged, because the return value of toUpperCase() was never captured. Line 4 prints "HYDERABAD", because this time the result was assigned back to city. The underlying reason is the same in both cases: String is immutable, so toUpperCase() never changes the original — the only way to see the new value is to capture what it returns.
String city = "hyderabad";
System.out.println(city); // line 2
city.toUpperCase();
System.out.println(city); // line 4 — is this "HYDERABAD"?
city = city.toUpperCase();
System.out.println(city); // now?Quick check
Why does calling city.toUpperCase(); on its own line, without assigning the result, leave city completely unchanged?
Why this lesson exists
Syllabus mapping
Nested and inner classes, exploring the String class.
Maps to course outcome CO1.