Module 6: Data Structures


Pointers in Java

 

Consider the following simple example:

class ObjX {
  ObjX next;
}


public class TestObjX {

  public static void main (String[] argv)
  {
    ObjX first = new ObjX ();         // 1.
    first.next = new ObjX ();         // 2.
    first.next.next = new ObjX ();    // 3.
  }

}

Note:
 


A simple linked list in Java

 

Let's start our study of linked lists in Java with a simple example.

Here is a simple linked list that stores integers: (source file)

// This is a "node" in the list.

class ListItem {
    public int data;  // Integer data field.
    ListItem next;    // The familiar "next" pointer for
                      // a "singly-linked" list.
}

// We will temporarily write the code for creating and printing
// the list inside "main".

public class TestList {

    public static void main (String[] argv)
    {
        // We need "front" and "rear" pointers.
        ListItem front = null;
        ListItem rear = null;

        // Let's insert the numbers 1 to 10.
        for (int i=1; i <= 10; i++) {
            if (front == null) {           // List empty.
                front = new ListItem ();       // Create listnode.
                front.data = i;                // Assign value.
                rear = front;
                rear.next = null;
            }
            else {                             // List non-empty.
                rear.next = new ListItem ();   // Create listnode at rear.
                rear.next.data = i;            // Assign value.
                rear.next.next = null;
                rear = rear.next;              // Adjust rear pointer.
            }
        }

        // Print the contents.
        ListItem listPtr = front;      // Start from front.
        int i = 1;
        while (listPtr != null) {
            System.out.println ("item# " + i + ": " + listPtr.data);
            i++;
            listPtr = listPtr.next;    // Walk to next item.
        }

    } // "main"

} // End of class "TestList"

Note:

 


Linked list - version 2

 

Let us create an improved linked list by placing the list manipulation functions in a separate class: (source file)


// List node.

class ListItem {
    public int data = 0;
    ListItem next = null;
}


// A separate list class.

class LinkedList {

    // Instance variables.
    ListItem front = null;
    ListItem rear = null;

    // Instance method to add a data item.
    public void addData (int d)
    {
        if (front == null) {
            front = new ListItem ();
            front.data = d;
            rear = front;
            rear.next = null;
        }
        else {
            rear.next = new ListItem ();
            rear.next.data = d;
            rear.next.next = null;
            rear = rear.next;
        }
    }

    // Instance method to print the list.
    public void printList ()
    {
        ListItem listPtr = front;
        int i = 1;
        while (listPtr != null) {
            System.out.println ("Item# " + i + ": " + listPtr.data);
            i++;
            listPtr = listPtr.next;
        }
    }

} // End of class "LinkedList"


class TestList2 {

    public static void main (String[] argv)
    {
        // Create a new list object.
        LinkedList L = new LinkedList ();

        // Let's insert the numbers 1 to 10.
        for (int i=1; i <= 10; i++) {
            L.addData (i);                   // Use the addData() method.
        }

        // Print the contents.
        L.printList();

        // Create another list:
        LinkedList L2 = new LinkedList ();

        // and use it for whatever ...
    }

} // End of class "TestList2"

Note:

 
Exercise 6.1: Draw a "memory picture" showing the contents of memory after the second iteration of the for-loop in main().
 


Linked list - version 3

 

Next, instead of leaving the data field public, we will:

The code: (source file)


class ListItem {

    int data = 0;
    ListItem next = null;

    // Constructor.
    public ListItem (int d)
    {
        data = d;  next = null;
    }
  
    // Accessor.
    public int getData () 
    {
        return data;
    }
}


// Linked list class - also a dynamic class.
class LinkedList {

    ListItem front = null;
    ListItem rear = null;
    int numItems = 0;      // Current number of items.

    // Could this class use a constructor?

    // Instance method to add a data item.
    public void addData (int d)
    {
        if (front == null) {
            front = new ListItem (d);     // Note use of constructor.
            rear = front;
        }
        else {
           rear.next = new ListItem (d); // Constructor use again.
           rear = rear.next;
        }
        numItems++;
    }

    public void printList ()
    {
        ListItem listPtr = front;
        System.out.println ("List: (" + numItems + " items)");
        int i = 1;
        while (listPtr != null) {
            System.out.println ("Item# " + i + ": " 
                                + listPtr.getData());
                                // Note use of accessor.
            i++;
            listPtr = listPtr.next;
        }
    }

} // End of class "LinkedList"

Note:

 

Exercise 6.2: Add a method called memberOf (int d) to the above code so that you can test whether a particular integer is present in the list. Insert your code in the class LinkedList in the following template, which also tests your code. Draw a "memory picture" showing the contents of memory after the second iteration of the first for-loop in main().
 


A generic linked list - using Object

 

We will now create a generic list that can be used with any object:

 

Here's how it works: (source file)


class ListItem {

    Object data = null;    // The universal Object.
    ListItem next = null;

    // Constructor.
    public ListItem (Object obj)
    {
        data = obj;  next = null;
    }
  
    // Accessor.
    public Object getData () 
    {
        return data;
    }
}


// Linked list class - also a dynamic class.

class LinkedList {

    ListItem front = null;
    ListItem rear = null;
    int numItems = 0;     // Current number of items.

      // Instance method to add a data item.
    public void addData (Object obj)
    {
        if (front == null) {
            front = new ListItem (obj);
            rear = front;
        }
        else {
            rear.next = new ListItem (obj);
            rear = rear.next;
        }
        numItems++;
    }

    public void printList ()
    {
        ListItem listPtr = front;
        System.out.println ("List: (" + numItems + " items)");
        int i = 1;
        while (listPtr != null) {
            System.out.println ("Item# " + i + ": " 
                            + listPtr.getData());
                            // Must implement toString()
            i++;
            listPtr = listPtr.next;
        }
    }

} // End of class "LinkedList"


// An object to use in the list:

class Person {

    String name;
    String ssn;

    // Constructor.
    public Person (String nameInit, String ssnInit)
    {
        name = nameInit;  ssn = ssnInit;
    }

    // Override toString()
    public String toString ()
    {
        return "Person: name=" + name + ", ssn=" + ssn;
    }

} // End of class "Person"


// Test class.

class TestList4 {

    public static void main (String[] argv)
    {
        // Create a new list object.
        LinkedList L = new LinkedList ();

        // Create a Person instance and add it to list.
        Person p = new Person ("Terminator", "444-43-4343");
        L.addData (p);   // p is a subclass of Object and so
                         // p is automatically cast to Object

        // We don't really need a Person variable:

        L.addData (new Person ("Rambo", "555-54-5454"));
        L.addData (new Person ("James Bond", "666-65-6565"));
        L.addData (new Person ("Bruce Lee", "777-76-7676"));

        // Print contents.
        L.printList();
    }

} // End of class "TestList4"

Note:

 
Exercise 6.3: Draw a complete memory picture after the second Person instance is inserted into the list.
 

Exercise 6.4: In Exercise 6.2, you added a method called memberOf (int d) to the LinkedList class for membership testing. In this exercise, you will extend this idea to generic lists by using the fact that Object defines a method called equals(Object obj) to allow two objects to test whether they are equal. Accordingly, you need to override this method in class Person with the same signature:


    public boolean equals (Object obj)
Insert your code in the class Person using the following template, which also tests your code. You will need to cast the Object parameter into a Person variable.
 


Linked lists - adding enumeration functions

 

Often, it is convenient to be able to "walk" through a list and get its contents one-by-one.

A linked list that has this feature is sometimes called an enumeration object.

To create enumeration features, we will add the the following functions to the class LinkedList:

 

These functions can be used as follows (for example, with the class Person):


    L.startEnumeration();

    while (L.hasMoreElements()) {

        Person p = (Person) L.getNextElement();

        // ... Process p ...

        System.out.println (p);

      }

Here's the code: (source file)


class LinkedList {

    // ... stuff ...

    // The "cursor".
    ListItem enumPtr = null;

    // Start an enumeration.
    public void startEnumeration ()
    {
        enumPtr = front;
    }

    public boolean hasMoreElements()
    {
        if (enumPtr == null)
            return false;
        else
            return true;
    }

    public Object getNextElement()
    {
        Object obj = enumPtr.data;
        enumPtr = enumPtr.next;
        return obj;
    }

} // End of class "LinkedList"

(The entire file is not shown).
 
Exercise 6.5: Draw a complete memory picture just after L.printList() is called in main(). Then, draw a series of memory pictures showing how memory (both stack and heap) changes during each iteration of the while-loop that follows L.printList().
 


Generic lists and basic types

 

Earlier, we created a generic linked list and were able to insert into it any object.

However, can we insert basic types like int's?

For example, we were able to insert Person instances:


    L.addData (new Person ("Rambo", "555-54-5454"));
    L.addData (new Person ("James Bond", "666-65-6565"));
    L.addData (new Person ("Bruce Lee", "777-76-7676"));

One way to handle basic types is to convert each basic type into an equivalent object:

Thus, the following would work:


    // Create a new list object.
    GenericList L = new GenericList ();

    // Let's insert 10 random numbers between 1 and 100
    for (int i=1; i <= 10; i++) {
        int k = (int) UniformRandom.uniform (1,100);
        Integer K = new Integer (k);  // Integer object.
        L.addData (k);
    }

    // ... print etc ...

What is interesting is that the following will also work:


    // Create a new list object.
    GenericList L = new GenericList ();

    // Let's insert 10 random numbers between 1 and 100
    for (int i=1; i <= 10; i++) {
        int k = (int) UniformRandom.uniform (1,100);
        L.addData (k);    // Automatic conversion to object equivalent (autoboxing)
    }

    // ... print etc ...

NOTE: this autoboxing feature was not available until JDK 1.5.
 


Linked lists - separate files

 

One final note on linked lists: to be really useful, the generic linked list ought to be in a separate file by itself, such as the file GenericList.java:


class ListItem {

    // ... 

}


// Linked list class.
public class GenericList {

    // ...

} 

Then, this class can be used elsewhere, say in a file called GenericListTest.java:



public class GenericListTest {

    public static void main (String[] argv)
    {
        // Create a new list object.
        GenericList L = new GenericList ();

        // Let's insert 10 random numbers between 1 and 100
        for (int i=1; i <= 10; i++) {
            int k = (int) UniformRandom.uniform (1L,100L);
            L.addData (k);
        }

        // Enumerate items.
        L.startEnumeration();
        while (L.hasMoreElements()) {
            int k = (int) L.getNextElement();    // Note use of autoboxing
            System.out.println (k); 
        }
    }

}
 


Java Generics

 

While the flexibility of inserting any object is useful, it can also lead to problems, e.g., (source file)

class ListItem {
    // ... as before ...
}

class LinkedList {
    // ... as before ...
} 


public class ListProblem {

    public static void main (String[] argv)
    {
        LinkedList L = new LinkedList ();

        // Add stuff the list was originally designed for:
        L.addData (new Integer(5));
        L.addData (new Integer(6));

        // Does this belong?
        L.addData (new String("Blah-blah"));

        // An application that fails:
        L.startEnumeration ();
        int sum = 0;
        while ( L.hasMoreElements() ) {
            Integer I = (Integer) L.getNextElement ();
            sum = sum + I;
        }
        System.out.println ("sum=" + sum);

    }

} 
 
Exercise 6.6: Compile and run the above program. What kind of problem do you see and where does the problem occur?
 

To address this problem, Java 1.5 added generics, a new language feature.

First, let's look at an example of using generics (from the Java library):

// Need this for java.util.ArrayList
import java.util.*;

public class ListProblem2 {

    public static void main (String[] argv)
    {
        // NOTE: this a data-structure in the Java library.
        ArrayList<Integer> L = new ArrayList<Integer> ();

        // Add stuff the list was originally designed for:
        L.add (new Integer(5));
        L.add (new Integer(6));

        // This does not compile:
        L.add (new String("Blah-blah"));

    }

} 

Next, let's see if we can write our own such generic list:

 

Finally, let's look at the use of the for-each loop for such data structures: (source file)

// Need this for java.util.ArrayList.
import java.util.*;

public class TestGeneric2 {

    public static void main (String[] argv)
    {
        // This is a data-structure in the Java library:
        ArrayList<Integer> L = new ArrayList<Integer> ();

        // Add stuff the list was originally designed for:
        L.add (new Integer(5));
        L.add (new Integer(6));
        L.add (new Integer(7));
        L.add (new Integer(8));

        // The for-each loop:
        for (Integer I: L) {
            System.out.println (I);
        }

        // An interesting variant that uses autoboxing:
        for (int i: L) {
            System.out.println (i);
        }
    
    }

} 
 
Can we use the new for-each loop with our linked list?
 


Built-in data structures in the library

 

Java provides several built-in data structures in its vast library. They are all in the package java.util. For example:

We will next look at some of these data structures.
 


Built-in data structures - java.util.Stack

 

We have seen how to exploit the all-super superclass Object to store any user-defined object in a list.

The same idea applies to the Stack class in java.util.

An example shows you how: (source file)


// Need to import from java.util.
import java.util.*;

// An object to use in the stack:
class Person {

    String name;
    String ssn;

    // Constructor.
    public Person (String nameInit, String ssnInit)
    {
        name = nameInit;  ssn = ssnInit;
    }

    // Override toString()
    public String toString ()
    {
        return "Person: name=" + name + ", ssn=" + ssn;
    }

} // End of class "Person"


public class TestStack {

    public static void main (String[] argv)
    {
        // Create a new stack object.
        Stack S = new Stack ();

       // Create Person instances and add it to the Stack.
	S.push (new Person ("Rogue", "1111-12-1212"));
	S.push (new Person ("Storm", "222-23-2323"));
	S.push (new Person ("Black Widow", "333-34-3434"));
	S.push (new Person ("Jean Grey", "888-89-8989"));

       // Pop the top of the stack. Note the cast needed.

       Person p = (Person) S.pop();
       System.out.println (p);

       // Look at the top without popping. Note the cast needed.
       p = (Person) S.peek();
       System.out.println (p);

  }

} // End of class "TestStack"

Note:

 
Exercise 6.7: Compile the above program. What does the compiler indicate?
 
To avoid some of the casting and type problems, let's specify the type in creating a stack: (source file)
public class TestStack2 {

    public static void main (String[] argv)
    {
        // Create a new stack object, this time specifying the type.
        Stack<Person> S = new Stack<Person> ();

        // Create a Person instance and add it to the Stack.
	S.push (new Person ("Rogue", "1111-12-1212"));
	S.push (new Person ("Storm", "222-23-2323"));
	S.push (new Person ("Black Widow", "333-34-3434"));
	S.push (new Person ("Jean Grey", "888-89-8989"));

        // Pop the top of the stack. NOTE: no cast needed.
        Person p = S.pop();
        System.out.println (p);

        // Look at the top without popping. Again, no cast needed.
        p = S.peek();
        System.out.println (p);
  }

} 

Note:

 


Built-in data structures - the Hashtable class

 

The package java.util provides a simple hashtable that lets you:

The key itself can be an object.

Consider a simple example with strings:

import java.util.*;

public class TestHashtable {

    public static void main (String[] argv)
    {
        Hashtable h = new Hashtable ();

        // Insert a string and a key.
        h.put ("Ali", "Addled Ali");
        h.put ("Bill", "Bewildered Bill");
        h.put ("Chen", "Cantankerous Chen");
        h.put ("Dave", "Discombobulated Dave");

        // To retrieve, pass a key:
        String d = (String) h.get ("Dave");
        System.out.println (d);  // Prints "Discombobulated Dave"
    }

}

Note:

 
Exercise 6.8: Modify the above program to use generics. Your code should not need casting. Then, add additional lines in main(). to make a typed HashSet and add some new strings.
 

What happens if you insert two items that have the same key? For example:


public class TestHashtable2 {

    public static void main (String[] argv)
    {
        Hashtable h = new Hashtable ();

        // Insert a string and a key.
        h.put ("Ali", "Addled Ali");
        h.put ("Bill", "Bewildered Bill");
        h.put ("Chen", "Cantankerous Chen");
        h.put ("Dave", "Discombobulated Dave");

        // Only one value can be stored for a particular key.
        // Old value with same key is deleted.
        h.put ("Dave", "Distracted Dave");
        String d = (String) h.get ("Dave");
        System.out.println (d);  // Prints "Distracted Dave"

        // You can get back the old value at the 
        // time of insertion.
        String c = (String) h.put ("Bill", "Befuddled Bill");
        System.out.println (c);  // Prints "Bewildered Bill"

        // The return value is null otherwise.
        String e = (String) h.put ("Ed", "Egregious Ed");
        System.out.println (e);  // Prints "null".
    }

}

Note:

 

Thus far, we have used Hashtable with String's.

A Hashtable can store any kind of object. For example: (source file)


// A class to store in the hashtable.
class Person {

    // Instance data.
    String name;
    String nickname;

    // Constructor.
    public Person (String nameInit, String nickInit)
    {
        name = nameInit;  nickname = nickInit;
    }

    // Accessor for name: we will use it as a key:
    public String getName ()
    {
        return name;
    }

    // Overrides Object's toString()
    public String toString()
    {
        return "Person: name=" + name + ", nickname=" + nickname;
    }

} // End of class "Person"



public class TestHashtable3 {

    public static void main (String[] argv)
    {
        Hashtable h = new Hashtable ();

	Person p = new Person ("Franco", "Flummoxed Franco");
	h.put (p.name, p);
	p = new Person ("Gita", "Grumpy Gita");
	h.put (p.name, p);
	p = new Person ("Heinrich", "Huffy Heinrich");
	h.put (p.name, p);

        // When retrieving you get the whole object back.
        p = (Person) h.get ("Heinrich");
        System.out.println (p);  
    }

}

Note:

 

Instead of using String keys, any object can be used.

However, to make hashing work for a user-created object, several things need to be done:

 

To see what this means, consider the following example: (source file)


class NameAge {
    String name;
    int age;
    // Constructor.
    public NameAge (String nameInit, int ageInit)
    {
        name = nameInit;  age = ageInit;
    }
  
    // Override toString()
    public String toString ()
    {
        return "Name=" + name + ", Age=" + age;
    }

    // Overrides Object's hashCode()
    public int hashCode()
    {
        // Shift name's hashCode left by 8 bits.
        return 256*name.hashCode() + age;
    }

    // Overrides Object's equals()
    public boolean equals (Object obj)
    {
        NameAge n = (NameAge) obj;
        if ( (name.equals(n.name)) && (age==n.age) )
            return true;
        else
            return false;
    }

}

class Person {

    // Instance data.
    NameAge na;
    String nickname;

    // Constructor.
    public Person (String nameInit, int ageInit, String nickInit)
    {
        na = new NameAge (nameInit, ageInit);
        nickname = nickInit;
    }

    // Overrides Object's toString()
    public String toString()
    {
        // Observe use of NameAge's toString() here:
        return "Person: " + na + ", nickname=" + nickname;
    }

    // Accessor for NameAge so that it can be used as key.
    public NameAge getNameAge ()
    {
        return na;
    }

}


public class TestHashtable4 {

    public static void main (String[] argv)
    {
        Hashtable h = new Hashtable ();

	Person p = new Person ("Franco", 25, "Flummoxed Franco");
	h.put (p.getNameAge(), p);
	p = new Person ("Gita", 18, "Grumpy Gita");
	h.put (p.getNameAge(), p);
	p = new Person ("Heinrich", 43, "Huffy Heinrich");
	h.put (p.getNameAge(), p);

        // When retrieving you get the whole object back.
	NameAge query = new NameAge ("Heinrich", 43);
        p = (Person) h.get (query);
        System.out.println (p);  
    }

}

Note:

 
Exercise 6.9: Draw a detailed memory picture just before the System.out.println(). in main(). Also, print all the relevant variables in the equals() method of NameAge. Then, explain the output. For this exercise, you can ignore the internals of a hashtable; just assume that a hashtable has a way of storing the pairs of arguments given to it via the put() method.
 


Built-in data structures - using the Properties class

 

Suppose you have a "properties" text file that looks like:


 Circle1.center.x=100
 Circle1.center.y=250
 Circle1.radius=30
 Circle2.center.x=60
 Circle2.center.y=90
 Circle2.radius=10

Java defines a Properties class that reads in such a file and extracts individual property names and values.

The Properties class is similar to a Hashtable except that it works only with Strings and with files in the above form.
(In fact, Properties is derived from Hashtable).
 

First, consider creating a properties file: (source file)


public class TestProperties {

    public static void main (String[] argv)
    {
        // Create a new Properties object.
        Properties p = new Properties();

        // Insert some properties.
        p.put ("Circle1.center.x", "100");
        p.put ("Circle1.center.y", "250");
        p.put ("Circle1.radius", "30");

        // Note: both the property name and 
        // the value are String's.
        p.put ("Circle2.center.x", "60");
        p.put ("Circle2.center.y", "90");
        p.put ("Circle2.radius", "10");

        p.put ("Square1.topleft.x", "300");
        p.put ("Square1.topleft.y", "400");
        p.put ("Square1.side", "40");

        // Print all the properties to the screen.
        p.save (System.out, "Geometric objects");

        // Write the properties to text file called "geodata"
        try {
            // File-related IO requires try-catch.
            FileOutputStream f = new FileOutputStream ("geodata");
            p.save (f, "Geometric objects");
        }
        catch (IOException e) {
            System.out.println ("Cannot open file");
            System.exit(0);
        }

    }// "main"

} // End of class "TestProperties"

Note:

Here is how properties can be read from a file like "geodata": (source file)


public class TestProperties2 {

    public static void main (String[] argv)
    {
        // Create an instance of a Properties class.
        Properties p = new Properties();

        // Read from a text file with properties ("geodata").
        try {
            FileInputStream f = new FileInputStream ("geodata");
            // Use the "load" method in a Properties object.
            p.load (f);
        }
        catch (IOException e) {
            System.out.println ("Cannot open file");
            System.exit(0);
        }

        // Retrieve property values.
        String cx = p.getProperty ("Circle1.center.x");
        String cy = p.getProperty ("Circle1.center.y");
        String r =  p.getProperty ("Circle1.radius");

        System.out.println ("Circle1: cx=" + cx + ", cy=" + cy 
                            + ", r=" + r);

    }

}
 


Built-in library data structures - using an Enumeration class

 

Suppose we have read a properties file using a Properties class and suppose we want to scan the properties one by one.

Java defines a generic Enumeration class that lets you scan through components one by one.

Consider the following example with a Properties class: (source file)


public class TestProperties3 {

    public static void main (String[] argv)
    {
        Properties p = new Properties();

        // Read properties from the file "geodata".
        try {
            FileInputStream f = new FileInputStream ("geodata");
           p.load (f);
        }
        catch (IOException e) {
            System.out.println ("Cannot open file");
            System.exit(0);
        }

        // The entire list of property NAMES can be enumerated.
        System.out.println ("Property Names: ");
        Enumeration e = p.propertyNames();
        while (e.hasMoreElements()) {
            System.out.println (e.nextElement());
        }

    } // "main"

}

Note:

 
Exercise 6.10: The code above only prints out property names. Modify it so that both property names and values are printed out.
 


Using Properties to access system properties

 

The library class System in java.lang defines a method called:


    public Properties getProperties()

The idea is, you can call this method to learn something about the current system.

For example: (source file)


public class SystemProperties {

    public static void main (String[] argv)
    {
        // The System class has a static function that
        // returns a Properties object:
        Properties p = System.getProperties();

        // The entire list of properties can be enumerated.
        Enumeration e = p.propertyNames();
        while (e.hasMoreElements()) {
            String propName = (String) e.nextElement();
            String propVal = p.getProperty (propName);
            System.out.println (propName + "=" + propVal);
        }
    }

}

This example prints out various system properties such as the PATH, CLASSPATH, version of compiler, location of the system executables etc. Curious? Try it out!


© 1998, Rahul Simha (revised 2017)