Arrays of objects; interfaces
Table of contents
Order of evaluation
Consider the following code snippet.
int x = 5;
int y = 5;
int z = y;
We might visualize this by imagining having three boxes to store values (named x
, y
, z
respectively), all assigned to the value 5.
It’s important to note z = y
does not link the variables z
and y
. Assignment statements in Java follow very specific steps.
- Assignment statement
- Evaluate the right hand side of the assignment. Since it’s a variable, use the value of
y
, 5. - Store the value in the variable on the left hand side of the assignment. Assign
z
to the value 5.
- Evaluate the right hand side of the assignment. Since it’s a variable, use the value of
What happens when we evaluate y = y + 1?
y = y + 1
first evaluates the right hand side by using the current value of y
(5), adds 1 to that value (6), and then assigns the result to y
. Note that z
doesn’t change during this assignment since only the value for y
is reassigned.
Reference semantics
Consider the following Point
class and the code snippet below. (Note the style issue of declaring public
fields!)
Implementer
public class Point {
public int x;
public int y;
public Point(int xValue, int yValue) {
x = xValue;
y = yValue;
}
}
Client
Point p = new Point(1, 2);
Point q = new Point(1, 2);
Point r = q;
Many programmers picture this code incorrectly! The key misunderstanding here is the idea that the Point
somehow fits inside the variable p
.
- Incorrect
p
does not store the Point
itself, but rather a reference to the Point
instance.
The correct picture is shown below. Each Point
instance in the program can be thought of as a person with their own phone number (shown in the picture in purple). The variables p
, q
, and r
only store phone numbers, not the whole Point
instance.
- Correct
When Java executes the assignment r = q
, just as in the int
example, it did not somehow make a link between the variables q
and r
. The steps for how this assignment statement works are exactly the same as they were with primitive variables, with the key difference that the value of q
is a long phone number.
Summarize the previous paragraph in your own words.
When Java evaluates r = q
, Java doesn’t copy the object but rather the phone number.
Arrays of objects
When we create a new array, it initializes all the values to the default value. For numeric types like int
and double
, the default value is 0.
Client
int[] nums = new int[5];
System.out.println(Arrays.toString(nums)); // [0, 0, 0, 0, 0]
What about an array of Point
objects?
Client
Point[] points = new Point[5];
System.out.println(Arrays.toString(points)); // [null, null, null, null, null]
The default value for arrays of objects (such as Point[]
) is a special placeholder value known as null
.
null value
Some programmers might consider null
as an object that doesn’t exist. A better way of thinking about it using the phone number analogy is that null
represents a special, reserved phone number (maybe call it the number (000) 000-0000
) that no person can have. This special phone number has no associated object and it can’t be called.
According to this interpretation, a Java variable can store the value null
. The problem comes up when we try to use the null
value.
Client
points[0].x = 3;
Following the order of evaluation for assignment statements, we evaluate the right hand side to get the number 3. Then, in order to determine where to assign that value, we need to find the Point
instance stored in the array index points[0]
. However, points[0]
is null
.
The issue arises when we try to set the x
field for null
. The program crashes with a NullPointerException
. Java can’t “call the phone number” for null
! There’s no actual Point
object there, so it’s not possible to set the x
field of something that doesn’t exist.
In order to address this issue, we can initialize the array by creating new points.
Client
for (int i = 0; i < points.length; i++) {
points[i] = new Point(0, 0);
}
Now the array will store references to 5 new Point
objects that we can manipulate!
Syntactic sugar
Let’s return to the Shakespeare example. A couple lessons ago, we wrote a method countUnique
that used a Set
to store all of the unique words in a Scanner
.
Client
public static int countUnique(Scanner input) {
Set<String> words = new HashSet<String>();
while (input.hasNext()) {
String word = input.next();
words.add(word);
}
return words.size();
}
If we print out the words in the set, it turns out that there’s a lot of near-duplicates. Here’s all the strings in the words
set related to “conceit”.
- Conceit
- conceit
- conceit’s
- conceited
- conceitless
- conceits
What if we now want to remove certain words from the Set
? Let’s start by removing uppercase variants of words. We can outline the steps for a program to solve this problem.
- Loop over the set of
words
. - If a word has any uppercase characters, then remove it from the set.
Since sets don’t keep track of element indices, there’s no get(int index)
method! We can try to use a for-each loop instead.
Client
public static int countUnique(Scanner input) {
Set<String> words = new HashSet<String>();
while (input.hasNext()) {
String word = input.next();
words.add(word);
}
for (String word : words) {
if (...) {
words.remove(word);
}
}
return words.size();
}
It turns out that this code will compile but crash with a ConcurrentModificationException
on the call to words.remove(word)
because the for-each loop is read-only. Java doesn’t allow elements to be removed from the underlying data structure during a for-each loop.
There is a way to solve this problem in Java by understanding how for-each loops work under the hood. For-each loops are an example of “syntactic sugar”: a bit of convenient syntax hiding more complicated behavior. We’ve been using other kinds of syntactic sugar in our programming. For example, whenever we say, i++
, we really mean to say, i = i + 1
. i++
is the syntactic sugar that buys the programmer some convenience.
Iterators
Just as Java translates i++
to i = i + 1
, Java also translates for-each loops into iterators.
Client
for (String word : words) {
// Body of for-each loop
}
The above for-each loop translates into the following iterator and while loop.
Client
Iterator<String> iter = words.iterator();
while (iter.hasNext()) {
String word = iter.next();
// Body of for-each loop
}
An Iterator
is a lot like a Scanner
: if there are any more elements (hasNext
), get it for me (next
). The iterator is attached to the data structure and returns the next item in the data structure. All collections (including all List
and Set
implementations) have a method iterator()
that returns a new Iterator
instance.
The Iterator
interface has three methods.
hasNext
to return whether or not there are any more elements.next
to return the next element from the iterator.remove
to remove the last-returned element from the iterator.
Using the remove
method, we can modify the underlying data structure. Rather than remove the items from the set of words
directly (which will cause the iterator to crash the program!), we can instead use the iterator’s remove
method. This way, the iterator isn’t surprised when items are removed from the underlying data structure.
An analogy for iterators is that they’re like pharmacists. Pharmacists (iterators) work together with pharmarcies (data structures) and are trained to handle prescription drugs carefully (data structure elements). In order to purchase prescription drugs, we (as users) have to work with the pharmacist to fulfill prescriptions. Pharmacists help users fulfill prescriptions by returning the specific drugs that they need.
As a style consideration, we typically prefer using the for-each loop over using iterators directly. The main use for iterators is when we need to remove from a for-each loop.