Reading the Java library's own source, summarising a class with a UML diagram, the three kinds of variable a method can see, what happens to an object nobody refers to any more, and the mutable class that exists because building a long string out of immutable ones is wasteful. Follows Think Java 2e, Chapter 10 (Mutable Objects), Sections 10.6-10.11, pp. 173-179, cross-referenced against Java SE 21 API — java.lang.StringBuilder.
Subject: Java · 65 slides · code lesson
Open the interactive version of this deck
Title
Think Java 2e · Chapter 10 · Mutable Objects
Sections 10.6-10.11 · pp. 173-179
Objectives
This lesson follows Think Java 2e, Chapter 10 (Mutable Objects), Sections 10.6-10.11, pp. 173-179. Everything on these slides can be checked against those pages.
1. Locate and skim the Java library source, and say what most of Point.java consists of.
2. Read and draw a UML class diagram, including the public and private markers.
3. Distinguish parameters, local variables and attributes by when they are created and where they can be used.
4. Explain the order in which the compiler resolves a variable name.
5. Say when an object becomes eligible for garbage collection.
6. Choose between String and StringBuilder, and explain the cost of concatenating in a loop.
Warm-up
One idea from Chapter 4 and one from Chapter 9, both about to be extended.
Discussion prompt
From Lesson 4a: what is the scope of a local variable, and when does it disappear? And from Lesson 9a: what does it mean that Strings are immutable?
Hint: One is about frames; one is about objects that never change.
Answer:
A local variable can be used only inside its own method, and it disappears when the method returns — with its frame. Immutable means no method can change the object; every String method returns a new one.
This lesson adds a third kind of variable that outlives any method, and shows what immutability costs when you build a long string one piece at a time.
Concept
The previous lesson used Point and Rectangle. This one looks at how they are described — as a diagram, as source code, and in terms of what lives how long.
Figure (svg): UML class diagrams for Point and Rectangle showing their attributes and the methods introduced in this chapter
Downey & Mayfield, Think Java, 2nd edition (Green Tea Press / O'Reilly, 2020) — Think Java 2e, Chapter 10 (Mutable Objects), Sections 10.6-10.11, pp. 173-179 — Sections 10.6-10.11, printed pages 173-179.
Section
Section 10.6
Concept
So far you have used several classes from the Java library — System, String, Scanner, Math and Random. These classes are written in Java, so you can read the source code to see how they work.
// the library source ships as a ZIP archive named src.zip
// Linux: /usr/lib/jvm/.../lib
// macOS: /Library/Java/JavaVirtualMachines/.../Contents/Home/lib
// Windows: C:\Program Files\Java\...\lib| open | and you find |
|---|---|
| src.zip | folders corresponding to Java packages |
| the java folder, then awt | Point.java, Rectangle.java and the rest of java.awt |
| Point.java | the actual source of the class you used yesterday |
The library contains thousands of files, many of them thousands of lines long — that is more than one person could read and understand fully, but do not be intimidated. You are skimming, not auditing.
Notation
Open it and skim. It uses language features you have not met, and the proportions are the interesting part.
Annotate
@param and @return — often more lines of comment than of code.grow and translate in Rectangle. Think Java's own comment: there is more to them than you may have expected — real library methods handle overflow and edge cases your version would not.Appendix B covers Javadoc properly. The point here is simply that the library is not a black box: it is Java, written by people, and you can look.
Worked example
Compare your own moveRect from Lesson 10a with the library's translate. Both move a rectangle, and one of them is much longer.
// yours, from Lesson 10a
public static void moveRect(Rectangle box, int dx, int dy) {
box.x = box.x + dx;
box.y = box.y + dy;
}
// the library's, in outline
public void translate(int dx, int dy) {
int oldv = this.x;
int newv = oldv + dx;
if (dx < 0) {
... // overflow handling
}
...
}| moveRect | translate | |
|---|---|---|
| lines | 2 | dozens |
| handles ordinary moves | yes | yes |
| handles integer overflow | no | yes |
| documented with Javadoc | no | extensively |
Notice both do the same thing for ordinary inputs.
Why: Adding dx to x is the whole idea, and yours implements it correctly.
Notice what the library version adds.
Why: What happens if x + dx exceeds Integer.MAX_VALUE? Lesson 9a's constants exist because that limit is real.
Notice this is not a criticism of your version.
Why: For a program that moves rectangles by small amounts, moveRect is correct and clearer.
Draw the general lesson.
Why: Library code handles cases your version does not, which is a reason to prefer it — and a reason library code is longer than you expect.
Verify: Find Rectangle.java in src.zip and read translate. Count the lines and count the comments.
Why: This is worth doing once. It permanently changes how you read the phrase just use the library method, and it makes the advice from Lesson 4b — look for it before writing your own — concrete rather than abstract.
Prediction
Open it and look at the proportions.
Predict first
Skimming the file, what makes up the largest part of it?
Correct: Documentation comments with tags like @param and @return
Why: Think Java points this out explicitly: notice how much of Point.java is documentation. Each method carries comments and Javadoc tags, and the tool reads those to generate the HTML documentation you find online — so the source and the docs are literally the same text. Point.java has no main method at all, because it is a class to be used rather than a program to be run.
Concept
The comments in Point.java are not ordinary // comments. They start with /** and carry structured tags, and a tool turns them into the documentation you have been reading online.
| comment style | read by | used for |
|---|---|---|
| // line comment | people reading the source | explaining a line |
| /* block comment */ | people reading the source | explaining a section |
| /** doc comment */ | people AND the Javadoc tool | generating the API documentation |
| @param, @return | the Javadoc tool | describing parameters and return values |
You saw a doc comment in Lesson 3b's Convert program without a name for it. Appendix B is entirely about this, and the practical consequence is that the documentation you look up and the comments in the source are the same words — which is why library documentation is usually accurate.
Trap
Writing your own because it looks like two lines.
// "I'll just write my own instead of importing Rectangle"
public static void grow(Rectangle r, int h, int v) {
r.x = r.x - h;
r.y = r.y - v;
r.width = r.width + 2 * h;
r.height = r.height + 2 * v;
}| case | your version | the library's |
|---|---|---|
| ordinary values | correct | correct |
| width near Integer.MAX_VALUE | silently overflows to a negative | handled |
| negative growth larger than the rectangle | produces a negative width | handled |
The first row is the one you will test. The other two are why Rectangle.grow is longer than four lines — and why Think Java tells you to go and look at it.
Use the library, and read it when you are curious.
box.grow(50, 50); // tested, documented, handles the edges| reason to use the library version | |
|---|---|
| it handles cases you have not thought of | overflow, negatives, extremes |
| it is documented | with Javadoc you can read online |
| it says what it means | grow, rather than four assignments |
| it is already tested | by rather more people than you |
This is Lesson 4b's advice with evidence attached. Writing it yourself is how you understand it; using the library version is how you write correct code — and now you can go and read the difference.
Definition probe
The folders inside src.zip correspond to packages.
Sort into buckets
Sort each class by its package.
Real world
It could have shipped as compiled byte code only.
Discussion prompt
The Java library ships its source alongside the compiled classes. What does that make possible, and what would be harder if only the .class files were available?
Hint: Think about the last time a library method surprised you.
Answer:
It lets you answer questions the documentation does not. When a method behaves unexpectedly at an edge case, the source settles it — and it settles it faster than guessing or experimenting.
It is also how you learn what good code looks like. Most programmers read far more code than they write, and library source is professional code you can read at any time.
Without it you would be limited to the documentation plus experiment. That works, and it is slower — and it makes some questions, like why is grow so long?, effectively unanswerable.
Explain it to yourself
Think Java's comment about grow and translate.
Discussion prompt
The book says of Rectangle's grow and translate that there is more to them than you may have expected. What kinds of thing do you now think a library method has to handle that a two-line version would not?
Hint: What are the limits of an int?
Answer:
Overflow, mainly. x + dx can exceed Integer.MAX_VALUE and wrap around to a negative number — Lesson 9a's constants exist because that boundary is real, and a library used by millions of programs cannot ignore it.
Also degenerate cases: shrinking a rectangle by more than its own size, or growing one so large its width overflows. Your version produces nonsense; the library's makes a defined choice.
The general shape: library code is longer than your version because it handles inputs you have not thought of yet. That is what tested means, and it is the strongest argument for preferring it.
Section
Section 10.7
Concept
Point and Rectangle objects have attributes and methods — attributes are an object's data, methods are its code. An object's class definition specifies which it has, and UML defines a graphical way to summarise that.
Figure (svg): A UML class diagram for Point showing two int attributes and one method, with plus signs marking them public
UML — Unified Modeling Language, a standard way to draw diagrams for software engineering.
class diagram — An illustration of the attributes and methods for a class.
Each class is a box with the name of the class, a list of attributes, and a list of methods. Both Point and Rectangle have additional methods; the diagram shows only the ones introduced in this chapter.
Notation
Three conventions, and the first one catches Java programmers out because it is deliberately not Java.
Annotate
x: int rather than Java's int x. That is deliberate — the same diagram can describe a class in any language.blank.x.That last distinction is the important one. A memory diagram shows these particular objects, right now; a class diagram shows what any object of this class has.
Worked example
Four attributes and two methods, using the conventions above.
// From the chapter:
// attributes: x, y, width, height - all int, all public
// methods: translate(int, int)
// grow(int, int)| what you know | how it appears in UML |
|---|---|
| the class is called Rectangle | the name, at the top of the box |
| it has four int attributes | + x: int, and three more |
| they are public | the plus sign |
| translate takes two ints and returns nothing | + translate(int, int) |
Write the class name in the top compartment.
Why: Just the name — no keyword, no package.
List the attributes in the middle compartment.
Why: + x: int — visibility, name, colon, type.
List the methods in the bottom compartment.
Why: + translate(int, int) — parameter types only, since the names are not part of the interface.
Omit what is not relevant.
Why: Rectangle has many more methods; showing only the ones under discussion is normal and expected.
Verify: Compare your diagram with the one on the big-idea slide of this lesson.
Why: Then notice what the diagram does not tell you: it says a Rectangle has a width, not what any particular rectangle's width is. That is the compile-time against run-time distinction, made concrete.
Prediction
UML visibility markers.
Point
+ x: int
+ y: int| marker | meaning |
|---|---|
| + | public |
| - | private |
Predict first
What does + x: int tell you?
Correct: x is a public attribute of type int
Why: The plus sign marks public visibility, the name comes before the colon and the type after it — UML's language-independent syntax. That x is public is exactly why Lesson 10a could write blank.x; Chapter 11's private attributes, marked with a minus, could not be reached that way.
Concept
The two kinds of diagram in this book answer different questions, and confusing them is a common early mistake.
| class diagram | memory diagram | |
|---|---|---|
| shows | the source code's structure | objects and variables |
| when | compile time | run time |
| how many boxes for Rectangle | one, ever | one per object that exists |
| contains | attribute names and types | attribute values |
| answers | what does a Rectangle have? | what is in THIS rectangle now? |
The third row is the clearest test. A program with fifty Rectangles has fifty boxes in its memory diagram and still one in its class diagram — because there is only one Rectangle class.
Trap
Mixing the two kinds of diagram. Values belong to objects, not to classes.
// wrong - this is a memory diagram wearing a class diagram's clothes
Rectangle
------------------
+ x = 0
+ y = 0
+ width = 100
+ height = 200| what is wrong | why |
|---|---|
| values instead of types | the class has no values — objects do |
| it describes one rectangle | a class describes all of them |
| no type information | which is what the diagram is for |
This diagram cannot be right for two rectangles at once, which is the giveaway. A class diagram has to be true of every object of that class.
Types in the class diagram; values in the memory diagram.
// class diagram: what every Rectangle has
Rectangle
+ x: int
+ width: int
// memory diagram: what this one holds
box ---> Rectangle [ x = 0, width = 100 ]| question | which diagram |
|---|---|
| does Rectangle have a height? | class diagram |
| what is box's height right now? | memory diagram |
| how many objects exist? | memory diagram |
| is width public? | class diagram |
A quick test when drawing: could this box be true of two different objects at the same time? If not, you have drawn a memory diagram.
Discrimination
Which question is being asked?
Sort into buckets
Sort each question by the diagram that answers it.
Fill the middle
A public int attribute called height.
Fill in the blanks
+ height: int
Why: The plus sign marks it public, and UML puts the name before the type separated by a colon — the reverse of Java's int height. The notation is deliberately language-independent so the same diagram can describe a class written in any language.
Counterexample
It summarises a class completely, in one sense.
Discussion prompt
A UML class diagram lists every attribute and method of a class. What important things about a program does it still not show — and when would that matter?
Hint: Think about what you have to draw to debug an aliasing bug.
Answer:
It shows no values, no objects, and no relationships between particular objects — so it cannot show aliasing, or how many rectangles exist, or which variables point where.
It also shows no behaviour: translate(int, int) appears, but not what it does. For that you need the documentation or the source.
So the two diagram types are complementary rather than competing. A class diagram is a map of the code; a memory diagram is a photograph of the run. Lesson 10a's aliasing bug needed the second, and no amount of the first would have helped.
Section
Section 10.8
Concept
Section 4.5 introduced the idea that variables have scope — the part of a program where a variable can be used. The library's Rectangle.translate uses all three kinds at once.
public void translate(int dx, int dy) {
int oldv = this.x;
int newv = oldv + dx;
if (dx < 0) {
...| kind | in this example | created when | disappears when |
|---|---|---|---|
| parameter | dx, dy | the method is invoked | the method returns |
| local variable | oldv, newv | the method is invoked | the method returns |
| attribute | this.x | the object is created | the object is destroyed |
Parameters and local variables are created when a method is invoked and disappear when it returns. They can be used anywhere inside the method, but not in other methods and not in other classes.
Picture it
A frame lives from call to return. An object lives as long as something refers to it. Those are different clocks.
Figure (svg): Two panels comparing the lifetime of parameters and locals against the lifetime of attributes
Attributes are created when an object is created and disappear when the object is destroyed. They can be used in any of the object's methods using the keyword this — and if they are public, in other classes via a reference, as box1.x.
Worked example
When the Java compiler encounters a variable name, it searches backward for its declaration — and the order it searches in is fixed.
public void translate(int dx, int dy) {
int oldv = this.x;
int newv = oldv + dx;
...
}| name | found as | at which step |
|---|---|---|
| oldv | a local variable | 1 — locals first |
| dx | a parameter | 2 — then parameters |
| this.x | an attribute, named explicitly | the dot removes the search |
| a name matching all three | the local variable wins | 1 |
The compiler looks for local variables first.
Why: Declared inside the method body.
Then parameters.
Why: Declared in the method's header.
Then attributes.
Why: Declared in the class, outside any method.
Note why this.x is written explicitly.
Why: Writing this.x says the attribute, regardless of what local names exist — the same disambiguation dot notation provided in Lesson 10a.
Verify: Check the order by asking: if a method had a local variable named x, what would a bare x refer to?
Why: The local one, because locals are searched first. That is exactly why the library writes this.x rather than x — it removes any doubt, and it keeps working if someone later adds a local called x.
Definition probe
Where it is declared decides what it is.
Sort into buckets
Sort each variable from Rectangle.translate.
Concept
this is a reference to the object the method was invoked on. It is the missing piece that connects Lesson 10a's box.translate(50, 100) to the method's body.
box.translate(50, 100);
// inside translate:
// this refers to the object box refers to
// this.x is that object's x attribute
// dx is 50| at the call site | inside the method |
|---|---|
| box | this |
| box.x | this.x |
| the first argument, 50 | the parameter dx |
This is why invoking a method on an object works at all: the object comes in as this, without appearing in the parameter list. Chapter 11 makes this central when you write your own classes; for now it is enough to read it as the object this method was called on.
Trap
A local variable does not survive the method.
public void translate(int dx, int dy) {
int oldv = this.x; // local
this.x = oldv + dx;
}
// oldv is gone the moment translate returns.
// Nothing outside can see it, and the next call gets a fresh one.| variable | after the method returns |
|---|---|
| oldv | gone |
| dx | gone |
| this.x | still there — it belongs to the object |
This is Lesson 4a's rule, unchanged. What is new is the third row: an attribute outlives the method that changed it, which is exactly what makes an object able to remember anything.
Use an attribute for anything the object must remember.
// a value needed only during this call -> local variable
int oldv = this.x;
// a value the object must keep -> attribute
this.x = oldv + dx;| the value must last | declare it as |
|---|---|
| for one statement | a local variable |
| for one method call | a local variable |
| as long as the object | an attribute |
That table is the whole decision, and it is the same one as Lesson 7a's the accumulator goes outside the loop — one level up. Scope should match how long the value needs to live.
Ranking
When it meets a bare variable name.
Put in order
Why: The compiler first looks for local variables, then parameters, then attributes — innermost outward. That is why a local variable named x would hide an attribute named x, and why library code writes this.x explicitly rather than relying on the search finding the right one.
Prediction
A local variable shares a name with an attribute.
public void shift(int dx) {
int x = 100; // a local variable
this.x = x + dx; // which x is which?
}| written | resolves to |
|---|---|
| x | the local variable, 100 |
| this.x | the attribute, explicitly |
Predict first
If the object's x was 5 and dx is 3, what does the attribute become?
Correct: 103
Why: The bare x resolves to the local variable, because the compiler searches locals first — so the expression is 100 + 3. this.x names the attribute explicitly and is what receives the result. The object's original x of 5 is never read, which is exactly the confusion this exists to prevent.
Explain it to yourself
Inside a method, a bare x would usually find the attribute anyway.
Discussion prompt
In Rectangle.translate, the author wrote this.x rather than x. Since there is no local variable called x, both would work. Why write the longer form?
Hint: What happens if someone edits the method later?
Answer:
It says what it means, and it keeps working. A bare x relies on no local or parameter having that name — and someone adding one later would silently change the meaning of every bare x in the method.
It also makes the code readable without holding the whole method in your head: this.x is unmistakably the object's data, while x requires you to check.
This is the same argument as always writing braces in Lesson 5a: prefer the form that cannot be silently broken by a later edit. It costs five characters.
Section
Section 10.9
Concept
Attributes exist as long as the object exists. But when does an object stop existing? The answer is: when nothing refers to it any more.
Point blank = new Point(3, 4);
blank = null;| statement | references to the Point | can you reach it? |
|---|---|---|
| new Point(3, 4) | 1 — blank | yes |
| blank = null; | 0 | no |
garbage collection — The process of finding objects that have no references and reclaiming their storage space.
The first line creates a Point and makes blank refer to it. The second changes blank so that instead of referring to the object, it refers to nothing. After the second assignment there are no references to the Point object.
Picture it
The object is still in memory, taking up space. From the program's point of view it has ceased to exist.
Figure (svg): A variable holding null with no arrow, and a Point object with nothing pointing at it
If there are no references to an object, there is no way to access its attributes or invoke a method on it. As the program runs, the system automatically looks for such stranded objects and deletes them, so the space can be reused.
Worked example
Setting a variable to null is the clearest case and the rarest. Objects usually become unreachable for one of two other reasons.
// 1. the reference is overwritten
Point p = new Point(3, 4);
p = new Point(5, 6); // the first Point is now garbage
// 2. the reference goes out of scope
public static void f() {
Point q = new Point(1, 2);
} // q disappears; the Point is garbage
// 3. explicitly, with null
Point r = new Point(7, 8);
r = null;| cause | what happened to the reference | common? |
|---|---|---|
| overwritten | the variable now points elsewhere | very |
| went out of scope | the frame was discarded | very |
| set to null | explicitly cleared | rare |
Count the references, not the variables.
Why: An object is garbage when the count reaches zero — however that happened.
Notice case 2 is the usual one.
Why: Every method that creates an object and does not return it leaves garbage behind when it returns.
Notice that you never delete anything.
Why: You do not have to do anything to make garbage collection happen, and in general you do not have to be aware of it.
Notice what you might notice.
Why: In high-performance applications you may see a slight delay now and then while Java reclaims space.
Verify: Trace Lesson 10a's findCenter: it creates a Point and returns it, so that Point is not garbage — the caller holds a reference.
Why: Then trace a version that creates a Point and does not return it. That object becomes unreachable the instant the method returns, and you never had to think about it. That is the whole feature.
Prediction
The reference is overwritten.
Point p = new Point(3, 4);
p = new Point(5, 6);| object | references after the second line |
|---|---|
| Point(3, 4) | 0 |
| Point(5, 6) | 1 — p |
Predict first
What happens to the Point holding (3, 4)?
Correct: Nothing refers to it, so it becomes eligible for garbage collection
Why: Reassigning p moves its arrow to the new object, leaving the first with no references at all. It is not deleted at that instant — the system looks for stranded objects as the program runs and reclaims them when it does. Being eligible and being collected are different moments.
Concept
In languages without garbage collection, the programmer frees memory explicitly — and the two ways of getting that wrong are both serious.
| mistake | what happens | in Java |
|---|---|---|
| forget to free | a memory leak — the program grows until it fails | cannot happen this way |
| free too early | a reference to freed memory; unpredictable behaviour | cannot happen |
| free twice | corruption | cannot happen |
Java removes an entire category of bug by taking the decision away from you. The cost is the occasional pause while collection runs, and less control over exactly when memory is reclaimed — which is why some systems still use languages that make you do it by hand.
Trap
Setting a variable to null does not destroy anything.
Point a = new Point(3, 4);
Point b = a;
a = null;
System.out.println(b.x); // still 3| step | references to the Point |
|---|---|
| Point b = a; | 2 — a and b |
| a = null; | 1 — b still refers to it |
| is it garbage? | no |
Assigning null changes one variable. The object is collectable only when nothing at all refers to it — which is a fact about the whole program, not about any single line.
An object is garbage when its last reference goes.
Point a = new Point(3, 4);
Point b = a;
a = null;
b = null; // now it is unreachable| after | references | collectable? |
|---|---|---|
| a = null; | 1 | no |
| b = null; | 0 | yes |
This is the aliasing lesson again, from a different angle: count the references to the object, not the assignments to variables. It is the same question that decided whether b2 saw box1.grow.
Definition probe
Count the references to the object.
Sort into buckets
Sort each situation after the code shown.
Prediction
Java's answer differs from some other languages'.
Predict first
What must a Java programmer do to reclaim an object's memory?
Correct: Nothing — the system finds unreachable objects and reclaims them automatically
Why: You do not have to do anything to make garbage collection happen, and in general you do not have to be aware of it. Setting references to null is occasionally useful to make an object unreachable sooner, but it is not required — and there is no delete operator in Java at all.
Edge cases
It is meant to be invisible. Sometimes it is not.
Discussion prompt
Think Java says that in high-performance applications you may notice a slight delay now and then. What kind of program would that matter to, and what is the trade being made?
Hint: What if the pause happens at the wrong moment?
Answer:
Anything with a hard timing requirement: a game rendering sixty frames a second, audio processing, or a control system. A pause of a few milliseconds at the wrong moment is visible or audible.
The trade is safety and convenience against predictability. Java removes memory-management bugs entirely and gives up precise control over when memory is reclaimed.
That is why languages requiring manual memory management still exist for operating systems, game engines and embedded control. Neither choice is always right — which is the same conclusion the next section reaches about mutability.
Section
Sections 10.10-10.11
Concept
Points and Rectangles are mutable because their attributes can be modified — directly, like box.x = 15, or through methods like box.translate(15, 0). Strings and Integers are immutable: they allow no direct access to their attributes and provide no methods that change them.
Figure (svg): Two boxes comparing the advantages of immutable objects against those of mutable ones
| immutable objects | mutable objects |
|---|---|
| pass them without worrying about side effects | sometimes more efficient to modify than to recreate |
| two equal strings can be stored only once | some computations are expressed more naturally |
| easier to debug, more reliable | no new object per change |
Neither design is always better, which is why you will see both. Knowing which kind of object you hold is what matters.
Notation
Because strings cannot change, the compiler is allowed to share them — with a result that contradicts Lesson 6b's advice.
Annotate
s1 == s2 is true: they are not just equivalent, they are identical — the same object.== safe for strings. It works here only because both values are known at compile time; the moment one comes from input or is built at run time, it fails. Lesson 6b's rule stands: always use equals.A rule that works only in the cases you happen to test is worse than no rule. This example explains why == sometimes appears to work, which is exactly the knowledge that stops you relying on it.
Worked example
Building a long string by concatenating many small pieces is the case where mutable objects are both more efficient and arguably more natural.
String text = "";
for (int i = 0; i < 10; i++) {
String line = in.nextLine(); // new string
text = text + line + '\n'; // two more strings
}
System.out.print("You entered:\n" + text);| per iteration | strings created |
|---|---|
| in.nextLine() | 1 — a new string each time |
| text + line | 1 more |
| ... + '\n' | 1 more |
| per iteration | 3 |
| over 10 iterations | 30 |
Count the objects created in the loop body.
Why: nextLine returns a new string; concatenating creates another; appending the newline creates a third.
Multiply by the iterations.
Why: This loop creates 30 String objects, for a program that looks like it handles ten lines.
Notice what happens to them.
Why: At the end, text refers to the most recent one. Garbage collection deletes the rest — but that is a lot of garbage for a seemingly simple program.
Notice why immutability causes it.
Why: Every concatenation must create a new object, because it cannot modify either of the strings it was given.
Verify: Count for yourself: three objects per iteration, ten iterations.
Why: Now imagine ten thousand lines instead of ten. The cost grows with the number of pieces and with the length of the accumulated text, because each concatenation copies everything so far — which is why this pattern is genuinely slow rather than merely wasteful.
Prediction
Count per iteration, then multiply.
String text = "";
for (int i = 0; i < 10; i++) {
String line = in.nextLine(); // new string
text = text + line + '\n'; // two more strings
}
System.out.print("You entered:\n" + text);| source | objects per iteration |
|---|---|
| in.nextLine() | 1 |
| text + line | 1 |
| ... + newline | 1 |
Predict first
Over ten iterations, roughly how many String objects are created?
Correct: 30
Why: Three per iteration — the line read, the result of concatenating it, and the result of appending the newline — over ten iterations. At the end only one is still referenced and the other 29 become garbage, which is a great deal of allocation for a program that reads ten lines.
Concept
The Java library provides the StringBuilder class for exactly this reason. Because StringBuilder objects are mutable, they can implement concatenation far more efficiently.
StringBuilder text = new StringBuilder();
for (int i = 0; i < 10; i++) {
String line = in.nextLine();
text.append(line);
text.append('\n');
}
System.out.print("You entered:\n" + text);| String version | StringBuilder version | |
|---|---|---|
| objects created per iteration | 3 | 1 — just the input line |
| what append does | n/a | modifies the StringBuilder in place |
| objects created by the concatenation | 20 | 0 |
| needs an import? | no | no — it is in java.lang |
The append method takes a String and appends it to the end of the StringBuilder. Each time it is invoked, it modifies the StringBuilder; it does not create any new objects. If you need the result as a String, call toString(). Programs that manipulate large amounts of text run much faster with StringBuilder than with String.
Trap
Correct, readable, and quadratic. The cost is invisible in the source.
String result = "";
for (int i = 0; i < n; i++) {
result = result + something; // copies everything so far
}| n | objects created | characters copied |
|---|---|---|
| 10 | about 20 | a few hundred |
| 1,000 | about 2,000 | about half a million |
| 100,000 | about 200,000 | about five billion |
The third row is the problem. Each concatenation copies the whole accumulated string, so the total work grows with the square of the number of pieces — the same shape as the nested loops in Lesson 6a.
Use a StringBuilder when the number of pieces is large or unknown.
StringBuilder sb = new StringBuilder();
for (int i = 0; i < n; i++) {
sb.append(something); // modifies in place
}
String result = sb.toString();| situation | use |
|---|---|
| joining two or three known pieces | String concatenation — clearer |
| building text in a loop | StringBuilder |
| thousands of pieces | StringBuilder, definitely |
| you need the result as a String | call toString() at the end |
The first row matters too: do not reach for StringBuilder everywhere. "Total: " + n is clearer than three method calls, and for a couple of pieces the cost is irrelevant. The rule is about loops.
Definition probe
The deciding question is how many pieces, and whether it is in a loop.
Sort into buckets
Sort each situation.
Prediction
StringBuilder is mutable.
StringBuilder sb = new StringBuilder();
sb.append("hello");
System.out.println(sb);| String.concat | StringBuilder.append | |
|---|---|---|
| creates a new object? | yes | no |
| must you assign the result? | yes | no |
Predict first
Does sb.append("hello") need its return value assigned?
Correct: No — append modifies the StringBuilder in place
Why: StringBuilder is mutable, so append changes the object it is invoked on and creates nothing new — which is exactly the opposite of s.toUpperCase() from Lesson 9a. This is the single most useful practical consequence of the mutable/immutable distinction: for an immutable object you must assign the result; for a mutable one you must not need to.
Trade off
Fill the blanks from Sections 10.10 and 10.11.
Comparison matrix
| immutable (String) | mutable (StringBuilder) | |
|---|---|---|
| safe to share between variables? | yes — nothing can change it | no — a change is visible through every reference |
| building text in a loop | a new object per concatenation | modified in place, no new objects |
| methods return | a new object, which you must assign | nothing useful — the object itself changed |
Read across the last row: whether you have to assign a method's result is a reliable signal of which kind of object you are holding. It is the practical form of the whole distinction.
Comparison
This lesson introduced a third clock. Fill the blanks.
Comparison matrix
| local variable | attribute | object | |
|---|---|---|---|
| created when | the method is invoked | the object is created | new runs |
| disappears when | the method returns | the object is destroyed | nothing refers to it any more |
| visible where | inside its own method only | in any of the object's methods | anywhere a reference reaches |
The bottom-right cell is the one that makes aliasing possible, and the one garbage collection is about: an object's life is decided by references, not by scope.
Pattern
Two questions answer most of what this lesson covered, and both are about lifetime.
// how long must this value live?
int oldv = this.x; // one method call -> local variable
this.x = newv; // as long as the object -> attribute
// how many pieces am I joining?
String s = "Total: " + n; // a couple -> concatenation
StringBuilder sb = new StringBuilder(); // many, in a loop
for (...) { sb.append(piece); } // -> StringBuilder
String result = sb.toString();| question | the answer decides |
|---|---|
| how long must this value live? | local variable, or attribute |
| does anything still refer to this object? | whether it is garbage |
| how many pieces of text am I joining? | String, or StringBuilder |
| must I assign this method's result? | whether the object is immutable |
this.x removes all doubt.Check
Work it out before you click.
public void shift(int dx) {
int temp = this.x;
this.x = temp + dx;
}| variable | kind | survives the call? |
|---|---|---|
| dx | parameter | no |
| temp | local variable | no |
| this.x | attribute | yes |
Check your understanding
Which of these still exists after the method returns?
Answer: A
Why: Parameters and local variables are created when a method is invoked and disappear when it returns, along with the frame that held them. An attribute belongs to the object rather than to any method, so it lives as long as the object does — which is what allows an object to remember anything between calls.
Check
Work it out before you click.
Point a = new Point(1, 2);
Point b = a;
a = null;| after | references to the Point |
|---|---|
| Point b = a; | 2 |
| a = null; | 1 |
Check your understanding
Is the Point eligible for garbage collection?
Answer: A
Why: An object is collectable only when nothing refers to it at all. Setting a to null removes one of two references, and b still points at the object — so it remains reachable and its attributes can still be read. Counting references to the object rather than assignments to variables is the reliable method.
Check
Work it out before you click.
StringBuilder sb = new StringBuilder();
sb.append("a");
sb.append("b");
System.out.println(sb);| call | new object created? | sb holds |
|---|---|---|
| append("a") | no | "a" |
| append("b") | no | "ab" |
Check your understanding
What is displayed, and how many StringBuilder objects were created?
Answer: A
Why: StringBuilder is mutable, so append modifies the existing object rather than creating a new one — only the single new StringBuilder() allocates anything. Because the object itself changes, no assignment is needed, which is the opposite of every String method.
Real world
Mutable or immutable is not a Java detail. It is one of the recurring design questions in programming.
Discussion prompt
You have now seen the trade three times: Strings against StringBuilder, arrays against nothing, and Rectangles against a hypothetical immutable version. What is the trade, stated generally — and how would you decide for a class of your own?
Hint: One side buys safety; the other buys efficiency.
Answer:
Immutability buys safety: objects can be shared freely, passed to any method, and used as keys, with no possibility of one holder changing what another sees. Mutability buys efficiency and naturalness: no new object per change, and some operations really are actions on a thing.
The deciding questions are how much the object will be shared, and how often it will change. Something shared widely and changed rarely should be immutable; something private to one part of the program and changed constantly can be mutable.
Modern practice leans toward immutable by default, precisely because the aliasing bugs from Lesson 10a are so hard to find. But Think Java's conclusion is the honest one: neither design is always better, which is why the Java library gives you both String and StringBuilder.
Commit first
Commit to an answer and to your confidence.
Predict first
In String s1 = "Hi, Mom!"; String s2 = "Hi, " + "Mom!"; why is s1 == s2 true?
Correct: Both values are known at compile time, so the compiler creates one shared String — which is safe only because Strings are immutable
Why: The compiler evaluates "Hi, " + "Mom!" before the program runs, sees that the two values are equivalent, and — because a String can never change — creates a single object that both variables refer to. So they are not merely equal but identical. This does not make == safe for Strings: it works here only because both values are compile-time constants, and it fails the moment one comes from input or is built at run time. Lesson 6b's rule stands, and this example explains why the wrong rule sometimes appears to work.
Explain it
Two minutes, out loud.
Discussion prompt
A classmate is building a report by concatenating a thousand lines with + in a loop, and cannot understand why it is slow when the code looks so simple. Explain the cost, and explain why StringBuilder does not have the same problem.
Hint: What has to happen on each concatenation, given that Strings are immutable?
Answer:
Strings cannot change, so every + has to build a brand-new string containing everything you had plus the new piece — which means copying the whole accumulated text. By line 1000 each concatenation is copying 999 lines' worth of characters, so the total work grows with the square of the number of lines.
StringBuilder is mutable, so append adds to the end of the existing object without copying what is already there. One object, a thousand appends, no copying.
The general point worth adding: the cost is invisible in the source. Both loops look the same length, and one of them does a thousand times more work — which is the same lesson as the hidden loop inside inRange in Lesson 7b.
Exit ticket
One question before you close the deck.
Predict first
When does an object become eligible for garbage collection?
Correct: When there are no references to it anywhere in the program
Why: If there are no references to an object, there is no way to access its attributes or invoke a method on it — from the program's point of view it has ceased to exist, so its space can be reclaimed. Setting one reference to null is not enough if another still points at it, and an object created in a method survives if the method returns a reference to it. Java has no delete operator at all: you do not have to do anything to make garbage collection happen.
Connect it up
One page, from memory.
Draw it
Draw the UML class diagram for Rectangle, with its four attributes and two methods, marking visibility correctly and using UML's name-colon-type order. Beside it, draw the memory diagram for two Rectangle objects, and mark which diagram would tell you the width of one particular box. Then write the three kinds of variable with when each is created and destroyed. Finally, write the two versions of the ten-line reading loop and count the objects each one creates.
Recap
Six sections that step back from using objects to describing them — how they are documented, drawn, scoped, and reclaimed.
| if you remember one thing | it is this |
|---|---|
| about scope | let the lifetime decide where to declare it |
| about objects | references keep an object alive, not scope |
| about text | building it in a loop wants a StringBuilder |
+ for public and - for private.this.x removes any doubt.this refers to the object a method was invoked on, which is how box.translate(...) reaches box's data.Want this taught 1-on-1? Alexander tutors Java — $55/session, free consultation.