What separates a primitive from an object, the null reference and the exception it throws, why no String method can ever change a string, and the wrapper classes that let a number behave like an object — including the methods that convert between text and numbers. Follows Think Java 2e, Chapter 9 (Immutable Objects), Sections 9.1-9.4, pp. 147-152, cross-referenced against Java SE 21 API — java.lang.String.
Subject: Java · 65 slides · code lesson
Open the interactive version of this deck
Title
Think Java 2e · Chapter 9 · Immutable Objects
Sections 9.1-9.4 · pp. 147-152
Objectives
This lesson follows Think Java 2e, Chapter 9 (Immutable Objects), Sections 9.1-9.4, pp. 147-152. Everything on these slides can be checked against those pages.
1. Distinguish a primitive variable from an object variable by what each one stores.
2. Explain what null means and what causes a NullPointerException.
3. Say why toUpperCase appears to do nothing, and fix it by assigning the return value.
4. Explain why immutability makes strings safe to share.
5. Use Integer and the other wrapper classes, and compare them with equals rather than ==.
6. Convert between strings and numbers with parseInt and toString, and name the exception a bad conversion throws.
Warm-up
Two facts from earlier chapters are about to be given their proper names.
Discussion prompt
From Lesson 6b: why does answer == "yes" fail even when the user typed yes? And from Lesson 7a: what does an array variable actually hold?
Hint: The two answers are the same answer.
Answer:
== compares whether two operands are the same object, and a typed string is a different object from the literal in your source. An array variable holds a reference to the array, not the elements.
Both are instances of one distinction that this chapter finally names: some variables hold values, and some hold references to objects. Everything in Chapters 9 through 11 follows from it.
Concept
Java is an object-oriented language: it uses objects to represent data and to provide methods related to that data. An object is a collection of data that provides a set of methods — and you have been using them since Chapter 3 without the word.
Figure (svg): Two panels contrasting a primitive variable holding its value directly with an object variable holding a reference
Downey & Mayfield, Think Java, 2nd edition (Green Tea Press / O'Reilly, 2020) — Think Java 2e, Chapter 9 (Immutable Objects), Sections 9.1-9.4, pp. 147-152 — Chapter 9 opens on printed page 147.
Section
Section 9.1
Concept
int, double, char and boolean are primitive types. When you declare a variable with a primitive type, Java reserves a small amount of memory to store its value — the value is in the box.
int number = -2;
char symbol = '!';
char[] array = {'c', 'a', 't'}; // a reference to an array
String word = "dog"; // a reference to an object| declaration | the variable holds | kind |
|---|---|---|
| int number = -2; | the value -2 | primitive |
| char symbol = '!'; | the character ! | primitive |
| char[] array = ... | a reference to an array | reference |
| String word = "dog"; | a reference to a String object | reference |
object — A collection of related data that comes with a set of methods that operate on the data.
object-oriented — A way of organising code and data into objects, rather than independent methods.
Objects work like arrays: a box for the variable, and an arrow pointing to where the data actually lives. You have already met several objects — Scanner, System.out and System.in are all objects, and so is every String.
Picture it
An array variable, a String variable and any other object variable are drawn identically. Only the thing at the end of the arrow differs.
Figure (svg): Two variables each with an arrow: one to a three-character array and one to a String object holding dog
Objects and arrays are usually created with the new keyword, which allocates memory for them. Strings are the convenient exception: new String("dog") works, and so does the literal "dog", which creates a String object implicitly.
Worked example
Lesson 6b told you to use equals for strings. Now the reason can be stated precisely, and it explains when == is the right operator.
int a = 5;
int b = 5;
System.out.println(a == b); // true - same value
String s1 = new String("dog");
String s2 = new String("dog");
System.out.println(s1 == s2); // false - different objects
System.out.println(s1.equals(s2)); // true - same characters| comparison | what == asks | answer |
|---|---|---|
| a == b, two ints | are these the same value? | yes |
| s1 == s2, two Strings | are these the same object? | no |
| s1.equals(s2) | do they contain the same characters? | yes |
For a primitive, the variable holds the value.
Why: So comparing the variables compares the values, which is what you want.
For an object, the variable holds a reference.
Why: So comparing the variables compares the references — that is, the addresses.
Two objects with the same contents are still two objects.
Why: new was called twice, so there are two String objects and the references differ.
equals traverses the objects and compares contents.
Why: The equals method traverses the String objects and tests whether they contain the same characters.
Verify: Run all three lines and expect true, false, true.
Why: The middle line is the one worth checking: it compiles, runs and answers a question you did not mean to ask. That is why Lesson 6b's rule — always use equals for strings — is a rule rather than a preference.
Definition probe
Ask whether the variable holds the value itself.
Sort into buckets
Sort each declaration.
Concept
Objects are not new in this chapter. They have been in every program since Chapter 3, and naming them explains several things you took on faith.
| object | met in | what it provides |
|---|---|---|
| Scanner | Chapter 3 | methods for parsing input |
| System.out | Chapter 1 | methods for displaying output |
| System.in | Chapter 3 | a stream of input characters |
| String | Chapter 2 | characters, plus methods for manipulating them |
| arrays | Chapter 7 | elements, plus a length |
This also explains Lesson 3a's puzzle. Printing System.out gave java.io.PrintStream@685d72cd because it is an object, and printing an object variable shows its type and address rather than its contents — exactly as printing an array did in Lesson 7a.
Trap
Correct for primitives, wrong for objects — and the syntax is identical.
int x = 5;
int y = 5;
if (x == y) { ... } // fine: compares values
String s = in.nextLine();
if (s == "quit") { ... } // wrong: compares references| operands | == compares | usually what you want? |
|---|---|---|
| two ints | the values | yes |
| two doubles | the values | yes, but see Lesson 2b on rounding |
| two Strings | the references | no |
| two arrays | the references | no |
Nothing in the code distinguishes the two cases — you have to know the types. That is why the rule has to be learned rather than deduced from the symbol.
Ask what the variable holds. A value, or a reference?
// primitives: == compares values
if (x == y) { ... }
// objects: equals compares contents
if (s.equals("quit")) { ... }
if (Arrays.equals(a, b)) { ... }| the variable's type | compare with |
|---|---|
| int, double, char, boolean | == |
| String, Integer, any object | equals |
| an array | Arrays.equals |
A quick test: is the type's name capitalised? Capitalised types are classes, so their variables hold references, so use equals. Lowercase types are primitives, so use ==. That is why String is capitalised and int is not — the capitalisation was telling you this all along.
Prediction
Two String objects created separately.
String a = new String("cat");
String b = new String("cat");
System.out.println(a == b);
System.out.println(a.equals(b));| comparison | asks | result |
|---|---|---|
| a == b | the same object? | false — new was called twice |
| a.equals(b) | the same characters? | true |
Predict first
What are the two lines of output?
Correct: false then true
Why: new was called twice, so there are two distinct String objects and the references differ — == is false. The characters are identical, so equals, which traverses the objects and compares contents, is true. This is the clearest possible demonstration that the two operators ask different questions.
Matching
Value or reference?
Match the pairs
Why: int and char are primitives, so their variables hold values and == is exactly right. String and int[] are reference types, so == would compare addresses — you need a method that looks at the contents. The capitalisation of the type name is the giveaway in every case.
Socratic
Java could have made everything an object. It did not.
Discussion prompt
Every primitive has a corresponding class — int has Integer, char has Character. So why does Java keep the primitives at all, rather than making everything an object?
Hint: Compare the memory diagrams for int number = -2; and an Integer holding -2.
Answer:
Cost. A primitive is a small amount of memory holding the value directly. An object needs memory for the object itself, plus a reference to it, plus a step to follow the reference every time you use it.
For a loop counter incremented a million times, that difference is enormous. Arithmetic on primitives compiles to single machine instructions; arithmetic on objects does not.
So it is a deliberate trade: primitives for speed, objects for capability. The wrapper classes later in this lesson exist so you can move a value from one world to the other when you need to.
Section
Section 9.2
Concept
Often when you declare an object variable you assign it to reference an object. But sometimes you want a variable that does not refer to an object, at least initially. In Java the keyword null is a special value meaning no object.
String name = null;
int[] combo = null;
System.out.println(name.length()); // NullPointerException
System.out.println(combo[0]); // NullPointerException| what you do with null | result |
|---|---|
| assign it to an object variable | fine |
| pass it to a method | fine |
| return it from a method | fine |
| invoke a method on it | NullPointerException |
| access an element of it | NullPointerException |
In a memory diagram, null is a small box with no arrow. It is perfectly fine to pass a null reference as an argument or receive one as a return value — in those situations null often represents a special condition or indicates an error.
Picture it
These are different concepts and the distinction matters, because one of them supports method calls and the other does not.
Figure (svg): Two variables: one null with no arrow at all, and one pointing to an empty String object that exists
str1 is null, meaning it does not reference an object. str2 refers to the empty string, an object that exists and has a length of zero. str2.length() returns 0; str1.length() throws.
Worked example
This is one of the most common exceptions in Java, and its cause is always the same: a method was invoked on a variable that referred to nothing.
String name = null;
int n = name.length();| step | what happens |
|---|---|
| String name = null; | the variable exists and refers to no object |
| name.length() | Java follows the reference to invoke the method |
| there is no object | NullPointerException is thrown |
| the program | terminates, unless the exception is handled |
Find the variable named in the message.
Why: Modern Java says which variable was null, which usually identifies the bug immediately.
Ask where it was supposed to be assigned.
Why: Either it was never given an object, or something returned null and the result was not checked.
Note that the exception is at run time.
Why: The code compiles: assigning null to an object variable is perfectly legal, so the compiler has nothing to object to.
Fix by assigning an object, or by checking for null first.
Why: The second option is the subject of Lesson 9b's argument validation.
Verify: Run it and read the message; then change the declaration to String name = ""; and confirm length() returns 0 with no exception.
Why: That single change is the whole distinction: the empty string is an object you can call methods on, and null is not.
Prediction
The variable refers to no object.
String s = null;
System.out.println(s);
System.out.println(s.length());| line | result |
|---|---|
| println(s) | displays the word null — no method is invoked |
| s.length() | NullPointerException — a method IS invoked |
Predict first
What happens?
Correct: It prints null, then throws a NullPointerException
Why: Printing a null reference is safe — println handles it and displays the word null. Invoking a method on one is not, because there is no object to invoke it on. The distinction is whether the reference has to be followed.
Concept
null is not simply a hazard. It is a legitimate value with a legitimate use, and you have already seen its cousin.
| situation | what null expresses |
|---|---|
| a name that has not been supplied yet | there is no value here yet |
| a search that found nothing | no matching object exists |
| an optional argument that was omitted | the caller did not provide one |
| an array of objects, freshly created | every element is null until assigned |
The second row is worth connecting: indexOf returned -1 for a failed search because an index can never be negative. A method returning an object has no such spare value, so it returns null instead. Same idea, different type — and the same obligation on the caller to check.
Trap
Treating them as the same produces an exception on one and not the other.
String a = null;
String b = "";
System.out.println(b.length()); // 0 - fine
System.out.println(a.length()); // NullPointerException| variable | refers to an object? | length() |
|---|---|---|
| b = "" | yes — an empty one | 0 |
| a = null | no | NullPointerException |
Both look like nothing when printed — System.out.println(a) displays the word null and println(b) displays a blank line. The difference only appears when you call a method.
Check for null first, then for empty.
if (str == null || str.isEmpty()) {
return false;
}| str | str == null | str.isEmpty() | result |
|---|---|---|---|
| null | true | not evaluated — short circuit | true |
| "" | false | true | true |
| "Ada" | false | false | false |
The order matters, and Lesson 5b explains why: || short-circuits, so when str == null is true the second operand is never evaluated and isEmpty() is never called on null. Writing the two tests the other way round throws. Lesson 9b returns to this.
Definition probe
null can be moved around freely; it just cannot be dereferenced.
Sort into buckets
Sort each use of a null reference.
Fill the middle
Return false for a null or empty string.
Fill in the blanks
if (str == null || str.isEmpty()) ___
Why: || short-circuits, so when the string is null the second test is never evaluated and isEmpty() is never called on it. Writing the tests in the other order — str.isEmpty() || str == null — throws a NullPointerException on exactly the input the guard was meant to catch.
Counterexample
Its inventor called it a mistake. Consider both sides.
Discussion prompt
Tony Hoare, who introduced null references in 1965, later called them his billion-dollar mistake. What is the problem with null — and what would you lose if Java removed it?
Hint: What does the type String promise about a variable's contents?
Answer:
The problem is that every object variable can secretly be null, and the type does not say so. A parameter declared String str promises a String and may deliver nothing, so every method that uses one has to defend against it — or crash.
What you would lose is a way to say there is no value here. A search that finds nothing, a field not yet filled in, an optional argument — all need some way to express absence.
Modern languages answer this by making absence part of the type, so the compiler forces you to handle it. Java has Optional for the same purpose. The lesson for now is simpler: treat every object reference you did not create yourself as possibly null, and check it.
Section
Section 9.3
Concept
toLowerCase and toUpperCase are often a source of confusion, because it sounds as though they modify strings. Neither these methods nor any others can change a string, because strings are immutable. When you invoke one, you get a new String object as a result.
String name = "Alan Turing";
String upperName = name.toUpperCase();
// upperName refers to "ALAN TURING"
// name STILL refers to "Alan Turing"| variable | refers to | changed? |
|---|---|---|
| name | "Alan Turing" | no — never |
| upperName | "ALAN TURING" | a new object |
This is the same behaviour as substring in Lesson 6b, which returns a new string that copies letters from an existing one. Now the property has a name, and it applies to every String method without exception.
Picture it
Two variables, two objects. The method created the second one and returned a reference to it.
Figure (svg): Two String variables with arrows to two separate objects, one holding Alan Turing and one holding ALAN TURING
Nothing was modified. A new object was created, and the only way to make use of it is to keep the reference the method returned.
Worked example
A common mistake is to assume that toUpperCase somehow affects the original string. It compiles, runs, and does nothing observable.
// wrong: the return value is ignored
String name = "Alan Turing";
name.toUpperCase();
System.out.println(name); // Alan Turing
// right: assign the return value
String name = "Alan Turing";
name = name.toUpperCase();
System.out.println(name); // ALAN TURING| version | what the method does | what name refers to after |
|---|---|---|
| name.toUpperCase(); | creates a new string and returns it | "Alan Turing" — unchanged |
| name = name.toUpperCase(); | the same, and the result is stored | "ALAN TURING" |
Notice what the first version discards.
Why: The new String object is created and immediately thrown away — exactly the ignored return value from Lesson 4b.
Notice what the second version changes.
Why: Not the string — the variable. name is made to refer to a different object.
Read the assignment carefully.
Why: name = name.toUpperCase() evaluates the right side first, producing a new object, and then points name at it.
Apply the same rule to every String method.
Why: replace, substring, trim, toLowerCase — all return new strings, and all are useless without an assignment.
Verify: Run both and expect Alan Turing from the first and ALAN TURING from the second.
Why: Then try text = text.replace("Computer Science", "CS") and confirm the same rule holds. If you do not assign the return value, invoking the method has no effect — which is the single most useful sentence in this section.
Prediction
The return value is ignored.
String s = "hello";
s.toUpperCase();
System.out.println(s);| step | s refers to |
|---|---|
| String s = "hello"; | "hello" |
| s.toUpperCase(); | "hello" — a new string was made and discarded |
Predict first
What is displayed?
Correct: hello
Why: toUpperCase creates and returns a new String object, and the original is never modified because strings are immutable. Since the return value is not assigned to anything, the new string is discarded and s still refers to the original. Writing s = s.toUpperCase(); is what makes the change stick.
Concept
It would seem more convenient if a string could be modified in place. Strings are immutable by design, and the reasons are worth knowing.
| because strings cannot change | you get |
|---|---|
| passing one to a method is safe | the method cannot corrupt your copy |
| two variables can share one string | no aliasing bug, ever |
| a string can be used as a fixed label | its value is stable once created |
| no defensive copying is needed | simpler and faster code |
The second row is the important one. In Lesson 7a, two variables referring to the same array was a hazard — changing it through one changed it for both. Two variables referring to the same string is completely safe, because neither can change it. Immutability makes sharing free.
Trap
The method runs and its result is discarded. No error, no effect.
String text = "Computer Science is fun!";
text.replace("Computer Science", "CS");
System.out.println(text);
// Computer Science is fun!| step | what exists |
|---|---|
| before | one string: "Computer Science is fun!" |
| replace runs | two strings; the new one is "CS is fun!" |
| the return value is ignored | the new string is unreachable and discarded |
| text | still refers to the original |
The work really was done — a new string was built. Nothing kept a reference to it, so it may as well not have been. This is a logic error: it compiles, runs, and silently does nothing.
Assign the return value.
String text = "Computer Science is fun!";
text = text.replace("Computer Science", "CS");
System.out.println(text);
// CS is fun!| String method | returns | you must |
|---|---|---|
| toUpperCase() | a new string | assign it |
| toLowerCase() | a new string | assign it |
| replace(a, b) | a new string | assign it |
| substring(i) | a new string | assign it |
| trim() | a new string | assign it |
A reliable habit: any String method call on a line by itself is a mistake. If the result is not assigned, passed, printed or compared, the call accomplished nothing.
Error analysis
Four lines, each calling a String method. Two of them accomplish nothing at all.
Annotate
s.trim(); — creates a trimmed string and throws it away. s still has its leading and trailing spaces.s = s.toLowerCase(); — this one works, because the return value is assigned. s now refers to a new, lowercased string.s.replace("world", "java"); — creates a replaced string and throws it away. No assignment, no effect. hello world — lowercased, because that line assigned, but still untrimmed and with 'world' intact.Two of the four lines are pure waste and neither produces a warning. This is why immutability has to be understood rather than memorised — the symptom is silence.
Discrimination
The test is whether the return value is kept.
Sort into buckets
Sort each statement.
Edge cases
Some languages do. Ask what Java would lose.
Discussion prompt
If a String could be modified in place, s.toUpperCase() on its own would work as beginners expect. What would that cost — think about a string passed to a method, and about two variables referring to one string.
Hint: Compare with the array aliasing from Lesson 7a.
Answer:
You would get array-style aliasing bugs for strings. Passing a string to a method would mean the method could change your string, so callers would have to make defensive copies of anything they cared about.
And two variables referring to one string would be a hazard rather than a convenience — the exact situation Lesson 7a warned about for arrays.
So the trade is: beginners are surprised once, and everyone is safe afterwards. Java's own StringBuilder exists for the cases where you genuinely need to modify text repeatedly, and Chapter 10 introduces it — but the default is immutable, deliberately.
Section
Section 9.4
Concept
Primitive types cannot be null and do not provide methods — you cannot invoke equals on an int. But for each primitive type there is a corresponding wrapper class in the Java library.
int i = 5;
System.out.println(i.equals(5)); // compiler error
Integer j = Integer.valueOf(5);
System.out.println(j.equals(5)); // displays true| primitive | wrapper class |
|---|---|
| int | Integer |
| char | Character |
| double | Double |
| boolean | Boolean |
| long | Long |
The wrapper class for int is named Integer, with a capital I — the capitalisation rule again. The wrappers are in java.lang, so you can use them without importing them.
Notation
Four capabilities, and each of them is a reason a wrapper might be worth its cost.
Annotate
equals, which a primitive cannot have because it is not an object.MIN_VALUE and MAX_VALUE. Integer.MIN_VALUE is -2147483648 and Integer.MAX_VALUE is 2147483647 — so you do not have to remember them or write them out.Integer.parseInt and Integer.toString convert between text and numbers, which is the subject of the next idea.Note that the constants and the conversion methods belong to the class rather than to any particular object: you write Integer.MAX_VALUE, not someInteger.MAX_VALUE.
Worked example
Because wrappers are objects, the == versus equals distinction applies to them exactly as it does to strings — and the consequences surprise people.
Integer x = Integer.valueOf(123);
Integer y = Integer.valueOf(123);
if (x == y) { // false
System.out.println("x and y are the same object");
}
if (x.equals(y)) { // true
System.out.println("x and y have the same value");
}| comparison | asks | result |
|---|---|---|
| x == y | the same object? | false — two objects were created |
| x.equals(y) | the same value? | true |
Note that two objects were created.
Why: valueOf was called twice, so x and y refer to different objects.
== compares the references.
Why: Different objects means different references, so the test is false.
equals compares the values.
Why: Both hold 123, so it is true.
Only one message is displayed.
Why: Because x and y refer to different objects, this code displays only x and y have the same value.
Verify: Run it and confirm exactly one line of output.
Why: Now notice how easily this could hide: with small values Java sometimes reuses the same object, so == can be true for valueOf(5) and false for valueOf(1000). That is precisely why the rule is to always use equals — a test that works for small numbers and fails for large ones is worse than one that never works.
Prediction
Two wrapper objects holding the same value.
Integer x = Integer.valueOf(500);
Integer y = Integer.valueOf(500);
System.out.println(x == y);
System.out.println(x.equals(y));| comparison | asks | result |
|---|---|---|
| x == y | the same object? | false |
| x.equals(y) | the same value? | true |
Predict first
What are the two lines of output?
Correct: false then true
Why: Two separate Integer objects were created, so the references differ and == is false, while equals compares the values and is true. This is exactly the String situation from earlier in the lesson — wrapper objects are objects, so all the object rules apply to them.
Concept
Wrappers cost memory and a level of indirection. Use them where you need what they provide, and primitives everywhere else.
| situation | use |
|---|---|
| a loop counter | int — speed matters, and it is never null |
| arithmetic on ordinary numbers | int or double |
| a value that might be absent | Integer — because it can be null |
| reaching MIN_VALUE or MAX_VALUE | the class constant, whatever your variable's type |
| converting text to a number | Integer.parseInt — a class method |
The fourth and fifth rows are worth noticing: you use Integer.MAX_VALUE and Integer.parseInt constantly without ever creating an Integer object. They are provided by the class, and using them does not commit you to using wrapper objects anywhere.
Trap
It sometimes works, which is what makes it dangerous.
Integer a = Integer.valueOf(100);
Integer b = Integer.valueOf(100);
System.out.println(a == b); // may be true
Integer c = Integer.valueOf(1000);
Integer d = Integer.valueOf(1000);
System.out.println(c == d); // false| value | == result | why |
|---|---|---|
| 100 | true | Java caches small Integer objects and reuses them |
| 1000 | false | outside the cache, so two objects are created |
A test that passes for small values and fails for large ones is the worst possible outcome: it survives development and fails in production, on data nobody tested with.
Always use equals for wrapper objects, exactly as for strings.
Integer c = Integer.valueOf(1000);
Integer d = Integer.valueOf(1000);
System.out.println(c.equals(d)); // true, for every value| comparing | use |
|---|---|
| two ints | == |
| two Integers | equals |
| an Integer and an int | equals, or compare the values |
| two Strings | equals |
The rule from earlier in this lesson covers this case too: capitalised type means an object means equals. Integer is capitalised for the same reason String is.
Matching
Five primitives, five classes.
Match the pairs
Why: Three of the four wrappers are simply the primitive's name capitalised. int and char are the exceptions — they are abbreviations, and their wrappers spell the word out as Integer and Character. All four live in java.lang, so none needs importing.
Prediction
A constant on the class, not on an object.
System.out.println(Integer.MAX_VALUE);
System.out.println(Integer.MIN_VALUE);| constant | value | roughly |
|---|---|---|
| Integer.MAX_VALUE | 2147483647 | about 2 billion |
| Integer.MIN_VALUE | -2147483648 | about -2 billion |
Predict first
Why is MIN_VALUE one further from zero than MAX_VALUE?
Correct: Because zero takes one of the positive slots, so the negatives get one more
Why: An int has 32 bits and therefore about 4 billion possible values, split evenly between negative and non-negative. Zero counts as non-negative, so there is one fewer positive value than negative one — which is why the range is -2147483648 to 2147483647 rather than symmetric.
Explain it to yourself
Give the strongest case for each.
Discussion prompt
You can add, compare and print an int perfectly well. Name two things an Integer can do that an int cannot, and one thing an int does better.
Hint: Think about absence, and about speed.
Answer:
An Integer can be null, expressing no value, and it provides methods such as equals and compareTo. Neither is possible for a primitive.
An int is faster and smaller. The value sits directly in the variable rather than requiring an object plus a reference to follow, which matters enormously in a loop.
The practical rule: use primitives by default, and reach for a wrapper when you need nullability or a method. Chapter 12 gives a third reason — some Java library structures can only hold objects, never primitives.
Section
Section 9.4, continued
Concept
Wrapper classes provide methods for converting strings to and from primitive types. In this context parse means read and translate.
String str = "12345";
int num = Integer.parseInt(str); // text -> number
int n = 12345;
String s = Integer.toString(n); // number -> text| method | takes | returns |
|---|---|---|
| Integer.parseInt | a String | an int |
| Double.parseDouble | a String | a double |
| Boolean.parseBoolean | a String | a boolean |
| Integer.toString | an int | a String |
This finally answers a question from Chapter 2: "123" and 123 are different things, and these are the methods that convert between them. Other wrapper classes provide similar methods.
Picture it
Lesson 2a said a String cannot hold an integer and an int cannot hold text. These methods are the bridge.
Figure (svg): A pipeline showing a string being parsed into an int and an int being converted back into a string
It is always possible to convert a primitive value to a string, but not the other way around. Every number has a text form; not every piece of text is a number.
Worked example
The asymmetry has a consequence: parseInt can be given a string that is not a number, and it has to do something about it.
String str = "five";
int num = Integer.parseInt(str); // NumberFormatException| input string | result |
|---|---|
| "12345" | 12345 |
| "-3" | -3 |
| "five" | NumberFormatException |
| "12.5" | NumberFormatException — that is a double, not an int |
| "" | NumberFormatException |
Try to parse a non-numeric string.
Why: parseInt throws a NumberFormatException, because the characters in "five" are not digits.
Note that it compiles.
Why: The compiler cannot know what the string will contain at run time, so this is a run-time error.
Note the fourth row.
Why: "12.5" fails for parseInt because a decimal point is not a digit — you would need Double.parseDouble.
Guard against it.
Why: Either validate the input first, as in Lesson 5b, or handle the exception, which is Chapter 15.
Verify: Run all five inputs and confirm which throw.
Why: Then connect this to Lesson 5b: Scanner.hasNextInt() exists precisely so you can ask whether parsing will succeed before attempting it. Ask before you act — the same rule, applied to a different conversion.
Prediction
The string is not made of digits.
String s = "3.14";
int n = Integer.parseInt(s);| character | a digit? |
|---|---|
| 3 | yes |
| . | no |
| 1 | yes |
Predict first
What happens?
Correct: NumberFormatException — a decimal point is not a digit
Why: Integer.parseInt requires the entire string to be a valid integer, and a decimal point is not part of one. It does not truncate or give up partway — it refuses the whole string. Double.parseDouble("3.14") would succeed and give 3.14.
Concept
Integer.toString is explicit, and you have been converting numbers to strings since Chapter 2 without noticing.
| written | produces | when to prefer it |
|---|---|---|
| Integer.toString(n) | the String "12345" | when you want only the conversion |
| "" + n | the same string | concise, but slightly obscure |
| String.format("%d", n) | the same string | when you also want formatting |
| System.out.println(n) | prints it — no String is kept | when you only want to display it |
The second row is Chapter 2's concatenation rule: + with a String operand converts the other operand to text. It works and Integer.toString says what it means, which is why the book introduces the explicit form.
Trap
Any non-numeric input crashes the program.
Scanner in = new Scanner(System.in);
System.out.print("Enter a number: ");
String line = in.nextLine();
int n = Integer.parseInt(line); // throws on "hello"| the user types | result |
|---|---|
| 42 | n is 42 |
| hello | NumberFormatException, and the program stops |
| (nothing, just Enter) | NumberFormatException |
The third row is worth noticing: pressing Enter with no text gives an empty string, which is also not a number. Real users do this constantly.
Ask before you act, exactly as in Lesson 5b.
Scanner in = new Scanner(System.in);
System.out.print("Enter a number: ");
if (!in.hasNextInt()) {
System.err.println(in.next() + " is not a number");
return;
}
int n = in.nextInt();| approach | on bad input |
|---|---|
| parseInt with no check | NumberFormatException, program stops |
| hasNextInt first | your own message, and a controlled exit |
| catch the exception | Chapter 15 — also a valid answer |
This is Lesson 5b's validation pattern, and it is worth noticing that the same two options keep appearing: check first, or handle the failure. Both are legitimate; what is not legitimate is doing neither.
Definition probe
One conversion can always be done; the other cannot.
Sort into buckets
Sort each conversion.
Fill the middle
Text to number, and back.
Fill in the blanks
String s = "42";
int n = Integer.parseInt(s);
String back = Integer.toString(n);
Why: parseInt reads and translates text into an int, and toString produces the text form of a number. Both are called on the class rather than on an object — you never create an Integer object to use them.
Real world
Every value that arrives from outside your program arrives as text.
Discussion prompt
Command-line arguments, file contents, web form fields and network messages all arrive as strings. What does that mean about where NumberFormatException tends to occur, and what should you do about it?
Hint: Which of those sources can you trust?
Answer:
It means parsing happens at every boundary of your program, and every one of those boundaries is somewhere a user or another system supplies data you did not write.
So NumberFormatException clusters at the edges. The interior of a well-written program works with numbers that were validated on the way in — which is exactly the shape of the Logarithm program from Lesson 5b.
Validate at the boundary, trust inside. That is a principle worth carrying well beyond Java, and it is why the next lesson opens with command-line arguments and then immediately spends a section on validating them.
Comparison
Fill the blanks. Every row follows from what the variable holds.
Comparison matrix
| primitive (int) | object (String) | |
|---|---|---|
| the variable holds | the value itself | a reference to the object |
| can be null? | no | yes |
| compare with | == | equals |
| provides methods? | no | yes |
| printing it shows | the value | the contents for String; type@address for most objects |
The last row has an exception worth knowing: String has been given a proper toString, so printing one shows its characters. Most objects have not, which is why arrays and PrintStreams print as addresses — and Chapter 11 shows how to fix that for your own classes.
Pattern
Immutability turns every String method into the same shape: it computes a new object and hands it back, and the caller decides what to do with it.
// wrong - the result is discarded
s.toUpperCase();
s.trim();
s.replace("a", "b");
// right - the result is kept
s = s.toUpperCase();
s = s.trim();
String t = s.replace("a", "b");| question to ask | if the answer is no |
|---|---|
| is this variable a primitive or a reference? | == may be asking the wrong question |
| could this reference be null? | a method call on it will throw |
| did I keep what this String method returned? | the call accomplished nothing |
| can this string really be parsed as a number? | parseInt will throw |
equals.||.Check
Work it out before you click.
String s = "java";
s.toUpperCase();
String t = s.toUpperCase();
System.out.println(s + " " + t);| line | effect |
|---|---|
| s.toUpperCase(); | creates a string and discards it |
| String t = s.toUpperCase(); | creates a string and keeps it |
| s | unchanged throughout |
Check your understanding
What is displayed?
Answer: A
Why: Strings are immutable, so s never changes no matter how many methods are invoked on it — the second line creates a new string and throws it away. Only the third line keeps a result, in t, so s is still "java" and t is "JAVA".
Check
Work it out before you click.
String a = null;
String b = "";
System.out.println(b.length());
System.out.println(a.length());| variable | refers to an object? |
|---|---|
| b | yes — an empty String |
| a | no |
Check your understanding
What happens?
Answer: A
Why: The empty string is a real String object with a length of 0, so calling length() on it is fine. a refers to no object at all, so invoking any method on it throws NullPointerException. null and empty are different concepts, and only one of them supports method calls.
Check
Work it out before you click.
Integer a = Integer.valueOf(1000);
Integer b = Integer.valueOf(1000);| comparison | asks |
|---|---|
| a == b | the same object? |
| a.equals(b) | the same value? |
Check your understanding
Which comparison reliably tells you the two hold the same number?
Answer: A
Why: Integer is a class, so its variables hold references and == compares identity rather than value. equals compares the wrapped values and is correct for every number. == happens to work for small values because Java caches those objects, which makes it worse rather than better — it fails only on data you did not test.
Real world
Strings are immutable in Java, Python, JavaScript, C# and many other languages. That is not a coincidence.
Discussion prompt
Why do you think so many language designers independently chose to make strings unchangeable, given that it surprises every beginner? What problem are they all avoiding?
Hint: Think about what happens when two parts of a program share one string.
Answer:
They are avoiding shared mutable state. If a string could change, then passing one to a method, storing one in a collection, or using one as a lookup key would all become risky — any holder of a reference could change it under the others.
Immutability makes sharing free: you can hand a string to anyone without copying it and without worrying what they do with it. That is worth far more in aggregate than the convenience of modifying one in place.
The idea generalises well beyond strings. It is why Chapter 10 draws such a sharp line between mutable and immutable objects, and why 'make it immutable unless you need to change it' is standard advice in modern programming.
Commit first
Commit to an answer and to your confidence.
Predict first
After String s = "hello"; s.toUpperCase(); what does s refer to?
Correct: "hello" — the method returned a new string that was discarded
Why: Strings are immutable, so toUpperCase cannot modify s — it builds a new String object holding "HELLO" and returns a reference to it. Since nothing is done with that return value, the new object becomes unreachable and is discarded, leaving s pointing at the original. Writing s = s.toUpperCase(); is what makes the change stick, and the same rule applies to every String method: assign the return value, or the call accomplished nothing.
Explain it
Two minutes, drawing as you go.
Discussion prompt
A classmate wrote name.toUpperCase(); and cannot understand why the name is still lowercase. Draw the memory diagram before and after that line, and use it to explain. Then explain why Java makes strings behave this way rather than the way they expected.
Hint: Draw two objects, not one.
Answer:
Before the line there is one variable with an arrow to a String object. The method creates a second object holding the uppercase text and returns a reference to it — but nothing catches that reference, so the second object is unreachable and thrown away. The variable's arrow never moved.
Java does this so that strings can be shared safely. If the method could modify the string in place, then anyone else holding a reference to it would see their string change underneath them — the aliasing problem from arrays, but for text.
If the drawing has only one object in it, the explanation will not land. The whole point is that there are two.
Exit ticket
One question before you close the deck.
Predict first
What is the difference between a variable that is null and a variable holding the empty string?
Correct: null refers to no object at all; the empty string is an object with zero characters
Why: In a memory diagram, null is a box with no arrow, while a variable holding the empty string has an arrow to a real String object that happens to contain nothing. The practical consequence is that "".length() returns 0 while null.length() throws a NullPointerException — you can invoke methods on an empty object but not on the absence of one. That is also why a guard must test str == null before str.isEmpty().
Connect it up
One page, from memory.
Draw it
Draw four variables side by side: an int holding 5, a String holding "dog", a String that is null, and a String holding the empty string. Show clearly which have arrows and which do not. Beside each, write what == 5 or .length() would do. Then draw the before-and-after diagram for name = name.toUpperCase();, showing both objects, and write one sentence saying what would be different if the assignment were left off.
Recap
Four sections that name the distinction running through everything since Chapter 6, and draw out its consequences.
| if you remember one thing | it is this |
|---|---|
| about variables | value or reference — the capital letter tells you |
| about String methods | assign the return value or nothing happens |
| about null | check it before you call anything on it |
== compares values for primitives and identity for objects, which is why strings need equals.Want this taught 1-on-1? Alexander tutors Java — $55/session, free consultation.