What is a generic?

ArrayList

= ArrayList

= ArrayList

= ArrayList

= ArrayList

= ArrayList

= ArrayList






Real Java generics are not real generics, but pseudo-generics, because Java does type erasure at compile time. To understand Java generics, you must understand generic erasure. At run time, the JVM doesn’t recognize generics, so there’s no such thing as generics at run time. Generics only make sense at compile time.

That is, ArrayList

and ArrayList

are both types of ArrayList in the JVM, and ArrayList is also called a primitive type.

This code returns true, which tells us that they all return the same type, ArrayList.

But generics in C# are true generics, namely ArrayList

and ArrayList

are two types.

Type erasure

So what is type erasure?

Type erasure is when the compiler compiles Java code, erasing generics by replacing them with type Object if they are unbounded, or with the first bounded type if they are bounded.

A generic class

The most common is the presence of generics in a defined class. Let’s look at how type erasure is represented in a class.

  • Unbounded generics:
class Node<T> {
    T element;
    
    public T getNode(a){
        return element;
    }
    
    public void set(T t){
        this.element = t; }}Copy the code

What happens to such unbounded generics when compiled by the compiler, which, as we explained above, replaces the generic T with Object

class Node {
    Object element;
    
    public Object getNode(a){
        return element;
    }
    
    public void set(Object t){
        this.element = t; }}Copy the code
  • Bounded generics 1:

For bounded generics, instead of replacing T directly with Object, look at the following code:

class Node<T extends Comparable> {
    T element;
    
    public T getNode(a){
        return element;
    }
    
    public void set(T t){
        this.element = t; }}Copy the code

This code is a bounded generic, that is, the type of T must be Comparable or a subclass of Comparable, and the compiled generic will be replaced with Comparable

class Node<T extends Comparable<T>> {
    Comparable element;
    
    public Comparable getNode(a){
        return element;
    }
    
    public void set(Comparable t){
        this.element = t; }}Copy the code
  • Bounded generics 2:

If the type parameters of A bounded generic have both classes and interfaces, for example, A is A class and B and C are interfaces

class Node<T extends A & B & C> {
    T element;
    
    public T getNode(a){
        return element;
    }
    
    public void set(T t){
        this.element = t; }}Copy the code

A must be written to the far left, otherwise A compilation error will occur and the generic T will be replaced with type A.

  • Bounded generics 3:

If more than one type parameter exists in a bounded generic, only the leftmost type is used to replace the generic in type erasing.

class Node<T extends Comparable<T> & Serializable> {
    T element;
    
    public T getNode(a){
        return element;
    }
    
    public void set(T t){
        this.element = t; }}Copy the code

In this way, the generic T is replaced with Comparable.

class Node<T extends Serializable & Comparable<T>> {
    T element;
    
    public T getNode(a){
        return element;
    }
    
    public void set(T t){
        this.element = t; }}Copy the code

However, if we transpose the two types, placing the Serializable type on the far left, the generic T will be replaced with the Serializable type.

We can see from the examples of bounded generics 2 and 3 that, for bounded generics, generic erasing replaces the generic type with the first parameter type, whereas for parameter types that have both classes and interfaces, the class must be written in the first parameter type, that is, the class must precede the interface. That is, the type of the class is used before the interface type is used.

Generic method

Generics are not restricted to classes; they can also be applied to methods.

public T getNode(a){
    return element;
}
Copy the code

For non-static methods, type parameters can be class-defined or custom.

public <U> U get(U u){
    return u;
}
Copy the code

Similar to the generic use of a class, it can consist of a set of type parameters enclosed in Angle brackets and placed before the return value of a method. This method takes a parameter of type U and returns a value of type U.

For static methods, type parameters must be custom, not class-defined, and must precede the return value of the method.

public static <T> int print(T t){
    System.out.println(t);
}
Copy the code

As for why you can’t use generics defined in a class, it’s because generics defined in a class are used when creating objects, and static methods belong to a class, not any class. Such as:

class Node<T> {
    T element;
    
    public T getNode(a){
        return element;
    }
    
    public void set(T t){
        this.element = t;
    }
    
    // static method A, wrong way to write
    public static T get(a){
        return element;
    }
    
    // static method B, the correct way to write
    public static <T> T get(T t){
        returnt; }}Copy the code

When we write code, we can write Node

, Node

, so is T of type Integer or String in static methods? The JVM cannot infer because it would be incorrect to choose either.

T in static method B and T in class are not the same generic T; they are independent of each other.

When we use generic static methods, we generally do not need to write generics directly; the compiler automatically deduces them from the parameters passed in.

Node.<String>get("aaaa");

For example, in this code, we can omit the Angle bracket type argument, because the compiler will infer it for itself, equivalent to the following:

Node.get("aaaa");

Polymorphism and generics

Let’s consider this case:

class Node<T> {
    T element;
    
    public T getNode(a){
        return element;
    }
    
    public void set(T t){
        this.element = t; }}class MyNode extends Node<Integer>{
    @Override
    public void set(Integer t){
        super.set(t); }}Copy the code

Consider the following code:

MyNode myNode = new MyNode();
myNode.setEle(5);
Node n = myNode;
n.setEle("abc");
Integer x = myNode.getEle();
Copy the code

This code will pass at compile time, but will throw a cast exception at run time. What causes this whole exception to happen is that a cast will occur on the fourth line of code, and the cast will convert String to Integer, so an exception is thrown.

Because we know that the Node type is erased at compile time, when we accept MyNode with a variable statically of type Node, we see a method with the method signature set(Object t).

In actual execution, when we pass a string argument, it is the set(Object t) method in MyNode that executes (related to method dispatch, see section 8.3.2 in Understanding Java Virtual Machine 3). Set (Integer t) overrides the set(t t) method of the Node class. There is no Node type with a signature set(Integer t). Even after compiling, there is only one set(Object t). So how did the Java development team solve this problem?

When this happens, the compiler generates a bridge method in the MyNode class whose signature is SET (Object t), which actually overrides the set(Object t) in Node.

The internal implementation of the bridge method is quite simple:

public void set(Object t){
    set((Integer) t);
}
Copy the code

So the code in the MyNode class would look like this:

class MyNode extends Node<Integer>{

    // Bridge method, generated by the compiler
    public void set(Object t){
        set((Integer) t);
    }
	
    @Override
    public void set(Integer t){
        super.set(t); }}Copy the code

So when we call n.et (” ABC “), we are actually calling set(Object t) and casting a String value to an Integer, which is why we throw a cast exception at runtime.

Scenarios where generics cannot be used

Primitive types cannot be used as type parameters

class Pair<K.V> {

    private K key;
    private V value;

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

    // ...
}
Copy the code

When creating objects of this type, you cannot use the primitive type as the value of the type parameters K, V.

Pair

p = new pair<>(1, ‘a’); An error is thrown at compile time
,>

You can only use non-basic types as values of type parameters K and V.

Pair

p = new Pair(1, ‘a’); Correct usage
,>

Cannot create an instance of a type parameter

public static <E> void append(List<E> list) {
    E elem = new E();  // Error thrown at compile time
    list.add(elem);
}
Copy the code

We cannot create instances for type parameters, otherwise an error will be thrown.

We can use reflection to implement this requirement:

public static <E> void append(List<E> list, Class<E> c) {
    E elem = cls.newInstance();
    list.add(elem);
}
Copy the code

You cannot set the type of a statically typed field to a type parameter

public class MobileDevice<T> {
    private static T os; // Error thrown at compile time

    // ...
}
Copy the code

Because static fields belong to classes, not objects, it is impossible to determine what the exact type of the parameter type T is.

For example:

MobileDevice<Integer> md1 = new MobileDevice<>();

MobileDevice<String> md2 = new MobileDevice<>();

MobileDevice<Double> md3 = new MobileDevice<>();

Because the static field OS is shared by objects MD1, MD2, and md3, what is the type of the OS field? This cannot be inferred or determined, so you cannot set the type of a statically typed field to a type parameter.

You cannot use cast or instanceof with parameterized types

public static <E> void rtti(List<E> list) {
    if (list instanceof ArrayList<Integer>) {  // Error thrown at compile time
        // ...}}Copy the code

This is also easy to understand because generics are erased at compile time, so at run time, you don’t know what the type parameters are, so you can’t tell the difference between ArrayList

and ArrayList

, so run time can only recognize the primitive ArrayList.

All you can do is use a wildcard (wildcard? To verify that the type is ArrayList:

public static <E> void rtti(List<E> list) {
    if (list instanceofArrayList<? >) {/ / right
        // ...}}Copy the code

In general, we can’t convert a type to a parameterized type, either, unless the parameterized type is a wildcard modifier

List<Integer> li = new ArrayList<>();
List<Number>  ln = (List<Number>) li;  // Compile-time errorList<? > n = (List<? Li >);List
      )
Copy the code

However, in some cases, the compiler knows that type parameters are always valid and allows casting

List<String> l1 = ... ; ArrayList<String> l2 = (ArrayList<String>)l1;/ / right
Copy the code

Cannot create an array of parameterized types

List

[] arrays = new List

[2]; Error thrown at compile time

To understand this constraint, let’s start with a simple example:

Object[] arr = new String[10];
arr[0] = "abc"; / / right
arr[1] = 10; // Throw ArrayStoreException because the array can only accept strings
Copy the code

With that example in mind, let’s look at the following example:

Object[] arr = new List<String>[10]; // Assuming we could do this, it would actually throw a compile-time error
arr[1] = new ArrayList<String>(); // Normal execution
arr[0] = new ArrayList<Integer>(); // Based on the above example, ArrayStoreException should be thrown
Copy the code

Given that we can use arrays of parameterized types, the second example shows that the third line of code should throw an exception because the ArrayList

type does not conform to the List

type, but the reason this is not allowed is that the JVM cannot recognize it because of type erasings at compile time. After the type erasure, ArrayList is the only type the JVM knows about.

Cannot create or capture an object of parameterized type

A generic class cannot directly or indirectly inherit from a Throwable class.

class MathException<T> extends Exception { / *... * / } // Indirect inheritance, error thrown at compile time
Copy the code
class QueueFullException<T> extends Throwable { / *... * / // Inherit directly, throw an error at compile time
Copy the code

An instance of a type parameter cannot be captured in a method.

public static <T extends Exception, J> void execute(List<J> jobs) {
    try {
        for (J job : jobs)
            // ...
    } catch (T e) {   // Error thrown at compile time
        // ...}}Copy the code

However, you can throw type parameters in a method

class Parser<T extends Exception> {
    public void parse(File file) throws T {     / / right
        // ...}}Copy the code

You cannot override methods that have the same signature after type erasure

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

The code for these two methods after type erasure:

public class Example {
    public void print(Set strSet) {}public void print(Set intSet) {}}Copy the code

The signatures of the two methods are exactly the same, which is illegal under the Java language specification.

An unverifiable type

A type is a verifiable type if its type information is fully available at run time, including primitive types, non-generic types, primitive types, and generics bound to unbounded wildcards.

Type information for non-verifiable types is removed by type erasure at compile time. Non-verifiable types, such as ArrayList

and ArrayList

, do not have all the information available at run time. The JVM does not recognize the difference between the two types at run time, and ArrayList is the only type the JVM recognizes. So Java generics are pseudo-generics, which are useful at compile time.

Heap pollution

Heap contamination occurs when a variable with type parameters points to an object with no type parameters.

public class Main {
    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;     / / effective
        objectArray[0] = Arrays.asList(42);
        String s = l[0].get(0);       / / throw ClassCastException}}Copy the code

When the compiler encounters a variable argument method, the compiler converts the variable form argument to an array. However, in the Java language, there is no way to create arrays with parameterized types (as described in the fifth scenario of not using generics). We use the addToList method to describe it. The compiler will call T… Elements is converted to T[] elements, but, because of type erasers, eventually the compiler converts T… Elements is converted to Object[] elements, so heap pollution can occur here.

We see the faultyMethod method, where assigning the variable l to a variable of type Object[] is valid because variable Lis converted by the compiler to type List[], so we can put any Object of that type or a subclass of that type in it because the type has been erased. So we can put any List value in there, so we have an array object, we can put a List

object, we can put a List

object, or whatever. This is where heap pollution occurs.

Disallow warnings for mutable parameters that cannot be validated

If you can be sure that your mutable parameters will not be converted incorrectly, you can add the @Safevarags annotation to cancel the warning.

You can also cancel warnings by adding the @SuppressWarnings({“unchecked”, “Varargs “}) annotation. But this should only be added if you can ensure that your code is safe.

thinking

Type erasure experiment

Let’s go to the following code:

This code is used to retrieve Node type parameter types by reflection. This code is used to retrieve Node type parameter types by reflection.

We can decompile to see what the decompiled class file looks like. Let’s decompile the Node file:

Object

So where does type erasure occur?

Let’s look at another piece of code to make it clear:

The result printed here is of type Object. We can add a lower bound to the Node parameter type, make it inherit the Comparable interface, and then print the type:

References:

  1. Oracle documentation