Java’s Built-In Lists
1 Overview of Lists
2 Creating Lists in Java:   the Linked  List class
2.1 Integer versus int
2.2 Importing Linked  List
3 Operations on Lists:   Adding and Counting Elements
4 Creating Lists and Testing With Them
4.1 Aside:   What if A Method Modifies the List During Testing?
5 Iterating Over Java Lists
5.1 The General Shape to Process a List
6 Which Class Do These List Methods Go Into?
7 For Those with Prior Java:   why Linked  List?
8 Practice Problems Over Lists and For-Loops

Java’s Built-In Lists

Kathi Fisler

Lists are built into Java. There are several variations of them. We will mostly use the LinkedList class in this course. Since you know lists conceptually, we’ll do this introduction largely through examples.

1 Overview of Lists

Recall that our last set of notes showed you three versions of a checkOut method on Books:

  // VERSION 1

  // creates a new book

  Book checkOut() {

    return new Book(this.title, this.callNum,

                    this.timesOut + 1, false);

  }

  

  // VERSION 2

  // modifies the existing book then returns it

  Book checkOut() {

    this.isAvailable = false;

    this.timesOut = this.timesOut + 1;

    return this;

  }

  

  // VERSION 3

  // modifies the existing book but returns nothing

  void checkOut() {

    this.isAvailable = false;

    this.timesOut = this.timesOut + 1;

  }

This set of examples is very helpful in setting up lists in Java for those of you coming from cs1101/1102. Why? In Racket, lists behaved as in Version 1. In Java, lists behave as in Version 3.

Following version 3 means that Java lists are rather different than Racket ones. The cons equivalent does not return a new list; it simply modifies the existing list. One implication of this is that there is no notion of the "rest" of a list. Once you add to a list, the old list is gone. We will use an approach other than recursion to process lists in Java.

2 Creating Lists in Java: the LinkedList class

To keep things simple, we’ll start with just lists of strings, rather than lists of Books.

First, let’s create an empty linked list of strings, called Words:

  LinkedList<String> Words = new LinkedList<String>();

The <String> tells Java what type of data will go into the list. Since lists could contain any kind of data, Java needs to know what type of data is in any particular list to help it check types at compile time. If instead we wanted a list of numbers (say containing the points earned on each question in a quiz), we would write

  LinkedList<Integer> QuizPoints = new LinkedList<Integer>();

2.1 Integer versus int

Notice that we used <Integer> instead of <int> (as this course has used all along for numbers). The type given to a LinkedList must be the name of either a class or an interface. In Java, Integer is a class that holds a number, whereas int is just a number by itself (without a class). Operations on ints are faster than the corresponding ones on Integer (since there is no class involved), but if you need to treat a number as a class, you must use Integer instead.

2.2 Importing LinkedList

We have to tell Java whenever your code uses its built-in classes (beyond simple kinds of data like String). You’ve already experience the idea of import when you set up JUnit. Similarly, to use LinkedList class, we have to import the class definition into our program. So a complete file with our two list definitions would look like:

  import java.util.LinkedList;

  

  class Examples {

    LinkedList<String> Words = new LinkedList<String>();

    LinkedList<Integer> QuizPoints = new LinkedList<Integer>();

  }

Naturally, you can use LinkedList inside more than just the Examples class (we will do so shortly); we’re putting the lists in the Examples class here to give a simple way to play with the lists for now.

3 Operations on Lists: Adding and Counting Elements

The methods for adding elements to the front of a list and getting the length of a list are called addFirst and size, respectively. Here’s an example of of them, written as a series of interactions in DrJava.

  > import java.util.LinkedList

  > LinkedList<String> Words = new LinkedList<String>();

  > Words

  []

  > Words.size()

  0

  > Words.addFirst("pie")

  > Words.size()

  1

  > Words.addFirst("corn")

  > Words.size()

  2

  > Words

  [corn, pie]

If you instead wanted the final list to have pie first and corn second (ie, adding puts new elements on the end of the list), you would use the add method instead of addFirst. We’re using addFirst today as it matches the behavior of cons from Racket.

This sequence of statements shows that the list methods in Java modify the original list (which was not true in Racket–cons left the original list intact, and made it the programmer’s responsibility to save the new list). Once you add an element to a list in Java, you no longer have access to the list without the element. There is no analog to the Racket rest operator, because the previous list no longer exists.

[Side note: If you want to see the full list of operators provided on LinkedLists, check out the official LinkedList documentation.]

What does the sequence above suggest is the return type of the addFirst method? Nothing prints, so it seems to return void. Try the following interaction:

  Words.addFirst("milk").addFirst("tofu")

Java raises an error that "No method in void has name ’addFirst’". In Java, addFirst does indeed return void, so you cannot chain together expressions as you might have been used to in Racket.

Both of these traits of addFirstmodifying the original list and returning voidhave interesting implications for how we program. We will discuss these issues as they come up throughout the course. For now, the implication is that you can’t create lists using the same pattern as you did if you are coming from Racket.

4 Creating Lists and Testing With Them

So far, we created lists interactively at the prompt. How and where should you create lists in an actual Java program?

We’ve seen that you have to create lists through a sequence of separate calls to addFirst, such as:

  Words.addFirst("pie");

  Words.addFirst("tofu");

The question is where to put these lines. Java requires these to be inside a method. They cannot lie in a class without being in a method. Specifically, you CANNOT do the following:

  class Examples {

    LinkedList<String> Words = new LinkedList<String>();

    LinkedList<Integer> QuizPoints = new LinkedList<Integer>();

  

    //THIS DOESN'T WORK!! -- THESE LINES MUST BE IN A METHOD

    Words.addFirst("pie");

    Words.addFirst("tofu");

  }

So which methods can you put these lines in? You have three options:

If you want to use the same list across multiple test cases, creating it in the Examples constructor makes sense – this creates it once, then each test case can refer to the same list.

4.1 Aside: What if A Method Modifies the List During Testing?

But what if you want to test a method that modifies the list. Won’t that method destroy the original list and break other tests? Yes. When you are testing methods that modify lists, you have to reset the list contents before each test.

It turns out that JUnit4 handles this automatically – it will call your Examples constructor before running each @Test, just to guard against this. If you want to be more explicit about recreating the list, you can use another feature of JUnit called a @Before tag, which designates a method to run before each @Test method runs.

Here’s a sample class that shows all three approaches:

  import static org.junit.Assert.*;

  import org.junit.Test;

  import org.junit.Before;

  import java.util.LinkedList;

  

  public class Examples {

    LinkedList<String> Words = new LinkedList<String>();

    LinkedList<Integer> QuizPoints = new LinkedList<Integer>();

    LinkedList<String> Staff = new LinkedList<String>();

  

    // You need to know at least this version -- define test data in

    //   the constructor

    public Examples(){

      Words.addFirst("pie");

      Words.addFirst("tofu");

    }

  

    @Test

    public void testQuizSize() {

      // Build the data in the test. Do this if your data

      //   is specific to this particular test

      QuizPoints.addFirst(85);

      QuizPoints.addFirst(92);

      assertEquals(QuizPoints.size(), 2);

    }

  

    // this approach is optional, but is considered good Java practice

    //    because it explicitly tags the data setup method.

    // With the before tag, this method is run before each test.

    // Note you need to import Before in order to use the tag

    @Before

    public void setUp() {

      Staff.addFirst("Will");

      Staff.addFirst("Jake");

      Staff.addFirst("Yizhou");

    }

  

    @Test

    public void testStaffSize() {

      // since setUp runs before this method, the Staff list

      //   is automatically populated

      assertEquals(Staff.size(), 3);

    }

  }

5 Iterating Over Java Lists

Let’s write a method to sum up the numbers in a LinkedList of integers.

Since Java does not have an operator corresponding to rest in Racket, we can’t process lists using recursion. We need a separate construct.

To process a list in Java, we use something called a for loop. There are two forms of these loops in Java. We’ll show you one today, and another one in a couple of days.

The basic idea of a for loop is that it walks over a list, visiting each element exactly once. Your code maintains whatever history it needs from element to element, and returns that as the answer. Let’s see a concrete example.

  // produce the sum of integers in a list

  int sum (LinkedList<Integer> numList) {

    int runningSum;

  

    runningSum = 0;

    for ( Integer n : numList ) {

      runningSum = runningSum + n;

    }

    return runningSum;

  }

What’s going on here? The for loop walks down the list, letting each element from numList be called n exactly once. As the for loop progresses, the history we need to remember is the running sum of the elements. We set up something called a variable to hold the running sum. A variable is a name under which we can store a single value at any time. It can be updated using =. The runningSum is 0 at the start of the computation, and is increased by n each step through the loop. At the end of the loop, the entire sum is in the runningSum, so we return it.

You created local variables in Racket using local and define. In Racket, you didn’t usually update the value of the variable (though you did towards the end with set!). In Java, we often update the values of variables, since Java programs use history variables rather than recursion.

5.1 The General Shape to Process a List

If you like generalization, here is a general shape of a program to process a list in Java. What information the history holds will change from computation to computation, but the shape of the method – initializing the history, updating it the loop, and returning it at the end – is the same from loop to loop.

n the following code skeleton, substitute single expressions for terms in all-caps, and substitute computations for descriptions in square-brackets:

  RETURN-TYPE aMethod(LinkedList<TYPE> SPECIFIC-LIST) {

    RETURN-TYPE runningHistory;  // accumulate the history of the computation

  

    runningHistory = [the result when the list is empty]

    for ( TYPE VARNAME : SPECIFIC-LIST ) {

      runningHistory = [expression to build new result from current

                        value of result and VARNAME/history];

    }

    return runningHistory;

  }

For those who took 1101, this is just the accumulator-style of programs that you did in the last segment of that course. Here, we are simply adapting that style to Java. Java takes care of the work that the recursive call used to do. Whatever computation you used to pass to the accumulator parameter, you now use to update the result within the loop.

Getting used to these loops just requires a bit of practice.

If you’ve had Java before, you probably learned a different style of for loop based on maintaining an index variable. While that style has a place, and is sometimes necessary, good Java practice generally dictates using the element-based version presented here when your problem is amenable to it. We will discuss the reasons why in a few days when the rest of the class sees index-based for loops. For now, use the element-based ones unless you can justify needing the alternative.

6 Which Class Do These List Methods Go Into?

Remember the rule of thumb in object-oriented programming: methods go in the class that has their primary data. Here, we are creating the lists in the Examples class, so these methods would also go into the Examples class.

But usually, your lists are a field in some other class. Returning to our Book example, if we created a library, we would define a list of books in the library and put methods inside the Library class:

  import java.util.LinkedList;

  class Library {

    LinkedList<Book> holdings;

  

    Library() {

      this.holdings = new LinkedList<Book>();

    }

  

    // count available books in the library

    int countAvailable() {

      int count = 0;

      for (Book b : holdings) {

        if (b.isAvailable) {

          count = count + 1;

        }

      }

      return count;

    }

  }

7 For Those with Prior Java: why LinkedList?

If you’ve had Java before, you more likely used ArrayList or Array rather than LinkedList. Why are we using LinkedList? We will talk about arrays later this term. Basically, arrays and linked lists each peform better on certain operations. Linked lists are faster when you need to add and delete elements, and arrays are faster when you need to access elements by position. The examples we are using in this course do addition/deletion more often than positional access, so LinkedList is more appropriate.

At the (very small) scale of data we have in our examples, however, the difference is irrelevant in practice. Please use LinkedList however, as that will match what our test cases expect.

8 Practice Problems Over Lists and For-Loops

Here are two practice problems you can try over lists and for-loops.

Here are the solutions for both problems. Notice how we have filled in the pattern from before: the type of result matches the return type of the function, and the result gets updated by what we do to process each individual Dillo in the list:

  int countLongStrings(LinkedList<String> theStrs) {

    int result;

  

    result = 0;

    for ( String s : theStrs ) {

      if (s.length() > 5) {

        result = result + 1;

      }

    }

    return result;

  }

  LinkedList<Dillo> reviveAllDillos(LinkedList<Dillo> theDs) {

    LinkedList<Dillo> result;

  

    result = new LinkedList<Dillo>();

    for ( Dillo d : theDs ) {

      result.add(new Dillo(d.length, true)) ;

    }

    return result;

  }