Programming Tips and
API Design
There are several rules of thumb to consider when using collections.
One rule is to program in terms of interface types rather than
implementation types. For example:
List lst = new ArrayList();
is preferable to:
ArrayList lst = new ArrayList();
because it lessens dependence on particular implementations.
For instance, ArrayList might later change to
LinkedList, or a program might come to depend on
added methods found only in ArrayList and not
in the List interface.
Another rule, especially applicable when passing collection types as
parameters to some called method, is to use the least specific type,
for example, Collection instead of List, or
List instead of ArrayList. This promotes
generality and again works against depending on added methods.
For method return types, it's acceptable and even desirable to use
specific implementation types. For example, ArrayList has
very different performance characteristics than LinkedList,
and because of this, the user of a method needs to know just what type
of data structure is being returned. The return type can always be
converted to a more general type, for example:
ArrayList f() {...}
void g()
{
List lst = f();
}
Performance
The sorting and searching algorithms have execution performance
proportional to N * log(N) and log2(N)
respectively. Sorting is done by mergesort, which is stable, that
is, does not reorder equal elements, and its performance is guaranteed
not to be worse than N * log(N) (which quicksort cannot
guarantee).
ArrayList is implemented in terms of an underlying array of
objects, and so is space efficient with constant time positional access
(random access). By contrast, LinkedList used a linked
structure, and so requires more space, and is slow at random access.
But it comes out ahead in cases where you are doing heavy editing (adding
and removing elements) in the middle of the list.
HashSet is much faster than TreeSet (constant vs.
log(N)), but does not offer any guarantees on ordering, and
similarly for HashMap and TreeMap.
One performance tip is to use iterators whenever you can to access the
elements of a list, rather than depending on positional access. This is
a big gain, for example, if the underlying list is a LinkedList.
Customization
Wrappers, adapter classes, and convenience implementations, described
above, illustrate several ways of customizing collections. But what if
these are not adequate? What if for reasons of performance or desire
for enhanced functionality you need to go further?
There are at least three alternatives available:
-
Implement a core collection interface.
-
Subclass a primary implementation such as
ArrayList.
-
Extend an abstract class such as
AbstractList.
The first of these is beyond the scope of this paper. However, the
second can be illustrated by a simple example, one that enforces a
policy such that only String objects can be added to an
ArrayList:
import java.util.*;
class StringArrayList extends ArrayList {
public StringArrayList()
{
super();
}
// other constructors ...
public boolean add(Object o)
{
if (o instanceof String)
return super.add(o);
else
throw new UnsupportedOperationException();
}
// other add() and set() methods ...
}
public class extend1 {
public static void main(String args[])
{
StringArrayList list = new StringArrayList();
list.add("abc");
list.add("def");
list.add("ghi");
//list.add(new Object());
Iterator iter = list.iterator();
while (iter.hasNext())
System.out.println(iter.next());
}
}
This scheme works simply by intercepting calls and checking whether the
element to be manipulated is an instance of String. The
superclass (ArrayList) does all the work.
Another alternative, a much more ambitious illustration, is extending
AbstractList to implement run-length encoding. Such
encoding refers to efficiently representing adjacent identical elements.
For example, a photograph, represented in a data structure, might have
long runs of identical color elements that represent a stretch of blue sky.
RunArrayList is an implementation of List, based
on extending AbstractList. It stores elements in slots, with
each slot representing a range of contiguous element indices, with all
indices in the slot referring to the same element.
When runs of identical elements with average length of 25 are inserted
into a RunArrayList, the time and space requirements are
only 5-10% of those required by ArrayList. When elements
are inserted randomly, the worst case, RunArrayList requires
about twice the time and space as ArrayList.
Here is the implementation of RunArrayList:
import java.util.*;
/*
Implements List via a run-length encoding data
structure. The structure is divided into slots,
each slot representing one or more contiguous
elements that are equivalent, using equals() for
comparison.
The highest index for each slot is stored in the
"sublist" array, and the element values
themselves in "objlist".
*/
public class RunArrayList extends AbstractList
implements Cloneable, java.io.Serializable {
// highest index for each slot
private int sublist[];
// element value for each slot
private Object objlist[];
// highest current valid slot number
private int currmax;
// find the lowest valid index for a slot
private int slotlo(int index)
{
return (index == 0 ? 0 :
sublist[index - 1] + 1);
}
// find the highest valid index for a slot
private int slothi(int index)
{
return sublist[index];
}
// check whether two objects are equivalent
// (handles null)
private static boolean
equals(Object obj1, Object obj2)
{
return (obj1 == null ? (obj2 == null) :
obj1.equals(obj2));
}
// make room for a new slot, and push up others
private void makeslot(int slot, int n)
{
if (currmax + n >= sublist.length)
growlist();
if (slot < currmax) {
System.arraycopy(sublist, slot + 1,
sublist, slot + 1 + n, currmax - slot);
System.arraycopy(objlist, slot + 1,
objlist, slot + 1 + n, currmax - slot);
}
currmax += n;
}
// grow the subscript and object lists
private void growlist()
{
int len = sublist.length * 2;
int newsub[] = new int[len];
System.arraycopy(sublist, 0, newsub, 0,
sublist.length);
sublist = newsub;
Object newobj[] = new Object[len];
System.arraycopy(objlist, 0, newobj, 0,
objlist.length);
objlist = newobj;
}
// find the slot corresponding to an index
private int findslot(int index)
{
int lo = 0;
int hi = currmax;
// binary search
while (lo <= hi) {
int mid = (lo + hi) / 2;
if (index < slotlo(mid))
hi = mid - 1;
else if (index > slothi(mid))
lo = mid + 1;
else
return mid;
}
// should never get here
throw new Error();
}
// default constructor
public RunArrayList()
{
sublist = new int[10];
objlist = new Object[10];
currmax = -1;
}
// constructor from a Collection
public RunArrayList(Collection c)
{
this();
Iterator iter = c.iterator();
while (iter.hasNext())
add(iter.next());
}
// number of elements currently in the list
public int size()
{
return currmax == -1 ? 0 :
sublist[currmax] + 1;
}
// get an element value based on an index
public Object get(int index)
{
if (index < 0 || index >= size())
throw new IndexOutOfBoundsException();
return objlist[findslot(index)];
}
// set an element to a new value
public Object set(int index, Object element)
{
// remove then add
Object obj = remove(index);
add(index, element);
return obj;
}
// add a new element, pushing up other elements
public void add(int index, Object element)
{
int sz = size();
if (index < 0 || index > sz)
throw new IndexOutOfBoundsException();
// adding to the end of the list?
if (index == sz) {
if (sz > 0 &&
equals(objlist[currmax], element)) {
// same as current last element
sublist[currmax]++;
}
else {
// not same, append to end
if (currmax + 1 == sublist.length)
growlist();
currmax++;
sublist[currmax] = sz;
objlist[currmax] = element;
}
}
else {
int slot = findslot(index);
int startincr = slot;
if (!equals(objlist[slot], element)) {
if (index == slotlo(slot)) {
// push current slot up
makeslot(slot, 1);
sublist[slot + 1] =
sublist[slot];
sublist[slot] = index;
objlist[slot + 1] =
objlist[slot];
objlist[slot] = element;
startincr++;
}
else {
// split current slot
makeslot(slot, 2);
sublist[slot + 2] =
sublist[slot];
sublist[slot + 1] = index;
sublist[slot] = index - 1;
objlist[slot + 2] =
objlist[slot];
objlist[slot + 1] = element;
startincr += 2;
}
}
// bump up max indices
for (int i = startincr; i <= currmax; i++)
sublist[i]++;
}
}
// remove an element
public Object remove(int index)
{
if (index < 0 || index >= size())
throw new IndexOutOfBoundsException();
int slot = findslot(index);
Object obj = objlist[slot];
// if this index is the only one in the slot,
// delete the slot and shift down
if (slotlo(slot) == slothi(slot)) {
if (slot < currmax) {
System.arraycopy(sublist, slot + 1,
sublist, slot, currmax - slot);
System.arraycopy(objlist, slot + 1,
objlist, slot, currmax - slot);
}
objlist[currmax--] = null;
}
// decrement indices
for (int i = slot; i <= currmax; i++)
sublist[i]--;
return obj;
}
}
And here is a test driver program that exercises RunArrayList:
import java.util.*;
public class testrl1 {
static Random rn = new Random();
// return a random integer,
lo <= integer <= hi
static int rand(int lo, int hi)
{
return lo + (int)((hi - lo + 1) *
rn.nextFloat());
}
// do random operations on a RunArrayList
// and on an
// ArrayList, and periodically compare the
// results
static void test()
{
List rl = new RunArrayList();
List al = new ArrayList();
final int LISTRNG = 5;
final int LISTSZ = 50;
for (int i = 1; i <= 10000000; i++) {
if (i % 10000 == 0) {
System.out.println(i);
if (!al.equals(rl)) {
System.out.println("equals");
break;
}
}
int sz = al.size();
// create an object to add, and occasionally
// make it null
int r = rand(1, LISTRNG);
Object obj = new Integer(r);
if (rand(1, 25) == 1)
obj = null;
// bias toward adding if list is small,
// else toward removing
boolean add =
(sz <= LISTSZ &&
rand(1, 3) >= 2
|| sz > LISTSZ &&
rand(1, 3) < 2);
// do a random get()
if (sz > 0) {
int pos = rand(0, sz - 1);
Object o1 = al.get(pos);
Object o2 = rl.get(pos);
boolean b = (o1 == null ?
(o2 == null) : o1.equals(o2));
if (!b) {
System.out.println("get");
break;
}
}
// add
if (add) {
int choice = rand(1, 3);
if (choice == 1) {
rl.add(obj);
al.add(obj);
}
else if (choice == 2) {
int pos = rand(0, sz);
rl.add(pos, obj);
al.add(pos, obj);
}
else if (sz > 0) {
int pos = rand(0, sz - 1);
rl.set(pos, obj);
al.set(pos, obj);
}
}
// remove
else if (sz > 0) {
int pos = rand(0, sz - 1);
rl.remove(pos);
al.remove(pos);
}
}
}
public static void main(String args[])
{
test();
}
}
This is a production-quality implementation, except perhaps for
making a change to override the default iterator. The default uses
get, which has time log2(N), and an
overriding version could implement iterators in constant time.