Lec 05 - Generics

Generic Types

The motivations, (a.k.a benefits) of using generics are:

  1. enable you to detect errors at compile time rather than at runtime.

  2. make your code more generalizable.

Generics let you parameterize types. With this capability, you can define a class or a method with generic types that the compiler can replace with concrete types.

Here, to make it simple, let's use the type parameter - type argument notation. Here the parameter and argument are the same one as we use in the method. So, for example

class A<T> { }

A<Integer> a = new A<Integer>();
  • A or A<T> is a generic type with T as the type parameter.

  • A<Integer> is a parameterized type with Integer as the type argument for T.

Here, you may get a bit confused, but actually we are using the knowledge that in Java, type is the same as class!

Generic Class / Interface

Using our tradition, this generic class / interface can be called generic type as well.

A / Some generic type parameter(s) can be defined for a class or interface, such type parameters are called class-level type parameters.

Generic Type Declaration

To declare a generic type, you can just put the type parameters inside the < and > after the class/type name. For example,

Here, <S> and <T> represents the formal generic type parameter. And Pair<S,T> is a generic type.

The constructor for a generic type doesn't need <> operator!

Instantiate a Generic Type

To use a generic type, we have to pass in type arguments, which itself can be

1

A non-generic type

Here we want to instantiate the generic type Pair<S,T>, and we pass two reference types as the type arguments.

In this way, the type argument must be reference type. You cannot use primitive types, like int, double, etc, as type arguments.

2

A generic type

Here the generic type that needs to be instantiated is Comparable<T>, and we are passing Pair<S,T> as the type argument.

3

Another type parameter that has been declared.

Here the generic type that needs to be instantiated is Pair<S,T>, where

  • Its first type parameter S is fixed as String.

  • Its second type parameter T uses the first type parameter T in DictEntry as the type argument.

Once a generic type is instantiated, it is called a parameterized type.

Generic Methods

Similary, a generic type parameter can be defined for any method, and such type parameters are called method-level type parameters.

Declare a non-static generic method

Here, the method transform uses the class-level type parameter T.

Invoke a non-static generic method

We can just use instance.method() to invoke a non-static generic method.

Declare a static generic method

To declare a static generic method, you should put the type parameter immediately after the keyword static and before the return type. For example,

Invoke a static generic method

  • Syntax for specifying a method's type parameter: ClassName.<Type>method().

  • Class type parameters (e.g., <Integer> in A<Integer>) are irrelevant for static methods and cannot be mixed with method type parameters in the call.

For example,

Bounded Generic Type Parameter

A generic type parameter can be specified as a subtype of another type. Such a generic type parameter is called bounded.

Motivation: Since during the compile time, generic type parameter may not have the method that you want associated with it! So, to enable us to call the methods associated with our generic type parameter, we can used bounded type parameters! (Jump to Type Erasure process in Java if you want to understand it in advance)

For example, our getArea() can be generalized using the generics as follows

We use the keyword extends here to indicate that T must be a subtype of GetAreable. It is unfortunate that Java decides to use the term extends for any type of subtyping when declaring a bounded type parameter, even if the supertype (such as GetAreable) is an interface.

To speicify more than one bound, use & operator to link the bounds. But the first bound must be a class! It cannot be an interface, otherwise, a compile error will be generated.

An interesting example

Let's say we want to compare two Pair instances, by comparing the first element in the pair, we can define our class as follows:

Here, we have two Comparable<T> that needs to be instantiated, a.k.a, we want to make two types comaprable!

  1. For the first type parameter S in Pair<S,T>

  2. For the generic type Pair<S,T>

A bound like <S extends Comparable<S>> is a common pattern called self-referential bound. It ensures a type can be compared to others of its own kind.

Declaration vs. Usage of Generic Parameters:

1

Declaration

The first appearance of the generic type parameters (S and T) in the class definition is where they are declared. In our example, this is in <S extends Comparable<S>, T>.

2

Usage

After declaring them, these type parameters can be used throughout the class. For instance, they are used in the type signature of the Comparable interface (Comparable<Pair<S, T>>), in method parameters, return types, or field declarations.

Type Erasure

Implementing Generics

Different languages implement the Generics differently. Basically, we have the following two methods:

  1. Code specialization: it means that instantiating the generic types, like Pair<String, Integer> causes new code to be generated during compile-time. C++ and Rust use this method.

  2. Code sharing: it means that instead of creating a new type for every instantiation, it chooses to erase the type parameters and type arguments during compilation (after type checking, of course). Thus, there is only one representation of the generic type in the generated code, representing all the instantiated generic types, regardless of the type arguments. Java uses this method.

Part of the reason that Java uses code sharing is because of the backward compatibility since before Java 5, Java uses Object to implement classes that are general enough to work on multiple types.

Type Erasure process in Java

Type erasure is a compile-time process that removes generic type information to ensure backward compatibility with legacy Java code that doesn’t use generics. And the whole process of type erasure can be divided into:

  1. Type checking (Before type erasure)

  2. Type erausre (During type erasure)

Type Checking

Java will do the type checking during the compile time to make sure that the code compile!

Type Erasure

This happens in the compile-time also, after the type checking.

1

Replace generic type with its rawtype

The type parameters of the generic type will be discarded and replaced by its raw type during the type erasure. For example, Pair<String, Integer> will be erased to Pair.

2

Replacing Type Parameters used in the generic type(Type erasure starts)

  • Non-Bounded Type Parameters: If a type parameter is not bounded (e.g., <T>), it is replaced with Object.

  • Bounded Type Parameters: If a type parameter has an upper bound (e.g., <T extends GetAreable>), it is replaced with the first bound (in this case, GetAreable).

If multiple bounds exist (e.g., <T extends SomeClass & SomeInterface>), T will be erased the first bound, and then it is type casted to the second bound. (You can find more in Recitation 03)

Note that the first bound must be a class! It cannot be an interface, otherwise, a compile error will be generated!

3

Inserting Necessary Casts:

  • After replacing type parameters, the compiler inserts casts where needed. This ensures that type checking (done at compile time) is still enforced at runtime.

  • For example, when retrieving an element from a generic collection, the compiler adds a cast to the expected type because, after erasure, the collection is treated as holding Object references (or the specific bound type).

For example, in the following code where a generic type is instantiated and used, the code

is transformed into the following code after type erasure.

Some dangers of Type Erasure

One big danger of type erasure is the heap pollution, this is because generics and arrays can't mix. For example,

After type erause, it will become

Seems that this code will generate no compile-time error and run-time error! But you are actually storing Pair<Double, boolean> into the Pair array of <String, Integer>!

But in fact, the first code snippet cannot compile because generic array declaration is fine but generic array instantiation is not!

Unchecked Warnings

Basically, unchecked warnings will happen in the following two cases:

  1. the type casting process when you create an array with type parameters. See Create Arrays with Type parameters

  2. raw types are used. (This actually will cause a rawtype warning instead of an unchecked warning). See Raw Types

Generics are Invariant

In Java, generics are invariant. This means there is no subtype relationship between two generic types. For example,

Even if SS <: TT, we cannot say A<S> <: A<T>.

Create Arrays with Type parameters

As we have seen earlier, Java arrays and generics cannot mix together! This means that,

we cannot instantiate a Java array using the type parameter, e.g. new T[] is not allowed. However, we can declare a Java array using the type parameter, e.g. T[] a is allowed.

So, how can we create arrays with type parameters? To get around with this, we should

1

Determine the type Q of the type parameter after type erasure

Let's define the type as Q. For example,

  1. If the type parameter is unbounded, then Q will be Object

  2. If the type parameter is bounded, e.g., T extends Comparable<T>, then Q will be the bound type, which is Comparable in our example.

2

Create a Java array with type Q[] cast it to type T[]

For example, we will have

Explicit casting is allowable here because during compile time, we are sure that T will be subtype of Object or Comparable in this example.

3

Suppress the warning after we check that everything is okay manually

Till now, we are still not done. We may still get a warning as follows,

This is called an unchecked warning. And it is caused because the compiler doesn't know whether we can do the casting safely. a.k.a, we are not sure whether the all the elements in the Java array have a subtype relationship with T, thus an explicit casting maybe dangerous!

For example, the following code will generate a ClassCastException, which is a runtime error.


To suppress this warning, the first thing we need to do is

be 100% sure that all the elements in your array are of type T or at least have subtype relationship with T.

Then ,we can use the code as follows to suppress the warning

Using @SuppressWarnings actually means that we are more sure than the compiler that there will be no error with this piece of code!

Now, our final code should look like as follows,

@SuppressWarnings cannot apply to an assignment but only to the declartion. That's why we must use it before Line 16 instead of before Line 17.

Raw Types

A raw type is a generic type used without type arguments. For example, we have a Seq<T>,

The code will compile! But it's just that the compiler cannot help us to check the type-safety during the compile-time!

Raw type should never be used in your code! But till now, we have a small exception.

For example, in the following code snippet, new Comparable[size] is actually a use of raw types.

In fact, merely doing so will still give us a warning! And that is a rawtype warning because Line 6 actually is using raw type! But since we are sure T will be replaced by Comparable after type erausre, let's just allow this kind of stuff first.

But to fully make this code warning-free. We need to suppress the rawtype warning. Thus, we need to modify our code as follows,

Classic Problems

  1. There are some classic problems related to type erasure and generics covered during Generics. Remember to take a look before exams!

Last updated