References
Table of Contents
Introduction
When discussing types, we mentioned the fact that Java makes a distinction between two categories of types in the language: primitive types and reference types. At the time, we handwaived the difference and focused primarily on the primitive types provided by Java (byte, short, int, long, float, double, char, and boolean). In this chapter, we will take a look at what makes reference types so different as to justify its own category.
What Makes a Type Primitive?
Before we investigate reference types we should first recall what makes a primitive type primitive. The idea is simple, primitive types are "predefined by the Java programming language and named by its reserved keyword" (§ 4.2). Structurally, from the point of view of Java, primitive types are distinct from reference types insofar as the primitive types represent the atomic stuff of the language. Furthermore, the values of primitive types are predefined:
The numeric types are the integral types and the floating-point types.
The integral types are byte, short, int, and long, whose values are 8-bit, 16-bit, 32-bit and 64-bit signed two's-complement integers, respectively, and char, whose values are 16-bit unsigned integers representing UTF-16 code units (§3.1).
The floating-point types are float, whose values exactly correspond to the 32-bit IEEE 754 binary32 floating-point numbers, and double, whose values exactly correspond to the 64-bit IEEE 754 binary64 floating-point numbers.
The boolean type has exactly two values: true and false.
A primitive type is one of eight possible types given by the language, and each can only contain values within their respective ranges. We also find one other clause within the specification about primitive types:
Primitive values do not share state with other primitive values
This is a subtle point, but one that has massive implications. A code example will illustrate the idea:
1: public class PrimitiveState{ 2: public static void main(String[] args){ 3: int x = 10; 4: int y = x; 5: x += 1; 6: System.out.printf("x: %d%n", x); 7: System.out.printf("y: %d%n", y); 8: } 9: }
What would you expect Listing 1 to print out? If we compile and run it we get the following:
josephraskind@stargazer:/tmp/references$ javac PrimitiveState.java; java PrimitiveState x: 11 y: 10
We see that x contains the incremented value and y does not. This is because the state of a primitive value is not passed on to another variable during assignment. The int that x is tied to is distinct from the int that y is tied to because only its value was passed during the assignment statement. We have two distinct variables with two distinct values. We'll see that reference types do not share this behaviour!
What is a Reference Type?
By definition, a reference type is any type that is not a primitive type (i.e. anything that is not a byte, short, int, long, float, double, char, or boolean). We know now that types which we define through the use of classes are called objects when instantiated. Therefore, all objects in Java are reference types—this is also true of all arrays. What makes reference types special is their values "are pointers to these objects" (§ 4.3.1). What does that mean? A code example might be more instructive:
1: class MyInt{ 2: int i; 3: MyInt(int i){ 4: this.i = i; 5: } 6: } 7: 8: public class ReferenceState{ 9: public static void main(String[] args){ 10: MyInt x = new MyInt(10); 11: MyInt y = x; 12: x.i += 1; 13: System.out.printf("x: %d%n", x.i); 14: System.out.printf("y: %d%n", y.i); 15: } 16: }
You can see all I've done is switch out the primitive int for an object called MyInt which acts as a wrapper around a basic int. It wouldn't be too absurd to assume we would get the same printout of "11\n10", but when we compile and run the code we get:
josephraskind@stargazer:/tmp/references$ javac ReferenceState.java; java ReferenceState x: 11 y: 11
How could that be?! This behavior hinges on the fundamental difference between primitive and reference types. Primitive types cannot share state whereas reference types can. This is because reference types store pointers to the underlying objects in memory. The following diagram shows this phenomenon visually:
Primitive types (int):
int x = 10; int y = x; x += 1;
+-------+ +-------+ +-------+
| x | | x | | x |
| 10 | | 10 | | 11 |
+-------+ +-------+ +-------+
+-------+ +-------+
| y | | y |
| 10 | | 10 | ← unchanged
+-------+ +-------+
Each variable holds its own copy of the value.
Assignment copies the value, not a reference to it.
Reference types (MyInt):
MyInt x = new MyInt(10); MyInt y = x; x.i += 1;
+-------+ +-------+ +-------+
| x | | x | | x |
| *----+--+ | *----+---+ | *----+--+
+-------+ | +-------+ | +-------+ |
| | |
v +-------+ | v
+--------+ | y | | +--------+
| MyInt | | *----+---+ | MyInt |
| i: 10 | +-------+ | i: 11 |
+--------+ | +--------+
| ^
+-----------------------------+
Both x and y hold a pointer to the SAME object in memory.
Modifying x.i modifies the shared object, so y.i changes too.
Passing References
In Java, when we send in objects as parameters to methods we are always sending in references. For example, if we take the BankAccount class from the previous chapter:
1: public class PassingReferences{ 2: 3: static void applyBonus(BankAccount acc, double amount){ 4: acc.deposit(amount); 5: } 6: 7: public static void main(String[] args){ 8: BankAccount alice = new BankAccount("Alice", 1000.00); 9: 10: System.out.println("Before bonus:"); 11: alice.printBalance(); 12: 13: applyBonus(alice, 500.00); 14: 15: System.out.println("After bonus:"); 16: alice.printBalance(); 17: } 18: }
Here we can see that this new static method applyBonus takes a BankAccount object called acc. We then access the public method deposit and send in the amount. In the main method we print the bonus before and after a call to applyBonus. When we compile and run the above code we get the following output:
josephraskind@stargazer:/tmp/references$ javac PassingReferences.java; java PassingReferences Before bonus: Alice's balance: $1000.00 After bonus: Alice's balance: $1500.00
Even though acc is a different variable than alice they both point to the same object.
Immutable Objects
Over-reliance on the use of references can create situations where it becomes difficult to track updates to an object as side effects may occur in a variety of contexts. This has led to the design pattern of crafting immutable objects, or objects whose internal state cannot ever be updated. Instead of updating an object's state, a new object will be created to reflect the desired update. For example, we can update the BankAccount class to be made immutable:
1: class BankAccount { 2: final String owner; 3: final double balance; 4: 5: BankAccount(String owner, double initialBalance){ 6: this.owner = owner; 7: this.balance = initialBalance; 8: } 9: 10: BankAccount deposit(double amount){ 11: return new BankAccount(owner, balance + amount); 12: } 13: 14: BankAccount withdraw(double amount){ 15: return new BankAccount(owner, balance - amount); 16: } 17: 18: void printBalance(){ 19: System.out.printf("%s's balance: $%.2f%n", owner, balance); 20: } 21: 22: }
Now if we rerun PassingReferences:
josephraskind@stargazer:/tmp/references$ javac PassingReferences.java; java PassingReferences Before bonus: Alice's balance: $1000.00 After bonus: Alice's balance: $1000.00
Now the update is not reflected in the main method's object!
#+ATTR_HTML :class notes Java's String class is immutable.
The null Value
The Java Language Specification lays out two possible reference values: "pointers to… objects, and a special null reference, which refers to no object". By default, all uninitialized reference types will refer to the null value. Again, the null value indicates the lack of a particular object associated with a reference type variable. For example, I could create a variable of type BankAccount without initializing it:
1: public class NullObject{ 2: static BankAccount emptyAcc; 3: public static void main(String[] args){ 4: System.out.printf("emptyAcc: %s%n", emptyAcc); 5: } 6: }
When we compile and run the above code we find the following printed:
josephraskind@stargazer:/tmp/references$ javac NullObject.java; java NullObject emptyAcc: null
Because the null value refers to the lack of an instance tied to a given variable that means we cannot reference instance fields/method:
1: public class NullObject{ 2: static BankAccount emptyAcc; 3: public static void main(String[] args){ 4: null.printBalance() 5: } 6: }
Compiling and running the above we get:
josephraskind@stargazer:/tmp/references$ javac NullObject.java; java NullObject
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "BankAccount.printBalance()" because "NullObject.emptyAcc" is null
at NullObject.main(NullObject.java:4)
We get a runtime exception! Again, null refers to a non-existent object, therefore we cannot dereference it.
Comparing Reference Types
A simple mistake many beginner Java programmers make is to do basic equivalency checks between reference types using the == operator. == only checks equivalency between stored values. This is not a problem when comparing two primitive types as they store values themselves. This is a problem when comparing two reference types as they store pointers to objects. Take a simple equivalancy comparison between two Strings:
1: public class ComparingStrings{ 2: public static void main(String[] args){ 3: String s1 = new String("Dog"); 4: String s2 = new String("Dog"); 5: boolean result = s1 == s2; 6: System.out.printf("s1 == s2 returns %b%n", s1, s2, result); 7: } 8: }
When we compile and run the above we get:
josephraskind@stargazer:/tmp/references$ javac ComparingStrings.java; java ComparingStrings s1 == s2 returns false
What?! Again, this is because what's really going on is that Java is comparing the pointers stored in s1 and s2 and not the internal values themselves. Many built-in standard library Java objects have an equals method for direct equivalency checking:
1: public class ComparingStrings{ 2: public static void main(String[] args){ 3: String s1 = new String("Dog"); 4: String s2 = new String("Dog"); 5: boolean result = s1.equals(s2); 6: System.out.printf("s1.equals(s2) returns %b%n", s1, s2, result); 7: } 8: }
When we compile and run the above:
josephraskind@stargazer:/tmp/references$ javac ComparingStrings.java; java ComparingStrings s1 == s2 returns false
That's better!
Whenever you write your own classes in Java, make sure that you write your own methods for direct comparisons!
Exercises
- Compile and run Listing 1. Then change
inttodoubleand repeat the experiment with decimal values. Does the behavior change? What does this confirm about all primitive types? - Compile and run Listing 3. Then add a third variable
MyInt z = yand incrementz.iby 5. Printx.i,y.i, andz.i. Before running, predict each value. Explain the result in terms of references. - The chapter draws a distinction between copying a value and copying a reference. In your own words, explain what happens in memory when you write
int y = xversusMyInt y = x. Use the ascii diagrams from the chapter as a guide. - Compile and run Listing 5. Then modify
applyBonusto also print the balance ofaccinside the method before returning. Does the value printed insideapplyBonusmatch the value printed inmainafter the call? What does this confirm about references being passed to methods? - The chapter states that even though
accandaliceare different variables, they point to the same object. Write a program that demonstrates this by passingaliceto a method that callswithdrawrather thandeposit. Verify that the withdrawal is reflected inmainafter the method returns. - Replace the mutable
BankAccountwith the immutable version from Listing 7 and rerun Listing 5. The balance no longer updates inmain. ModifyapplyBonusso that it returns the newBankAccountobject produced bydepositand updatemainto capture and print the returned object. What does this pattern require the programmer to do differently compared to the mutable version? - The chapter states that Java's
Stringclass is immutable. Write a program that demonstrates this by attempting to change a character in aStringusing a method call. What happens? Now create a newStringusing concatenation and assign it back to the original variable. Does the originalStringobject change, or is a new one created? - Compile and run Listing 9. Then attempt to call
emptyAcc.deposit(100)instead of printing it. Record the exception. In your own words explain what aNullPointerExceptionis and why it occurs. How could you guard against it before calling a method on a reference type variable? - Compile and run Listing 13. Then change
new String("Dog")to just"Dog"for boths1ands2and rerun. Does==now returntrue? Research why this happens (hint: look up "string interning" in Java) and write a short explanation. - Compile and run Listing 15. Notice the
printfformat string uses%bbut passes three arguments (s1,s2,result). What does the output look like? Fix the format string so it correctly prints the result and recompile. Add an
equalsmethod to theBankAccountclass that returnstrueif twoBankAccountobjects have the sameownerandbalance:boolean equals(BankAccount other){ // your implementation here }
Test it with two accounts that have identical fields and two that differ. Then test what happens when you use
==instead — explain why the results differ.- The chapter explains that the immutable
BankAccountreturns a new object fromdepositandwithdrawrather than modifying the existing one. Write a short program that callsdepositon an immutableBankAccountbut does not assign the return value. Print the balance after. What do you observe? What does this tell you about the risk of ignoring return values from immutable objects? - Write a method
transferthat takes twoBankAccountobjects and adoubleamount and transfers that amount from the first account to the second. Use the mutableBankAccountdefined in the previous chapter. Verify that both balances update correctly after the call. - The chapter states that uninitialized reference type fields default to
null. Write a class with three fields: anint, aString, and aBankAccount. Instantiate the class without setting any fields and print all three. What are their default values? Why doesinthave a different default than the reference types? The following code contains a subtle bug related to references:
BankAccount a = new BankAccount("Alice", 500.00); BankAccount b = a; b = new BankAccount("Alice", 500.00); System.out.println(a == b); System.out.println(a.equals(b));
Before running, predict the output of both print statements. Then run it and verify. Explain what happens to the reference stored in
bafter the second line and whyais unaffected by the reassignment ofb.