Unit 1 · Lesson 815 minAcademic review pending

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.

Choose explanation

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.
01

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.

A helper class that lives where it is used
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
}
02

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.

The most common String bug in one example
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"
03

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?

Select an answer to check your thinking.

Why this lesson exists

Syllabus mapping

Nested and inner classes, exploring the String class.

Maps to course outcome CO1.