CS-151 Labs > Lab 4. My LinkedList
Warmup
Iterating over Lists
The act of iterating through a data structure in order to examine or operate
on each element is fundamental for many types of algorithms.
You have done this many times already. For example, if you want to sum up all the values in the
ArrayList of integers, you can iterate through it and access each element using get(i)
:
ArrayList<Integer> list = new ArrayList<Integer>();
// Populate array with numbers
...
int sum = 0;
// Sum up all the elements of the list.
for (int i = 0; i < list.size(); i++) {
sum += list.get(i);
}
Although this approach works quite well with an ArrayList
, it really breaks
down when we switch to something like a LinkedList
. The problem comes
from the list.get(i)
method. Recall that a linked list stores a pointer to
the head of the list (and perhaps a pointer to the tail of the list). In order
to get to element at index i
, the implementation of get()
has to start at
the head of the list and walk through the linked list of nodes. The code
could look something like:
public E get(int index) {
Node node = this.head;
while (index > 0) {
node = node.next;
index--;
}
return node.data;
}
(This isn’t the whole thing. We’d need of course to handle the case of reaching the end of the list before index hits 0 and throw an exception.)
In contrast, the ArrayList.get()
method needs only access the i
-th element
of its underlying array in a constant time:
public E get(int index) {
return this.data[index];
}
Let’s explore this problematic behavior of Linked Lists in practice.
Create a new Java project
named lab4
. Create a new class called Iteration
with the package
warmup4
and for convenience have Eclipse create a main
method for you.
Add these imports:
import java.time.LocalTime;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
In the Iteration
class, define a handy constant:
static final int LIST_SIZE = 100000;
Write a static method:
static void addElements(List<Integer> list)
that adds LIST_SIZE
elements to list
using list.add(...)
. You may add
whatever values you desire.
Write a method:
static int sum1(List<Integer> list)
that sums up all of the elements in list
using a for
loop and
list.get(i)
to get the i
-th element of the list.
Now, let’s time how long it takes to sum the elements of an ArrayList
of
size LIST_SIZE
. Inside the main
method, create a new List<Integer> arrayList = new ArrayList<Integer>()
and call addElements(arrayList)
.
One of many possible ways to time how long an operation takes is to get the current time,
perform the operation, and then get the current time again. The difference in
times is the duration of the operation. Let’s do exactly that using static method now()
defined in class LocalTime
.
LocalTime start = LocalTime.now();
sum1(arrayList);
LocalTime end = LocalTime.now();
System.out.println("sum1(arrayList): " + Duration.between(start, end).toString());
When I run that on my laptop, it prints out sum1(arrayList): PT0.012678S
.
You can ignore the PT
, they just stand for “period” and “time” in a
particular international standard that Java follows. The key part is
0.012678S
which means it took 0.012678 seconds.
Now, create a new List<Integer> linkedList = new LinkedList<Integer>()
.
Use addElements(linkedList)
to add
the same number of elements to the list as before.
Print out the time
sum1(linkedList)
takes using similar code:
start = LocalTime.now();
sum1(linkedList);
end = LocalTime.now();
System.out.println("sum1(linkedList): " + Duration.between(start, end).toString());
Run the code and you’ll see that it takes much longer! On my laptop, it takes more than 350 times as long!
Intuitively, it should not take that long to iterate over the Linked List: in fact we need only a single iteration to sum up all the values stored in the Linked List.
If only we could get the handle on the head
node of the linked list, then we could have summed up all the values in the list like this:
Node node = head;
int sum = 0;
while (node != null) {
sum += node.data;
node = node.next;
}
return sum;
There are two problems with this idea:
- We cannot access the head of the actual underlying Linked List becasue it is not publicly available.
- If we make Array List and Linked List behave differently, then we could not use polymorphism and pass them as parameters to a method
sum
that accepts object of typeList
. We would be forced to write differentsum
methods for every implementation of the List interface.
Iterators
The real solution is to have each List implementation to return its own Iterator, which would efficiently and uniformly dispense the values of the underlying data sequence.
An
Iterator<E>
is an interface with two key methods:
boolean hasNext()
which returnstrue
if the iterator is able to return a next value; andE next()
which returns the next element; this element has typeE
.
Every Java collection class (including ArrayList<E>
and LinkedList<E>
)
have a method iterator()
which returns the corresponding concrete class that implements
Iterator<E>
.
Once you get the Iterator from the List, you can use it to iterate over list values:
while (iter.hasNext()) {
E obj = iter.next();
// Do something with obj
}
Write a new static int sum2(List<Integer> list)
method that computes the sum
of the integers in the list but uses list.iterator()
to get hold on the underlying data structure
and uses hasNext()
and next()
in a loop to compute the
sum.
Add some more code to main
to print out how long it takes to call sum2
on
arrayList
and linkedList
.
start = LocalTime.now();
sum2(arrayList);
end = LocalTime.now();
System.out.println("sum2(arrayList): " + Duration.between(start, end).toString());
start = LocalTime.now();
sum2(linkedList);
end = LocalTime.now();
System.out.println("sum2(linkedList): " + Duration.between(start, end).toString());
What do you notice about the times? How do they compare to the sum1
times?
Document your observations in the special file README.txt
.
Collections and for-each loops
The paradigm of retrieving iterator()
from a collection and then using
iter.hasNext()
and iter.next()
in a loop is so common in Java, that there
is special syntax for it, sometimes called a “for-each” loop.
for (E obj : collection) {
// Do something with obj.
}
This is the same as:
Iterator<E> iter = collection.iterator();
while (iter.hasNext()) {
E obj = iter.next();
// Do something with obj.
}
Write a static int sum3(List<Integer> list)
that uses for (Integer num : list)
to compute the sum.
Add code to main
to print out how long it takes
to call sum3
on the arrayList
and the linkedList
.
You probably found that the only one of the six runs that’s very
slow is sum1
on the linkedList
.
Iterable Collections
The reason that the for (E obj : collection)
works is that all of the Java
Collections implement the
Iterable
interface. This interface declares a single method:
Iterator<E> iterator();
When we create our own storage classes we can make them implement Iterable
and hence enable them to work with the for-each loops.
Let’s create a class that holds a list of professors’ names. We’ll implement
Iterable
and have the iterator()
return a class that implements
Iterator<String>
.
Create a new class called Professors
. Have Eclipse create a main
method.
Add a private instance variable String[] names
to the class and create a
constructor that takes in an array of names, and makes a copy of it. Something
like:
Professors(String[] names) {
this.names = names.clone();
}
(We made a copy so that if the names
array were subsequently changed, it
wouldn’t change the names of the professors!)
Next, add some code to main
to create a new Professors
object containing
some professors’ names. Something like this.
String[] names = {
"Eck",
"Feldman",
"Taggart",
"Taylor"
};
Professors profs = new Professors(names);
At this point, profs
is just a class which stores a single array of Strings.
Try to iterate over professors using a for-each loop:
for (String prof : profs) {
System.out.println(prof);
}
The compiler complains: the for-each loop can only be used if we are looping over an array or an Iterable object.
Let’s make Professors
implement Iterable
. To do this, we need to change
the public class Professors
line to indicate it implements
Iterable<String>
:
public class Professors implements Iterable<String>
This gives us an error in Eclipse because we haven’t implemented the required
method iterator()
yet, so let’s get Eclipse to add the method stub for us. You’ll notice
that Professors
is underlined in red. Mouse over it and it’ll pop up a quick-fix window.
Click “add unimplemented methods.”
In the iterator()
we want to actually return something that implements
Iterator<String>
, but what? Let’s create a ProfessorIterator
inner class, nested inside the Professors
class.
You can read more about Java inner classes
here. At the top
(or bottom, your choice) of the Professors
class, add:
private class ProfessorIterator implements Iterator<String> {
}
Notice that Professors
implements Iterable<String>
but ProfessorIterator
implements Iterator<String>
.
ProfessorIterator
is underlined in red because we haven’t implemented the required methods.
Again, hover over it and click “add unimplemented methods.”
Now, we just need to write a constructor and implement hasNext()
and
next()
. A ProfessorIterator
needs to know two pieces of information:
- What list of professors is it iterating over?
- How far into that list has it already iterated?
To keep track of that information, we need to add two instance variables to
the ProfessorIterator
. First, it’ll need a reference to the Professors
object and second, it’ll need to keep an index into the list of names. Add the
instance variables:
private Professors profs;
private int index;
Write the constructor public ProfessorIterator(Professors profs)
which just
assigns (without cloning) profs
to this.profs
and sets index
to 0. Now,
write hasNext()
by checking if this.index
is less than
this.profs.names.length
. Finally, write next()
to return the next
professor’s name with "Professor "
prepended to it. E.g,
String prof = "Professor " + this.profs.names[index++];
Make the iterator()
method of Professors
class return new ProfessorIterator(this)
.
If you’ve done everything correctly, you should be able to iterate over the
professors. To test this, try to run the for-each loop in the main
:
for (String prof : profs) {
System.out.println(prof);
}