Use TypedCollection.decorate()
to create a Collection
that only accepts objects of a specified type. Supply an existing
Collection
along with the Class
that all elements should be constrained
to. TypedCollection
will decorate
this existing Collection
, validating
elements as they are added to a Collection
. The following example creates a
Collection
that will only accept
strings:
List existingList = new ArrayList( ); Collection typedCollection = TypedCollection.decorate( existingList, String.class ); // This will add a String typedCollection.add( "STRING" ); // And, This will throw an IllegalArgumentException typedCollection.add( new Long(28) );
Similarly, if you want to constrain keys and values to specified
types, pass a Map
to TypedMap.decorate( )
method, specifying a
Class
for both the key and the value.
In the following example, typedMap
only accepts String
keys and Number
values:
Map existingMap = new HashMap( ); Map typedMap = TypedMap.decorate( existingMap, String.class, Number.class ); // This will add a String key and a Double value typedMap.put( "TEST", new Double( 3.40 ) ); // Both of these throw an IllegalArgumentException typedMap.put( new Long(202), new Double( 3.40 ) ); typedMap.put( "BLAH", "BLAH" );
TypedCollection
and TypedMap
will decorate any existing Collection
or Map
and will throw an IllegalArgumentException
if you try to add an
incompatible type.
A Map
frequently contains keys
and values with consistent types; for instance, an application that
keeps track of Person
objects by name
most likely has a personMap
with
Person
values and String
keys. Rarely does a Map
hold a wide diversity of types. Collection
s and Map
s are not type-safe, and this lack of type
safety means that unexpected objects may be cast to incompatible types,
causing nasty ClassCastException
s. It
is unlikely that every time you call get(
)
and cast the resulting object, you catch ClassCastException
; and, in most systems, it
is reasonable to assume that no one has put an incompatible type into a
Map
. But, if a Map
plays a central role in a critical
application, you may want an extra layer of validation; decorate your
maps with TypedMap
to ensure that a
Map
contains consistent types. There
is little penalty for decorating a Map
as such, and if someone writes code to
insert invalid input, your application should fail immediately with an
IllegalArgumentException
.
If your application uses a TypedMap
, it is easier to track down defects.
If a ClassCastException
is thrown
when calling get( )
, you then need to
work backward to find out where the offending object was put into a
Map
. An alternative is to validate
each object as it is added to a Map
.
If the put( )
method throws IllegalArgumentException
, it will be easier to
identify the offending code.
Java 5.0 adds the idea of generics—compile-time type safety for
any number of objects including Collection
s and Map
s. But, if you are stuck with an older
version of the JDK, you can use Commons Collections to create a Collection
that only accepts input of a
certain type. TypedSet
, TypedBag
, TypedList
, TypedMap
, TypedBuffer
, TypedSortedSet
, TypedSortedBag
, TypedSortedMap
all provide the same decoration
as TypedCollection
, but they return a
specific interface; for example, TypedList
decorates and returns a List
, and TypedSet
decorates and returns a Set
. Example
5-13 demonstrates the use of the TypedList
decorator to return a List
instead of a Collection
.
Example 5-13. Using TypedList to decorate a list
package com.discursive.jccook.collections.typed; import java.util.ArrayList; import java.util.List; import org.apache.commons.collections.list.TypedList; public class TypedListExample { private List hostNames; public static void main(String[] args) { TypedListExample example = new TypedListExample( ); example.start( ); } public void start( ) { // Make sure that items added to this hostNames = TypedList.decorate( new ArrayList( ), String.class ); // Add two String objects hostNames.add( "papp01.thestreet.com" ); hostNames.add( "test.slashdot.org" ); // Try to add an Integer try { hostNames.add( new Integer(43) ); } catch( IllegalArgumentException iae ) { System.out.println( "Adding an Integer Failed as expected" ); } // Now we can safely cast without the possibility of a // ClassCastException String hostName = (String) hostNames.get(0); } }
If a List
decorated with
TypedList
encounters an invalid
object, the add( )
method will throw
an IllegalArgumentException
.
A Typed<X>
decorated
Collection
will not be able to
provide the compile-time type safety of Java 5.0's generics, but it
will enforce a restriction on what it can accept—it is up to you to
catch the runtime exception.
TypedMap
allows you to
constrain both the keys and values of a map. TypedMap.decorate( )
takes three parameters:
the Map
to decorate, the key Class
, and the value Class
. To create a Map
that only constrains key types, pass in a
null
value for the value type. To
create a Map
that only validates the
type of the value, pass in a null
for
the key type. Example 5-14 uses
TypedMap.decorate( )
to create a
Map
that only accepts String
keys and Number
values.
Example 5-14. Decorating a map with TypedMap
package com.discursive.jccook.collections.typed; import java.util.HashMap; import java.util.Map; import org.apache.commons.collections.map.TypedMap; public class TypedMapExample { private Map variables; public static void main(String[] args) { TypedMapExample example = new TypedMapExample( ); example.start( ); } public void start( ) { // Make sure that items added to this variables = TypedMap.decorate( new HashMap( ), String.class, Number.class ); // Add two String objects variables.put( "maxThreads", new Integer(200) ); variables.put( "minThreads", new Integer(20) ); variables.put( "lightSpeed", new Double( 2.99792458e8 ) ); // Try to add a String value try { variables.put( "server", "test.oreilly.com" ); } catch( IllegalArgumentException iae ) { System.out.println( "Adding an String value Failed as expected" ); } // Try to add an Integer key try { variables.put( new Integer(30), "test.oreilly.com" ); } catch( IllegalArgumentException iae ) { System.out.println( "Adding an Integer key Failed as expected" ); } // Now we can safely cast without the possibility of a ClassCastException Number reading = (Number) variables.get("lightSpeed"); } }
Java 5.0 has added generics—a welcome addition. For more information about generics, look at the release notes for Java 5.0 at http://java.sun.com/j2se/1.5.0/docs/relnotes/features.html#generics.
For more information about the decorator design pattern, read the classic Design Patterns: Elements of Reusable Object-Oriented Software, by Erich Gamma et al., or take a look at this onJava.com article by Budi Kurniawan: http://www.onjava.com/pub/a/onjava/2003/02/05/decorator.html, which deals with the decorator pattern as applied to Java Swing development, but this pattern also has relevance outside of a GUI development context.
This TypedCollection
decorator
is a specialized version of a PredicatedCollection
. Type-safety is
implemented through the use of an InstanceofPredicate
, and the next recipe
discusses the use of a PredicatedMap
.