1. Generic base

Generics, a new feature introduced in JDK 5.0, are similar to the C++ template classes, meaning that you can write code that can be reused by different types of objects.

Before using generics, if you wanted to implement a generic method that could handle different types of data, we would use the Object’s public parent:

public class Storage {
    Object value;
    public Object getValue(a) {
        return value;
    }
    public void setValue(Object value) {
        this.value = value; }}Copy the code

In this way, the Storage class can access any type of data. As long as no strong type exceptions occur.

Storage storage = new Storage();
storage.setValue(134);
int value = (int) storage.getValue();
storage.setValue("hello");
String value1 = (String) storage.getValue();
Copy the code

The type of the value attribute is parameterized, which is passed in for external calls:

public class Storage<T> {
    T value;
    public T getValue(a) {
        return value;
    }
    public void setValue(T value) {
        this.value = value; }}Copy the code

When using generic Storage, you need to pass in the type as a parameter, and then you do not need to strong-type the return value of getValue().

Storage<Integer> storage1 = new Storage<Integer>();
storage1.setValue(134);
int value =  storage1.getValue();

Storage<String> storage2 = new Storage<String>();
storage2.setValue("hello");
String value1 = (String) storage2.getValue();
Copy the code

By comparing the above two methods, it can be concluded that:

  • When Object is used, all parameters and return values are of Object type, and each use requires type strong-casting. And the compiler has no way of knowing if the conversion is correct, onlyAt runtime,I realized the program was extremely insecure.
  • With generics, once the type is determined, the compiler can specify everythingTLocation type, no longer need to perform type coercion. And if there is a type mismatch, the compilation fails, for example, it cannot be executedstorage1.setValue("abc");, it only accepts arguments of type Integer.

So, we can summarize several characteristics of generics:

  • The nature of generics is parameterized types, which parameterize the data types of a class so that they can be passed in from outside, thus extending the scope of the data that a class can handle.
  • When the type is determined, the generic type will check the type. If it does not match the type, the compilation fails.
  • Improve code readability without having to wait until runtime to cast. The type of data that can be manipulated when defined or instantiated.

2. Use of generics

Generics can be applied to classes, interfaces, and methods.

2.1 a generic class


can be understood as a type parameter declaration, and T stands for type parameter and is used to refer to any type.

public class testGenerics<T> {
    private T value;
    public testGenerics(T v) { value = v; }}// Generic classes can also accept multiple type parameters
public class testGenerics<T.E> {
    private T value;
    private E element;
    public testGenerics(T v, E e) { value = v; element = e; }}Copy the code

T is a parameter that can actually be replaced by another character, but Java recommends that we use a single uppercase letter for type arguments due to coding specifications. Common ones are:

  • T: Type, which generally stands for any class;
  • E: Element, which generally stands for Element type, or Exception, which stands for Exception;
  • K: Key, Key value type.
  • V: Value, Value Value type, usually used with K.
  • S: Subtype: indicates the Subtype.

When you create an instance using a generic class, you simply declare the corresponding type in Angle brackets, and type parameters such as T and E are replaced with the corresponding type.

2.2 Generic interfaces

Generic interfaces are used in much the same way as generic classes:

public interface Comparator<T> {
    public int compare(T lhs, T rhs);
    public boolean equals(Object object);
}
Copy the code

When implementing an interface, specify a specific type:

public class StringCompare implements Comparator<String> {
    @Override
    public int compare(String lhs, String rhs) {... }}Copy the code

2.3 Generic methods

If the class of the generic method is a generic class, and the method handles the same type of data as the declared type of the generic class, you can simply use the parameters of the class declaration.

public class testGenerics<T> {
    private T value;
    public testGenerics(T v) {
        value = v;
    }
    
    public T compare(T a, T b) {... }}Copy the code

You need to declare your own type if you are in a non-generic class or if the method needs to handle data types that are different from those declared by the generic class. When you define a generic method, you must declare that it is a generic method by preceded by a

(T can also be replaced by another character).

public class testGenerics<T> {
    public <E> E compare(E a, E b) {... }public <E> T compare(T a, T b) { // This is the same as if there were no 
      
        declaration. The compiler still replaces T with the specified type
      . }}Copy the code

Generic methods of type parameter, associated with a generic class is no generic method type is determined at runtime, and because E is not like a generic class by the specified, so when the call a generic method, although the parameter may be declared as type E, but actually the type of argument can be different, are not related parameter types.

public class testGenerics<T> {
    public <E> E compare(E a, E b) {... } } testGenerics<Integer> g =new testGenerics<Integer>();
g.compare("abc"."abc");
g.compare("abc".1);// Although both parameters are declared as type E, they are not actually related, and two parameters of different types can be passed
Copy the code

3. Generic wildcards

3.1 Unrestricted Wildcard <? >

Indicates that any type can be held and can be used when you are unsure or do not care about the actual type to operate on. ? Unlike Object, List<? > represents a List of unknown type, while List<Object> represents a List of type Object.

public void setList(List
        list) {
    if(list.get(0) != null && list.get(1) != null) {
        list.add(list.get(0));// Failed to compile
        list.set(0, list.get(1));// Failed to compile
    }
    if(list.size() > 0) {
        list.remove(0);// Write operations independent of type can be performed}}Copy the code

3.2 Upper bound Wildcard <? extends E>

The parameter in the generic must be a subclass of E or E. If the type passed is not a subclass of E or E, the compilation fails and E’s methods can be used in the generic. Limits are defined, and only the ability to read, not write.

tells the compiler to deal with a subclass of type E or E, so the class is not yet determinable. To ensure type safety, the compiler does not allow any type of data to be added to it, lest a cast exception occur.

Type E data cannot be written either: Superclasses cannot be strong-subclassed. For example, if you want to change “Fruit” to “Apple” and put it in your “Apple” List, you can’t do that.

public void test(List<? extends Fruit> list) {
    list.add(new Fruit()); // Failed to compile
}
Copy the code

3.3 Lower Wildcard <? super E>

Super means that the argument to the generic type must be E or a parent of E. Defines the lower limit, has the ability to read and part of the ability to write, subclass can be strong into the parent class.

describes a superclass object/superclass container of E, but since the superclass is uncertain, only subclasses of E/E can be added /set. Since all subclasses of E can be converted to any superclass of E, set/add is absolutely safe. We can only get an Object because we don’t know which parent class it is.

public void test(List<? super Fruit> list) {
    list.get(0).read();// Compile failed because the parent of Fruit might not have defined the read method
    Fruit f = list.get(0); Fruit 
      
        List
       
         List
        
          List
         
           List
          
            List
           
             List
            
              List
             
               List
              
                List
               
                 List
                
                  List
                 
                   List
                  
                    List
                   
                     List
                   
                  
                 
                
               
              
             
            
           
          
         
        
       
      
    list.add(new Apple("apple"));
    list.add(new Fruit("fruit"));
}
Copy the code

3.4 the principle of PECS

The PECS principle is the basic principle for using wildcards: Producer extends, Consumer super.

If the parameterized type represents a producer, use <? Extends E >; If a consumer is represented, use <? Super E >; If you are both producer and consumer, you should not use wildcards.

Producers and consumers are distinguished from parameter containers: if the parameter container is suitable for reading but not writing, it is a producer. And if the parameter container is suitable for writing but not for reading, only in but not out, it is the consumer.

From the above analysis, it can be seen that
Cannot write data to the undefined type to avoid type exceptions; However, the data it reads can be guaranteed to be E or E subclasses, so it is read-only, not written, only out, not in, belonging to the producer class; The
allows the subclass data of E or E to be written. However, due to the uncertainty of the parent class, only Object instance can be read. Therefore, it is suitable for writing but not reading.

Generic type erasure

4.1 Erasing mechanism

Java generics were designed to be compatible with older code by introducing an erasing mechanism for generics. Java generics actually belong to the concept of compilation layer. The generated bytecode does not contain the type information in generics. The compiler will automatically complete the translation from Generic Java to ordinary Java, and the Java virtual machine will not be aware of generics when running.

Generic erase: When the compiler compiles code with generics, it performs type checking and type inference and generates plain bytecode without generics.

Keep only the primitive type after erasing: THE primitive type is the true type of the type variable that ends up in the bytecode after erasing the generic information.

When a generic type is defined, the corresponding primitive type is automatically provided. Type variables are erased at compile time and replaced with their qualified types (unqualified types are replaced with Object).

For example, a simple generic class is defined:

public class Storage<T> {
    T value;

    public T getValue(a) {
        return value;
    }

    public void setValue(T v) { value = v; }}Copy the code

As can be seen from the compiled bytecode, the type of the attribute value value, the return value of getValue(), and the argument to setValue() are all replaced with Object:

When the compiler compiles, the generic parameter T is erased and replaced with the original type, Object in this case. The Storage class is equivalent to the following form:

public class Storage {
    Object value;

    public Object getValue(a) {
        return value;
    }

    public void setValue(Object v) { value = v; }}Copy the code

So, generics are actually a syntactic sugar in Java, erased by generics in the same way that we defined a generic class before generics, and replaced it with a primitive replaceable class.

4.2 Signature Attribute Table

One might look at this and wonder: Since whatever type argument is passed in is eventually erased and replaced with the original type, what happens to the type argument passed in?

In fact, erasing does not mean that the type parameters passed in are lost within the JVM. The actual type of any object is visible to the JVM at run time, based on which the reflection system can be implemented and cast exceptions can be fired.

When generics were introduced in JDK 5, the JVM made changes to support generics, most importantly the Signature property sheet. After Java is compiled into bytecode, its declared generic information is stored in Signature, and any generic information that can be retrieved through reflection comes from this property.

For example, we create an instance object of the generic class Storage and a method with generic parameters:

public class testGenerics extends Storage<String>{

    Storage<Integer> storage;

    public testGenerics(a) {
        storage = new Storage<>();
    }

    public void test(List<? extends Number> list) {
        for(int i = 0; i < list.size(); i++) {
            if(list.get(i) instanceof Integer) {
                storage.setValue((Integer)list.get(i));
                break; }}int value =  storage.getValue();
    }

    public List<Integer> getList(a) {
        return null; }}Copy the code

To view its compiled bytecode, run the javap command:As you can see,Parent generics, member variables, method parameters, and return valuesThe generic parameter is retained in the Signature table and retrieved when the generic parameter needs to be used during the run.

5. Problems and solutions about generic erasure

5.1 How do I guarantee qualified types of generic variables?

After a generic erasure, both String and Integer are converted to Object. How do I determine whether a variable should use String or Integer?

To ensure that the type used is correct, Java checks the referenced type before type erasure and performs type erasure only after checking that there is no problem. So when we try to add a string to a list declared Integer, it will not compile.

Type checking is for references, which variable is a reference, and calling a generic method with that reference does type checking for the method called by that reference, regardless of the object it actually refers to.

5.2 Automatic type conversion Problems

Because of generic erasure, all generic types are replaced with primitive types, but in practice, the data returned by the get method does not need to be cast. It is the type data that corresponds to the generic parameter type we pass in.

Let’s test the Storage class:

public class testGenerics{
    private static Storage<Integer> storage;

    public static void main(String[] args) {
        storage = new Storage<>();
        storage.setValue(1); System.out.print(storage.getValue().getClass()); }}Copy the code

The output result is:class java.lang.IntegerAs you can see, it does automatically convert from Object to the incoming Integer class. Let’s look at its bytecode:

The compiler also added the checkcast directive, which checks the type conversion, to force an Integer. So the conversion of generics is done by the compiler adding the checkcast directive.

5.3 Type erasure conflicts with polymorphism

The previous generic class Storage, we define a subclass to inherit from it. In a subclass, the two methods of the superclass are overridden. Super calls the method of the superclass, and the generic type of the superclass is limited to String.

As you can see from the @Override tag, there is no problem overwriting parent methods this way.

public class Save extends Storage<String> {
    
    @Override
    public void setValue(String v) {
        // TODO Auto-generated method stub
        super.setValue(v);
    }

    @Override
    public String getValue(a) {
        // TODO Auto-generated method stub
        return super.getValue(); }}Copy the code

However, after the type is erased, all the generic types of the parent class become the original type Object, and the parent class is compiled as:

public class Storage {
    Object value;
    public Object getValue(a) {
        return value;
    }
    public void setValue(Object v) { value = v; }}Copy the code

As you can see, the setValue method has a parent class of type Object and a subclass of type String. The argument types are different, so it is not overridden but overridden.

So after type erasure, the intent of the override to implement polymorphism is not fulfilled, subclass overwriting becomes overloading, and type erasure conflicts with polymorphism.

So how was the conflict resolved? We looked at the bytecode of the Save class through Javap: it went from two methods to four, with two more set and GET methods for parameters and return values of Object.

When we call the set and GET methods of the Save class, we actually call the methods generated by the two compilers, which then call our own overridden methods.

The two methods generated by the compiler are also called bridge methods, and the bridge method is the one that actually overrides the parent class. Our own overrides are just an illusion. Conflicts between type erasers and polymorphisms are resolved by an automatically generated intermediate bridge method.

5.4 Runtime Type Check exception –instanceof

ArrayList

list = new ArrayList<>(); If (List instanceof ArrayList

) is not allowed at run time because the type erasers leave ArrayList

with the original type.


Java only limits this type of query: if(list instanceof ArrayList
).

5.5 Generics and exception catching

5.5.1 Generic class objects cannot be thrown or caught

Generic classes are not allowed to extend the Throwable interface, and the following class definitions will not compile:

public class GenericException<T> extends Exception {... }Copy the code

Because exceptions are thrown and caught at run time, if you allow the GenericException generic class definition above, you get the following exception catch:

try{... }catch(GenericException<Integer> e) {
    ...
} catch(GenericException<String> e) {
    ...
}
Copy the code

GenericExceptione, which is the same exception caught. This is not allowed and cannot be compiled.

5.5.2 Generic variables cannot be used in catch clauses

Java forbids the use of generic variables in catch statements and will not compile as follows.

public static <T extends Throwable> void test(Class<T> t) {
    try{... }catch(T e) {// Failed to compile. }}Copy the code

If catch generic variables are allowed, the following definitions are allowed:

public static <T extends Throwable> void test(Class<T> t) {
    try{... }catch(T e) {// Failed to compile. }catch(IndexOutOfBounds e) { ... }}Copy the code

The principle of exception catching is that the child class comes first and the parent class comes later. Assuming that the T type used here is ArrayIndexOutofBounds, the capture order here appears to be in order (IndexOutOfBounds is a parent of ArrayIndexOutofBounds); But ArrayIndexOutofBounds is replaced by Throwable after compilation, violating the order of exception catches. So to avoid this, Java disallows the use of generic variables in catch statements.

However, it is legal to use generic variables in exception declarations:

public static <T extends Throwable> void test(T t) {
    try{... }catch(Throwable e) {
        t.initCause(e);
        throwt; }}Copy the code

5.6 Generic type arrays cannot be created

Java does not allow you to create arrays of parameterized types.

Storage<Integer>[] storages = new Storage<Integer>[10];// Failed to compile
Copy the code

Assuming it compacts, the type of the storages becomes Storage[] after erasing. Since arrays are covariant, it can be converted to an Object[].

Object[] objs = storages;
Copy the code

But sometimes the wrong use of the covariant nature of arrays can cause security problems, as in the following two examples:

objs[0] = "abc";// The compiler passes, but since arrays can remember the types of elements they store, ArrayStoreException is thrown at runtime

objs[0] = new Storage<String>();// The storage type of the array is checked
int n = storages[0].getValue();// A type conversion error occurs at this point
Copy the code

To avoid security risks associated with parameterized arrays, Java does not allow you to create parameterized arrays. If you need to, you can use ArrayList

> instead.

5.6 other

  • A generic type variable cannot be a primitive data type.

  • ArrayList

    list = new ArrayList
    (); Will result in failure to compile. As mentioned above, type checking is for references. When we use list.get(), we should return a String, and the list behind it actually holds an Object, which causes a type conversion exception. To avoid this error, this reference is not allowed to pass.

  • Generic types cannot be instantiated, a = new T(); Is not allowed because new cannot allocate memory for indeterminate types; You can’t create a generic array, but you can use reflection to construct generic objects and arrays, using reflection to call newInstance.

    public static <T> void add(Box<T> box, Class<T> clazz) {
        // Because T is reflected at runtime, what type is it
        try {
            T item = clazz.newInstance();   // Use bytecode through reflection
        } catch(Exception e) {
            ...
        }
        box.add(item);
    }
    Copy the code
  • You cannot use generics to create a method with the same name as a parent method.

    For example, if you define an equals method, it will become equals(Object value) after type erasure, which conflicts with the equals method of Object.

    public boolean equals(T value) {
        return null;
    }
    Copy the code
  • To support erasing conversions, it is necessary to enforce that a class or type variable cannot be a subclass of two interfaces that are different parameterizations of the same interface.

    For example, a parent class and a subclass both implement the Comparable interface, and a subclass inherits the Comparable interface, resulting in the PairChild class implementing both the Comparable and Comparable interfaces, which are different parameterized implementations of the same interface.

    public class Pair implements Comparable<Pair> {... }public class PairChild extends Pair implements Comparable<PairChild> {... }// Compile error
    Copy the code
  • Static methods and variables in a generic class may not use the generic type parameters declared by the generic class, because generic parameters are specified at instantiation time and static methods and variables are not objects.

Reference: Generic erasure Why can the runtime still get specific generic types through reflection, problems with generic erasure, generic basics