The battle of the container frameworks: which should you use?

We compare ObjectSpace's JGL and Sun's Collections Framework to help you pick the best option

No self-respecting developer wants to spend his or her talents and precious time writing from scratch a linked-list or queue data structure, nor a simple sort or search routine to operate on the former. In this context, Sun made a big mistake by not including a container framework in the JDK beginning with version 1.0. Fortunately, ObjectSpace Inc. offered JGL -- The Generic Collection Library for Java (formerly known as the Java Generic Library). JGL gave those who refused to reinvent the wheel an extremely powerful framework for data structures and algorithms; a framework that made those who took the time to understand its philosophy more productive than those who didn't.

JavaWorld's own recent Collections-versus-JGL poll generated passionate responses from supporters of both frameworks. By now, JGL is an old and faithful friend to many Java developers (and my integrity as a reporter demands that I disclose my membership in this group), so it didn't come as a surprise that the new kid on the block, Collections, was greeted with mixed feelings -- at least by the majority of respondents to the poll.

In this article I attempt to compare the two frameworks objectively, without letting adrenaline (or testosterone) affect my evaluation. For the record, my personal goal for this comparison exercise was to determine the true value of Sun's Collections Framework. Before porting my JGL-based code to Collections, I need to know whether the hardly negligible resources spent on porting will be a worthwhile, long-term investment. I suspect many of you are considering the same question, and hope this comparison will help you as much as it has helped me.

This article does not provide an introduction to ObjectSpace's JGL nor Sun's Collections, although the fundamental nature of these libraries should mean that most readers will be able to follow the material and discussion that will follow. Refer to JavaWorld's previous articles on JGL and Collections for detailed introductions. (See the Resources section at the end of this article for links.)

In the course of this article, I will present some tables (in the same style as those in my comparative book reviews) that will necessarily have framework A on the right, and framework B on the left. After careful consideration, I've chosen to put JGL on the left and Collections on the right of all side-by-side tables -- mainly because JGL, in most instances, provides more features than Collections. This ordering does not imply that this comparison is a guide-in-camouflage to porting from JGL to Collections (nor that porting from Collections to JGL is an unreasonable option to consider).

The versions of the libraries compared are:

  • JGL version 3.0.2 (from now on, simply called JGL)
  • Collections from JDK 1.2 Beta 4 (from now on, simply called Collections)

(Although I -- and JavaWorld -- normally frown upon comparisons between unfinished and finished products, Sun assured me that the Collections API is highly unlikely to change in any significant way from beta 4 to its final release, so the advanced beta status for Collections should be irrelevant. Only the implementation, which lies thoroughly behind the API facade, may change between now and the final release of the Java 2 platform.)

Philosophical differences

The immediate, high-level difference between Collections and JGL appears to be one of philosophies. Sun and ObjectSpace have different outlooks, and offer us solutions that reflect their distinct perspectives.

JGL is a direct descendant of STL, C++'s Standard Template Library. (The major STL/JGL difference: STL is hard to exploit properly, while exploiting JGL is a breeze. This difference has little to do with the libraries themselves, but instead reflects the difference in ease of use of the two targeted languages: C++ and Java.) JGL inherits STL's fundamental library architecture, as depicted by Figure 1 below.

Figure 1: The STL/JGL architecture.

Three of the four JGL pillars -- containers, algorithms and iterators -- are the core concepts of the "generic programming" paradigm (the brainchild of Alexander Stepanov and Meng Lee). Hence, the philosophy behind JGL is that of generic programming. (If you're unfamiliar with this paradigm, you really owe it to yourself to explore generic programming in depth. The insights you'll gain from the exercise will stay with you for the rest of your career; they're as profound as those gained from discovering design patterns. See Resources at end of article.)

With Collections, the name of the framework itself is tell-tale: Collections focuses mainly on containers, rather than on containers and algorithms. This is a very different approach from JGL, which treats algorithms and containers as equals. This fundamental difference leads to more differences down the line, eventually diverging into large areas where the two frameworks have little in common, as we shall see later.

The Collections Framework also is discussed using a language that differs from that used in JGL/STL circles: When studying Sun's Collections documentation, the main focus seems to be that of "interfaces" and "framework." No mention is made of generic programming. Instead, Sun concentrates on another commendable technique: the rigid separation of the interface from multiple, possibly competing, implementations thereof. Hence Sun's frequent accentuation of the word framework (an open, and often mainly abstract, architecture or design that third parties can implement). Although the Collections Framework does come with an implementation of the framework itself, Collections is primarily a set of Java interfaces for container data structures.

Comparing JGL and Collections

This comparison will focus on the following key issues:

  • Package organization
  • Interface hierarchies and APIs
  • Container implementations
  • Algorithms
  • Major and minor differences

Without further delay, let's look under those hoods.

Package organization

External packaging may not be a reliable guide to the contents of a book, but in the world of class libraries, packaging -- in the logical sense, by means of Java package hierarchies -- is an important indicator of quality and the amount of thought that went into a library.

Table 1 compares the package approach for both frameworks.

JGLCollections
com.objectspace.jgljava.util
com.objectspace.jgl.adapters 
com.objectspace.jgl.algorithms 
com.objectspace.jgl.functions 
com.objectspace.jgl.predicates 
com.objectspace.jgl.util 

Table 1. Package hierarchy and package names for JGL and Collections

(Note that I've omitted ObjectSpace's Voyager™ support packages: com.objectspace.jgl.voyager and com.objectspace.jgl.voyager.algorithms. While ObjectSpace may think these packages have a logical place in the jgl.* hierarchy, I disagree.)

As you can see, the JGL approach to package organization is fine-grained. JGL's six-package approach is an absolute necessity, because the total number of JGL classes and interfaces would simply become a confusing blur if stored in less packages. (See Table 2 for package statistics.) Sun elected to store the Collections Framework in the venerable java.util package (I think a new package called java.util.collections would have been a more sensible choice.)

 JGLCollections
Interfaces1210*
Classes15113**
Exceptions12***

Table 2: Package statistics

* Excludes java.util interfaces Enumeration, EventListener and Observer, since these are not part of the Collections Framework.

** Excludes pre-Collections classes like Vector, Stack, Bitset.

*** Includes java.lang.UnsupportedOperationException

The order-of-magnitude difference in the number of classes (151 versus 13) seems, at first sight, to be a major advantage for Collections (fewer classes means a shorter learning curve). The numbers are deceptive, though: JGL's classes are organized in very clean groups that one can mentally keep apart quite easily. A more informative presentation of the same numbers can be found in Table 3, which shows the statistics for each of JGL's subpackages.

 *.jgl*.jgl.adapters*.jgl.algorithms*.jgl.functions*.jgl.predicates*.jgl.utiljava.util
Interfaces120000010
Classes2229222249713
Exceptions1000002

Table 3:Subpackage statistics

JGL's (initially) daunting size ("150-plus classes") effectively deflates upon closer examination:

  • All 29 classes within the jgl.adapters package are trivial. They are adapter classes that enable you to turn native Java arrays into JGL containers. Having learned how to use one adapter class, you'll know how to use all others.

  • All 22 classes within the jgl.algorithms package contain only static methods (the algorithms themselves). These classes are therefore as easy to use as java.lang.Math. Additionally, the names of 18 of the 22 classes in jgl.algorithms end in -ing (Sorting, Filtering, Replacing, and so on), further aiding mental compartmentalization.

  • The 71 function and predicate classes in packages jgl.functions and jgl.predicates are optional algorithm "plug-ins." As with most plug-in architectures (such as those found in Web browsers), these classes tremendously boost the flexibility of the entity being plugged into (JGL in this case). Since these plug-in objects are entirely optional, you can initially ignore them completely.

Within Collections, the following class-naming conventions help flatten its already-gentle learning curve:

  • Abstract implementations of the Collections interfaces start with Abstract-.

  • Concrete implementations of the Collections interfaces end with the name of the implemented interface (ArrayList implements the List interface; HashMap implements the Map interface; and so on).

Having surveyed the package structure of both frameworks, let's now look at the most important facet shared by both frameworks: their respective container interface hierarchies.

Container interface hierarchies

Both frameworks have at their hearts a set of Java interfaces for their containers and iterators. First, we shall look at the container interfaces, since these are the most important. Figures 2 and 3 depict the JGL and Collections container interface hierarchies, respectively.

Figure 2. ObjectSpace JGL container interface hierarchy
Figure 3. Sun Collections container interface hierarchy (note: two different branches)

Comparing the two interface inheritance hierarchies, the immediate surprise is that the Collections container interface hierarchy does not define a single super-interface for the entire framework, like JGL does, but defines two interfaces: Collection and Map.

In the Collections Framework, maps are not regarded as collections of objects, whereas JGL does regard maps as collections of objects (key-value pairs). In view of this, the name "Collections Framework" is a bit misleading. A more accurate name would be "Collections and Maps Framework." The important side-effect of not having a single root interface for the entire Collections Framework is that developers will, on occasion, slam into the immovable divide between Collections and Maps (but see Map methods entrySet(), keySet() and values() for possible ways round this problem.)

If we restrict our interface hierarchy analysis to the branches rooted in JGL's Container and the Collections Framework's Collection (that is, if we ignore the Collections Framework's Maps), we see that the two frameworks are actually very similar: a JGL Sequence is the logical equivalent to a Collections List, and a JGL Set is equivalent to a Collections Set (hallelujah!). Sequences/Lists are abstract data structures that allow indexed access to their contained elements. Sets generally are regarded as orderless containers (like an everyday "bag," for example). Container frameworks, in general, differ as to whether sets may or may not contain element duplicates. Collections rigidly enforces the no-duplicates approach, while JGL defaults to no-duplicates sets. (JGL does, however, allow duplicates in sets through an overriding mechanism.)

Container interface APIs

The container interface, JGL's Container interface and Collections's Collection interface, is the most important interface in each framework. These interfaces determine each framework's basic container capabilities and limitations, so we need to look at them in some detail. Listings 1 and 2 are the source code for interfaces com.objectspace.jgl.Container and java.util.Collection, respectively.

public interface Container {

Object add(Object object) // Add an object to myself. If appropriate, // return the object that displaced it, // otherwise return null. void clear() // Remove all of my objects. Object clone() // Return a shallow copy of myself. int maxSize() // Return the maximum number of objects // that I can contain. int size() // Return the number of objects that I contain. boolean isEmpty() // Return true if I contain no objects.

Enumeration elements() // Return an Enumeration of the components // in this container. ForwardIterator start() // Return an iterator positioned at my first item. ForwardIterator finish() // Return an iterator positioned immediately // after my last item.

Object remove(Enumeration pos) // Remove the element at a particular position. int remove(Enumeration first, Enumeration last) // Remove the elements in // the specified range. }

Listing 1. ObjectSpace JGL's Container interface

public interface Collection {

// Basic Operations boolean add(Object o) // (Optional) Ensures that this Collection // contains the specified element. int size() // Returns the number of elements in this Collection. boolean isEmpty() // Returns true if this Collection contains no elements. boolean contains(Object o) // Returns true if this Collection contains // the specified element. boolean remove(Object o) // (Optional) Removes a single instance of // the specified element from this Collection, // if it is present. Iterator iterator() // Returns an Iterator over the elements // in this Collection.

// Bulk Operations boolean addAll(Collection c) // (Optional) Adds all of the elements in the // specified Collection to this Collection. boolean containsAll(Collection c) // Returns true if this Collection contains // all of the elements in the specified Collection. boolean removeAll(Collection c) // (Optional) Removes from this Collection all // of its elements that are contained in the // specified Collection. boolean retainAll(Collection c) // (Optional) Retains only the elements in this // Collection that are contained in the specified // Collection. void clear() // (Optional) Removes all of the elements from // this Collection.

// Bulk Operations Object[] toArray() // Returns an array containing all of the elements // in this Collection. Object[] toArray(Object[] a) // Returns an array containing all of the elements // in this Collection, whose runtime type is that // of the specified array. }

Listing 2. Sun Collections Framework's Collection interface

Scrutinizing Listings 1 and 2 unearths a whole catalog of important differences. Table 4 effectively highlights these differences in a side-by-side comparison, with particular attention given to the semantics of the methods.

JGL Container

Shared By Both

Collections's Collection
Object add(Object object)

Yes(1)

boolean add(Object o)
void clear()

Yes

void clear()
boolean isEmpty()

Yes

boolean isEmpty()
int size()

Yes

int size()
Enumeration elements()

Yes(2)

Iterator iterator()
ForwardIterator start()

No

 
ForwardIterator finish()

No

 
int maxSize()

No

 
Object remove(Enumeration pos)

No

 
int remove(Enumeration first, Enumeration last)

No

 
String toString()

No

 
 

No

boolean addAll(Collection c)
 

No

boolean contains(Object o)
 

No

boolean containsAll(Collection c)
 

No

boolean remove(Object o)
 

No

boolean removeAll(Collection c)
 

No

boolean retainAll(Collection c)
 

No

Object[] toArray()
 

No

Object[] toArray(Object[] a)

Table 4. Direct semantics-based comparison of interfaces Container (JGL) and Collection (Collections Framework)

  1. The semantics of the respective add() methods aren't identical, but they are close enough in practice.
  2. Although the names of the methods differ, the semantics of Container.elements() and Collection.iterator() are essentially identical (that is, to return an iterator over the container).

The striking aspect of this comparison of methods dictated by JGL's Container and Sun's Collection interfaces is how little they have in common. Since these interfaces determine in many ways the "logical look and feel" of their respective frameworks, it follows that the frameworks themselves are quite different.

The methods common to both interfaces are the absolute essence of containerhood: adding new elements (add()), emptying a container (clear()), size determination (size()), and obtaining an iterator over the container (elements() or iterator()).

To spoil the simplicity of the previous statement, Collections specifies that its add() and clear() methods are optional (by "optional," Sun means that such methods may throw UnsupportedOperationException to signal clients that these methods aren't a logical part of some implementation of the Collection interface.) These "optional" methods introduce a fairly major and unwelcome complexity at such a high level in the framework. (E.g., How can I know whether a Collection object obtained at runtime from an external system will let me add objects to it? and Do I have to wrap all my Collections code with try-catchUnsupportedOperationException?, and so on.)

Leaving the interface similarities behind, if you study the differences between JGL's Container and the Collections Framework's Collection, you'll see in particular that the element removal methods vary significantly. JGL uses its iterators to identify (or "point to") one or more candidate elements for removal, while Collections identifies elements for removal using the elements themselves (cfr. argument passing by reference and by value). Clearly, the performance of the two approaches will, in general, be very different. Iterator-based removal should be very quick (often of the order of O(1)), while object-based removal will often be slow to very slow (since removal needs to find the object first. With many container implementations, this search will have to be an expensive O(n) sequential search). From an abstraction point of view, however, the Collections approach is higher-level, and therefore more programmer-friendly.

A second difference between Container and Collection stems from the difference in philosophies that spawned the two frameworks. JGL's Container interface contains less "algorithmic" functionality (methods to find elements, remove elements, and so on.) than does the Collections Framework's Collection interface. This is because JGL aims to decouple containers as much as possible from any algorithms that may operate on the containers. On the other hand, the Collections Framework doesn't regard algorithms as first-class citizens, so it doesn't have any philosophical objections to including extra algorithms in its primordial Collection container interface. For example, Collection.addAll(), Collection.removeAll(), and Collection.retainAll() are essentially set operations (in the mathematical sense). JGL offers more generic set operations in its separate algorithms class SetOperations. Also in contrast with Collections, JGL's SetOperations class contains a complete series of set operations (difference, symmetric difference, intersection and union).

Iterator interface hierarchies

Both JGL's Container and the Collections Framework's Collection interfaces specify methods to return an iterator over a container. While JGL and Collections iterators both are incarnations of the general object-oriented Iterator design pattern (as defined by the classic "Gang of Four" book, Design Patterns, [Addison-Wesley, 1995]), iterators are used differently by the two frameworks. Studying the iterator interface hierarchies and the definition of the most important iterator interfaces will explain why their uses differ in practice.

Figures 4 and 5 depict the iterator interface hierarchies for JGL and Collections.

Figure 4. JGL iterator interface hierarchy
Figure 5. Collections iterator interface hierarchy

Figures 4 and 5 clearly show that Collections is a lot simpler than JGL when it comes to iterators. JGL's overly complex approach to iterators often leads to programmer's headache (due to the numerous permutations of incompatible iterator types -- if an algorithm requires a RandomAccessIterator, but all you have is a ForwardIterator, you're stuck). The Collections approach greatly reduces this problem. The flip-side, of course, is that the reduced number of iterator types in Collections will prevent iterator implementations in that framework from being performance-optimized to the same extent as those in JGL. However, as the entire Java phenomenon is proving so convincingly, concentrating on correctness before performance is the right approach; so the Collections iterators approach is undoubtedly superior to JGL's.

Iterator Interface APIs

As far as actual iterator APIs are concerned, Figures 6 and 7 further confirm that JGL iterators are a lot more complex than Collection iterators.

Figure 6. JGL iterator interfaces APIs.

If you look closely at JGL interfaces InputIterator and OutputIterator, and their multiply-inheriting subinterface ForwardIterator, you should notice some object-oriented heresy: both InputIterator and OutputIterator contain methods advance() and advance(int n). These methods ought to be factored out and moved to a higher level in the hierarchy. Secondly, since these iterator-advancing methods logically seem to belong to ForwardIterator, which is lower in the hierarchy, it's clear that JGL's iterator hierarchy is rather messy right out of the starting block. Further complicating the JGL iterator picture is the recent non-backward-compatible addition (between JGL 2.0 and JGL 3.0) of method isCompatibleWith() in InputIterator, resulting in lots of JGL 2.0 code breaking en route to JGL 3.0.

Figure 7 depicts the refreshing simplicity and internal symmetry of the Collections Framework's iterator API.

Figure 7. Collections Iterator and ListIterator interface APIs

Interface Iterator basically is the same as java.util's Enumeration interface, with element removal thrown in. Interface Iterator's subinterface, ListIterator, is roughly equivalent to JGL's most flexible iterator, RandomAccessIterator. Table 5 attempts a best-fit comparison of ListIterator and RandomAccessIterator.

JGL RandomAccessIterator

Shared By Both

Collections ListIterator
boolean hasMoreElements()

Yes

boolean hasNext()
Object nextElement()

Yes

Object next()
int index()

Yes

int nextIndex(), int previousIndex()
void put(Object)

Yes(1)

void set(Object o)
boolean atBegin()

Almost

boolean hasPrevious()
boolean atEnd()

Almost

boolean hasNext()
 

No

Object previous()
 

No

void add(Object o)
 

No

void remove()
void put(int offset, Object object)

No

 
Object get()

No

 
Object get(int offset)

No

 
void advance()

No

 
void advance(int)

No

 
void retreat()

No

 
void retreat(int n)

No

 
int distance(ForwardIterator iterator)

No

 
boolean less(RandomAccessIterator iterator)

No

 
Container getContainer()

No

 
boolean isCompatibleWith(InputIterator)

No

 
Object clone()

No

 

Table 5. Direct semantics-based comparison of interfaces RandomAccessIterator (JGL) and ListIterator (Sun).

  1. The semantics of RandomAccessIterator.put() and ListIterator.set() aren't identical, but they're close enough in practice.

The interface hierarchy and API differences between JGL's and the Collections Framework's iterator approach is further complicated by the fact that the fundamental iterator semantics aren't the same in both frameworks. First of all, JGL iterators conceptually point directly at an element, whereas Collections iterators point between elements. This difference leads to JGL's get()/put()and advance()/retreat() methods as opposed to Collections's shorter next()/previous() methods.

Secondly, Collections iterators are able to alter the size of a collection by removing elements (and adding elements, in the case of ListIterators), while normal JGL iterators support only element overwriting (which never changes the size of a container). The latter is understandable if you consider that JGL iterators are used as abstract pointers (in the C/C++ sense) to elements within containers.

Acting on container subranges

JGL relies a lot on this particular "pointers view" of iterators since it lets users pass two iterators as a means of specifying a subrange of a container. When you remove an element from a JGL container, any iterators pointing into this container normally are invalidated. So, clearly, JGL doesn't let its iterators remove or add elements because such a capability would sabotage most of its range-based algorithms. Roughly half of JGL's algorithm entry points support the operation of an algorithm on a subrange of a container, instead of operating on the entire container. (There is one important exception to this "JGL iterators can't change the size of a container" rule: jgl.util's InsertIterator does allow you to insert new elements in a container, by way of a Container's add() method. Sophisticated developers can effectively use this less-than-elementary iterator.)

Collections uses a much cleaner approach to container subranges: Every List can generate a range view of some part of itself, using the following method:

List List.subList(int fromIndex, int toIndex)

As you can see from the method's signature, though, obtaining range views works only for Lists, not all Collections. Nevertheless, this elegant approach allows Collections to do away with any subrange-specific methods (such methods form roughly half of all JGL algorithm methods!).

Container Implementations

Both frameworks provide general-purpose implementations of their respective core container interfaces. Table 6 shows that Collections isn't far behind JGL when it comes to concrete, usable container classes.

JGL ContainerJGL Interfaces ImplementedClosest equivalent Collections containerCollections interfaces implemented
ArrayContainer, SequenceArrayList, VectorCollection, List
DequeContainer, Sequence  
DListContainer, SequenceLinkedListCollection, List
SListContainer, Sequence(LinkedList)Collection, List
HashMapContainerHashMap, HashtableMap
OrderedMapContainerTreeMapMap, SortedMap
HashSetContainer, SetHashSetCollection, Set
OrderedSetContainer, SetTreeSetCollection, Set, SortedSet
PriorityQueueContainer  
QueueContainer  
StackContainerStackCollection, List

Table 6. JGL and Collections container implementations

Both frameworks' container implementations enhance their respective basic container interfaces with extra "convenience" methods. A useful discussion of these methods (and their cross-framework comparison) would unfortunately require far more time and effort than the scope of this article permits, therefore, exploring these methods is left to the enterprising reader. One important thing to note, though, is that unlike JGL, Collections doesn't add many of these convenience methods to its collection implementations because such methods necessarily lie outside the interface framework: a philosophical no-no for Collections! Even when Collections does add noninterface methods, the majority of these methods appear to be mere performance-tuning knobs, not truly "functional" methods.

Algorithms

The Collections Framework focuses almost exclusively on containers, the abstract data structures designed to hold objects. Yet computer science rarely looks at data structures without, in the same breath, also looking at associated algorithms. JGL acknowledges this fact by treating containers and algorithms on an equal footing. Collections doesn't, yet it does scatter a few algorithms in a number of places. Table 7 compares the two frameworks for ready-to-use algorithms.

JGL Class

JGL Algorithm

[Number of overloaded variants]

Closest Collections Equivalent
ApplyingApplying.forEach() [2] 
 Applying.inject() [2] 
ComparingComparing.equal() [2] 
 Comparing.lexicographicalCompare() [4] 
 Comparing.median() [1] 
 Comparing.mismatch() [4] 
CopyingCopying.copy() [3]Collections.copy() [1]
 Copying.copyBackward() [1] 
CountingCounting.count() [2] 
 Counting.countIf() [2] 
 Counting.accumulate() [4] 
 Counting.adjacentDifference() [6] 
FillingFill.fill() [2]Collections.fill() [1]
 Fill.fillN() [1]Collections.nCopies() [1]
FilteringFiltering.reject() [2] 
 Filtering.select() [2] 
 Filtering.unique() [4] 
 Filtering.uniqueCopy() [6] 
FindingFinding.find() [2] 
 Finding.findIf() [2] 
 Finding.adjacentFind() [4] 
 Finding.detect() [2] 
 Finding.every() [2] 
 Finding.some() [2] 
HashingHashing.orderedHash() [2] 
 Hashing.unorderedHash() [2] 
HeapHeap.makeHeap() [2] 
 Heap.popHeap() [2] 
 Heap.pushHeap() [2] 
 Heap.sortHeap() [2] 
MinMaxMinMax.maxElement() [4]Collections.max() [2]
 MinMax.minElement() [4]Collections.min() [2]
OrderedSetOperationsOrderedSetOperations.setDifference() [3]Collection.removeAll() [1]
 OrderedSetOperations.setIntersection() [3]Collection.retainAll() [1]
 OrderedSetOperations.setSymmetricDifference() [3] 
 OrderedSetOperations.setUnion() [3]Collection.addAll() [1]
 OrderedSetOperations.includes() [3]Collection.containsAll() [1]
PermutingPermuting.nextPermutation() [2] 
 Permuting.prevPermutation() [2] 
PrintingPrinting.print() [2] 
 Printing.prinln() [2] 
 Printing.toString() [2] 
RemovingRemoving.remove() [2]Collection.remove() [1]
 Removing.removeIf() [2] 
 Removing.removeCopy() [2] 
 Removing.removeCopyIf() [2] 
ReplacingReplacing.replace() [2] 
 Replacing.replaceIf() [2] 
 Replacing.replaceCopy() [3] 
 Replacing.replaceCopyIf() [3] 
ReversingReversing.reverse() [2]Collections.reverse() [1]
 Reversing.reverseCopy() [3] 
RotatingRotating.rotate() [1] 
 Rotating.rotateCopy() [1] 
SetOperationsSetOperations.setDifference() [3] 
 SetOperations.setIntersection() [3] 
 SetOperations.setSymmetricDifference() [3] 
 SetOperations.setUnion() [3] 
 SetOperations.includes() [3] 
ShufflingShuffling.randomShuffle() [4]Collections.shuffle() [1]
SortingSorting.sort() [4]Collections.sort() [2]
 Sorting.iterSort() [4] 
SwappingSwapping.swapRanges() [2] 
 Swapping.iterSwap() [1] 
TransformingTransforming.transform() [6] 
 Transforming.collect() [2] 

Table 7. Ready-to-use algorithm support in JGL and Collections

(In the Collections column above, note that Collection.x() stands for a method x() in interface Collection, whereas Collections.x() [note additional s] stands for a static method in class Collections.)

JGL's support for a wide range of algorithms is plain to see in Table 7. Collections provides a smaller, less-flexible set of ready-to-use algorithms -- mainly copying, filling, sorting, binary search, set operations, min/max, simple object removal, and collection shuffling. Surprisingly, JGL doesn't provide a binary search.

While browsing the source code for Collections Framework algorithms, I stumbled upon an artifact that clearly highlights (and confirms) the philosophical differences between JGL and Collections: The class java.util.Arrays contains seven, near-identical implementations of a sort algorithm: one implementation for each of the primitive types byte, char, short, int, long, float and double. While this code duplication is tucked away inside the Collections Framework, and hence doesn't affect the application programmer, it shows a programming approach that is diametrically opposite to JGL's generic programming style.

Other major framework differences

So far we've concentrated on obvious framework aspects that bear comparison: general framework architecture (underlying philosophy, packages, interface hierarchies and APIs), container implementations, and algorithm implementations. This effort has already highlighted many differences between ObjectSpace's JGL and Sun's Collections Framework. The remainder of the article will explore more differences resulting from aspects in each framework that lack obvious analogs in the framework's respective counterpart.

What? No function objects?

JGL's treatment of algorithms as library pillars naturally leads to a feature that is completely lacking in Collections: function objects (and predicate objects, which are simply function objects that embody a boolean function). Function objects are objects that you pass to an algorithm to modify and/or extend that algorithm's default behavior. In other words, function objects are the procedural-language equivalent of method pointers. My 18 months of experience with JGL lets me firmly conclude that function and predicate objects are the most powerful and flexible aspect of JGL, since they let you create and extend algorithms without having to descend into low-level, implementation-specific techniques such as (algorithm) subclassing or reuse-sabotaging algorithm inlining. To use function objects, you simply construct application-specific functionality as a function class, and tell one of JGL's standard, generic algorithms to use it. After a very short while, this programming style starts to feel like playing with Lego™ blocks, which is a Good Thing. (Both JGL examples shown later in this article use function and predicate objects. My November 1998 JavaWorld article, "Speeding up batch file processing," also illustrates this technique in a real-life project.)

How to add function objects to Collections

Of course, function objects are based on a few trivial JGL interfaces, similar to the one below:

interface Function {
   Object execute(Object object);
}

So there is no reason -- at first sight -- why you can't infuse your Collections-based code with similar flexibility.

But there's an important difference between adding function objects yourself and having them supported by the framework. The lack of direct support for function objects in Collections presents two unfortunate disadvantages:

  • Without explicit and manifest support for function objects, many developers (except JavaWorld readers!) will remain eternally oblivious to the significant benefits of the function-objects approach (read: plug-ins for standard algorithms).

  • Collections algorithms can't be customized beyond the mechanism afforded by the use of Comparator objects. Comparator objects aren't flexible enough to act as function objects.

The algorithms Collections does offer weren't designed to be customized using function object plug-ins. Furthermore, it's impossible for you, the application programmer, to retrofit the Collections algorithms to support such plug-ins. If you really want to use function objects and the Collections Framework together, you'll have to rewrite the algorithms from scratch -- exactly the thing that JGL's generic programming style strives to eliminate.

Synchronized versus unsynchronized access

From a multithreaded software viewpoint, JGL and Collections differ greatly. JGL containers use synchronized methods for their accessors and manipulators whereas Collections containers do not. This major difference exists in the name of performance (Collections), and in the name of programmer-friendliness (JGL). Leaving out the synchronized keywords on relevant Collections methods eliminates any object locking/unlocking overhead inherent as a result of synchronized code execution.

Luckily, Sun realizes as much as ObjectSpace that thread-safety is critical to any Java software, so the Collections Framework allows you to wrap a thread-safe wrapper around a thread-unsafe collection. This wrapping is done using utility methods in the Collections class. Table 8 lists these methods.

Collections utility method

Purpose

static Collection synchronizedCollection(Collection c)Returns a synchronized (thread-safe) Collection backed by the specified Collection.
static List synchronizedList(List list)Returns a synchronized (thread-safe) List backed by the specified List.
static Map synchronizedMap(Map m)Returns a synchronized (thread-safe) Map backed by the specified Map.
static Set synchronizedSet(Set s)Returns a synchronized (thread-safe) Set backed by the specified Set.
static SortedMap synchronizedSortedMap(SortedMap m)Returns a synchronized (thread-safe) SortedMap backed by the specified SortedMap.
static SortedSet synchronizedSortedSet(SortedSet s)Returns a synchronized (thread-safe) SortedSet backed by the specified SortedSet.

Table 8. Collections factory utility methods to transform thread-unsafe collections into thread-safe collections

Collections read-only wrappers

In a similar vein to providing synchronized-access wrapper objects, Collections also lets you obtain read-only wrappers for any of its containers. Table 9 lists the relevant factory methods of class Collections.

Collections utility method

Purpose

static List unmodifiableList(List list)Returns an unmodifiable view of the specified List.
static Map unmodifiableMap(Map m)Returns an unmodifiable view of the specified Map.
static Set unmodifiableSet(Set s)Returns an unmodifiable view of the specified Set.
static SortedMap unmodifiableSortedMap(SortedMap m)Returns an unmodifiable view of the specified SortedMap.
static SortedSet unmodifiableSortedSet(SortedSet s)Returns an unmodifiable view of the specified SortedSet.

Table 9. Collections factory utility methods to construct read-only views of specific collections

JGL doesn't include the concept of, or explicit support for, read-only containers.

Minor framework differences

By now we've seen the major framework differences between JGL and Collections -- differences that will cause headaches for those who must port from one framework to the other, and will even lead some to determine that migrating from one framework to another is simply too costly or problematic.

The next set of differences are, in my opinion, far less important than those discussed so far, but still warrant consideration.

Support for pre-Java 2 Vector and Hashtable

Before the advent of the Java 2 platform, Java had two main workhorse container classes: java.util's Vector and Hashtable. Both frameworks acknowledge these "legacy" bread-and-butter classes in different ways.

JGL has its adapter class VectorAdapter, which lets you use any Vector-based code and enhance it using JGL features. By wrapping a VectorAdapter around any Vector, your Vector objects essentially become JGL-compatible containers, allowing you to throw any JGL algorithm at them.

Collections, on the other hand, does something ObjectSpace's developers probably would have given their left arms to do, but never could: Collections directly enhances Vector to implement interface Collection! So in the Java 2 platform Collections universe, Vector doesn't require adaptation, it simply is a Collection (and a List).

(As an aside, note how flexible Java's class hierarchies are: with Java 2, Vector implements brand new interfaces, adds new methods, and yet is still 100 percent backward-compatible with JDK 1.1's Vector. Bjarne Stroustrup, try that in C++!)

Hashtable receives the same kind of "let's rewrite the rule book" treatment from Collections: it has now become a Map (but remember, Maps are not Collections).

Support for native arrays

Collections and JGL again differ in the way they provide extra support for native Java arrays (which of course never can descend from some type that implements an interface, and therefore never can be directly first-class containers in any Java container framework).

JGL features adapter classes that can turn native arrays (for example, main()'s one-dimensional "args" String array) into JGL-compatible containers. These adapter classes are listed in Table 10.

Fixed-length native array-to-container adapters

Resizing native array-to-container adapters

ArrayAdaptern/a
BooleanArrayBooleanBuffer
ByteArrayByteBuffer
CharArrayCharBuffer
DoubleArrayDoubleBuffer
FloatArrayFloatBuffer
IntArrayIntBuffer
LongArrayLongBuffer
ObjectArrayn/a
ShortArrayShortBuffer
VectorArrayn/a

Table 10. JGL native array adapter classes

Note that, with JGL, native arrays containing primitive values (boolean, char, byte, short, int, long, float, double) are intelligently adapted: values are wrapped or unwrapped into and/or from objects as required.

Collections doesn't support adaptation of arrays of primitive types, only arrays of Objects. It does this by providing a factory method (Arrays.asList(Object[])), which returns a List, thus further eliminating the need for a new class (such as JGL's ObjectArray).

Collections abstract classes

Collections encourages the creation of brand new implementations of its core collection interfaces by providing abstract "part-implemented" classes as halfway houses between the pure abstract of interfaces and the pure concrete of instantiatable classes.

Abstract collectionPurpose
AbstractCollectionThis class provides a skeletal implementation of the Collection interface, to minimize the effort required to implement this interface.
AbstractListThis class provides a skeletal implementation of the List interface to minimize the effort required to implement this interface backed by a "random access" data store (such as an array).
AbstractMapThis class provides a skeletal implementation of the Map interface, to minimize the effort required to implement this interface.
AbstractSequentialListThis class provides a skeletal implementation of the List interface to minimize the effort required to implement this interface backed by a "sequential access" data store (such as a linked list).
AbstractSetThis class provides a skeletal implementation of the Set interface to minimize the effort required to implement this interface.

Table 11. The Collections Framework's abstract collection classes

These abstract classes are yet another clue as to the philosophical differences between the two frameworks: Collections is primarily a framework of Java interfaces, with its collection implementations provided as one example implementation only. The provision in Collections (but not in JGL) of part-implemented abstract collections clearly suggests that application developers themselves are encouraged to implement more Collection classes. JGL's design shows no signs of similar thinking. Its documentation repeatedly states that JGL's implementations are highly optimized, implying that implementing your own improved classes (whether for container or algorithm) would be counterproductive (and most definitely going against the core generic programming philosophy of not reinventing wheels).

It's either in the core, or it isn't

There's one advantage Collections has over JGL which may dwarf any advantage, whether technologically superior or not, JGL has over Collections: Collections is a product of Sun, and has -- quite logically -- been given Core API status. While this means that anyone who uses a Java 2-compliant Java environment should have access to the entire Collections Framework, it also means -- and this is much more important in the long run -- that other standard (Core/Extension) APIs eventually will adopt the Collection "look" for APIs, wherever this makes sense.

Here are just a few examples of places in the existing 1.1 Core API where Collections should logically impose its interfaces in post-Java 2 releases:

  • AWT/Swing's Container class is a collection of GUI Components. It should become a Collection.

  • JDBC's ResultSet is collection of database records. It should become a Collection.

  • java.lang.ThreadGroup is a collection of threads. It should become a Collection.

Personally, I have my doubts that Sun will tinker with all these thoroughly established, bread-and-butter Java APIs in the ways suggested above. Developers dislike constantly changing APIs, instead preferring stability, regardless of the level of internal API inconsistency. How many Java developers do you know who didn't curse the AWT quakes between JDK 1.0.2 and JDK 1.1? I suspect Sun has learned an important lesson from its mid-course AWT corrections and will use the Collections interfaces for future APIs only, and leave existing ones as they are -- warts and all.

The frameworks in action: A few examples

Up to now, this article has hovered mere nanometers above both frameworks' APIs. In real life, though, people use tools based on how useful they are in helping them achieve real-life goals. I will now present some example (real-life) problems, along with a sample of solutions using JGL and Collections. I shall leave it entirely up to you, the reader, to pick which respective solutions you find more readable, elegant, efficient, modifiable, maintainable, and so on.

Example 1: Genetic mutations

The program requirements for this example are as follows: Given two strands of parental DNA, sexually combine them to form a child strand. Apply a user-defined amount of gene mutation to the combining process.

Here's a solution using the JGL framework:

//======================================================================== // GeneMutations (C) December 1998 JavaWorld - All Rights Reserved // ------------- // // History: // -------- // 28/09/98: started this file from new.java //========================================================================

import COM.objectspace.jgl.*; // for Array import COM.objectspace.jgl.algorithms.*; // for Sorting import COM.objectspace.jgl.functions.*; // for Print

import java.util.Random; // JDK 1.2 Random class !

public class GeneMutations {

// Some constants.. protected final static int STRAND_LENGTH = 20;

protected final static String[] DNA_BASES = {"A","C","G","T"};

/************************************************************************* * main() entry point *************************************************************************/ public static void main (String[] args) {

// read user-specified mutation rate (0%..100%) int mutationRate = Integer.parseInt(args[0]);

// create parent strands Sequence parentGeneSequenceA = randomGeneSequence(STRAND_LENGTH); Sequence parentGeneSequenceB = randomGeneSequence(STRAND_LENGTH);

System.out.println("Sequence A: " + parentGeneSequenceA); System.out.println("Sequence B: " + parentGeneSequenceB); System.out.println("");

// create child strand from parent strands Sequence childSequence = combineGeneSequences( parentGeneSequenceA, parentGeneSequenceB, mutationRate);

System.out.println("Child Seq : " + childSequence); }

/************************************************************************* * Generate a random gene sequence of a given length *************************************************************************/ static Sequence randomGeneSequence(int geneSequenceLength) {

DList sequence = new DList(); for (int i=0; i < geneSequenceLength; i++) { sequence.add( randomGene() ); }

return sequence; }

/************************************************************************* * Combine two "parent" gene sequences to produce a "child" sequence * As in nature, the combining can be affected by mutations (the virulence * of which is controlled by the mutationRate). *************************************************************************/ static Sequence combineGeneSequences(Sequence a, Sequence b, int mutationRate) {

DList newSequence = new DList( a.size() );

InputIterator aStart = a.start(); InputIterator aFinish = a.finish(); InputIterator bStart = b.start(); OutputIterator newStart = newSequence.start();

BinaryFunction combineGenes = new CombineGenes(mutationRate);

Transforming.transform(aStart, aFinish, bStart, newStart, combineGenes);

return newSequence; }

static String randomGene() { int baseNumber = (int)(Math.random()*4); return DNA_BASES[baseNumber]; }

} // End of Class GeneMutations

class CombineGenes implements BinaryFunction {

protected int mutationRate; protected Random randomizer;

/************************************************************************* * CombineGenes function constructor. * * @param mutationRate is a percentage value 0..100. * 0 Means no mutations * 100 Means mutations on every base (i.e. total destruction of genetic material) *************************************************************************/ CombineGenes(int mutationRate) { this.mutationRate = mutationRate;

randomizer = new Random(); }

public Object execute(Object baseA, Object baseB) {

boolean mutateBase = (randomizer.nextInt(100) < mutationRate);

if ( mutateBase ) { return GeneMutations.randomGene(); } else {

// evenly distribute genes from both parents (50-50) boolean fromOrganismA = randomizer.nextBoolean();

return fromOrganismA ? baseA : baseB; } }

} // End of JGL function CombineGenes

And here's a solution using the Collections Framework:

//======================================================================== // GeneMutations2 (C) December 1998 JavaWorld - All Rights Reserved // -------------- // // History: // -------- // 28/09/98: started this file from new.java // 05/10/98: Modified to use Java Collections instead of JGL //========================================================================

import java.util.*;

public class GeneMutations2 {

final static int STRAND_LENGTH = 20; final static String[] BASES = {"A","C","G","T"}; final static Random rnd = new Random();

public static void main (String[] args) { int mutationRate = Integer.parseInt(args[0]);

List parentGeneSequenceA = randomGeneSequence(STRAND_LENGTH); List parentGeneSequenceB = randomGeneSequence(STRAND_LENGTH);

System.out.println("Sequence A: " + parentGeneSequenceA); System.out.println("Sequence B: " + parentGeneSequenceB); System.out.println();

List childSequence = combineGeneSequences( parentGeneSequenceA, parentGeneSequenceB, mutationRate);

System.out.println("Child Seq : " + childSequence); }

/** * Generate a random gene sequence of a given length */ static List randomGeneSequence(int geneSequenceLength) { List sequence = new ArrayList(); for (int i=0; i < geneSequenceLength; i++) sequence.add(randomGene()); return sequence; }

static String randomGene() { int baseNumber = rnd.nextInt(4); return BASES[baseNumber]; }

/** * Combine two "parent" gene sequences to produce a "child" sequence. * As in nature, the combining can be affected by mutations (the virulence * of which is controlled by the mutationRate). * mutationRate is a percentage value 0..100. 0 Means no mutations. */ static List combineGeneSequences(List a, List b, int mutationRate) { List newSequence = new ArrayList(a.size()); for (Iterator ai = a.iterator(), bi = b.iterator(); ai.hasNext(); ) { newSequence.add(combine(ai.next(), bi.next(), mutationRate)); } return newSequence; }

static Object combine(Object baseA, Object baseB, int mutationRate){ return (rnd.nextInt(100) < mutationRate ? randomGene() : (rnd.nextBoolean() ? baseA : baseB)); } }

Example 2: Object-oriented database query

The program requirements for this example are as follows: Given an (in-core) object database containing Employee and Consultant objects, let an end-user query the database to find consultants who are paid more than a user-specified hourly rate.

Before reviewing the framework-dependent solution samples, take a look at the Person, PaidPerson, Employee, and Consultant classes that form the container framework-independent context for our problem.

class Person {

String title; // Mr., Miss, .. String firstName; String lastName; Date dateOfBirth;

Person(String lastName) { this.lastName = lastName; }

public String toString() { return lastName; } }

class PaidPerson extends Person {

double hourlyRate;

PaidPerson(String lastName, double hourlyRate) { super(lastName); this.hourlyRate = hourlyRate; }

public String toString() { return super.toString() + " " + hourlyRate + " $/hr."; }

}

class Employee extends PaidPerson {

String employeeID;

Employee(String lastName, double yearlySalary) { super(lastName, yearlySalary/365*8); } }

class Consultant extends PaidPerson {

Consultant(String lastName, double hourlyRate) { super(lastName, hourlyRate); }

}

Here's a solution to the database query problem using the JGL framework:

//======================================================================== // ExpensiveConsultants (C) December 1998 JavaWorld - All Rights Reserved // -------------------- // // History: // -------- // 28/09/98: started this file from new.java //========================================================================

import java.util.Date;

import COM.objectspace.jgl.*; // for Array import COM.objectspace.jgl.algorithms.*; // for Sorting import COM.objectspace.jgl.functions.*; // for Print import COM.objectspace.jgl.predicates.*; // for UnaryAnd

public class ExpensiveConsultants {

//------------------------------------------------------------------- // main() entry point //------------------------------------------------------------------- public static void main (String[] args) {

double expensiveRate = Double.valueOf(args[0]).doubleValue();

Container teamMembers = new Array();

teamMembers.add( new Employee("Black", 40000.0) ); teamMembers.add( new Employee("Flint", 52500.0) ); teamMembers.add( new Employee("Jones", 75000.0) ); teamMembers.add( new Employee("Smith", 238000.0) );

teamMembers.add( new Consultant("Golding", 120.0) ); teamMembers.add( new Consultant("King", 200.0) ); teamMembers.add( new Consultant("Jackson", 85.0) );

UnaryPredicate allConsultants = new UPIsInstanceOf(Consultant.class);

UnaryPredicate earningMoreThan = new PaidPersonEarningMoreThan(expensiveRate);

// create complex predicate by logically "anding" simple predicates UnaryPredicate allConsultantsEarningMoreThan = new UnaryAnd(allConsultants, earningMoreThan);

// locate consultants that are too expensive Container tooExpensiveConsultants = Filtering.select(teamMembers, allConsultantsEarningMoreThan);

// display found set Applying.forEach(tooExpensiveConsultants, new Print() );

} // End of main()

} // End of Class ExpensiveConsultants

class PaidPersonEarningMoreThan implements UnaryPredicate {

protected double hourlyRate;

PaidPersonEarningMoreThan(double hourlyRate) { this.hourlyRate = hourlyRate; }

public boolean execute(Object o) { PaidPerson paidPerson = (PaidPerson) o; return ( paidPerson.hourlyRate > this.hourlyRate ); } }

//======================================================================== // UPIsInstanceOf (C) Sep 1998 Laurence VanhelsuwÈ - All Rights Reserved // -------------- // Unary predicate that returns true if argument object is an instance of // some class (specified at predicate construction). // // History: // -------- // 28-Sep-1998: started this file from new.java // // Author email: classlib@telework.demon.co.uk //========================================================================

/*************************************************************************** * Unary predicate that returns true if argument object is an instance of * some class (specified at predicate construction). * * @version 1.0 28-Sep-1998 * @author Laurence VanhelsuwÈ (classlib@telework.demon.co.uk) **************************************************************************/ class UPIsInstanceOf implements UnaryPredicate {

protected Class instanceClass;

/************************************************************************* * UPIsInstanceOf constructor. * * @param instanceClass the Class for the instanceof test. *************************************************************************/ public UPIsInstanceOf(Class instanceClass) { this.instanceClass = instanceClass; }

public boolean execute(Object anObject) { return instanceClass.isInstance(anObject); }

} // End of Class UPIsInstanceOf

And here's a solution using the Collections Framework:

//======================================================================== // ExpensiveConsultants2 (C) December 1998 JavaWorld - All Rights Reserved // --------------------- // // History: // -------- // 28/09/98: started this file from new.java // 05/10/98: Modified to use Java Collections instead of JGL //========================================================================

import java.util.*;

public class ExpensiveConsultants2 {

public static void main (String[] args) {

double expensiveRate = Double.valueOf(args[0]).doubleValue();

Collection teamMembers = new ArrayList();

teamMembers.add( new Employee("Black", 40000.0) ); teamMembers.add( new Employee("Flint", 52500.0) ); teamMembers.add( new Employee("Jones", 75000.0) ); teamMembers.add( new Employee("Smith", 238000.0) );

teamMembers.add( new Consultant("Golding", 120.0) ); teamMembers.add( new Consultant("King", 200.0) ); teamMembers.add( new Consultant("Jackson", 85.0) );

Collection tooExpensiveConsultants = new ArrayList();

for (Iterator i=teamMembers.iterator(); i.hasNext(); ) { PaidPerson worker = (PaidPerson) i.next(); if (worker instanceof Consultant && worker.hourlyRate > expensiveRate) tooExpensiveConsultants.add(worker); }

System.out.println(tooExpensiveConsultants); } }

Conclusion

Comparing ObjectSpace Inc.'s JGL and Sun's Collections Framework turns out to be like comparing apples and kiwi fruits. At first sight, the two frameworks seem to be competing for the same developers, but after a closer inspection it is clear that the two cannot be compared fairly without acknowledging first that the two frameworks have different goals.

If, like Sun's documentation states, Collections is going to homogenize Sun's own APIs (core API, extensions, etc.), then clearly Collections has to be great news, and a good thing, even to the most fanatic JGL addict. Provided Sun doesn't break its promise in this area, I'll be happy to invest my resources in adopting Collections in earnest.

At the same time, JGL's balanced "algorithms plus data structures" agenda results in it being more powerful when it comes to applying algorithms to your containers. As long as Collections marginalizes the algorithmic side of the coin, I believe JGL still has a long life ahead of it.

Laurence Vanhelsuwé is an independent software engineer. Self-taught, he dropped out of college and immediately started a professional career writing arcade games. He has worked on such diverse technologies as X.25 WAN routers, virtual reality flight simulation, Postscript, and real-time digitized video-based traffic analysis.

Learn more about this topic

  • ObjectSpace's JGL home page http://www.objectspace.com/developers/jgl/
  • Sun's Collections Framework home page http://java.sun.com/products/jdk/1.2/docs/guide/collections/index.html
  • See the JavaWorld article on JGL "Need a good set of abstract data structures? ObjectSpace's JGL packs a punch!" http://www.javaworld.com/javaworld/jw-06-1997/jw-06-jgl.html
  • See the JavaWorld article "Get started with the Java Collections Framework" http://www.javaworld.com/javaworld/jw-11-1998/jw-11-collections.html
  • See the JavaWorld article "Speed up batch file processing using generic programming and core reflection" http://www.javaworld.com/javaworld/jw-11-1998/jw-11-batch.html
  • The Java Tutorial section on Sun's Collection Framework is available at http://java.sun.com/docs/books/tutorial/collections/index.html
  • For an interview with Alexander Stepanov, see http://www.bml.ca/marine/stepanov.htm
  • David R. Musser provides a page on generic programming (Musser worked with Stepanov) http://www.cs.rpi.edu/~musser/gp/
  • Matthew Austern and Alex Stepanov did some generic programming work for JavaJAL (Java Algorithm Library) http://reality.sgi.com/austern/java/index.html
Join the discussion
Be the first to comment on this article. Our Commenting Policies
See more