Last Modified 2022.1.18

Generic Types

Java 5 introduced Generic for generic algorithm implementations and rigorous type checking at compile time. The following example defines a box where you can store all kinds of references.

package net.java_school.examples;

public class Box<T> {
  private T t;

  public void set(T t) {
    this.t = t;
  }

  public T get() {
    return t;
  }
}

T is called a type parameter. You can use T anywhere in the class body but must specify the type of T when creating the Box object as follow.

Box<Integer> intBox = new Box<Integer>();//In <Integer>, Integer is called a type argument.

Due to Java 7 type inference, you can reduce the above statement to:

Box<Integer> intBox = new Box<>();

Type Parameter Naming Conventions

  • E - Element (used extensively by the Java Collections Framework)
  • K - Key
  • N - Number
  • T - Type
  • V - Value
  • S,U,V etc. - 2nd, 3rd, 4th types

Raw Types

A raw type is the name of a generic class or an interface without any type arguments.

To create a parameterized type of Box<T>, you supply an actual type argument for the formal type parameter T:

Box<Integer> intBox = new Box<Integer>();

If you omit the actual type argument, you can create the primitive type of Box<T> as follow.

Box rawBox = new Box();//Box is the raw type of the generic type Box<T>

Note that a non-generic class or interface type is not a raw type.

Raw types show up in legacy code because many API classes (such as the Collections classes) were not generic before Java 5. When using raw types, you essentially get pre-generics behavior - a Box gives you Objects. For backward compatibility, assigning a parameterized type to its raw type is allowed:

Box<Integer> intBox = new Box<Integer>();
Box rawBox = intBox;//OK

But if you assign a raw type to a parameterized type, you get a warning:

Box rawBox = new Box();
Box<Integer> intBox = rawBox;//warning: unchecked conversions

You also get a warning if you use a raw type to invoke generic methods defined in the corresponding generic type:

Box<String> strBox = new Box<>();
Box rawBox = strBox;
rawBox.set(8);//warning: unchecked invocation to set(T)

The warning shows that raw types bypass generic type checks, deferring the catch of unsafe code to runtime. Therefore, you should avoid using raw types.

Generic Methods

Generic methods are methods that introduce their type parameters. The type parameter's scope is limited to the method where it is declared. Static and non-static generic methods and generic class constructors are allowed.

The syntax for a generic method includes a list of type parameters, inside angle brackets, which appear before the method's return type.

The Util class includes a generic method, compare, which compares two Pair objects.

package net.java_school.examples;

public interface Pair<K,V> {
  public K getKey();
  public V getValue();
}
package net.java_school.examples;

public class IdPasswdPair<K,V> implements Pair<K,V> {
  private K key;
  private V value;

  public IdPasswdPair(K key, V value) {
    this.key = key;
    this.value = value;
  }

  @Override
  public K getKey() {
    return key;
  }

  @Override
  public V getValue() {
    return value;
  }
}
package net.java_school.examples;

public class Util {

  //Generic Method
  public static <K,V> boolean compare(Pair<K,V> p1, Pair<K,V> p2) {
    return p1.getKey().equals(p2.getKey()) && p1.getValue().equals(p2.getValue());
  }

  public static void main(String[] args) {
    if (args.length == 2) {
      Pair<String, String> inputData = new IdPasswdPair<>(args[0], args[1]);
      Pair<String, String> storedData = new IdPasswdPair<>("xman31", "1987qwertY");
      boolean isSame = Util.compare(inputData, storedData);
      if (isSame) {
        System.out.println("Login succeeded.");
      } else {
        System.out.println("Login failed. Please check your ID and password.");
      }
    } else {
      System.out.println("How to run: java net.java_school.examples.Util 'ID' 'Password'");
    }
  }
}

The complete syntax for invoking the compare method would be:

boolean isSame = Util.<String, String>compare(inputData, storedData);

The type has been explicitly provided, as shown in bold. Generally, this can be left out and the compiler will infer the type that is needed:

boolean isSame = Util.compare(inputData, storedData);

Generally, the type in bold can be left out, and the compiler will infer the needed type.

boolean isSame = Util.compare(inputData, storedData);

This feature, known as type inference, allows you to invoke a generic method as an ordinary method without specifying a type between angle brackets.

Bounded Type Parameters

A method that operates on numbers may only want to accept Number or its subclasses instances. Bounded type parameters allow you to create such methods. To declare a bounded type parameter, list its name, followed by the extends keyword, followed by its upper bound.

package net.java_school.examples;

public class Box<T extends Number> {
  private T t;

  public void set(T t) {
    this.t = t;
  }

  public T get() {
    return t;
  }
}

<T extends Number> is called a type parameter section in the Box class declaration. Bounds can be interfaces.

<T extends java.io.Serializable>

Even if the bound is an interface, use the extends keyword.

You can place one or more bounds in the Type Parameters section. If one of the bounds is a class, it must be specified first.

<T extends Aclass & Binterface & Cinterface>

Generic Methods and Bounded Type Parameters

Bounded type parameters are key to the implementation of generic algorithms. Consider the following method that counts the number of elements in an array T[] that are greater than a specified element elem.

public static <T> int countGreaterThan(T[] anArray, T elem) {
  int count = 0;
  for (T e : anArray)
    if (e > elem)  // compiler error
      ++count;
  return count;
}

The implementation of the method is straightforward, but it does not compile because the greater than operator (>) applies only to primitive types such as short, int, double, long, float, byte, and char. You cannot use the > operator to compare objects. To fix the problem, use a type parameter bounded by the Comparable<T> interface:

public interface Comparable<T> {
  public int compareTo(T o);
}

Java already has the Comparable<T> interface.

package java.lang;

public interface Comparable<T> {
  public int compareTo(T o);
}

The resulting code will be:

public static <T extends Comparable<T>> int countGreaterThan(T[] anArray, T elem) {
  int count = 0;
  for (T e : anArray)
    if (e.compareTo(elem) > 0)
      ++count;
  return count;
}

The compareTo method of the Comparable<T> interface has a promised implementation. Assuming it runs like a.compareTo(b), the compareTo method should return a value like this:

if a == b, 0.
if a > b, 1.
if a < b, -1.

The Integer class inherits the Number class, and it also implements Comparable<Integer> Interface. Double, Long, Float, Short, and Byte inherit the Number class and implement the Comparable interface.

package net.java_school.examples;

public class GenericMethodsWithBoundedTypeParametersTest {

  public static <T extends Comparable<T>> int countGreaterThan(T[] array, T elem) {
    int count = 0;
    for (T e : array)
      if (e.compareTo(elem) > 0)
        ++count;
    return count;
  }

  public static void main(String[] args) {
    Integer[] arr = {1,2,3,4,5,6,7,8,9,10};
    int count = countGreaterThan(arr,7); //The number of elements in the array arr whose values are greater than 7
    System.out.println(count);
  }
}
3

Generics, Inheritance, and Subtypes

Now consider the following method:

public void boxTest(Box<Number> n) { /* ... */ }

As you might expect, are you allowed to pass in Box<Integer> or Box<Double>? The answer is "no" because Box<Integer> and Box<Double> are not subtypes of Box<Number>.

Number and Integer

Box<Number> and Box<Integer> Box<Integer> is not a subtype of Box<Number> even though Integer is a subtype of Number.

Object and Box<Number> and Box<Integer>

Note: Given two concrete types A and B (for example, Number and Integer), MyClass<A> has no relationship to MyClass<B>, regardless of whether or not A and B are related.

Using the Collections classes as an example, ArrayList<E> implements List<E>, and List<E> extends Collection<E>. So ArrayList<String> is a subtype of List<String>, which is a subtype of Collection<String>. So long as you do not vary the type argument, the subtyping relationship is maintained.

4

Type Inference

Type inference is the ability of the Java compiler to determine the type arguments by looking at method declarations and calls.

The following generic method addBox() defines a type parameter called U.

public static <U> void addBox(U u, List<Box<U>> boxes) {...}

To call this generic method, you need to specify the type argument.

BoxDemo.<Integer>addBox(Integer.valueOf(10), listOfIntegerBoxes);

Due to Type Inference, the user does not need to specify type arguments in most cases. So, you can call the above generic method like a regular method as follows.

BoxDemo.addBox(Integer.valueOf(10), listOfIntegerBoxes);

Type Inference allows simple diamond replacement of type arguments required to call the constructor of a generic class. <> is informally called a diamond.

For example, consider the following variable declaration:

Map<String, List<String>> myMap = new HashMap<String, List<String>>();

You can substitute the parameterized type of the constructor with an empty set of type parameters (<>):

Map<String, List<String>> myMap = new HashMap<>();

Constructors can have their own formal type parameters in both generic and non-generic classes.

class MyClass<X> {
  <T> MyClass(T t) {
    //...
  }
}

The following code creates an instance from MyClass.

MyClass<Integer> myObject = new MyClass<>("");

The compiler infers the type Integer for the formal parameter X of the generic class MyClass<X> and the type String for the formal parameter T of the constructor of this generic class. Before Java 7. You had to specify the type arguments as follows.

MyClass<Integer> myObject = new MyClass<Integer>("");

The inference algorithm makes inferences using invocation arguments and target types.

Target Types
Integer i = Integer.parseInt("10");
The return type of the parseInt method is int, but the type of variable i that stores this value is Integer. Therefore, the target type is Integer.

The following is the Collections' emptyList method declaration.

static <T> List<T> emptyList();

Due to the Type Inference, You can call the Collections' emptyList method like below.

List<String> listOne = Collections.emptyList();

List<String> is the target type. Since the emptyList method returns a List<T> type, the compiler infers T is a String. Before Java 7, you have to specify the type of T as follows.

List<String> listOne = Collections.<String>emptyList();

Let's look at another situation.

void processStringList(List<String> stringList) {
  //process
}

In Java 7, the following statement does not compile:

processStringList(Collections.emptyList());

Java 7 compiler generates the following error message:

List<Object> cannot be converted to List<String>

Since the compiler needs a value of type T, it starts with the value Object. Therefore, when Collections.emptyList is called, a value of type List<Object> is returned, which is not compatible with the method processStringList. Thus, in Java 7, you must specify the value of the type argument as follows.

processStringList(Collections.<String>emptyList());

Java 8 extended the notion of a target type to include method arguments. The Java 8 compiler infers the type parameter T as String because the processStringList method needs the List<String> type argument. Thus, in Java 8, the following statement compiles:

processStringList(Collections.emptyList());

Wildcards

The question mark (?), called the wildcard, represents an unknown type in generic code. You cannot use the wildcard as a type argument for a generic method invocation, a generic class instance creation.

Upper Bounded Wildcards

To write the method that works on lists of Number and the subtypes of Number, such as Integer, Double, and Float, you would specify List<? extends Number>. The term List<Number> is more restrictive than List<? extends Number> because the former matches a list of type Number only, whereas the latter matches a list of type Number or any of its subclasses.

package net.java_school.examples;

import java.util.Arrays;
import java.util.List;

public class WildCardTest {

  public static double sumOfList(List<? extends Number> list) {
    double s = 0.0;
    for (Number n : list) {
      s += n.doubleValue();
    }
    return s;
  }

  public static void main(String[] args) {
    List<Integer> li = Arrays.asList(1,2,3);
    System.out.println("sum = " + sumOfList(li));
  }
}
sum = 6.0
Arrays.asList
The Arrays.asList method converts the specified array and returns a fixed-size list. Note the fixed size. The following causes a runtime error.
List<Integer> list = Arrays.asList(1,2,3);
list.add(4);//Runtime error

Unbounded Wildcards

A wildcard, such as List<?>, is called Unbounded Wildcards. There are two scenarios where an unbounded wildcard is a helpful approach:

  • If you are writing a method that can be implemented using functionality provided in the Object class.
  • If you are writing the code using methods in the generic class that don't depend on the type parameter. For example, List.size or List.clear. Class<?> is often used because most Class<T> methods do not depend on T.

Consider the following method, printList:

package net.java_school.examples;

import java.util.Arrays;
import java.util.List;

public class PrintListTest {
  public static void printList(List<Object> list) {
    for (Object elem : list) {
      System.out.print(elem + " ");
    }
    System.out.println();
  }

  public static void main(String[] args) {
    List<Integer> li = Arrays.asList(1,2,3);
    List<String> ls = Arrays.asList("one","two","three");
    printList(li);//compile-time error
    printList(ls);//compile-time error
  }
}

Note: The following compilation error occurs in printList(li); and printList(ls);.
The method printList(List<Object>) is not applicable for the arguments (List<Integer>)
The method printList(List<Object>) is not applicable for the arguments (List<String>)

The goal of printList is to print a list of any type, but it fails to achieve that goal — it prints only a list of Object instances; it cannot print List<Integer>, List<String>, List<Double>, and so on, because they are not subtypes of List<Object>.

To write a generic printList method, use List<?>. Because for any concrete type A, List<A> is a subtype of List<?>, you can use printList to print a list of any type:

package net.java_school.examples;

import java.util.Arrays;
import java.util.List;

public class PrintListTest {
  public static void printList(List<?> list) {
    for (Object elem : list) {
      System.out.print(elem + " ");
    }
    System.out.println();
  }

  public static void main(String[] args) {
    List<Integer> li = Arrays.asList(1,2,3);
    List<String> ls = Arrays.asList("one","two","three");
    printList(li);
    printList(ls);
  }
}
1 2 3
one two three

Lower Bounded Wildcards

A lower bounded wildcard is expressed using the wildcard character ('?'), followed by the super keyword, followed by its lower bound: <? super A>.

To write the method that works on lists of Integer, the supertypes of Integer, such as Integer, Number, and Object, you would specify List<? super Integer>. The term List<Integer> is more restrictive than List<? super Integer> because the former matches a list of type Integer only, whereas the latter matches a list of any type that is a supertype of Integer.

public static void addNumbers(List<? super Integer> list) {
  for (int i = 1; i <= 10;i++) {
    list.add(i);
  }
}

Wildcards and Subtyping

Although Integer is a subtype of Number, List<Integer> is not a subtype of List<Number>, and these two types are not related. The common parent of List<Number> and List<Integer> is List<?>.

Generic listParent

Type Erasure

For generics, the Java compiler applies type erasure to:

  • Replace all type parameters in generic types with their bounds or Object if the type parameters are unbounded. Therefore, the produced bytecode contains only regular classes, interfaces, and methods.
  • Insert type casts if necessary to preserve type safety.
  • Generate bridge methods to preserve polymorphism in extended generic types.

Consider the following generic class that represents a node in a singly linked list:

package net.java_school.examples;

public class Node<T> {

  private T data;
  private Node<T> next;

  public Node(T data, Node<T> next) {
    this.data = data;
    this.next = next;
  }

  public T getData() {
    return data;
  }
}

Because the type parameter T is unbounded, the Java compiler replaces it with Object:

package net.java_school.examples;

public class Node {

  private Object data;
  private Node next;

  public Node(Object data, Node next) {
    this.data = data;
    this.next = next;
  }

  public Object getData() {
    return data;
  }
}

In the following example, the generic Node class uses a bounded type parameter:

package net.java_school.examples;

public class Node<T extends Comparable<T>> {

  private T data;
  private Node<T> next;

  public Node(T data, Node<T> next) {
    this.data = data;
    this.next = next;
  }

  public T getData() {
    return data;
  }
}

The Java compiler replaces the bounded type parameter T with the first bound class, Comparable:

package net.java_school.examples;

public class Node {

  private Comparable data;
  private Node next;

  public Node(Comparable data, Node next) {
    this.data = data;
    this.next = next;
  }

  public Comparable getData() {
    return data;
  }
}

The Java compiler also erases type parameters in generic method arguments. Consider the following generic method:

//Counts the number of occurrences of elem in anArray.
public static <T> int count(T[] anArray, T elem) {
  int cnt = 0;
  for (T e : anArray) {
    if (e.equals(elem)) {
      ++cnt;
    }
  }
  return cnt;
}

Because T is unbounded, the Java compiler replaces it with Object:

public static int count(Object[] anArray, Object elem) {
  int cnt = 0;
  for (Object e : anArray) {
    if (e.equals(elem)) {
      ++cnt;
    }
  }
  return cnt;
}

Suppose the following classes are defined:

class Shape { /* ... */ }
class Circle extends Shape { /* ... */ }
class Rectangle extends Shape { /* ... */ }

You can write a generic method to draw different shapes:

public static <T extends Shape> void draw(T shape) { /* ... */ }

The Java compiler replaces T with Shape:

public static void draw(Shape shape) { /* ... */ }

Effects of Type Erasure and Bridge Methods

Sometimes type erasure causes a situation that you may not have anticipated. The following example shows how this can occur.

package net.java_school.examples;

public class Box<T> {

  private T t;

  public void set(T t) {
    this.t = t;
  }

  public T get() {
    return t;
  }
}
package net.java_school.examples;

public class IntBox extends Box<Integer> {

  @Override
  public void set(Integer t) {
    super.set(t);
  }

  @Override
  public Integer get() {
    return super.get();
  }
}
package net.java_school.examples;

public class BoxTest {

  public static void main(String[] args) {
    IntBox ibox = new IntBox();
    Box box = ibox;
    box.set("Hello World!");//runtime error!
  }
}

box.set("Hello World!") throws Runtime exception as follows:

Exception in thread "main" java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Integer
	at net.java_school.examples.IntBox.set(IntBox.java:1)
	at net.java_school.examples.BoxTest.main(BoxTest.java:8)

ClassCastException: The message "java.lang.String can not be cast to java.lang.Integer" is due to the bridge method created by the compiler. Box and IntBox are changed through the type erase process as follows.

When compiling a class or interface that extends a parameterized class or implements a parameterized interface, the compiler may need to create a synthetic method called a bridge method as part of the type erasure process. You usually don't need to worry about bridge methods, but you might be puzzled if one appears in a stack trace.

After type erasure, the Box and intBox classes become:

package net.java_school.examples;

public class Box {

  private Object t;

  public void set(Object t) {
    this.t = t;
  }

  public Object get() {
    return t;
  }
}
package net.java_school.examples;

public class IntBox extends Box {

  @Override
  public void set(Integer t) {
    super.set(t);
  }

  //Bridge method generated by the compiler
  public void set(Object t) {
    set((Integer) t);
  }

  @Override
  public Integer get() {
    return super.get();
  }
}

After type erasure, the method signatures do not match. The Box's set(T) becomes set(Object). As a result, the IntBox set(Integer) method does not override the Box set(Object) method.

A Java compiler generates a bridge method to ensure that subtyping works as expected to solve this problem and preserve the polymorphism of generic types after type erasure.

For the IntBox class, the compiler generates the following bridge method for the set(Integer).

//Bridge method generated by the compiler
public void set(Object t) {
  set((Integer) t);
}

The bridge method IntBox.set(Object) delegates to the original IntBox.set(Integer) method. As a result, the box.set("Hello World!"); statement calls the method IntBox.set(Object), and a ClassCastException is thrown because "Hello World!" can't be cast to Integer.

Non-Reifiable Types

A reifiable type is a type whose type information is fully available at runtime. It includes primitives, non-generic types, raw types, and invocations of unbound wildcards.

Non-reifiable types are types where information has been removed at compile-time by type erasure — invocations of generic types that are not defined as unbounded wildcards. A non-reifiable type does not have all of its information available at runtime. Examples of non-reifiable types are List<String> and List<Number>; the JVM cannot tell the difference between these types at runtime.

Heap Pollution

Variable of a parameterized type referring to not parameterized type object occurs heap pollution. This situation occurs if the program performed some operation that gives rise to an unchecked warning at compile-time. An unchecked warning appears if, either at compile-time (within the limits of the compile-time type checking rules) or runtime, you cannot verify the correctness of an operation involving a parameterized type (for example, a cast or method call). For example, heap pollution occurs when mixing raw and parameterized types or performing unchecked casts.

In typical situations, when compiling all code simultaneously, the compiler issues an unchecked warning to draw your attention to potential heap pollution. But when you compile sections of your code separately. It is difficult to detect the potential risk of heap pollution. If you ensure that your code compiles without warnings, then no heap pollution can occur.

Potential Vulnerabilities of Varargs Methods with Non-Reifiable Formal Parameters

Generic methods that include vararg input parameters can cause heap pollution.

Note: Varargs is a feature introduced in Java 5. It allows a method to take an arbitrary number of values as arguments.

package net.java_school.examples;

public class VarargsTest {

  public static void sum(int ... a) {
    int sum = 0;
    for (int i : a) {
      sum += i;
    }
    ystem.out.println(sum);
  }
	
  public static void main(String[] args) {
    sum();
    sum(1);
    sum(1,2,3);
    sum(1,2,3,4);
  }
}
0
1
6
10

Consider the following ArrayBuilder class:

public class ArrayBuilder {

  public static <T> void addToList (List<T> listArg, T... elements) {
    for (T x : elements) {
      listArg.add(x);
    }
  }

  public static void faultyMethod(List<String>... l) {
    Object[] objectArray = l;     // Valid
    objectArray[0] = Arrays.asList(42);
    String s = l[0].get(0);       // ClassCastException thrown here
  }
}

The following example, HeapPollutionExample, uses the ArrayBuiler class:

public class HeapPollutionExample {

  public static void main(String[] args) {

    List<String> stringListA = new ArrayList<String>();
    List<String> stringListB = new ArrayList<String>();

    ArrayBuilder.addToList(stringListA, "Seven", "Eight", "Nine");
    ArrayBuilder.addToList(stringListB, "Ten", "Eleven", "Twelve");
    List<List<String>> listOfStringLists =
      new ArrayList<List<String>>();
    ArrayBuilder.addToList(listOfStringLists,
      stringListA, stringListB);

    ArrayBuilder.faultyMethod(Arrays.asList("Hello!"), Arrays.asList("World!"));
  }
}

When compiled, the following warning occurs by the definition of the ArrayBuilder.addToList method:

warning: [varargs] Possible heap pollution from parameterized vararg type T

When the compiler encounters a varargs method, it translates the varargs formal parameter into an array. However, the Java programming language does not permit the creation of arrays of parameterized types. In the method ArrayBuilder.addToList, the compiler translates the varargs formal parameter T... elements to the formal parameter T[] elements, an array. However, the compiler converts the varargs formal parameter to Object[] elements because of type erasure. Consequently, there is a possibility of heap pollution.

The following statement assigns the varargs formal parameter l to the Object array objectArgs:

Object[] objectArray = l;

This statement can potentially introduce heap pollution.

Restrictions on Generics

To use Java generics effectively, you must consider the following restrictions:

  • Cannot Instantiate Generic Types with Primitive Types
  • Cannot Create Instances of Type Parameters
  • Cannot Declare Static Fields Whose Types are Type Parameters
  • Cannot Use Casts or instanceof With Parameterized Types
  • Cannot Create Arrays of Parameterized Types
  • Cannot Create, Catch, or Throw Objects of Parameterized Types
  • Cannot Overload a Method Where the Formal Parameter Types of Each Overload Erase to the Same Raw Type

Cannot Instantiate Generic Types with Primitive Types

Consider the following parameterized type:

class Password<T> {
  private T t;

  public Password(T t) {
    this.t = t;
  }
}

When creating a Password object, you cannot substitute a primitive type for the type parameter T:

Password<int> pw = new Password<>(19019);//compile-time error

Cannot Create Instances of Type Parameters

You cannot create an instance of a type parameter. For example, the following code causes a compile-time error:

public static <E> append(List<E> list) {
  E elem = new E();//compile-time error
  list.add(elem);
}

Cannot Declare Static Fields Whose Types are Type Parameters

A class's static field is a class-level variable shared by all non-static objects of the class. Hence, static fields of type parameters are not allowed. Consider the following class:

public class MobileDevice<T> {
  private static T os;

  // ...
}

If static fields of type parameters were allowed, then the following code would be confused:

MobileDevice<Smartphone> phone = new MobileDevice<>();
MobileDevice<Pager> pager = new MobileDevice<>();
MobileDevice<TabletPC> pc = new MobileDevice<>();

Because the static field os is shared by phone, pager, and pc, what is the actual type of os? It cannot be Smartphone, Pager, and TabletPC at the same time. You cannot, therefore, create static fields of type parameters.

Cannot Use Casts or instanceof With Parameterized Types

Because the Java compiler erases all type parameters in generic code, you cannot verify which parameterized type for a generic type is being used at runtime:

public static <E> void rtti(List<E> list) {
  if (list instanceof ArrayList<Integer>) { //compile-time error
    //..
  }
}

The runtime does not distinguish between ArrayList<Integer> and ArrayList<String>. The most you can do is to use an unbounded wildcard to verify that the list is an ArrayList:

public static void rtti(List<?> list) {
  if (list instanceof ArrayList<?>) { //OK; instanceof requires a reifiable type
    //..
  }
}

Cannot Create Arrays of Parameterized Types

You cannot create arrays of parameterized types. For example, the following code does not compile:

List<Integer>[] arrayOfLists = new ArrayList<Integer>[2];//compile-time error

The following code illustrates what happens when you insert different types into an array:

Object[] strings = new String[2];
strings[0] = "Hello"; //OK
strings[1] = 2019;//An ArrayStoreException is thrown.

If you try the same thing with a generic list, there would be a problem:

Object[] stringLists = new List<String>[2]; //compiler error, but pretend it's allowed
stringLists[0] = new ArrayList<String>();// OK
stringLists[1] = new ArrayList<Integer>();// An ArrayStoreException should be thrown, but the runtime can't detect it.

If arrays of parameterized lists were allowed, the previous code would fail to throw the desired ArrayStoreException.

Cannot Create, Catch, or Throw Objects of Parameterized Types

A generic class cannot extend the Throwable class directly or indirectly. For example, the following classes will not compile:

// Extends Throwable indirectly
class MathException<T> extends Exception { .. } //compile-time error
// Extends Throwable directly
class QueneFullException<T> extends Throwable { .. } //compile-time error

A method cannot catch an instance of a type parameter.

public static <T extends Exception> void execute(List<T> jobs) {
  try {
    for (T job : jobs) {
      //..
    }
  } catch (T e) {//compile-time error: Cannot use the type parameter T in a catch block
    //..
  }
}

You can, however, use a type parameter in a throws clause:

class Parser<T extends Exception> {
  public void parse(File file) throws T { //OK
    //..
  }
}

Cannot Overload a Method Where the Formal Parameter Types of Each Overload Erase to the Same Raw Type

A class cannot have two overloaded methods that will have the same signature after type erasure.

public class Example {
  public void print(Set<String> strSet) { .. }
  public void print(Set<Integer> intSet) { .. }
}

The above code will generate a compile-time error.

References