Articles Index
How to Build Data Structures in JavaTM
Terence Parr, MageLang Institute
Programmers are comfortable building data structures using languages that
feature pointers, but ask "How do I build data structures without pointers
in Java?"
The truth is that you cannot build data structures in any real sense without
some way to have data elements refer to each other. In Java, all object
identifiers (variables) are references to objects, which end up
being pointers to objects in their implementation. So there is no mystery
surrounding Java and data structures. Furthermore, Java provides a wonderful
mechanism for separating the abstract notion of a collection of data from a
data structure implementation. This article presents general
suggestions for building data structures in Java, for using them most effectively,
and for organizing your collections library using packages.
Article Summary and Navigation Aid:
Single Collection--Multiple Implementations
A single collection can have many implementations that can be
used to optimize time or space consumption in your application.
Code that refers to collections rather than their implementation
does not need to be changed or recompiled when the collection
implementation changes.
Single Implementation--Multiple Perspectives
A single object can implement many collection interfaces,
which provides for multiple perspectives on the same data structure.
Multiple Perspectives as a Constraint Mechanism
Other team members can be restricted from illegal or dangerous
access to your data structure by giving them restricted perspectives
on your data.
Homogeneous Collections
Collections can be restricted to accept only objects of a certain
type. Most solutions catch errors at run-time rather than compile-time
because the compile-time solutions require significant programming overhead.
Enumerations of Collection Elements
Java provides a standard interface called Enumeration for walking your data structures.
Implementation Details
Data Structure Cells
Data structures are generally a set of wrappers or cells that contain references
to the actual objects stored in a collection and references to other collection cells.
Suggested Package Structure
A simple package structure to prevent symbol collisions, promote data hiding,
and organize your collections library.
Source code
Source code for List, Stack, LList, LLCell using the package structure suggested
in this article.
Common Article Trails:
Summary trail for programmers interested in the central concepts behind
collections and their implementation in Java:
-
Collections versus Data
Structures
-
Enumerations of
Collection Elements
-
Single
Collection--Multiple Implementations
-
Single
Implementation--Multiple Perspectives
-
Suggested Package Structure
-
Data Structure Cells
Complete trail for programmers who prefer to begin the learning process
with concepts and organization: use the order given in the
navigation aid.
Complete trail for programmers who prefer to begin the learning process
with source code:
-
Collections versus Data Structures
-
Data Structure Cells
-
Source Code
-
Enumerations of Collection Elements
-
Single Collection--Multiple
Implementations
-
Single Implementation--Multiple
Perspectives
-
Multiple Perspectives
as a Constraint Mechanism
-
Homogeneous Collections
-
Suggested Package Structure
Collections versus Data Structures
During the design process, think of collections of data in the
abstract, such as List, Set, and Queue. It is only at the programming
stage that actual data structure implementations should be chosen for each
collection. Naturally, appropriate data structures are chosen according to
memory constraints, and how the collections will be accessed (for example, how
fast can data be stored, retrieved, and sorted.) In Java, you can
separate the abstract notion of a collection of data from its data
structure implementation using
interfaces. For example, consider a List interface versus a linked list implementation.
A simple List might have the following behavior:
interface List {
public void add(Object o);
public boolean includes(Object o);
public Object elementAt(int index);
public int length();
}
whereas a linked list
(that can play the role of a List collection) would be a class that implements interface List:
class LList implements List {
public void add(Object o) {...}
public boolean includes(Object o) {...}
public Object elementAt(int index) {...}
public int length() {...}
}
Such a list could be used as follows:
LList list = new LList();
list.add("Frank");
list.add("Zappa");
list.add(new Integer(4));
if (list.includes("Frank"))
System.out.println("contains Frank");
Notice that any object, regardless of type, can be added to this general list.
Many programmers will argue that this is not a feature, but a bug.
See Homogeneous Collections
for information on how to restrict the types of collection elements.
Single Collection--Multiple Implementations
Also consider how other parts of your program could reference your list.
Should another team member care what implementation you used to make that list?
No. Therefore, they would refer to your linked list using the List interface.
For example, you could pass a linked list off to a method that referred to it by its role--a List:
LList list = new LList();
list.add(...);
paySalaries(list);
where paySalaries() could be defined as:
public void paySalaries(List employees) {
for (int i=0; i<employees.length(); i++) {
Employee empl = employees.elementAt(i);
empl.pay();
}
}
By referring to the linked list as a List, other pieces of code may pass a different List implementation to paySalaries() without forcing
changes to paySalaries(). For example, if you have a dynamic array class that also implemented the List interface, you could pass dynamic array objects to the paySalaries method as well:
DynamicArray anotherList = new DynamicArray();
anotherList.add(...);
paySalaries(anotherList);
where DynamicArray would also implement List:
class DynamicArray implements List {
public void add(Object o) {...}
public boolean includes(Object o) {...}
public Object elementAt(int index) {...}
public int length() {...}
}
Had paySalaries() been defined as:
// BAD: implementation specific!
public void paySalaries(LList employees) {...}
then you could not pass a DynamicArray to paySalaries() even though a DynamicArray can behave just like a LList.
Different implementations of the List interface allow you to tune your application for speed or space efficiency without having to change code that refers to List objects. Note that linked lists are not time-efficient to get the nth element whereas dynamic arrays are very fast at arbitrary element access. However, linked lists are more flexible and sometimes more space efficient than dynamic arrays because they do not have to allocate portions of the list in large chunks (for example, a dynamic array might initially allocate 100 cells even though you only want to store 3 elements). If application constraints force you to care about element access speed, you would create a DynamicArray. On the other hand, if you cared about memory efficiency, you would create a LList. The key idea is that any code referring to a List could use either implementation without modification, or even recompilation! This is the essence of polymorphism and dynamic binding.
Single Implementation--Multiple Perspectives
We have seen how a collection such as a List can be implemented in
many different ways, trading off time and space. Conversely, a
single implementation such as a linked list may be accessed in multiple ways;
that is, a linked list can implement many collection interfaces such
as a List, Queue, and Stack. For example,
class LList implements List, Stack, Queue {
// Satisfy List Interface
public void add(Object o) {...}
public boolean includes(Object o) {...}
public Object elementAt(int index) {...}
public int length() {...}
// Satisfy Stack Interface
public void push(Object o) {...}
public Object pop() {...}
public int height() {...}
// Satisfy Queue Interface
public void getInLine(Object o) {...}
}
where, for example, the Stack interface looks like:
interface Stack {
public void push(Object o);
public Object pop();
public int height();
}
Satisfying multiple interfaces provides multiple perspectives or
organizations of the same data. For example, you might want to create a
Stack of objects that can also be viewed as a simple List when you want to
access elements below the top. (This situation is commonly handled in other languages by
extending the definition of a Stack to include non-stack-top operations.) A similar,
but more complicated situation occurs when you have a binary tree that occasionally
you want to view as a List in order to walk the tree (Java provides an Enumeration
infrastructure for this particular case; see
Enumerations.)
The alternative to satisfying multiple interfaces is to subclass LList to create LLStack, for example, that implements Stack:
class LLStack extends LList implements Stack {
public void push(Object o) {...}
public Object pop() {...}
public int height() {...}
}
The problem with this approach is that it reduces the number of
places you can use your LList class. For example, when LList
implements both List and Stack, a LList can be used as both:
List list;
Stack stack;
LList llist = new LList();
list = llist; // no problem
stack = llist;// no problem
When LLStack is used, a LList cannot be treated as a Stack. A specific LLStack object must be created.
List list;
Stack stack;
LList llist = new LList();
list = llist; // no problem
stack = llist;// ERROR: must create LLStack
The clue here is that LLStack does nothing but implement a
few more methods (extend the interface). Extending the
behavior to create an LLStack implies that you are interested
purely in adding a new perspective on the same object. In this
situation, implementing multiple interfaces is better than subclassing
and implementing a single interface.
Multiple Perspectives as a Constraint Mechanism
Multiple perspectives on the same object also provide an important
constraint mechanism. Imagine that you have a linked list of operands
in a "reverse polish" calculator and that you want to prevent other team
members from accessing anything but the top of stack. You could pass them a Stack
perspective of your linked list to restrict the operations allowed on that collection.
class RPNCalculator {
// for flexibility, use LList
LList operands = new LList();
// assume LList implements Stack
Stack getOperandStack() {
return operands;
}
void optimizeOperandStack() {
/* examine operands as List not
* Stack to do constant folding etc...
*/
}
}
where your team member's code might look like:
void add() {
Stack o = calculator.getOperandStack();
Integer a = (Integer)o.pop();
Integer b = (Integer)o.pop();
Integer n =
new Integer(a.intValue() + b.intValue());
o.push( n );
}
which is infinitely better than unrestricted code with a comment indicating which accesses are legal:
// this method should only access
// the linked list as a stack.
void add() {
LList o = calculator.operands;
Integer a = (Integer)o.pop();
Integer b = (Integer)o.pop();
Integer n =
new Integer(a.intValue() + b.intValue());
o.push( n );
}
Another important restriction may also be enforced: read/write access.
For example, to allow code to examine a list, but prevent it from modifying that list,
you could create a ReadOnlyList interface:
interface ReadOnlyList {
public boolean includes(Object o);
public Object elementAt(int index);
public int length();
}
which is the same as List minus the add method.
Any attempt to add an element would result in a compile-time error:
void foo(ReadOnlyList list) {
// ERROR: compiler indicates list has no add()
list.add("Jim");
}
Note: given this new interface, List could be redefined as:
interface List extends ReadOnlyList {
public void add(Object o);
}
Homogeneous Collections
The collections described so far are considered heterogeneous
collections as they can hold any type of element. In order to
restrict the type of the elements in a particular collection,
you can easily subclass a given collection implementation. Consider
making a linked list of Employee objects. You only have to restrict the add method:
class EmployeeLList extends LList {
public void add(Object o)
{ /* throw exception */ }
public void add(Employee e)
{ super.add(o); }
}
An EmployeeLList can be passed to any code that expects a List,
but a run-time
exception will occur if the code tries to add a non-Employee object to the list. For example,
EmployeeLList elist = new EmployeeLList();
foo(elist);
...
void foo(List list) {
list.add(new Employee("Jim"));
// add(Object) called and error
// caught at run-time
list.add("ack");
}
Compile-time errors are generally considered more desirable than
run-time errors in this situation. To generate compile-time support for
homogeneous lists, you must define a new interface--one that is specific to a particular type of object:
interface EmployeeList {
public void add(Employee o);
public boolean includes(Employee o);
public Employee elementAt(int index);
public int length();
}
Further, an EmployeeLList that uses Employee instead of Object references is still required:
EmployeeLList elist = new EmployeeLList();
foo(elist);
...
void foo(EmployeeList list) {
list.add(new Employee("Jim"));
// ERROR: compiler warns add(String) not found.
list.add("ack");
}
Neither of the solutions presented so far in this section is
terribly pleasant, because in a typical program many implementation subclasses and interface
variants would have to be defined. This is one area
where C++ templates would make sense. Unfortunately, adding templates to Java
does not increase execution speed or functionality, but adds considerable
complexity to the compiler and raises serious memory space concerns.
A reasonable solution for enforcing homogeneity,
albeit at runtime, revolves around Java's run-time type system.
The programmer is not required to create any subclasses or
additional interfaces--the type of the collection elements is
passed to the collection constructor in a similar manner to a
template (parameterized type). For example, a linked list
that knows the type of its elements would have the following use:
TypedLList tlist =
new TypedLList(Class.forName("Employee"));
tlist.add(new Employee("Jim"));
tlist.add("this String causes a run-time exception");
Such a list would be an extension of a regular linked list:
public class TypedLList extends LList {
Class elementType;
public TypedLList(Class c) { elementType=c; }
public void add(Object o) {
if ( o.getClass() == elementType ) {
super.add(o);
}
else {
// tried to add invalid type to TypedLList.
throw new InvalidTypeCollectionType();
}
}
}
The one disadvantage is that subclasses of the collection
element type would not be allowed. For example, if Manager were a subclass of Employee, then:
// unfortunately, also causes run-time exception
tlist.add(new Manager("Mike"));
would not be allowed.
A more sophisticated version of TypedLList would accept interface
objects instead of class objects and would then check to see if the
object to be added implemented the required interface. Because,
when discussing collections, the concern is mostly with the
behavior of an object, rather than its exact type. Checking interfaces may prove more satisfactory for you.
Enumerations of Collection Elements
The java.util package includes an interface called
Enumeration that allows enumerations of your data structure. A
class that implements this interface is essentially an ordered walk of
your data where there may be many walks of the same data (for example,
depth-first or breadth-first tree walks). An enumeration is not
usually a copy of the data, but rather a control mechanism for walking. For example,
an enumerator could walk a list as follows:
Enumeration e = list.elements();
for (; e.hasMoreElements();) {
System.out.println(e.nextElement());
}
The linked list implementation can be augmented to construct enumeration objects:
class LList implements List {
public void add(Object o) {...}
public boolean includes(Object o) {...}
public Object elementAt(int index) {...}
public int length() {...}
public Enumeration elements() {
return new LLEnumeration(this);
}
}
where a linked list enumeration looks like:
final class LLEnumeration implements Enumeration {
public LLEnumeration(LList l) {...}
public boolean hasMoreElements() {...}
public Object nextElement() {...}
}
Implementation Details
Data Structure Cells
Java can only allocate objects on the heap, therefore,
objects cannot contain other objects--objects can only refer to other objects.
Consequently, most data structures need small "wrapper" objects
or "cells" to encapsulate references to the data objects and to the other cells.
For example, a linked list will need a cell like:
class LLCell {
Object data;
LLCell next;
public LLCell(Object o) { data = o; }
}
The size of a LLCell Java object from a programmer's perspective is the size of two object
references, whereas in C or C++ a struct or object could be directly instantiated into a cell:
/* C/C++ code */
struct EmployeeLLCell {
Employee cell; // space for employee
// struct made here
EmployeeLLCell *next;
};
Note the inflexibility of instantiating the contained object
inside the cell; instead of pointing or referring to it--the
type must be known so that space can be allocated in the cell. A
new cell type must be defined for each type of list you create.
A LList object would probably hold a head and tail reference to the
first and last cell in the list. To add an object to a LList, a cell is
created to hold a reference to that object and a reference to the next cell managed by the LList:
public class LList {
LLCell head=null, tail=null;
public void add(Object o) {
LLCell n = new LLCell(o);
if ( tail==null ) head=tail=n;
// if the list is empty
else {
tail.setNext(n);
tail=n;
}
}
...
}
The following diagram illustrates the structure of the linked lists: