Monday, March 16, 2009

The Java Collections Class

One of my favorite standard Java classes is the Collections class. This is not surprising considering how often I find myself using the Java Collections Framework. Each Java Collection interface and implementation is useful in its own right, but the Collections class provides some convenience methods that are highly useful in working with Java collections.

The Javadoc API documentation for java.util.Collections explains the basics of this class such as the fact that all of its methods are static and that they all either operate on a provided collection or return a collection (here I am using "collection" more broadly to include Map as opposed to narrowly focusing on collections implementing the Collection interface). There are so many highly useful methods in this class that I am going to only focus on a subset of them to keep what is already a lengthy blog posting from becoming too large.

Empty Collections

Several of the methods provided by java.util.Collections perform similar functionality on different types of collections. For example, the methods Collections.emptySet(), Collections.emptyMap(), and Collections.emptyList() perform the same functionality, but on Sets, Maps, and Lists respectively. In the case of these methods, they each return the appropriate collection type that is empty (no elements in it), typesafe, and immutable. In other words, the provided collection is empty and nothing can be added to it. As I have blogged about previously, this is useful for implementing the recommendation of Effective Java to return empty collections rather than null.

The following sample code shows how one of these "empty" methods can be used and the image below the code demonstrates the UnsupportedOperationException that is thrown when the code execution tries to add an element to this empty collection that is immutable. For this particular example, I am using Collections.emptySet(), but the principle is the same for the List and Map versions.

demonstrateEmptySet()


/**
* Provide an empty set.
*/
public void demonstrateEmptySet()
{
log("===== DEMONSTRATING EMPTY SET =====", System.out);
final Set<String> emptySet = Collections.emptySet();
log("Size of returned emptySet(): " + emptySet.size(), System.out);
log("----- Adding String to Collections.emptySet() returned Set -----", System.out);
emptySet.add("A new String to add.");
}


Results of Running demonstrateEmptySet()




Single-Element Collections

Another functionality provided by Collections for Set, List, and Map is providing of a single-element collection that, like its empty element sibling, is immutable and typesafe. To illustrate this, the next code sample and output screen snapshot will demonstrate use of Collections.singletonList(T), though the same principles apply to Collections.singletonMap(K,V) and Collections.singleton(T) (no "Set" in method name is not an accidental omission on my part though that method does apply to the Set).

demonstrateSingletonList()


/**
* Provide a Set with a single element.
*/
public void demonstrateSingletonList()
{
log("===== DEMONSTRATING SINGLETON LIST ======", System.out);
final List<String> singleElementList =
Collections.singletonList("A single String to add.");
log( "Size of returned singletonList(): "
+ singleElementList.size()
+ NEW_LINE,
System.out);
log(
"----- Adding String to Collections.singletonList() returned List -----",
System.out);
singleElementList.add("Another String to add.");
}


Results of Running demonstrateSingletonList()



I have found these various "singleton" methods to be useful for passing a single value to an API that requires a collection of that value. Of course, this works best when the code processing the passed-in value does not need to add to the collection.


Unmodifiable Collections

The methods already covered for returning empty collections and single-element collections provided these collections as unmodifiable collections. For situations in which an unmodifiable collection is desired with more than one element, appropriate methods are Collections.unmodifiableList(List), Collections.unmodifiableMap(Map), Collections.unmodifiableSet(Set), and the most general Collections.unmodifiableCollection(Collection). In addition to these, there are also methods for returning a Set or Map that is sorted in addition to being unmodifiable: Collections.unmodifiableSortedMap and Collections.unmodifiableSortedSet.

A source code example of using Collections.unmodifiableMap(Map) and the results of running that example are shown next.

demonstrateUnmodifiableMap()


/**
* Demonstrate use of Collections.unmodifiableMap(). Also demonstrates
* how the underlying collection (or Map in this case) can be changed even
* when set to an unmodifiable version.
*/
public void demonstrateUnmodifiableMap()
{
log("===== DEMONSTRATING UNMODIFIABLE MAP =====", System.out);
final Map<MovieGenre, String> unmodifiableMap =
Collections.unmodifiableMap(this.favoriteGenreMovies);
log(
"Map BEFORE MODIFICATION: " + NEW_LINE + unmodifiableMap.toString(),
System.out);

log(
"----- Putting a new value in Map for existing key in underlying Map. -----",
System.out);
this.favoriteGenreMovies.put(MovieGenre.JAMES_BOND, "Thunderball");
log( "The Unmodifiable Map AFTER MODIFICATION: " + NEW_LINE
+ unmodifiableMap.toString(), System.out);

log(
"----- Putting a completely new key in the underlying map. -----",
System.out);
this.favoriteGenreMovies.put(MovieGenre.MYSTERY, "The Usual Suspects");
log( "The Unmodifiable Map AFTER MODIFICATION: " + NEW_LINE
+ unmodifiableMap.toString(), System.out);

log(
"----- Now try to 'put' to the unmodifiable wrapper collection -----",
System.out);
unmodifiableMap.put(MovieGenre.MYSTERY, "Rear Window");
}


Results of Running demonstrateUnmodifiableMap()



As a brief side note here, I intentionally added to this example the changing of values of the Map that underlies the unmodifiable Map to demonstrate that the source Collections upon which unmodifiable versions are returned can still be changed as needed. It is only the returned collection that is unmodifiable in the sense that it cannot have elements added to it or elements removed from it.


Checked Collections

All of the methods on the Collections class examined in this posting so far have returned unmodifiable collections as either empty, single-element, or multi-element collections. However, the Collections class is capable of much more than simply provide unmodifiable wrappers on collections. The "checked" methods [Collections.checkedCollection(Collection, Class), Collections.checkedList(List, Class), Collections.checkedMap(Map, Class), and Collections.checkedSet(Set, Class)] are useful for dealing with mixes of collections that use generic types and collections handling based one raw collections.

Before the introduction of generics with J2SE 5, we were required to find out about type problems associated with collections as we pulled an item out of a collection and cast it to the expected type at runtime. The advent of J2SE 5 generics enabled us to generally move this type mismatch detection from runtime on extraction of the items from a collection to compile time on insertion into the collection. This is highly advantageous because we can find the problem where it really occurs originally (at insertion) and because we can find it sooner (at compile time rather than at runtime).

Unfortunately, there are ways in which this type checking can be circumvented. For example, if a module out of our control accesses our collection as a raw collection, that module will be able to insert non-compliant items in the collection. Of course, we could also do the same ourselves if we're not careful or have legacy code that did not get fully ported. The Javadoc documentation for the Collections.checkedCollection method explains that these "checked" methods are also useful for debugging problems associated with ClassCastExceptions and generically typed collections by wrapping a collection instantiation in one of these calls.

To illustrate the problem that can occur when generically typed collections and raw collections are mixed, the following code intentionally does that mixture and the resulting problems are documented in the screen snapshot with its output.

demonstrateProblemWithoutCheckedCollection()


/**
* Demonstrate problems that can occur when Collections.checkedCollection is
* not used.
*/
private void demonstrateProblemWithoutCheckedCollection()
{
log("1. Demonstrate problem of no checked collection.", System.out);
final Integer arbitraryInteger = new Integer(4);
final List rawList = this.favoriteBooks;
rawList.add(arbitraryInteger);
final List<String> stringList = rawList;
for (final String element : stringList)
{
log(element.toUpperCase(), System.out);
}
}


Results of demonstrateProblemWithoutCheckedCollection




This problem may look easy to address, but it is much more challenging if the mixed use of raw collections with generically typed collections is separated by many lines of code, different methods, or even different classes. In fact, the error upon access of the non-compliant collection element could happen well after its insertion in terms of both time passed and number of lines of code executed.

The use of the "checked" collection method brings some of the advantages of generically typed collections back even when raw collections are mixed. The following code sample and the screen snapshot of the output it generates are shown next.

demonstrateProblemFixedWithCheckedCollection()


/**
* Demonstrate how Collections.checkedCollection helps the problem.
*/
private void demonstrateProblemFixedWithCheckedCollection()
{
log("2. Demonstrate problem fixed with checked collection", System.out);
final Integer arbitraryInteger = new Integer(4);
final List<String> checkedList = Collections.checkedList(this.favoriteBooks, String.class);
final List rawList = checkedList;
rawList.add(arbitraryInteger);
final List<String> stringList = rawList;
for (final String element : stringList)
{
log(element.toUpperCase(), System.out);
}
}


Results of demonstateProblemFixedWithCheckedCollection()



Although use of the "checked" method here still results in the error being detected at runtime, it provides the advantage of detecting the error where it originally occurs (insertion) rather than some unknown amount of time later.


Enumerations and Collections

The Enumeration has been available since JDK 1.0. Because the Enumeration interface is used with several key legacy APIs, it can be useful to be able to easily convert back and forth between an Enumeration and a collection. Two methods that support this conversion are Collections.list(Enumeration) [converts the provided Enumeration into a List] and Collections.enumeration(Collection) [provides an enumeration over a Collection].

The following source code example demonstrates the conversion of an Enumeration to a List and the screen snapshot after it demonstrates its output.

demonstrateEnumerationToList()


/**
* Demonstrate use of Collections.list(Enumeration).
*/
public void demonstrateEnumerationToList()
{
log("===== Demonstrate Collections.list(Enumeration) =====", System.out);
final Enumeration properties = System.getProperties().propertyNames();
final List propertiesList = Collections.list(properties);
log(propertiesList.toString(), System.out);
}


Results of demonstrateEnumerationToList()



The next code listing and its resulting screen snapshot demonstrate converting a Collection to an Enumeration.

demonstrateCollectionToEnumeration()


/**
* Demonstrate use of Collections.enumeration(Collection).
*/
public void demonstrateCollectionToEnumeration()
{
log("===== Demonstrate Collections.enumeration(Collection) =====", System.out);
final Enumeration books = Collections.enumeration(this.favoriteBooks);
while (books.hasMoreElements())
{
log(books.nextElement().toString(), System.out);
}
}


Results of demonstrateCollectionToEnumeration()




List Order Change-ups

The Collections class supports randomly reordering a List [two versions of Collections.shuffle method], reversing the order of a List [Collections.reverse(List) method], and rotating entries in a list by a prescribed number of entries [Collections.rotate(List, int) method]. Code samples and the associated screen snapshots for each of these follows.

The "shuffle" method is used to randomly reorder items in a List.

demonstrateShuffle()


/**
* Demonstrate Collections.shuffle(List).
*/
public void demonstrateShuffle()
{
log("===== Demonstrate Collections.shuffle(List) =====", System.out);
log("Books BEFORE shuffle: " + NEW_LINE + this.favoriteBooks, System.out);
Collections.shuffle(this.favoriteBooks);
log("Books AFTER shuffle: " + NEW_LINE + this.favoriteBooks, System.out);
}


Results of demonstrateShuffle()



The "reverse" method simply reverses the order of items in a List.

demonstrateReverseList()


/**
* Demonstrate use of Collections.reverse(List).
*/
public void demonstrateReverseList()
{
log("===== Demonstrate Collections.reverse(List) =====", System.out);
log("List BEFORE reverse:" + NEW_LINE + this.favoriteBooks, System.out);
Collections.reverse(this.favoriteBooks);
log("List AFTER reverse:" + NEW_LINE + this.favoriteBooks, System.out);
}


Results of demonstrateReverseList()



The "rotate" method rotates elements in a List by the provided number of spots.

demonstrateRotate()


/**
* Demonstrate Collections.rotate(List, int).
*/
public void demonstrateRotate()
{
log("===== Demonstrate Collections.rotate(List, int) =====", System.out);
log("Books BEFORE rotation: " + NEW_LINE + this.favoriteBooks, System.out);
Collections.rotate(this.favoriteBooks, 3);
log("Books AFTER rotation: " + NEW_LINE + this.favoriteBooks, System.out);
}


Results of demonstrateRotate()




So Many More

The Collections class provides significantly more functionality than even that shown here. It includes methods that support collection wrappers that can be used in concurrent environments [such as Collections.synchronizedCollection(Collection)], support filling a List with a particular item (Collections.fill), support counting the number of times a particular object exists in a Collection (Collections.frequency), sorting, searching, swapping, and several more types of functionality.


Conclusion

The Collections class is one of the most valuable classes in the Java SDK. This blog posting has attempted to demonstrate some of its highly useful methods, but there are many more in addition to those shown here. Use of the Collections class not only makes working with Java collections easier, but it also provides support for best practices related to Java collection use.

2 comments:

@DustinMarx said...

JOKe (Naiden Georgiev Gochev) has posted a blog posting Using of Collections.emptyList() the Right Way in Java 1.5+. In that post he demonstrates the ability to specify the generics type parameter (Java generics) type of the newly created empty List. David Flanagan's Generic Types, Part 2 (bottom of section "Invoking generic methods") also contrasts this approach with the approach I used in my blog post (allowing the local declared variable to supply the information needed for automatic type inference). Flanagan states that "an explicit type parameter is necessary when you use the return value of the emptySet() method within a method invocation expression."

javin paul said...

Great article and you have indeed covered the Java Arrylist and Java HashMap <a in great detail with code example. this is one of most useful class java API provides to us and detail knowledge of it really helps.