Lec 05 - Generics
Generic Types
The motivations, (a.k.a benefits) of using generics are:
enable you to detect errors at compile time rather than at runtime.
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>();AorA<T>is a generic type withTas the type parameter.A<Integer>is a parameterized type withIntegeras the type argument forT.
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.
Instantiate a Generic Type
To use a generic type, we have to pass in type arguments, which itself can be
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.
Using raw types (e.g., new Pair() without type arguments) can lead to unchecked conversion warnings. Always use the diamond operator <> (this will let the compiler do the type inference) or provide explicit type arguments inside the <> to ensure type safety.
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.
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
Sis fixed asString.Its second type parameter
Tuses the first type parameterTinDictEntryas the type argument.
Once a generic type is instantiated, it is called a parameterized type.
Always make sure which generic type is the one you want to instantiate!
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>inA<Integer>) are irrelevant for static methods and cannot be mixed with method type parameters in the call.
For example,
If you pass type argument to class A, e.g. A<String>, you will get a compilation error also!
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)
"Bounded" used to mean adding restrictions, but here it actually serves to add more usages! Interesting!
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.
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!
For the first type parameter
SinPair<S,T>For the generic type
Pair<S,T>
Declaration vs. Usage of Generic Parameters:
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>.
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:
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.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:
Type checking (Before type erasure)
Type erausre (During type erasure)
Type Checking
Java will do the type checking during the compile time to make sure that the code compile!
See more application from Lab 03!!! Must see!!!
Type Erasure
This happens in the compile-time also, after the type checking.
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.
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 withObject.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).
This step is done implicitly.
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
Objectreferences (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:
the type casting process when you create an array with type parameters. See Create Arrays with Type parameters
raw types are used. (This actually will cause a
rawtypewarning instead of anuncheckedwarning). 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 <: , 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[] ais allowed.
So, how can we create arrays with type parameters? To get around with this, we should
Determine the type Q of the type parameter after type erasure
Let's define the type as Q. For example,
If the type parameter is unbounded, then
Qwill beObjectIf the type parameter is bounded, e.g.,
T extends Comparable<T>, thenQwill be the bound type, which isComparablein our example.
Create a Java array with type Q[] cast it to type T[]
For example, we will have
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
Tor at least have subtype relationship withT.
Then ,we can use the code as follows to suppress the warning
Now, our final code should look like as follows,
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
There are some classic problems related to type erasure and generics covered during Generics. Remember to take a look before exams!
Last updated