Making Linked Lists in Java

Robert M. Keller

Copyright 1997 by Robert M. Keller, all rights reserved.

The following source files are referenced herein.

2 general notions:

"open" lists -

invite sharing

"closed" lists -

don't


Closed list can be an encapsulated open list.


Example

L1 [a,b,c,d]

L2 [c,d]

could share [c,d]


rex list model is open lists

Abstractions:

first

rest

cons

isEmpty (called null in rex)

nil empty list


first([A|L]) ==> A

rest([A|L]) ==> L

cons(A,L) ==> [A | L]

isEmpty([]) ==> true

isEmpty([A|L]) ==> false

nil == []


Numerous ways to implement open lists in Java, depending on goals.


Method 1

list = reference to first cell of list

[ ] = null (reference)

This works best with static methods (functions).


Method 1 Illustrated

class openList
  {                             // two components of a list:
  Object First;                 //   the first element
  openList Rest;                //   the rest of the elements
 
  static openList nil = null;                   // null list
 
  openList(Object First, openList Rest)         // constructor for class
    {
    this.First = First;
    this.Rest = Rest;
    }
 
  static openList cons(Object First, openList Rest)   // create new list
    {
    return new openList(First, Rest);
    }
 
  // essential functions (static methods)
 
  static Object first(openList L)                // return first element
    {
    return L.First;
    }
 
  static openList rest(openList L)               // return all but first
    {
    return L.Rest;
    }
 
  static boolean isEmpty(openList L)                   // check if empty
    {
    return L == null;
    }
 
  static openListEnum elements(openList L)
    {
    return new openListEnum(L);
    }
  }


Notes:

1. We use Object as the type of element, but any fixed type could be used.

Using Object allows nesting.

2. Calling first(nil) or rest(nil) will throw a nullPointerException.


Example reverse using method 1

  static openList reverse(openList L)
    {
    openList result = nil;
 
    while( !isEmpty(L) )
      {
      result = cons(first(L), result);
      L = rest(L);
      }
    return result;
    }


Exercise

Write append using method 1.

Link to a source file containing openList definitions and append.


We might wish to use

instead of

first(L)

rest(L)

isEmpty(L)


With method 1, we can't :

e.g. L.isEmpty()

can't apply method to null


Method 2 allows the object style

We define a class Cell ;

List is a wrapper for a reference to the first Cell.

 

Cell is similar to openList without the static methods.


Method 2 Illustrated

 
class Cell
  {
  Object First;
  Cell Rest;
 
  Cell(Object First, Cell Rest)
    {
    this.First = First;
    this.Rest = Rest;
    }
  }
 
class openList
  {
  Cell firstCell;             // reference to the first cell in the list
 
  static openList nil = new openList(null);
 
  openList(Cell firstCell)
    {
    this.firstCell = firstCell;
    }
 
  // essential functions
 
  static openList cons(Object First, openList Rest)   // create new list
    {
    return new openList(new Cell(First, Rest));
    }
 
  Object first()                                         // return first element
    {
    return firstCell.First;
    }
 
  openList rest()                                // return all but first
    {
    return firstCell.Rest;
    }
 
  boolean isEmpty()                                    // check if empty
    {
    return firstCell == null;
    }

etc.

  }
Link to a source file containing Method 2 definitions.


Method 3 allows the object style but does not define a Cell;

instead it uses inheritance :

This is the method currently used in Poly.


In Poly, first() and rest()

can throw EmptyListException

Calls to them on a List must be within try...catch.

Calls to them on a NonEmptyList do not.

Catching a List to a NonEmptyList can generate a ClassCastException if the original was an EmptyList.


In Poly (as in Vector)

.elements()

returns an Enumeration which can be used to traverse the list without try...catch...

 

Doing nextElement() can throw a NoSuchElement exception.


Reverse using an Enumeration

  static List reverse(List L)
    {
    List result = nil;
    for( Enumeration e = L.elements();
            e.hasMoreElements(); )
      {
      result=cons(e.nextElement(),result);
      }
    }


Defining an Enumeration class for open lists

Enumeration in Java is an "interface", sort of like an abstract class definition.

However, classes don't inherit from an interface; they implement the interface.


class openListEnum implements Enumeration { openList remainder; openListEnum(openList initial) { remainder = initial; }   public Object nextElement() throws NoSuchElementException { if(remainder.isEmpty()) throw new NoSuchElementException(); Object result = remainder.first(); remainder = remainder.rest(); return result; }   public boolean hasMoreElements() { return !remainder.isEmpty(); } }


Incorporation of the Enumeration in openList class

class openList
  {
  :
  :
  Enumeration elements()
    {
    return new openListEnum(this);
    }
  }


Exercise

Create an enumeration class for an array of Object.

 

class arrayEnum implements Enumeration
  {
 
  arrayEnum(Object a[])       // constructor
    {
    }
 
  Object nextElement()        // methods required by interface def'n
    {
 
    }
 
  boolean hasMoreElements()
    {
    }
  }


map function using openLists

Ideally pass static method as argument.

However this is not legal in Java;

Bummer!

Can pass Objects as arguments.

Simulate passing a function by passing an object with the function as a method.

This method is given a standard name such as

  Object apply(Object ob)


Create an abstract class for classes having apply as a method, say Applicable.

  abstract class Applicable
    {
    abstract Object apply(Object ob);
    }


Now define map:

  static List map(Applicable fun, List L)
    {
    if(L.isEmpty())
      {
      return nil;
      }
    return cons(fun.apply(L.first()), map(fun,L.rest()));
    }


Example map usage:

Square each element of a list

First define function

class Square extends Applicable
  {
  Object apply(Object ob)
    {
    double value = ((Double)ob).doubleValue();
    return new Double(value*value);
    }
  }
 

Apply instance of function:

map(new Square(), someList)     // returns List


Example map usage:

Scale a list by a factor c

class Scaler extends Applicable
  {
  double c;
 
  Scaler(double c)
    {
    this.c = c;
    }
 
  Object apply(Object ob)
    {
    double value = ((Double)ob).doubleValue();
    return new Double(c*value);
    }
  }

Apply instance of function:

map(new Scaler(100.), someList)     // returns List


Methods 2&3 can also be used to implement closed lists.

Example:

push method treats list as a stack

class List
  {
  :
  :
  void push(Object ob)
    {
    firstCell = new Cell(ob, firstCell);
    }
 
  Object pop()
    {
    Object result = firstCell.First;
    firstCell = firstCell.Rest;
    return result;
    }


A closed list can be reversed in place,

i.e. without creating new cells.


void reverseInPlace() { if(firstCell == null) return; Cell previous = null; while( firstCell.Rest != null) { Cell temp = firstCell; firstCell = temp.Rest; temp.Rest = previous; previous = temp; }


General iteration

after temp=firstCell;


firstCell=temp.Rest;


temp.Rest=previous;


previous=temp;

Just like at top!


Similarly, a closed list can be

appended to destructively.


A queue is similar to a stack,

but defines

enqueue

dequeue

instead of push/pop:

push ~ dequeue

pop removes youngest item in stack

dequeue removes oldest item in queue


Queue using Cells

 

Problem

Implement class Queue, with methods:

void enqueue(Object ob)

Object dequeue()

boolean isEmpty()


Bi-Directional Lists

Permit traversal in either direction

Cost: 1 extra reference per cell

Excercise

Develop abstraction of bi-directional lists. Then implement the abstraction.


Closed Lists are best if

destructive modification is desired.

inserting new elements

deleting new elements

splicing in lists

We need some way to say where to do these things.

 

A nice abstraction is that of a cursor.

Cursor is seperate from list; refers to list.

Multiple cursors could be on one list.


List in the abstract

 A B C ... Z
         

cursor positions

insert inserts element where cursor is

remove removes value after cursor (say)

splice splices a whole list at the cursor

forward moves cursor forward

backward moves cursor backward

after returns value after

before returns value before


class Cursor { closeList myList; Cell before,after;   Cursor(closedList myList { this.myList = myList; before = null; after = myList.firstCell; }  

etc.