<< back to articles

Best Practices for the Java Collections API

In this article, we provide an overview/review of the Java Collections API and how it evolved, concluding with a listing of best practices for using this API in your applications.

The Collections API became a part of Java back when JDK 1.2 came on the scene, but many developers are still not familiar with it. Many of us are more than happy to continue using arrays and legacy collection classes like Hashtable and Vector that we are familiar with. The philosophy of "if it ain't broke, don't fix it" seems to hold.

But the Collections API offers a structured paradigm for manipulating groups of objects in an ordered fashion, and employs design patterns (e.g., the iterator pattern) that make common programming tasks much simpler. With this in mind, let's review how the Collections API came to be, and offer best practices for its usage in Java applications.

If you are not familiar with the Collections API, or with the notion of collections as a programming construct, then you should definitely read the overview that follows. If you understand the fundamentals of Java Collections, know the difference between a Set, a List, and a Map, and have used these constructs in your own development work, then feel free to skip ahead to the Best Practices section of this article. But... it might still be worth your while to look through the overview anyway to gain some additional insight into the purpose behind the Collections API.

Why a Collections API?

When we refer to "collections," we are talking about mathematical formalizations that define groups of entities based on their characteristics. These characteristics, such as the presence or absence of a discrete ordering of the group's members, and the allowance or prohibition of duplicates within the group, evince themselves in real world collections. For example, in the set of all people who have a car, your purchase of a second car does not make you a member of this set "twice". However, in the list of people waiting for an appointment to get your car fixed, you should appear in the list twice if you need two separate appointments. Thus, the type of group you define should depend on how you intend to use that group and enumerate its members.

In mathematics, there are specific names for these various types of collections. For instance, as we intimated in the previous paragraph, a set is a collection of elements that cannot contain any duplicates. Adding an element to a set that is already a member of that set (e.g., a person who already has a car to the set of people who own cars) does not add that element a second time. That person does not appear twice in the set, only once, and when the number of members in this set is counted, that person is only counted once. A set, by definition, has no inherent order. What matters when defining a set is not an element's "position" within the set, but simply its presence or absence.

On the other hand, a list is an ordered collection of elements that may contain duplicates. Note the two fundamental differences between a set and a list: a list can contain duplicates, and unlike a set, the order in which an element is added does matter. So, if you are planning to have each of your two cars serviced by the dealer (or even if you need to take the same car in twice), you will appear twice on the service department's list of appointments.

There is another type of group that does not strictly qualify as a "collection" in the purest mathematical sense, but is nonetheless included in the Java Collections API, namely the map. As the name implies, it is a mapping between a set of keys (literally a set—no duplicates allowed) and the values associated with those keys. Thus, it is analogously in mathematical terms to a function—a particular argument (the key) provided to a function yields a single consistent result, namely the value associated with the key. One example of a map would be the use of a unique identifying code, like a part number, to identify one and only one actual part. A company may manufacture many different parts that are called "voomsquoll anodizer nanodes," but part number VSAN-1258 is a "key" that refers to one particular voomsquoll anodizer nanode. The relationship between part numbers and the parts themselves is a mapping.

Historical Perspective

Before we go into detail about how these mathematical formalisms are represented in the Java Collections API, let's examine how "legacy" collection classes found in earlier versions of Java tried to provide similar functionality, and where they fall short.

Vector

The Vector class was a more flexible extension of the notion of arrays common in virtually all programming languages. Before the existence of a Vector class, programmers used arrays to hold groups of similar objects. You could access and replace elements of an array by their position (i.e., via an array subscript), but you could not add (or remove) elements from the array without going through various conniptions to do so (usually involving deleting and reallocating the array with a greater or smaller size).

The Vector class allowed dynamic addition and removal of members (without going through the aforementioned conniptions), which was a big improvement over arrays. What was lost was type safety: any type of object could be added to a Vector, and there was no requirement that the elements of a Vector all be of the same type. It became the responsibility of the programmer to cast an accessed element of the Vector into its appropriate type.

int initialSize = 10 ;
Vector v = new Vector(initialSize) ;
String blahblah = "blah blah blah" ;
String question = "What was that?" ;
String answer   = "It was me!" ;

v.add(blahblah) ;
v.add(question) ;
v.insertElementAt(answer, 0) ;
v.setElementAt("not blah blah anymore", 1) ;

String firstElement = (String) v.elementAt(0) ;

Enumeration

Even with all these changes, the process of traversing the members of a Vector still required the same coding structure as for arrays—a for-loop that walks through the element subscripts, ranging from zero to the size minus one, to access each element by its subscript.

for (int i = 0; i < v.size(); i++) {
    String elementValue = (String) v.elementAt(i) ;
    System.out.println("Element " + i + " = " + elementValue) ;
}

Enter the Enumeration interface, which encapsulated the process of walking through a group of entities in order according to the well-known Iterator pattern. The elements() method in both the Vector and Hashtable classes returns an Enumeration of the elements they contain.This meant that to traverse the elements of a Vector you could do the following:

Enumeration e = v.elements() ;

while (e.hasMoreElements()) {
    String elementValue = (String) e.nextElement() ;
    System.out.println("Element " + i + " = " + elementValue) ;
}

This example demonstrates some of the efficiencies associated with using Enumerations. Note that there is no need for a "loop variable" to keep track of position within the array or Vector; all of the loop control occurs through the use of the hasMoreElements() and nextElement() methods. Ultimately, though, Enumerations do only half the job, especially when it is Enumerations that are returned by methods like getHeaderNames() in the Servlet API. If a "get" method returns an Enumeration over some collection of objects, all you could do with it is (as the name implies) to enumerate through it. You could not easily test for presence or absence of an element in the Enumeration without walking through it element by element.

Ideally, what should be returned from a method like getHeaderNames() is an object that provides methods that test for presence or absence of a particular element and that return an Iterator to walk through the members if desired. In other words, a Collection object with a well-defined set of semantics for accessing and manipulating elements.

Hashtable

Mapping functionality prior to the advent of the Java Collections API was provided by the Hashtable class. It included methods for adding or replacing a value in the Hashtable and associating it with a key (put(Object key, Object value)), for retrieving the value associated with a provided key (get(Object key)), for determining whether the Hashtable contains a particular key or value (contains(Object value) and containsKey(Object value)), and for asking how many elements the Hashtable contains (size()). Pretty much all the Java classes that were used for mapping were descendants of the Hashtable class, including the Properties class. (Hashtable extended the abstract Dictionary class.)

Hashtable ht = new Hashtable() ;
String name = "..." ;
CustomClass cc = new CustomClass(...) ;
ht.put(name, cc) ;

...

CustomClass cc2 = (CustomClass) ht.get(name) ;
if (ht.containsKey(name)) {
    ...
}

Overview of the Collections API

The goals of the Java Collections API (paraphrased from the original overview document) are:

  1. to support "a unified architecture for representing and manipulating collections, allowing them to be manipulated independently of the details of their representation,"
  2. to provide "useful data structures and algorithms so you don't have to write them yourself,"
  3. to encourage interoperability by "establishing a common language to pass collections back and forth,"
  4. to base this framework on interfaces, fostering extensibility and enabling the creation of custom implementations of the various collection interfaces that are compliant with the API, while "keeping the number of core interfaces small,"
  5. to include abstract skeletal implementations of these interfaces that implement as many operations as possible in terms of other operations, minimizing the amount of effort required to build custom collection classes,
  6. to provide mechanisms for bulk operations that add the contents of one collection to another, remove the contents of one collection from another, and compare the contents of one collection to another (e.g., to perform set difference operations),
  7. to support the notion of unmodifiable collections by making operations like add and remove "optional" (i.e., collection classes that are intended to be unmodifiable throw an UnsupportedOperationException if a modification method is invoked), and
  8. to continue support for legacy classes like Vector and Hashtable, by having their enhanced implementations implement the List and Map interfaces, respectively, while retaining their existing APIs.

The Collections API has at its root a Collection interface, which the Set and List interfaces extend. The Collection interface includes methods for addition and removal of elements, as expected. It also includes a contains(Object obj) method, which returns a boolean value depending on whether the Collection contains the referenced object instance or not, obviating programmers of the need to walk through a collection to determine if it contains a particular element. It also contains many other methods to support more elaborate operations and tests, including a clear() method, a size() method, and an isEmpty() method.

By definition, every Collection must include an iterator() method that returns an object that implements the Iterator interface, allowing traversal over the Collection's elements. The Iterator interface's hasNext() and next() methods correspond closely to the hasMoreElements() and nextElement() methods of the Enumeration interface. However, Iterators differ from Enumerations in that they also have a remove() method that deletes the current element being traversed. As with the Collections themselves, the remove() method in an Iterator can throw an UnsupportedOperationException if the underlying Collection is unmodifiable.

Back and Forth
Lists, in addition to an Iterator, can also produce a ListIterator, which provides bidirectional traversal through a List's elements through the previous() and hasPrevious() methods, as well as providing information about position within the List via the nextIndex() and previousIndex() methods.

The Map interface, while not strictly a "collection" in the mathematical sense, plays well with the other members of the API by providing collection views of a Map's keys and values via the keySet() and values() methods. In fact, Maps are implemented as a Set of instances of the Map.Entry inner class, and a "set view" of the Map is available via the entrySet method, thus making the Map interface, strictly speaking, a Collection (actually a Set) under the hood. Naturally, since these collection views are Collection objects, their elements can be traversed by producing an Iterator through the iterator() method, e.g.:

Iterator i = myMap.keySet().iterator();

The following table summarizes what each of the various interfaces in the Collections API does, and delineates their associated implementations. This is only a summary, and the Java API documentation should be examined for more complete information.

Interface
and Methods
Implementations Legacy
Implementations
Collection
add(Object obj)
addAll(Collection coll)
remove(Object obj)
removeAll(Collection coll)
retainAll(Collection coll)
contains(Object obj)
containsAll(Collection coll)
clear()
iterator()
size()
isEmpty()
toArray()
toArray(Object[] array)
AbstractCollection  
Set
same as Collection
AbstractSet
HashSet
 
SortedSet
same as Set plus…
comparator()
first()
last()
headSet(Object toElement)
tailSet(Object fromElement)
subSet(Object fromElement,
       Object toElement)
remove(int index)
TreeSet  
List
same as Collection plus…
add(int index, Object obj)
addAll(int position,
       Collection coll)
get(int position)
set(int position, Object obj)
indexOf(Object obj)
lastIndexOf(Object obj)
listIterator()
listIterator(int startPos)
AbstractSequentialList
ArrayList
Vector
Stack
Map
put(Object key, Object value)
putAll(Map map)
get(Object key)
remove(Object key)
containsKey(Object obj)
containsValue(Object obj)
clear()
keySet()
values()
entrySet()
size()
isEmpty()
AbstractMap
HashMap
Dictionary
Hashtable
SortedMap
same as Map plus…
comparator()
firstKey()
lastKey()
headMap(Object toKey)
tailMap(Object fromKey)
subMap(Object fromKey,
       Object toKey)
TreeMap  
Iterator
hasNext()
next()
remove()
  Enumeration*
ListIterator
same as Iterator plus…
hasPrevious()
previous()
previousIndex()
nextIndex()
add(Object obj)
set(Object obj)
   
Reminder
Remember that methods associated with modification of a collection (e.g., add(Object obj)) are considered "optional" in a Collection interface implementation—modification operations may be written to throw an UnsupportedOperationException.

Best Practices

Now that we've covered the fundamentals of the Java Collections API, let's examine the best practices for making use of it.

Arrays vs. Collections

  • Arrays are by far the fastest but least flexible implementation of a "collection." Use an array only if you must access the elements via a static array index, and only if you expect the array order and content to be immutable.
  • If you want a collection that contains a more generalized group of items with well-known characteristics (e.g., order unimportant and duplicates not allowed), use one of the Collection classes. The use of the add(Object obj) and remove(Object obj) methods for addition and removal of members, and the contains(Object obj) method to test for existence of members, in and of themselves, will be an enormous savings over traversing through arrays manually to search for members.

Using Sets

  • Use a Set when you have a "set" of objects that will not contain duplicates, where you're concerned more about presence/absence of a particular element value than order. In a HashSet, you can add and remove elements at will. Adding an element value that's already there does not change the set. The Set's Iterator will return the element values, but not in any guaranteed order.
    Set group = new HashSet() ;
    
    group.add(someValue) ;
    group.add(anotherValue) ;
    
    if (group.contains(anotherValue)) {
        // DO WONDROUS THINGS HERE
    }
    
    Iterator i = group.iterator() ;
    while (i.hasNext()) {
        // Casting always necessary
        Class object = (Class) i.next() ;
        System.out.println(object.toString()) ;
    }
    
  • Use a SortedSet if you want the Set's Iterator to return the element values in a natural sorted order (e.g., alphabetical, numeric). The add(Object obj) method will ensure that the element is put in the "right" place. Note that there is added cost in creating and using a SortedSets, because the addition and removal operations are obviously more complex. The primary implementation of the SortedSet interface is the TreeSet.
    Set orderedGroup = new TreeSet() ;
    
    orderedGroup.add("b") ;
    orderedGroup.add("a") ;
    orderedGroup.add("c") ;
    
    Iterator i = orderedGroup.iterator() ;
    while (i.hasNext()) {
        Class object = (Class) i.next() ;
        System.out.println(object.toString()) ;
    }
    
    // Result = "a", "b", "c"
    
  • You always have the option of taking a non-sorted Set and constructing a TreeSet from it. The Iterator from the resulting object will provide elements in natural sorted order. Remember, though, that there is a cost associated with creating a new object on the fly for this purpose. If you require that members always be iterated in a sorted order (not necessarily the order in which they were entered—use a List for that!), construct a TreeSet from the beginning. (The same principle holds true for Maps—there is a SortedMap interface and a corresponding TreeMap implementation that returns the Map's keys in a sorted order.)
    Iterator i = (new TreeSet(myHashSet)).iterator() ;
    

Using Lists

  • Use a List when the order in which an element is added to the collection matters…
    List myList = new ArrayList() ;
    myList.add("x") ;
    myList.add("17b") ;
    myList.add("aaa") ;
    
    … and where you want the ability to access, add, remove and replace elements from particular positions in the list at will.
    myList.add(0, "goodbye") ; // inserts "goodbye" before "x"
    myList.add(0, "hello") ;   // inserts "hello" before "goodbye"
    myList.remove(4) ;         // then deletes "aaa"
    myList.set(3, "23j") ;     // and replaces "17b" with "23j"
    
    The Iterator associated with a List returns its items in the order that they were entered (taking into account additions/removals/replacements).
    Iterator i = myList.iterator() ;
    while (i.hasNext()) {
        String s = (String) i.next() ;
        System.out.println(s) ;
    }
    System.out.println((String) s.get(2)) ;
    
    // Result = "hello", "goodbye", "x", "23j", then "x"
    
    Its ListIterator (accessible via the List.listIterator() method) supplies not only the standard hasNext() and next() methods associated with an Iterator, but also hasPrevious() and previous() methods if you want to go back and forth at will.

    The subList(from, to) method returns a List consisting a subset of the original List.

    You can convert a List into an array using the toArray() method, which returns an Object[]. You can also convert an array to a List using the Arrays.asList(Object[]) method.

    If there is a good deal of insertion and deletion, use a LinkedList.

Using Maps

  • If you want a lookup or mapping, use a Map class rather than the deprecated Hashtable. Add key-value mappings using the put(key, value) method.
    Map myMap = new HashMap() ;
    myMap.put("id", new Integer(12345)) ;
    myMap.put("name", "John Doe") ;
    

    To traverse through the mappings, first get the keys using the keySet() method, which returns the keys as a Set. Once again, if you have a Map that does not employ natural ordering and you desire to go through the keys in natural sorted order, you can wrap the keys in a TreeSet.

    Map myMap = new HashMap() ;
    
    Set keys = new TreeSet(myMap.keySet()) ;
    Iterator i = keys.iterator() ;
    while (i.hasNext()) {
        Class key = (Class) i.next() ;
        Class value = (Class) myMap.get(key) ;
            …
    }
    

Using Wrapper Classes

  • Only objects can be used as members in Collections. This means that Java primitives like int, long, and char can't be used as members in Collections, since strictly speaking they are not Java objects. Thus, in order to include a primitive in a Collection, it must be "wrapped" in its corresponding wrapper class.
    List myList = new Vector(12) ;
    int myNumber = 17 ;
    myList.add(new Integer(myNumber)) ;
    
    Map myMap = new HashMap() ;
    int id = 123456 ;
    String name = "Somebody's Name" ;
    myMap.put(new Integer(id), name) ;
        ...
    String retrievedName = (String) myMap.get(new Integer(123456)) ;
    

Bulk Methods for Addition, Removal, and Testing

  • Use the xxxxxAll(Collection c) methods to perform sophisticated collection operations, e.g., checking if all required elements are present, set difference operations, etc.
    List availableOffers = … ;
    Set seenOffers = … ;
    
    // Delete all offers that have been seen from available offer list
    availableOffers.removeAll(seenOffers) ;
    
    Set myChosenItems = … ;
    Set requiredItems = … ;
    // Do something if not all required items were chosen
    if (! myChosenItems.containsAll(requiredItems)) {
        …
    }
    
    mySet.addAll(someOtherSet) ;
    

Comparing and Sorting with Comparables and Comparators

  • There are two ways to provide custom sorting of elements in a Collection:
    1. create your own custom class, implementing the Comparable interface (and its required compareTo(Object obj) method), as well as overriding the equals(Object obj) and hashcode() methods, or
    2. create your own Comparator class that performs a custom comparison between two objects.
  • By default, sorted collections (e.g., SortedSet) use the compareTo(Object obj) method of each object in the collection to determine ordering (i.e., where each object goes in the sequence). This method essentially compares the value of this object with that of another object. It returns:
    • a negative number if this object is less than the other object,
    • zero if the two objects are equal, or
    • a positive number if this object is less than the other object.
  • For this reason, all classes of objects added to sorted Collection objects (or used as keys or values in Maps) must implement the Comparable interface, which essentially requires that this compareTo method be implemented. This interface is implemented by the classes most commonly used in Collections, namely the String class and the various wrapper classes used to enclose Java primitives (e.g., Integer for int).
  • Any custom classes of your own that you intend to use as members of Collections should also implement this interface and provide an implementation of the compareTo method. In addition, you should override your class's equals(Object o) and hashcode() methods to ensure proper behavior of these objects within Collections. Both equals(Object o) and hashcode() have "default" implementations in the java.lang.Object class (from which all other classes are descended), but these implementations are too generalized for use in custom classes, especially if those classes are to be used as members of Collections.
    As per Java language recommendations, the compareTo method should be "consistent with equals", meaning that for any comparand (other object) that causes an object's compare(Object o) method returns zero, the equals(Object o) method should return true, and vice versa.

    The hashcode() method is used to facilitate storage and lookup of objects stored as members of a collection, and thus this method should always return the same value for two objects considered to be "equal" by the equals(Object o) method. You should explicitly implement this method in your custom classes, especially custom Collection classes.
  • If you want a special kind of sorted ordering (e.g., case insensitive), you can build a custom Comparator class and construct a TreeSet using an instance of that Comparator class as an argument to the constructor.
    public class MyCaseInsensitiveComparator implements Comparator {
        public int compare(Object o1, Object o2)
        throws ClassCastException {
            String s1 = ((String) o1).toLowerCase() ;
            String s2 = ((String) o2).toLowerCase() ;
            return s1.compareTo(s2) ;
        }
    }
    
    Set strings = new TreeSet(new MyCaseInsensitiveComparator()) ;
    strings.add("acaa") ;
    strings.add("aBAa") ;
    strings.add("aaaa") ;
    Iterator i = strings.iterator() ;
    while (i.hasNext()) {
        String s = (String) i.next() ;
        System.out.println(s) ;
    }
    
    // Result = "aaaa", "aBAa", "acaa"
    
    Note that for this particular application there is no need for a custom Comparator: the String class contains a case-insensitive Comparator available as a static constant, String.CASE_INSENSITIVE_ORDER. Thus, the constructor in the example above can be replaced with the following:
    Set strings = new TreeSet(String.CASE_INSENSITIVE_ORDER) ;
    
  • Finally, remember that for all Collection classes, members inserted into a collection must be objects, not primitives. This means you must enclose integers, booleans, etc. in their respective wrapper objects before adding them to a collection.
    Set specialNumbers = new HashSet() ;
    specialNumbers.add(new Integer(23)) ;
    specialNumbers.add(new Integer(42)) ;
    
    if (specialNumbers.contains(new Integer(42)) {
        ...
    }
    
    Because the wrapper classes have equals(Object o) methods (and compareTo(object o) methods) that return true (or zero) for another object containing the same primitive value, the test above will return true. It is a very good idea to ensure that any custom objects you intend to add to collections (as members, values, or keys) should likewise implement the Comparable interface, and override the compareTo(Object o) method for your class to ensure proper behavior of these objects within collections.

Using the Static Utility Methods of the Collections Class

  • The Collections class is a utility class that contains a number of static methods for wrapping Collection objects to make them synchronized or immutable.
  • Make any collection object immutable by enclosing it in an unmodifiable wrapper. This essentially wraps the object in another object where the modification methods such as add() and remove() have been overridden so that they throw an UnsupportedOperationException.
    Set immutableSet = Collections.unmodifiableSet(mySet) ;
    
  • Most Collection classes are by default not synchronized. You can make any Collection object synchronized by enclosing it in an synchronized wrapper. This essentially wraps the object in another object where methods have been overridden so that they are all synchronized. This is useful when seeking a synchronized Map class that behaves like a Hashtable (which is synchronized while classes like HashMap are not).
    Map threadSafeMap = Collections.synchronizedMap(myMap) ;
    

Writing Your Own Custom Classes

  • If you want to write your own collection classes, there are two approaches:
    1. Extend the abstract collection classes (AbstractSet, AbstractList, AbstractMap), since they have implemented most of the tricky interrelated methods already. Each of these classes leaves only a few specific methods to be implemented in the descendent concrete class.
      public class MyPowerfulSet extends AbstractSet implements Set {
          …
      }
      
    2. Write a wrapper class by including a collection object as a member variable in your class, then proxying the implementation of all methods required by the collection interface you are using to operate on the underlying member collection object, overriding only when necessary to implement specific behavior.
      public class MyPowerfulList implements List {
      
          private List m_innerList ;
      
          public MyPowerfulList(List p_list) {
              m_innerList = p_list ;
          }
      
          public boolean add(Object o) {
              return innerList.add(Object o) ;
          }
          
          ...
          
      }
      

Using Collection Objects as Member Variables in JavaBeans

  • When a member variable in a JavaBean class should be a container for a group of items, a Collection class is often a better choice than an array.
  • There's usually no need to provide setter methods for member variables that are Collections. Instead, define a member variable using one of the interfaces associated with the Collections API (Set, List, or Map), then initialize it to a new concrete Collection object. You shouldn't ever have to set the member variable itself to a new object instance, but elements in this Collection can be added, removed, and modified.
  • Furthermore, methods can be defined at the class level to add, remove, and modify elements atomically. It's more secure and efficient to create explicit methods at the class level that perform these functions than to expose the Collection object itself and require developers to manipulate these objects themselves.
    public class CheeseInformationService {
    	private Set m_favoriteCheeses = new TreeSet() ;
    	private Map m_cheeseSharpnessMap = new HashMap() ;
    	
    	public void addFavoriteCheese(String cheeseName) {
    	    m_favoriteCheeses.add(cheeseName) ;
    	}
    	
    	public void removeFavoriteCheese(String cheeseName) {
    	    m_favoriteCheeses.remove(cheeseName) ;
    	}
    	
    	public void setCheeseSharpness(String cheeseName, int sharpness) {
    	    m_cheeseSharpnessMap.put(cheeseName, new Integer(sharpness)) ;
    	}
    	
    	public int getCheeseSharpness(String cheeseName) {
    	    return ((Integer) m_cheeseSharpnessMap.get(cheeseName).intValue() ;
    	}
    
    	// BAD - Because it exposes the object to external manipulation
    	public Map getCheeseSharpnessMap { ... }
    
    	// BAD - Replaces the member collection with a new collection object
    	public void setFavoriteCheeses(Set p_set) { ... }
    
    	// BETTER - Use xxxxAll methods to control bulk additions/removals
    	public void addFavoriteCheeses(Set anotherCheeseSet) {
    	    m_favoriteCheeses.addAll(anotherCheeseSet) ;
    	}
    
    }
    
    CheeseInformationService cis = ... ;
    
    int sharp = cis.getCheeseSharpness("gouda") ;
    cis.setCheeseSharpness("cheddar", 80) ;
    
    
    Encapsulation
    Remember that it is better to encapsulate this kind of functionality in a discrete method than to expose the underlying object to programmers and rely on them to perform the manipulations appropriately. Say, for instance, that the specification for setting cheese sharpness changes, requiring that some other operation be performed in addition to setting the value for an entry in the Map—e.g., perhaps a boolean flag needs to be set. You must now search your application's code base for all situations where cheese sharpness was set manually, and modify the code accordingly. You must also ensure that, in the future, programmers remember to set that boolean flag manipulating the underlying Map directly.
    cis.getCheeseSharpnessMap.put("jalapeño pepper jack", 150) ;
    boolean m_hasVerySharpCheese = true ;
    Hiding the Map from the peering eyes of programmers (by making it a private member variable in the class) gives you more control over how modifications to its content occur. The only way programmers can modify its contents is through discretely defined methods. This strategy also allows these methods to become more complex (e.g., setting that boolean flag in addition to modifying Map entries) without impacting other code that performs these modifications (i.e., programmers will not need to remember to also perform other tasks when performing these modifications if specifications change—they continue to make one discrete method call).
    public void setCheeseSharpness(String cheeseName, int sharpness) {
        m_cheeseSharpnessMap.put(cheeseName, new Integer(sharpness)) ;
        if (sharpness > 100) {
        	m_hasVerySharpCheese = true ;
        }
    }
    
    ...
    
    cis.setCheeseSharpness("jalapeño pepper jack", 150) ;
    

Other Tips and Tricks

  • It is good programming practice to use the highest level abstraction for object specification whereever possible. In other words, variables should be defined at the highest level of abstraction possible, as well as argument types and return types in method specifications.
    public List getMyStuff(Map m) { 
        List myList = new Vector() ;
        myList.addAll(m.values()) ;
        return(myList.subList(1, myList.size() - 2) ;
    }
    HashMap hm = new HashMap() ;
        ...
    List stuff = getMyStuff(hm) ;
    
  • Normally, inserting an item into a Map requires two arguments: the key and the value. By defining one of the object's attributes as an "ID" (like a primary key in a database table), you can write methods that add it to a Map with one argument. Remember that if the object's id is a primitive like an int, you must convert it to a wrapper object in order to use it as a key in a Map.
    private Map m_entityMap = new HashMap() ;
    
    public void addEntityToMap(Entity entity) {
        int id = entity.getId() ;
        m_entityMap.put(new Integer(id), entity) ;
    }
    
    public void getEntityFromMap(int id) {
        return m_entityMap.get(new Integer(id)) ;
    }
    
  • Collections can, of course, be populated from external sources such as databases. You can use a form of lazy initialization to get the required mapping from the database when the key-value mapping is explicitly requested for the first time, then save it in the Map for subsequent retrievals. In this case the getter is in essence a setter, too (since it "sets" the value when you try to "get" it), but you cannot set a value "manually". (Obviously this is a read-only implementation of this idea; there could be setter functions that modify the contents of this Map and persist them to the database when necessary.
    private Map m_entityMap = new HashMap() ;
    
    public void getEntity(int id) {
        Integer idInteger = new Integer(id) ;
        Entity e ;
        if (! m_entityMap.contains(idInteger)) {
            e = Dao.getEntity(id) ;
            m_entityMap.put(idInteger, e) ;
        }
        else {
            e = m_entityMap.get(idInteger) ;
        return e ;
    }
    

Why Upgrade from Legacy Collection Classes?

The so-called legacy classes that have been retrofitted to conform to the Collections API, namely Vector and Hashtable, will continue to work for the forseeable future. But there are a number of good reasons to make the jump to the newer classes and interfaces. Your applications will be more flexible if you code your methods to use interfaces rather than implementations, refering to concrete implementations only when initializing variables (e.g., "List myData = new Vector(200)").

If your methods currently specify concrete implementations as arguments and return values (e.g., public Vector getMyStuff(Hashtable ht)), these methods are limited to using just those implementations, but if you specify more abstract types (e.g., interfaces) for arguments and return values, other applications can interact more readily with your code. If you rewrite the example method above as public List getMyStuff(Map m), your horizons are broadened: you can pass any Map object as an argument to this method, and any method requiring a List can accept the return value from this method as an argument.

We hope this article has given you some new insights into the workings of the Java Collections API, and has provided some guidance in how to use it.