Primitives vs Objects, null, and Immutable Strings

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

What this lesson covers

The lesson, slide by slide

1. Primitives vs Objects, null, and Immutable Strings

Title

Think Java 2e · Chapter 9 · Immutable Objects

Sections 9.1-9.4 · pp. 147-152

2. What you will be able to do

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.

3. Retrieve before you read

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.

4. Two kinds of variable

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.

5. Primitives against objects

Section

Section 9.1

6. Not everything in Java is an object

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
declarationthe variable holdskind
int number = -2;the value -2primitive
char symbol = '!';the character !primitive
char[] array = ...a reference to an arrayreference
String word = "dog";a reference to a String objectreference

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.

7. The same picture, three times

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.

8. Why == means two different things

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
comparisonwhat == asksanswer
a == b, two intsare these the same value?yes
s1 == s2, two Stringsare 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.

9. Primitive or reference?

Definition probe

Ask whether the variable holds the value itself.

Sort into buckets

Sort each declaration.

holds a value — primitive
int count = 0;; double ratio = 2.5;; char initial = 'A';
holds a reference
String name = "Ada";; int[] scores = new int[10];
prim
A primitive type, written in lowercase. Java reserves a small amount of memory and stores the value directly in it.
ref
A class or an array type. The variable holds a reference, and the data lives elsewhere — which is why == compares identity and equals compares contents.

10. What you have already been using

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.

objectmet inwhat it provides
ScannerChapter 3methods for parsing input
System.outChapter 1methods for displaying output
System.inChapter 3a stream of input characters
StringChapter 2characters, plus methods for manipulating them
arraysChapter 7elements, 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.

11. Using == on objects out of habit

Trap

The 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== comparesusually what you want?
two intsthe valuesyes
two doublesthe valuesyes, but see Lesson 2b on rounding
two Stringsthe referencesno
two arraysthe referencesno

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.

The fix

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 typecompare with
int, double, char, boolean==
String, Integer, any objectequals
an arrayArrays.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.

12. What does this display?

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));
comparisonasksresult
a == bthe same object?false — new was called twice
a.equals(b)the same characters?true

Predict first

What are the two lines of output?

  • false then true
  • true then true
  • false then false
  • true then false

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.

13. Match the type to how you compare it

Matching

Value or reference?

Match the pairs

  • a. int
  • b. String
  • c. char
  • d. int[]
  • r1. == compares the values
  • r2. equals compares the characters
  • r3. == compares the characters
  • r4. Arrays.equals compares the elements

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.

14. Why have primitives at all?

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.

15. The null keyword

Section

Section 9.2

16. A variable that refers to nothing

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 nullresult
assign it to an object variablefine
pass it to a methodfine
return it from a methodfine
invoke a method on itNullPointerException
access an element of itNullPointerException

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.

17. null against the empty string

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.

18. Reading a NullPointerException

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();
stepwhat 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 objectNullPointerException is thrown
the programterminates, 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.

19. What does this do?

Prediction

The variable refers to no object.

String s = null;
System.out.println(s);
System.out.println(s.length());
lineresult
println(s)displays the word null — no method is invoked
s.length()NullPointerException — a method IS invoked

Predict first

What happens?

  • It prints null, then throws a NullPointerException
  • It throws immediately on the first println
  • It prints null twice
  • It prints an empty line, then 0

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.

20. When null is the right answer

Concept

null is not simply a hazard. It is a legitimate value with a legitimate use, and you have already seen its cousin.

situationwhat null expresses
a name that has not been supplied yetthere is no value here yet
a search that found nothingno matching object exists
an optional argument that was omittedthe caller did not provide one
an array of objects, freshly createdevery 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.

21. Confusing null with the empty string

Trap

The 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
variablerefers to an object?length()
b = ""yes — an empty one0
a = nullnoNullPointerException

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.

The fix

Check for null first, then for empty.

if (str == null || str.isEmpty()) {
    return false;
}
strstr == nullstr.isEmpty()result
nulltruenot evaluated — short circuittrue
""falsetruetrue
"Ada"falsefalsefalse

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.

22. Legal or NullPointerException?

Definition probe

null can be moved around freely; it just cannot be dereferenced.

Sort into buckets

Sort each use of a null reference.

perfectly legal
String s = null;; System.out.println(s); someMethod(s); if (s == null)
NullPointerException
s.length()
ok
Assigning, passing, returning, printing and comparing a null reference are all fine — none of them needs to follow the reference to an object.
npe
Invoking a method or accessing an element requires an object to act on, and there is none. This is the only thing that throws.

23. Guard against null

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.

24. Is null a good idea?

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.

25. Strings are immutable

Section

Section 9.3

26. No String method can change a string

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"
variablerefers tochanged?
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.

27. The original is untouched

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.

28. The mistake this section exists to prevent

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
versionwhat the method doeswhat 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.

29. What does this print?

Prediction

The return value is ignored.

String s = "hello";
s.toUpperCase();
System.out.println(s);
steps refers to
String s = "hello";"hello"
s.toUpperCase();"hello" — a new string was made and discarded

Predict first

What is displayed?

  • hello
  • HELLO
  • nothing
  • a compile error

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.

30. Why immutability is a feature

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 changeyou get
passing one to a method is safethe method cannot corrupt your copy
two variables can share one stringno aliasing bug, ever
a string can be used as a fixed labelits value is stable once created
no defensive copying is neededsimpler 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.

31. Ignoring the return value of a String method

Trap

The 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!
stepwhat exists
beforeone string: "Computer Science is fun!"
replace runstwo strings; the new one is "CS is fun!"
the return value is ignoredthe new string is unreachable and discarded
textstill 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.

The fix

Assign the return value.

String text = "Computer Science is fun!";
text = text.replace("Computer Science", "CS");
System.out.println(text);
// CS is fun!
String methodreturnsyou must
toUpperCase()a new stringassign it
toLowerCase()a new stringassign it
replace(a, b)a new stringassign it
substring(i)a new stringassign it
trim()a new stringassign 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.

32. Find the calls that do 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.
  • The output is hello world — lowercased, because that line assigned, but still untrimmed and with 'world' intact.
  • The test to apply: every String method returns a new string, so a call whose result goes nowhere has done nothing. Scan for method calls on a line by themselves.

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.

33. Does this line change anything?

Discrimination

The test is whether the return value is kept.

Sort into buckets

Sort each statement.

the result is used
s = s.toUpperCase();; System.out.println(s.toUpperCase());; String t = s.trim();
the result is discarded
s.toUpperCase();; s.substring(1);
use
The new string is assigned to a variable or passed somewhere that uses it, so the work is not lost.
waste
A new string is created and immediately becomes unreachable. The statement compiles, runs and accomplishes nothing.

34. Could Java have made strings mutable?

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.

35. Wrapper classes

Section

Section 9.4

36. An object that holds a primitive

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
primitivewrapper class
intInteger
charCharacter
doubleDouble
booleanBoolean
longLong

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.

37. What a wrapper gives you that a primitive does not

Notation

Four capabilities, and each of them is a reason a wrapper might be worth its cost.

Annotate

  • Methods. A wrapper object provides methods — most importantly equals, which a primitive cannot have because it is not an object.
  • Nullability. An Integer variable can be null; an int cannot. That matters when you need to express no value.
  • Constants. Each wrapper class defines 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.
  • Conversion methods. Integer.parseInt and Integer.toString convert between text and numbers, which is the subject of the next idea.
  • And like strings, wrapper objects are immutable — so the same rule applies: methods return new objects rather than modifying existing ones.

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.

38. Comparing wrapper objects

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");
}
comparisonasksresult
x == ythe 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.

39. What does this display?

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));
comparisonasksresult
x == ythe same object?false
x.equals(y)the same value?true

Predict first

What are the two lines of output?

  • false then true
  • true then true
  • false then false
  • true then false

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.

40. When to use a wrapper

Concept

Wrappers cost memory and a level of indirection. Use them where you need what they provide, and primitives everywhere else.

situationuse
a loop counterint — speed matters, and it is never null
arithmetic on ordinary numbersint or double
a value that might be absentInteger — because it can be null
reaching MIN_VALUE or MAX_VALUEthe class constant, whatever your variable's type
converting text to a numberInteger.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.

41. Comparing wrappers with ==

Trap

The 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== resultwhy
100trueJava caches small Integer objects and reuses them
1000falseoutside 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.

The fix

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
comparinguse
two ints==
two Integersequals
an Integer and an intequals, or compare the values
two Stringsequals

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.

42. Match the primitive to its wrapper

Matching

Five primitives, five classes.

Match the pairs

  • a. int
  • b. char
  • c. double
  • d. boolean
  • r1. Integer
  • r2. Character
  • r3. Double
  • r4. Boolean

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.

43. What is Integer.MAX_VALUE?

Prediction

A constant on the class, not on an object.

System.out.println(Integer.MAX_VALUE);
System.out.println(Integer.MIN_VALUE);
constantvalueroughly
Integer.MAX_VALUE2147483647about 2 billion
Integer.MIN_VALUE-2147483648about -2 billion

Predict first

Why is MIN_VALUE one further from zero than MAX_VALUE?

  • Because zero takes one of the positive slots, so the negatives get one more
  • Because it is a typo in the Java library
  • Because negative numbers need more memory
  • They are actually the same distance from zero

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.

44. Why does Integer exist if int already works?

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.

45. Converting between strings and numbers

Section

Section 9.4, continued

46. parseInt reads, toString writes

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
methodtakesreturns
Integer.parseInta Stringan int
Double.parseDoublea Stringa double
Boolean.parseBooleana Stringa boolean
Integer.toStringan inta 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.

47. Text and numbers are different worlds

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.

48. When parsing fails

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 stringresult
"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.

49. What does parseInt throw here?

Prediction

The string is not made of digits.

String s = "3.14";
int n = Integer.parseInt(s);
charactera digit?
3yes
.no
1yes

Predict first

What happens?

  • NumberFormatException — a decimal point is not a digit
  • It returns 3, ignoring the rest
  • It returns 3.14 as an int
  • It compiles but returns 0

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.

50. Three ways to turn a number into text

Concept

Integer.toString is explicit, and you have been converting numbers to strings since Chapter 2 without noticing.

writtenproduceswhen to prefer it
Integer.toString(n)the String "12345"when you want only the conversion
"" + nthe same stringconcise, but slightly obscure
String.format("%d", n)the same stringwhen you also want formatting
System.out.println(n)prints it — no String is keptwhen 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.

51. Parsing without validating

Trap

The 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 typesresult
42n is 42
helloNumberFormatException, 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.

The fix

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();
approachon bad input
parseInt with no checkNumberFormatException, program stops
hasNextInt firstyour own message, and a controlled exit
catch the exceptionChapter 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.

52. Which direction always works?

Definition probe

One conversion can always be done; the other cannot.

Sort into buckets

Sort each conversion.

always succeeds
int to String; double to String
may throw
String to int; String to double
safe
Every number has a text representation, so converting a primitive to a String can never fail.
risky
Not every string is a number, so parsing may throw NumberFormatException. The input must be validated or the exception handled.

53. Convert both ways

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.

54. Where does parsing happen in real programs?

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.

55. Primitives, objects, and null

Comparison

Fill the blanks. Every row follows from what the variable holds.

Comparison matrix

primitive (int)object (String)
the variable holdsthe value itselfa reference to the object
can be null?noyes
compare with==equals
provides methods?noyes
printing it showsthe valuethe 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.

56. The pattern to carry away

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 askif 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

57. Check: immutability

Check

Work it out before you click.

String s = "java";
s.toUpperCase();
String t = s.toUpperCase();
System.out.println(s + " " + t);
lineeffect
s.toUpperCase();creates a string and discards it
String t = s.toUpperCase();creates a string and keeps it
sunchanged throughout

Check your understanding

What is displayed?

  • A. java JAVA (correct)
  • B. JAVA JAVA
  • C. java java
  • D. JAVA java

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

Why B tempts people
This assumes the second line modified s in place, which no String method can ever do.
Why C tempts people
This ignores that t was assigned the return value of toUpperCase and so holds the uppercased string.
Why D tempts people
This has the two variables the wrong way round — s is printed first and was never changed.

58. Check: null against empty

Check

Work it out before you click.

String a = null;
String b = "";
System.out.println(b.length());
System.out.println(a.length());
variablerefers to an object?
byes — an empty String
ano

Check your understanding

What happens?

  • A. It prints 0, then throws a NullPointerException (correct)
  • B. It prints 0 twice
  • C. It throws on the first line
  • D. It prints 0, then prints null

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.

Why B tempts people
This treats null as though it were an empty string, but there is no object for length() to run on.
Why C tempts people
The first line is perfectly valid — an empty String is still an object.
Why D tempts people
Printing a null reference would display 'null', but this line invokes a method rather than printing the reference.

59. Check: comparing wrappers

Check

Work it out before you click.

Integer a = Integer.valueOf(1000);
Integer b = Integer.valueOf(1000);
comparisonasks
a == bthe same object?
a.equals(b)the same value?

Check your understanding

Which comparison reliably tells you the two hold the same number?

  • A. a.equals(b) (correct)
  • B. a == b
  • C. Either — they always agree
  • D. Neither — you must convert to int first

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.

Why B tempts people
This compares references. It gives the right answer for small cached values and the wrong one for large values like 1000.
Why C tempts people
They disagree here: == is false and equals is true, because two separate objects hold the same value.
Why D tempts people
Converting to int would work, but equals already does the job directly and is what the library provides.

60. Immutability outside Java

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.

61. How sure are you?

Commit first

Commit to an answer and to your confidence.

Predict first

After String s = "hello"; s.toUpperCase(); what does s refer to?

  • "hello" — the method returned a new string that was discarded
  • "HELLO" — the method changed s
  • null
  • It depends on the Java version

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.

62. Explain it to someone else

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.

63. Exit ticket

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?

  • null refers to no object at all; the empty string is an object with zero characters
  • They are the same thing written two ways
  • null is for numbers and the empty string is for text
  • The empty string is null once it has no characters

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().

64. Draw the whole lesson

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.

65. Recap

Recap

Four sections that name the distinction running through everything since Chapter 6, and draw out its consequences.

if you remember one thingit is this
about variablesvalue or reference — the capital letter tells you
about String methodsassign the return value or nothing happens
about nullcheck it before you call anything on it

Sources

  1. 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
  2. Java SE 21 API — java.lang.String
  3. Think Java 2e — free online edition and source code

Want this taught 1-on-1? Alexander tutors Java — $55/session, free consultation.

Book on Wyzant · Text (657) 465-8108