Generics in J2SE 5.0
by Budi Kurniawan07/06/2005
Generics are the most important feature in J2SE 5.0. They enable
you to write a type (a class or an interface) and create an
instance of it by passing a reference type or reference types. The
instance will then be restricted to only working with the type(s).
For instance, the java.util.List interface in Java 5
has been made generic. When creating a List object,
you pass a Java type to it and produce a List instance that can
only work with objects of that type. That is, if you pass
String, the List instance can only hold
String objects; if you pass Integer, the
instance can only store Integer objects. In addition
to parameterized types, you can create parameterized methods,
too.
The first benefit of generics is stricter type checking at compile time. This is most apparent in the Collections framework. In addition, generics eliminate most type castings you had to perform when working with the Collections framework in pre-5 JDKs.
This article teaches you to use and write generic types. It starts with the section "Life without Generics," which reminds us what we missed in earlier versions of JDKs. Then, it presents some examples of generic types. After the discussions of the syntax and the use of generic types with bounds, this article concludes with a section that explains how to write generic types.
Life Without Generics
All Java classes derive from java.lang.Object,
which means that all Java objects can be cast to Object.
Because of this, in JDKs prior to version 5, many methods in the
Collections framework accept an Object argument. That
way, the collections became general-purpose utility types that
could hold objects of any type. That imposed unpleasant
consequences.
For example, the add method of the
List class in pre-5 JDKs accepted an
Object argument:
public boolean add(java.lang.Object element)
As a result, you could pass an object of any type to
add. The use of Object is by design.
Otherwise, it could only work with a specific type of object, and
there would then have to be different List types; e.g.,
StringList, EmployeeList, AddressList, etc.
The use of Object in add is fine, but
consider the get method, which returns a member
element of a List. Here is its signature prior to JDK
5.
public java.lang.Object get(int index)
throws IndexOutOfBoundsException
get returns an Object. Here is where
the unpleasant consequences start to kick in. Suppose you have
stored two String objects in a List:
List stringList1 = new ArrayList();
stringList1.add("Java 5");
stringList1.add("with generics");
When retrieving a member from stringList1, you get
an Object. In order to work with the original type of
the member element, you must first downcast it to
String.
String s1 = (String) stringList1.get(0);
In addition, if you ever add a non-String object to
stringList1, the code above will throw a
ClassCastException.
With generic types, you can also create List
instances with special purposes. For example, you can create a
List instance that only accepts String
objects, another that only accepts Employee objects,
and so on. This also applies to other types in the Collections
framework.
Introducing Generic Types
Just like a method can have parameters, a generic type can accept parameters, too. This is why a generic type is often called a parameterized type. Instead of passing primitives or object references in parentheses as it is with methods, you pass reference type(s) in angle brackets to generic types.
Declaring a generic type is like declaring a non-generic one,
except that you use angle brackets to enclose the list of type
variables for the generic type (MyType<typeVar1, typeVar2,
...>).
For example, to declare a java.util.List in JDK 5, you write
List<E> myList;.
E is called a type variable, meaning a variable
that will be replaced by a type. The value substituting for a type
variable will then be used as the argument type or the return type
of a method or methods in the generic type. For the
List interface, when an instance is created,
E will be used as the argument type of add and other
methods. E will also be used as the return type of
get and other methods. Here are the signatures of
add and get.
boolean add<E o>
E get(int index)
Note: A generic type that uses a type variable
E allows you to pass E when declaring or
instantiating the generic type. Additionally, if E is
a class, you may also pass a subclass of E; if
E is an interface, you may also pass a class
implementing E.
If you pass String to a declaration of
List, as in:
List<String> myList;
then the add method of myList will expect a String
object as its argument and its get method will return
a String. Because get returns a specific
object type, no downcasting is required.
Note: By convention, you use a single uppercase letter for type variable names.
To instantiate a generic type, you pass the same list of
parameters as when you declare it. For instance, to create an
ArrayList that works with String, you
pass String in angle brackets.
List<String> myList = new ArrayList<String>();
As another example, java.util.Map is defined as:
public interface Map<K,V>
K is used to denote the type of map keys and
V the type of map values. The put and
values methods have the following signatures:
V put(K key, V value)
Collection<V> values()
Note: A generic type must not be a direct or indirect
child class of java.lang.Throwable, because exceptions
are thrown at run time, and therefore it is not possible to predict
what type of exception that might be thrown at compile time.
As an example, Listing 1 compares List in JDK 1.4
and JDK 5.
package com.brainysoftware.jdk5.app16;
import java.util.List;
import java.util.ArrayList;
public class GenericListTest {
public static void main(String[] args) {
// in JDK 1.4
List stringList1 = new ArrayList();
stringList1.add("Java 1.0 - 5.0");
stringList1.add("without generics");
// cast to java.lang.String
String s1 = (String) stringList1.get(0);
System.out.println(s1.toUpperCase());
// now with generics in JDK 5
List<String> stringList2 = new ArrayList<String>();
stringList2.add("Java 5.0");
stringList2.add("with generics");
// no need for type casting
String s2 = stringList2.get(0);
System.out.println(s2.toUpperCase());
}
}
In Listing 1, stringList2 is a generic
List. The declaration List<String>
tells the compiler that this instance of List can only hold
String objects. Of course, on other occasions, you can
create instances of List that work with other types of
objects. Note, though, that when retrieving member elements of the
List instance, no downcasting is necessary because its
get method returns the intended type, namely
String.
Note: With generic types, type checking is done at compile time.
What's interesting here is the fact that a generic type is
itself a type and can be used as a type variable. For example, if
you want your List to store lists of Strings, you can
declare the List by passing
List<String> as its type variable, as in:
List<List<String>> myListOfListsOfStrings;
To retrieve the first String from the first List in
myList, you would use:
String s = myListOfListsOfStrings.get(0).get(0);
The next listing presents a ListOfListsTest class
that demonstrates a List (named
listOfLists) that accepts a List of
String objects.
package com.brainysoftware.jdk5.app16;
import java.util.ArrayList;
import java.util.List;
public class ListOfListsTest {
public static void main(String[] args) {
List<String> listOfStrings = new ArrayList<String>();
listOfStrings.add("Hello again");
List<List<String>> listOfLists = new ArrayList<List<String>>();
listOfLists.add(listOfStrings);
String s = listOfLists.get(0).get(0);
System.out.println(s); // prints "Hello again"
}
}
Additionally, a generic type can accept more than one type
variable. For example, the java.util.Map interface has
two type variables. The first defines the type of its keys and the
second the type of its values. Here's an example of how to use a
generic Map.
package com.brainysoftware.jdk5.app16;
import java.util.HashMap;
import java.util.Map;
public class MapTest {
public static void main(String[] args) {
Map<String, String> map = new HashMap<String, String>();
map.put("key1", "value1");
map.put("key2", "value2");
String value1 = map.get("key1");
}
}
In this case, to retrieve a String value indicated
by key1, you do not need to perform type casting.
Pages: 1, 2 |