| Home | Resources | Papers | Essays | Short Fiction | About |

Generics

Table of Contents

Introduction

So far we've dealt with defining classes where absolutely everything about the class was known at the moment we finished writing the code. For example, when we wrote the BankAccount class we defined it as having a name field of type String and a balance field of type double. This meant that for every single instantiation of BankAccount there would always be a String and a double contained within the object. That structure worked well because a BankAccount object only ever represented a programmatic version of a bank account. However, there are certain classes where it would be more advantageous for the definition allow for the use of any number of types. The simplest analogue would be an array—we can declare an array to be of any type. If we could only declare arrays that stored int​s they wouldn't be nearly as useful to us. It turns out that Java allows us to have that sort of type flexibility with our own user-defined classes in the form of generics.

This is often a very challenging subject for students. If you are struggling with generics that's okay! Keep practicing and it will begin to make sense.

Declaring a Generic Class

The Java Language Specification (§ 8.1.2) states that:

A class is generic if the class declaration declares one or more type variables.

Which, of course, isn't very helpful in and of itself. Like with most things programming-wise, an example will prove useful:

1: public class MyObject<T>{
2:     public T genericObject;
3: }

This, mostly, looks exactly the same to what we've seen before in class definitions. The biggest difference is we now see <> after the class name and a symbol, T, in between them which is used in the next line before the variable genericObject. The T in this instance is a type variable. Type variables are essentially stand-ins for the type that will eventually be filled in when the object is instantiated. Looking at how we can create an instance of MyObject will show what I mean:

1: public class UsingGenerics{
2:     public static void main(String[] args){
3:         MyObject<Integer> m_i = new MyObject<Integer>();
4:         m_i.genericObject = 10;
5:         System.out.println(m_i.genericObject);
6:     }
7: }

You can see that in Line 3 the T has been replaced by Integer. T in the class definition was a type variable which was assigned the "value" of the type Integer in Listing 2. If we compile and run the above code we get:

josephraskind@stargazer:/tmp$ java UsingGenerics.java 
10

The output shouldn't be too surprising. We create an instance of MyObject whose type variable is set to Integer. This means that genericObject, a type-parameterized instance field, will also be set to Integer and therefore can be used as if it was an Integer in Line 4.

One of the benefits of using generics is that one definition will work for any type. Let's modify MyObject to include slightly more functionality:

 1: public class MyObject<T>{
 2:     public T genericObject;
 3:     MyObject(T obj){
 4:         genericObject = obj;
 5:     }
 6:     @Override
 7:     public String toString(){
 8:         return String.format("This MyObject holds %s", genericObject.getClass().getSimpleName());
 9:     }
10: }

I've added a toString method to MyObject alongside the ability to construct it with an object. We can test out this functionality using the code below:

 1: public class UsingGenerics{
 2:     public static void main(String[] args){
 3:         MyObject<Integer> m_i = new MyObject<>(10);
 4:         System.out.println(m_i);
 5: 
 6:         MyObject<String> m_s = new MyObject<>("Hello World");
 7:         System.out.println(m_s);
 8: 
 9:         MyObject<MyObject<Integer>> m_mo = new MyObject<>(m_i);
10:         System.out.println(m_mo);
11:     }
12: }

You'll notice some new syntax on the instantiation lines. The <> on either side do not precisely match. This is because Java allows the user to eschew a set of type parameters on the right side of an assignment statement as the compiler can infer that they should both match. Compiling and running this code we get:

josephraskind@stargazer:/tmp$ java UsingGenerics.java 
This MyObject holds Integer
This MyObject holds String
This MyObject holds MyObject

We can see that we were able to instantiate three different MyObject objects that hold three different types!

Generic Methods

Java also allows the declaration of generic methods. Generic methods are of a more limited utility, but they work just the same:

 1: class MyInt{
 2:     int i;
 3:     MyInt(int i){
 4:         this.i = i;
 5:     }
 6:     @Override
 7:     public String toString(){
 8:         return String.format("MyInt[%d]", i);
 9:     }
10: }
11: 
12: public class GenericMethods{
13: 
14:     public static <T> void foo(T var){
15:         System.out.println(var);
16:     }
17: 
18:     public static void main(String[] args){
19:         GenericMethods.<String>foo("Hello World");
20:         GenericMethods.<Double>foo(2.5);
21:         GenericMethods.<MyInt>foo(new MyInt(17));
22:     }
23: }

After compiling and running we get:

josephraskind@stargazer:/tmp$ java GenericMethods.java 
Hello World
2.5
MyInt[17]

Although it appears that three different versions of foo are being called, it turns out that only one version is.

Using Wildcards and Boundaries

It turns out that generics with total freedom with regard to the sort of type parameters sent in can sometimes hamper the usefulness of a particular class. For example, let's imagine an alternate version of MyObject called MyNumber. If we used the exact same definition as MyObject in Listing 1 it would be far too easy to instantiate a non-number:

1: public class MyNumber<T>{
2:     public T genericNumber;
3:     public static void main(String[] args){
4:         MyNumber<String> m_n = new MyNumber<>();
5:         m_n.genericNumber = "Hello World";
6:     }
7: }

The above code will compile just fine which is a problem if I only what MyNumber to contain Number objects. I can employ the use boundaries to provide restrictions to the possible type parameters that can be sent in to the type declaration:

1: public class MyNumber<T extends Number>{
2:     public T genericNumber;
3:     public static void main(String[] args){
4:         MyNumber<String> m_n = new MyNumber<>();
5:         m_n.genericNumber = "Hello World";
6:     }
7: }

If I try to compile the above I get:

josephraskind@stargazer:/tmp$ javac MyNumber.java 
MyNumber.java:4: error: type argument String is not within bounds of type-variable T
        MyNumber<String> m_n = new MyNumber<>();
                 ^
  where T is a type-variable:
    T extends Number declared in class MyNumber
MyNumber.java:4: error: incompatible types: cannot infer type arguments for MyNumber<>
        MyNumber<String> m_n = new MyNumber<>();
                                           ^
    reason: inference variable T has incompatible bounds
      equality constraints: String
      upper bounds: Number
  where T is a type-variable:
    T extends Number declared in class MyNumber
2 errors

I can no longer use just any type. I must use a type that can be matched to the boundaries provided. In Listing 10, T has to be a type that extends, or is a subclass of, the Number type!

Non-class type parameters can also leverage lower bounds in the form of the super keyword, as well as utilize wildcards. In Java, the wildcard symbol for type parameters is ?. It acts as a standin to be used in place of a single type. Here is a set of generic methods which leverage the wildcard symbol and boundary keywords for type parameters:

 1: import java.util.ArrayList;
 2: import java.util.List;
 3: 
 4: public class WildcardExamples {
 5: 
 6:     // 1. Unbounded wildcard -- accepts a list of any type
 7:     //    can only read as Object, cannot write
 8:     static void printAll(List<?> list){
 9:         for(Object o : list)
10:             System.out.println(o);
11:     }
12: 
13:     // 2. Upper bounded wildcard (? extends) -- accepts List<Number>
14:     //    or any subtype: List<Integer>, List<Double> etc.
15:     //    can READ as Number, cannot write (except null)
16:     static double sum(List<? extends Number> list){
17:         double total = 0;
18:         for(Number n : list)
19:             total += n.doubleValue();
20:         return total;
21:     }
22: 
23:     // 3. Lower bounded wildcard (? super) -- accepts List<Integer>
24:     //    or any supertype: List<Number>, List<Object>
25:     //    can WRITE Integers into it, can only READ as Object
26:     static void addIntegers(List<? super Integer> list){
27:         list.add(1);
28:         list.add(2);
29:         list.add(3);
30:     }
31: 
32:     public static void main(String[] args){
33: 
34:         // unbounded wildcard
35:         List<String> strings = List.of("a", "b", "c");
36:         printAll(strings);   // fine
37:         List<Integer> ints = List.of(1, 2, 3);
38:         printAll(ints);      // also fine
39: 
40:         // upper bounded wildcard
41:         System.out.println(sum(List.of(1, 2, 3)));       // 6.0
42:         System.out.println(sum(List.of(1.5, 2.5, 3.0))); // 7.0
43: 
44:         // lower bounded wildcard
45:         List<Number> numbers = new ArrayList<>();
46:         addIntegers(numbers);
47:         System.out.println(numbers);  // [1, 2, 3]
48: 
49:         List<Object> objects = new ArrayList<>();
50:         addIntegers(objects);         // also fine
51:         System.out.println(objects);  // [1, 2, 3]
52: 
53:     }
54: }

Type Erasure and Raw Types

Java does not allow the use of primitives in place for type parameters in generics. The Java Language Specification does not justify this choice, but it is clear that it comes as a result of what's called type erasure. Essentially, the type information of a generic is erased at compile-time. When we write code like ArrayList<Integer> l = new ArrayList<>() the <Integer> is dropped during the runtime of the application leaving the JVM with only the ArrayList type. Here's an example that shows how strange type erasure can be:

 1: import java.util.*;
 2: public class TypeErasure{
 3:     public static void main(String[] args){
 4:         final List<Integer> li = new ArrayList<>(); //<> tells the compiler to infer the type as <Integer>
 5:         final List<Float> lf = new ArrayList<>(); 
 6:         if (li.getClass() == lf.getClass()) { // evaluates to true
 7:             System.out.println("Equal");
 8:         }    
 9:     }
10: }

after compiling and running the above:

josephraskind@stargazer:/tmp$ java TypeErasure.java 
Equal

Despite the fact that at compile-time it seems like the two ArrayList objects have different types, they actually have the same type during the runtime!

Why Generics?

If Java did not provide generics the only other option would be to use the Object type exclusively when defining container classes. In fact, this was exactly how Java handled such classes before generics were introduced in Java 5 (2004). The major use-case comes in the form of compilation-type checking. Take the following example sourced from Wikipedia:

1: import java.util.ArrayList;
2: import java.util.List;
3: 
4: List v = new ArrayList();
5: v.add("test");                 // A String that cannot be cast to an Integer
6: Integer i = (Integer)v.get(0); // Run time error

The problem with the code above is that the String content of "test" is lost the moment it is added to the List. Although it is clear to us that it cannot be converted to an Integer at compile time, the Java compiler cannot make the same logical leap. For all it "knows", what is stored in v is an Object and an Object is allowed to be cast down to an Integer as Integer is a subclass of Object. If we introduce generics:

1: import java.util.ArrayList;
2: import java.util.List;
3: 
4: List<String> v = new ArrayList<String>();
5: v.add("test");                 // A String that cannot be cast to an Integer
6: Integer i = (Integer)v.get(0); // (type error) compilation-time error

The above code will be stopped at compile-time. Why? It is because the List now has the extra type information from the generic to know that anything stored within it must be a String. This means that the compiler can be confident that v.get(0) will return a String and check that String cannot be converted to an Integer. Ostensibly, the extra type information is what makes Java an attractive OOP language to be writing in the first place.

Exercises

  1. The chapter shows that without generics, a MyObject class would need to use Object as its field type. Write a non-generic MyObject class with an Object value field and getValue~/ ~setValue methods. Then write a program that stores a String in it and retrieves it, casting back to String. Now store an Integer and accidentally cast it to String. What happens and when does the error occur? Compare this to the generic MyObject<T> — at what point does the equivalent error occur?
  2. Compile and run Listing 1. Then attempt to instantiate MyObject<int> instead of MyObject<Integer>. Record the compiler error. Explain in your own words why primitives cannot be used as type parameters and what you must use instead.
  3. The chapter introduces bounded type parameters with extends. Write a generic method max that takes two arguments of type T extends Comparable<T> and returns the larger of the two. Test it with Integer, Double, and String arguments. Why is the Comparable<T> bound necessary — what would go wrong without it?
  4. The chapter shows that T is erased to Object at runtime. Write a generic class Pair<A, B> that holds two values of potentially different types and provides getFirst and getSecond methods. Then call getClass().getSimpleName() on both fields after retrieving them from a Pair<String, Integer>. What do you observe about the runtime types?
  5. The chapter introduces the unbounded wildcard <?>. Write a method printMyObject that accepts a MyObject<?> and prints its contents. Verify it works with a MyObject<String>, a MyObject<Integer>, and a MyObject<Dog>. Then attempt to call setValue inside printMyObject with a String argument. What does the compiler say and why?
  6. The chapter explains the upper bounded wildcard <? extends T>. Write a method sumMyObjects that takes a List<? extends Number> and returns the sum of all values as a double. Test it with a List<MyObject<Integer>>… wait — does List<MyObject<Integer>> satisfy List<? extends Number>? Why or why not? What would the correct bound need to be?
  7. The chapter explains the lower bounded wildcard <? super T>. Write a method fillWithZero that takes a List<? super Integer> and adds ten zeros to it. Call it with a List<Integer>, a List<Number>, and a List<Object>. Then attempt to call it with a List<Double>. What happens and why?
  8. The chapter states that a class type parameter can use extends but not super. Write a generic class Cage<T extends Animal> that holds an Animal subtype and has a method release that calls eat on the contained animal. Instantiate it with Dog and Mammal. Then attempt to instantiate it with String and record the compiler error.
  9. The chapter introduces generic methods with their own type parameters independent of the class. Write a generic method swap that takes an array of type T[] and two indices and swaps the elements at those indices. Test it with a String[] and an Integer[]. Could you write the same method without generics using Object[]? What would you lose?
  10. The chapter shows that MyObject<Dog> is not a subtype of MyObject<Animal> even though Dog is a subtype of Animal. Write a program that demonstrates this by attempting to assign a MyObject<Dog> to a MyObject<Animal> reference. Record the compiler error. Then rewrite the assignment using a wildcard MyObject<? extends Animal> and verify it compiles. Explain in your own words why this invariance exists and what problem it prevents.
Contact: [email protected] | rss feed | Compiled with org-mode | Licensed under CC BY-NC-SA 4.0