Since the original 1.0 release in 1995, many new features have been added to Java. One that has had a profound impact is generics. Introduced by JDK 5, generics changed Java in two important ways. First, it added a new syntactical element to the language. Second, it caused changes to many of the classes and methods in the core API. Today, generics are an integral part of Java programming, and a solid understanding of this important feature is required.
Generic Fundamentals
- The term generics means parameterized types.
- Using generics, it is possible to create a single class, for example, that automatically works with different types of data.
- A class, interface, or method that operates on a parameterized type is called generic, as in generic class or generic method.
- Generics added the type safety.
- With generics, all casts are automatic and implicit.
A Simple Generic Example
/*
* A simple generic class. Here, T is a type parameter that will be replaced by
* a real type when an object of type Gen is created.
*/
public class MyGen<T> {
// attributes
T value;
// constructor
public MyGen(T value) {
this.value = value;
}
// method
public T getValue() {
return value;
}
public void showType() {
System.out.println("Type T is " + value.getClass().getName());
}
}
public class GenericExample {
public static void main(String[] args) {
// integer
MyGen<Integer> intGen = new MyGen<Integer>(90);
System.out.println("value is " + intGen.getValue());
intGen.showType();
// string
MyGen<String> strGen = new MyGen<String>("hello");
System.out.println("value is " + strGen.getValue());
strGen.showType();
// box
MyGen<Box> boxGen = new MyGen<Box>(new Box(2, 3, 4));
System.out.println("Box volume is " + boxGen.getValue().volume()); // call method on box object
boxGen.showType();
}
}
Bounded Types
Sometimes it is useful to limit the types that can be passed to a type parameter. For example, assume that you want to create a generic class that contains a method that returns the average of an array of numbers. Furthermore, you want to use the class to obtain the average of an array of any type of number, including integers, floats, and doubles. Thus, you want to specify the type of the numbers generically, using a type parameter.
/*
* Bounded Type generic class Rule: T must be a Number or any classes than
* extends Number class (subclasses)
*/
public class MyStats<T extends Number> {
// instance variables
// array of numbers
private T[] numbers;
// constructor
public MyStats(T[] numbers) {
this.numbers = numbers;
}
// methods - calculate average
public double average() {
double sum = 0.0;
for (int i = 0; i < numbers.length; i++) {
sum += numbers[i].doubleValue();
}
return sum / numbers.length;
}
}
public class BoundedTypeExample {
public static void main(String[] args) {
// integer
Integer[] integers = { 1, 2, 3, 4, 5 };
MyStats<Integer> first = new MyStats<Integer>(integers);
System.out.println("Average of integers is " + first.average());
// double
Double[] doubles = { 2.3, 1.4, 5.6, 7.8 };
MyStats<Double> second = new MyStats<Double>(doubles);
System.out.println("Average of doubles is " + second.average());
// string - this will give error since string is not number
String[] names = { "John", "Bob", "Charles" };
// MyStats<String> third = new MyStats<String>(names);
}
}
Wildcard Arguments
public class MyStats<T extends Number> {
// instance variables
// array of numbers
private T[] numbers;
// constructor
public MyStats(T[] numbers) {
this.numbers = numbers;
}
// methods - calculate average
public double average() {
double sum = 0.0;
for (int i = 0; i < numbers.length; i++) {
sum += numbers[i].doubleValue();
}
return sum / numbers.length;
}
// method with wildcard argument
public boolean sameAverage(MyStats<?> s) {
// check if average values are same
if (average() == s.average()) {
return true;
}
return false;
}
}
public class WildcardExample {
public static void main(String[] args) {
// all other codes here
// compare average between integers and doubles
// first - integer
// second - double
// call sameAverage from first object and pass second as parameter
if (first.sameAverage(second)) {
System.out.println("same average");
} else {
System.out.println("different average");
}
// let's swap
// call sameAverage from second object and pass first as parameter
if (second.sameAverage(first)) {
System.out.println("same average");
} else {
System.out.println("different average");
}
}
}
Here, Stats<?> matches any Stats object, allowing any two Stats objects to have their averages compared.
One last point: It is important to understand that the wildcard does not affect what type of Stats objects can be created. This is governed by the extends clause in the Stats declaration. The wildcard simply matches any valid Stats object.
Bounded Wildcard
Wildcard arguments can be bounded in much the same way that a type parameter can be bounded. A bounded wildcard is especially important when you are creating a generic type that will operate on a class hierarchy.
Consider the following hierarchy of classes that encapsulate coordinates:
//TwoD class contains 2 integers a and b
class TwoDim {
public int a, b;
public TwoDim(int a, int b) {
this.a = a;
this.b = b;
}
}
//ThreeD is a subclass of TwoD, this class has it's own data called c
class ThreeDim extends TwoDim {
public int c;
public ThreeDim(int a, int b, int c) {
super(a, b);
this.c = c;
}
}
//FourD is a subclass of ThreeD, this class also has it's own data called d
class FourDim extends ThreeDim {
public int d;
public FourDim(int a, int b, int c, int d) {
super(a, b, c);
this.d = d;
}
}
At the top of the hierarchy is TwoDim, which encapsulates a two-dimensional, AB coordinate. TwoDim is inherited by ThreeDim, which adds a third dimension, creating an ABC coordinate. ThreeDim is inherited by FourDim, which adds a fourth dimension (time), yielding a four-dimensional coordinate.
Shown next is a generic class called Coords, which stores an array of coordinates:
//bounded type generic class
class Coordinates<T extends TwoDim> {
// array of coordinates object
public T[] list;
public Coordinates(T[] list) {
this.list = list;
}
}
Notice that Coords specifies a type parameter bounded by TwoDim. This means that any array stored in a Coords object will contain objects of type TwoDim or one of its subclasses.
Now, assume that you want to write a method that displays the A and B coordinates for each element in the coords array of a Coords object. Because all types of Coords objects have at least two coordinates (A and B), this is easy to do using a wildcard, as shown here:
// wildcard argument
public static void showAB(Coordinates<?> c) {
System.out.println("A B coordinates");
for (int i = 0; i < c.list.length; i++) {
System.out.println(c.list[i].a + " " + c.list[i].b);
}
System.out.println(); // empty line
}
Because Coords is a bounded generic type that specifies TwoDim as an upper bound, all objects that can be used to create a Coords object will be arrays of type TwoDim, or of classes derived from TwoDim. Thus, showAB( ) can display the contents of any Coords object.
However, what if you want to create a method that displays the A, B, and C coordinates of a ThreeDim or FourDim object? The trouble is that not all Coords objects will have three coordinates, because a Coords<TwoDim> object will only have A and B. Therefore, how do you write a method that displays the A, B, and C coordinates for Coords<ThreeDim> and Coords<FourDim> objects, while preventing that method from being used with Coords<TwoDim> objects? The answer is the bounded wildcard argument.
A bounded wildcard specifies either an upper bound or a lower bound for the type argument. This enables you to restrict the types of objects upon which a method will operate. The most common bounded wildcard is the upper bound, which is created using an extends clause in much the same way it is used to create a bounded type.
Using a bounded wildcard, it is easy to create a method that displays the A, B, and C coordinates of a Coords object, if that object actually has those three coordinates. For example, the following showABC( ) method shows the A, B, and C coordinates of the elements stored in a Coords object, if those elements are actually of type ThreeDim (or are derived from ThreeDim):
// bounded wildcard
public static void showABC(Coordinates<? extends ThreeDim> c) {
System.out.println("A B C coordinates");
for (int i = 0; i < c.list.length; i++) {
System.out.println(c.list[i].a + " " + c.list[i].b + " " + c.list[i].c);
}
System.out.println(); // empty line
}
public class BoundedWildcardExample {
public static void main(String[] args) {
// define list of objects
TwoDim[] first = { new TwoDim(1, 2), new TwoDim(3, 4), new TwoDim(5, 6) };
ThreeDim[] second = { new ThreeDim(1, 2, 1), new ThreeDim(3, 4, 3), new ThreeDim(5, 6, 5) };
FourDim[] third = { new FourDim(1, 2, 1, 2), new FourDim(3, 4, 3, 4), new FourDim(5, 6, 5, 6) };
// create coordinate object
Coordinates<TwoDim> coords1 = new Coordinates<TwoDim>(first);
Coordinates<ThreeDim> coords2 = new Coordinates<ThreeDim>(second);
Coordinates<FourDim> coords3 = new Coordinates<FourDim>(third);
// call method with wildcard argument
showAB(coords1); // TwoDim
showAB(coords2); // ThreeDim
showAB(coords3); // FourDim
// call method with bounded wildcard
// showABC(coords1); // TwoDim - error
showABC(coords2); // ThreeDim
showABC(coords3); // FourDim
}
}
Generic Methods
The following program declares a non-generic class called GenMethodExample and a static generic method within that class called isIn( ). The isIn( ) method determines if an object is a member of an array. It can be used with any type of object and array as long as the array contains objects that are compatible with the type of the object being sought.
public class GenMethodExample {
// Determine if an object is in an array.
static <T extends Comparable<T>, V extends T> boolean isIn(T x, V[] y) {
for (int i = 0; i < y.length; i++)
if (x.equals(y[i]))
return true;
return false;
}
public static void main(String[] args) {
// Use isIn() on Integers.
Integer nums[] = { 1, 2, 3, 4, 5 };
if (isIn(2, nums))
System.out.println("2 is in nums");
if (!isIn(7, nums))
System.out.println("7 is not in nums");
System.out.println();
// Use isIn() on Strings.
String strs[] = { "one", "two", "three", "four", "five" };
if (isIn("two", strs))
System.out.println("two is in strs");
if (!isIn("seven", strs))
System.out.println("seven is not in strs");
// Oops! Won't compile! Types must be compatible.
// if(isIn("two", nums))
// System.out.println("two is in strs");
}
}
Generic Constructor
It is possible for constructors to be generic, even if their class is not.
public class GenConstructorExample {
public static void main(String[] args) {
MyGenConstructor test = new MyGenConstructor(100); // integer
MyGenConstructor test2 = new MyGenConstructor(123.5F); // float
test.showval();
test2.showval();
}
}
class MyGenConstructor {
private double val;
<T extends Number> MyGenConstructor(T arg) {
val = arg.doubleValue();
}
void showval() {
System.out.println("val: " + val);
}
}
Generic Interfaces
In addition to generic classes and methods, you can also have generic interfaces. Generic interfaces are specified just like generic classes. Here is an example. It creates an interface called MinMax that declares the methods min( ) and max( ), which are expected to return the minimum and maximum value of some set of objects.
//A generic interface example.
//A Min/Max interface.
interface MinMax<T extends Comparable<T>> {
T min();
T max();
}
//Now, implement MinMax
class MyGenImplementation<T extends Comparable<T>> implements MinMax<T> {
T[] vals;
MyGenImplementation(T[] o) {
vals = o;
}
//Return the minimum value in vals.
public T min() {
T v = vals[0];
for (int i = 1; i < vals.length; i++)
if (vals[i].compareTo(v) < 0)
v = vals[i];
return v;
}
//Return the maximum value in vals.
public T max() {
T v = vals[0];
for (int i = 1; i < vals.length; i++)
if (vals[i].compareTo(v) > 0)
v = vals[i];
return v;
}
}
public class GenInterfaceExample {
public static void main(String[] args) {
Integer inums[] = { 3, 6, 2, 8, 6 };
Character chs[] = { 'b', 'r', 'p', 'w' };
MyGenImplementation<Integer> iob = new MyGenImplementation<Integer>(inums);
MyGenImplementation<Character> cob = new MyGenImplementation<Character>(chs);
System.out.println("Max value in inums: " + iob.max());
System.out.println("Min value in inums: " + iob.min());
System.out.println("Max value in chs: " + cob.max());
System.out.println("Min value in chs: " + cob.min());
}
}
Some Generic Restrictions
There are a few restrictions that you need to keep in mind when using generics. They involve creating objects of a type parameter, static members, exceptions, and arrays.
Type Parameters Can’t Be Instantiated
It is not possible to create an instance of a type parameter. For example, consider this class:
//Can't create an instance of T.
class Gen<T> {
T ob;
Gen() {
ob = new T(); // Illegal!!!
}
}
Here, it is illegal to attempt to create an instance of T. The reason should be easy to understand: the compiler does not know what type of object to create. T is simply a placeholder.
Restrictions on Static Members
No static member can use a type parameter declared by the enclosing class. For example, both of the static members of this class are illegal:
class Wrong<T> {
// Wrong, no static variables of type T.
static T ob;
// Wrong, no static method can use T.
static T getob() {
return ob;
}
}
Although you can’t declare static members that use a type parameter declared by the enclosing class, you can declare static generic methods, which define their own type parameters.
Generic Array Restrictions
There are two important generics restrictions that apply to arrays. First, you cannot instantiate an array whose element type is a type parameter. Second, you cannot create an array of type-specific generic references. The following short program shows both situations:
//Generics and arrays.
class Gen<T extends Number> {
T ob;
T vals[]; // OK
public Gen(T o, T[] nums) {
ob = o;
//This statement is illegal.
//vals = new T[10]; // can't create an array of T
//But, this statement is OK.
vals = nums; // OK to assign reference to existent array
}
}
public class Test {
public static void main(String args[]) {
Integer n[] = { 1, 2, 3, 4, 5 };
Gen<Integer> iOb = new Gen<Integer>(50, n);
//Can't create an array of type-specific generic references.
//Gen<Integer> gens[] = new Gen<Integer>[10]; // Wrong!
//This is OK.
Gen<?> gens[] = new Gen<?>[10]; // OK
}
}
Generic Exception Restriction
A generic class cannot extend Throwable. This means that you cannot create generic exception classes.