Chapter 8 of JAVA core technology Volume I - Generic Programming

Chapter 8 - Generic Programming

catalogue

1. Why use generics

  • Generic programming means that the code written can be reused for many different types of objects

  • Generics can improve the security and readability of code

    Take the ArrayList class as an example. Before generics, the ArrayList class only maintains an array referenced by an Object. Such a class must be cast whenever a value is obtained, and there is no error checking

    ArrayList files = new ArrayList();
    String filename = (String)files.get(0);
    files.add(new File("..."));	//No error will be reported in the compilation stage, and no error may be reported until it is called and converted to String in the run stage
    

    The type parameter can be used to indicate the type of element, which makes the code more readable and safe

    ArrayList<String> files = new ArrayList<>();
    

    The compiler can also make full use of this type information. When calling get, there is no need to cast. The compiler knows that the return value type is String, not Object

    String filename = files.get(0);	//No cast
    

    The compiler also knows that the add method of ArrayList < String > has a parameter of type String, which is much safer than having a parameter of type Object. The compiler can now check to prevent inserting objects of the wrong type

    files.add(new File("..."));	//Unable to compile
    

2. Generic classes and generic methods

2.1 generic classes

  • A generic class is a class with one or more type variables. Take Pair class as an example

    public class Pair<T>
    {
        private T first;
        private T second;
        
        public Pair() {first = null; second = null;}
        public Pair(T first, T second){this.first = first; this.second = second;}
        
        public T getFirst() {return first;}
        public T getSecond() {return second;}
    }
    
    //A generic class can have multiple type variables, such as public class pair < T, u > {...}. Type variables are used in the entire class definition to specify the return type of methods and the types of fields and local variables
    //A common practice is to use uppercase letters for type variables. The Java library uses the variable E to represent the element type of the collection, and K and V represent the key and value types of the table respectively. T. U, S means "any type"
    

2.2 generic methods

  • You can define a method with type parameters. Generic methods do not have to be defined in generic classes

    class ArrayAlg
    {
        public static <T> T getMiddle(T ... a){
            return a[a.length/2];
        }
    }
    
    //In most cases, you can omit type parameters and call generic methods directly
    String middle = ArrayAlg.getMiddle("John","Q.","Public");
    //In rare cases, this may cause the compiler to be unable to select an appropriate method from multiple generic methods. In this case, you can choose to supplement generics or adjust parameters
    

2.3 limitation of type variables

  • Sometimes, classes or methods need to constrain type variables. For example, a method restricts that generic T can only be a class that implements the Comparable interface. This can be achieved by setting a bound on the type variable T

    //T and qualified types can be classes or interfaces.
    public static <T extends Comparable> T min(T[] a){...};
    
    //A type variable or wildcard can have multiple qualifications. The qualified types are separated by "&", and commas are used to separate type variables
    public static <T extends Comparable & Serializable,U> T min(T[] a){...};
    

    In type qualification, there can be multiple interfaces for type qualification, but at most one qualification can be a class. If there is a class as a qualification, it must be the first qualification in the qualification list.

3. Implementation principle of generics

Whenever a generic type is defined, it will automatically provide a corresponding raw type. The name of the raw type is the generic type name after removing the type parameters. The type variable is erased and replaced with its qualified type. For example, the original type of Pair above is as follows:

public class Pair
{
    private Object first;
    private Object second;
    
    public Pair() {first = null; second = null;}
    public Pair(Object first, Object second){this.first = first; this.second = second;}
    
    public Object getFirst() {return first;}
    public Object getSecond() {return second;}
}

//If there is a qualified type, the original type replaces the type variable with the first qualified. If the above example says < T extends comparable & serializable >, all objects will be replaced with comparable
//If the above order is reversed < T extends Serializable & Comparable >, all objects will be replaced with Serializable, and the compiler will insert cast to Comparable when necessary. For efficiency, label interfaces (interfaces without methods) should be placed at the end of the qualified list

When writing a generic method call, if the return type is erased, the compiler inserts a cast. For example:

Pair<Employee> buddles = ...;
Employee buddy = buddies.getFirst();
//After getFirst erases the type, the return type is Object. The compiler automatically inserts cast to Employee.

3.1 bridge method

  • Bridge method in rewriting

Bridge methods have appeared as early as when overriding methods: when overriding methods, when one method overrides another method, you can specify a more strict return type (such as return subclass). Take the clone method as an example

public class Employee implements Cloneable
{
    public Employee clone() {...};
}

On the surface, Employee clone() overrides Object clone(). At this time, there are actually two clone() methods in Employee, one is Employee clone() and the other is Object clone(). When calling the clone method overridden in Employee, the compiler will first call the object clone () method according to the rewriting rules, and then call Employee clone () through the bridge method

//Automatic bridge generation method
public Object clone() {clone((Employee) e);}
  • Bridge method in type erasure

Type erasure also occurs in generic methods. Generally speaking, the erasure method is no different from that of classes. However, when it comes to overriding methods between child and parent classes, things become complicated. For example:

//Before erasure
class DataInterval extends Pair<LocalDate>
{
    public void getSecond(LocalDate second){...};
}

//After erasure
class DataInterval extends Pair
{
    public void getSecond(LocalDate second){...};
}

//Normally, there is only one getSecond of LocalDate type in DataInterval, which is used to override getSecond in Pair. However, due to the existence of type erasure, DataInterval also inherits getSecond of Object type in Pair
//At this time, if we operate as follows, ambiguity will appear
DataInterval interval = new DateInterval();
Pair<LocalDate> pair = interval;
pair.setSecond(aDate);

//Pair is an Object of pair < LocalDate > type. According to the erasure principle, it will become pair type, and all methods in it will become Object type. Now let the erased pair call setSecond. According to polymorphism, it will search for whether setSecond of Object type has been rewritten in interval. Unfortunately, it happens to be in the interval. According to the original assumption, pair.setSecond should call setSecond of LocalDate type instead of setSecond of Object type
//In order to solve this problem, the bridge method appeared
public void setSecond(Object second) {setSecond((LocalDate) second);}
  • For the transformation of Java generics, keep the following points in mind
    • There are no generics in the virtual machine, only ordinary classes and methods
    • All type parameters are replaced with their qualified types
    • Will synthesize bridge methods to maintain polymorphism
    • To maintain type safety, cast types are inserted when necessary

4. Limitations and limitations of generics

  1. Type parameters cannot be instantiated with primitive types

    There is no Pair < double >, only Pair < double >. Erasure causes this problem because after erasure, the Pair class contains fields of type Object, and Object cannot store double values

  2. Runtime type queries apply only to primitive types

    Due to erasure, all type queries only generate original types. For example, Pair <... > is always unified as Pair in type query

    if(a instanceof Pair<String>);
    Pair<String> p = (Pair<String>) a;
    //In fact, these are just testing whether a is a Pair, and specific generic types cannot be detected at all
    //Similarly, the getClass method always returns the original type
    Pair<String> stringPair = ...;
    Pair<Employee> employePair = ...;
    if(stringPair,getClass() == employePair.getClass());	//Return true
    //The specific values of both are Pair.class
    
  3. Cannot create an array of parameterized types

    Arrays of specific generic types cannot be created due to erasure

    Pair<String> table = new Pair<>[10];	//error
    //After erasure, the array type is Pair []. Although elements that do not belong to Pair type can be prevented from entering, elements such as Pair < employee > cannot be prevented. So Java fundamentally prevents the creation of arrays of specific generic types
    //However, variables such as pair < string > [] can still be declared, but such arrays are not allowed to be initialized
    

    If you need to collect parameterized type objects, it is safer and more effective to use ArrayList: ArrayList < pair >

  4. Varargs warning

    As above, since the creation of arrays of parameterized types is not supported, it is not possible for variable parameters

    public static <T> void addAll(Collection<T> coll, T ... ts){
        for(T t : ts) coll.add(t);
    }
    //In fact, ts is an array. If there are the following calls
    Collection<Pair<String>> table = ...;
    Pair<String> pair1 = ...;
    Pair<String> pair2 = ...;
    addAll(table,pair1,pair2);
    //The virtual machine will have to create a pair < string > array, which violates the rule in 3
    

    Although in theory, this will not produce errors, but just get a warning. But it's better not to use this method

  5. Cannot instantiate type variable

    //Type variables cannot be used in expressions like new T(...)
    public Pair() {first = new T(); second = new T();}	//error
    

    Erasing will turn T into an Object. You must not want to call new Object(). There are two solutions

    1. Method references after Java 8

      //Create a makePair method to receive a supplier < T >
      //Supplier < T > is a functional interface that represents a function with no parameters and return type of T
      Pair<String> p = Pair.makePair(String::new); //Specifies to create a String type object
      
      public static <T> Pair<T> makePair(Supplier<T> constr){
          return new Pair<>(constr.get(),constr.get());
      }
      
    2. reflex

      //Reflection is a bit more complicated because the normal instance creation method doesn't work
      first = T.class.newInstance();
      //The expression T.class is illegal. After all, it will become Object.class after erasure
      
      
      //Correct method
      Pair<String> p = Pair.makePair(String.class);
      
      public static <T> Pair<T> makePair(Class<T> cl){
          try{
              return new Pair<>(cl.getConstructor().newInstance(),cl.getConstructor().newInstance());
          }
          catch (Exception e) {return null;}
      }
      
  6. Cannot construct a generic array

    Similar to 5. The type will be erased, which will bring potential risks. The solution is similar to 5

    1. Method reference

    String[] names = ArrayAlg.minmax(String[]::new,"Tom","Dick","Harry");
    
    public static <T extends Comparable> T[] minmax(InFunction<T[]> constr, T...a){
        T[] result = constr.apply(2);
        //...
    }
    

    2. Reflection

    public static <T extends Comparable> T[] minmax(T...a){
        T[] result = (T[])Array.newInstance(a.getClass().getComponentType(),2);
        //...
    }
    
  7. Invalid type variable in static context of generic class

    Type variables cannot be referenced in static fields or methods. There is only one Singleton after erasure, which will cause conflicts

    public class Singleton<T>
    {
        private static T singleInstance; //ERROR
        
        public static T getSingleInstance()	//ERROR
        {
            //...
        }
    }
    
  8. Instances of generic classes cannot be thrown or captured

    public static <T extends Throwable> void doWork(Class<T> t)
    {
        try
        {
            //...
        }
        catch (T e)	//error
        {
            Logger.global.info(...);
        }
    }
    
  9. Additional restrictions

    Erasure can lead to many potential conflicts, especially for methods, such as the following examples

    class Pair<T>{
        private T first;
        private T second;
    
        public Pair() {first = null; second = null;}
    
        public boolean equals(T value){...} //ERROR
    }
    //It seems that the equals method here is different from the equals method in the Object, but it is actually repeated with the equals method in the Object after erasure. Even from the perspective of rewriting, it is not allowed, and an error will be reported directly
    

    The generic specification refers to a principle: "in order to support erasure conversion, a restriction should be imposed: if two interface types are different parameterizations of the same interface, a class or type variable cannot be used as a subclass of the two interface types at the same time."

    class Employee implements Comparable<Employee> {...};
    class Manager extends Employee implements Comparable<Manager> {...}; //ERROR
    //Manager implements comparable < employee > and comparable < manager >, which are different parameterizations of the same interface
    //The specific reasons behind this may be related to the synthesis of bridge methods
    

5. Wildcard type

  • In Wildcard types, changes in type parameters are allowed.

  • There are two main advantages of wildcards:

    1. Improve generality: allow methods or classes to adapt more types of generics within a certain range
    //printBuddies allows employees and their subclasses to use
    public static void main(String[] args) {
    
            Manager ceo = new Manager();
            Manager cfo = new Manager();
            Employee lowlyEmployee = new Employee();
        	Pair<Employee> pair2 = new Pair<>(ceo,cfo);
            Pair<Employee> pair1 = new Pair<>(lowlyEmployee,lowlyEmployee);
    
            printBuddies(pair1);
    		printBuddies(pair2);
        }
    
        public static void printBuddies(Pair<? extends Employee> p){
            Employee first = p.getFirst();
            Employee second = p.getSecond();
            System.out.println(first + " " + second);
        }
    
    1. Improve security: you can distinguish between secure accessor methods and insecure changer methods, and limit read / write operations by specifying wildcards
    Pair<? extends Employee>
    //Represents any generic Pair type whose type parameter is a subclass of Employee (excluding Employee)
        
    Pair<Manager> managerBuddies = new Pair<Manager>(ceo,cfo);
    Pair<? extends Employee> wildcardBuddies = managerBuddies;
    wildcardBuddies.setFirst(lowlyEmployee);	//Error, set not allowed
    wildcardBuddies.setFirst(ceo2);	//Error, set not allowed
    

    Generally <? Extensions [type] > is called a subtype qualified wildcard. This type of wildcard is only allowed to read (get) and not allowed to change (set); The corresponding is <? Super [type] > is called a super type qualified wildcard. Contrary to the above, it only allows changes (set) and cannot be accurately read (get)

    The reason why it can be so limited can be inferred from the back. For subtype qualified wildcards, if you want to read, you only need to set a [type] variable to receive the value of any subclass, which will not cause security problems. If you want to change, the compiler does not know what variables to set. If the level of the variables set is too high, an error will occur. For super type qualified wildcards, if you want to read, you can only use the Object variable. If you want to change, you only need to set a [type] variable. The [type] variable is specific enough and will not cause security problems.

    < for more in-depth usage of super type qualified wildcards, see books P350 ~ P351 >

  • You can use fundamentally infinite wildcards, such as pair <? >, It is very different from the original pair type. Pair<?> Similar to pair <? Extensions [type] > and pair <? A combination of super [type] >, which can neither be read accurately nor allowed to be changed. Only set(null) is allowed, or a value of type Object is returned. This type is fragile. Infinite wildcards are generally only used to detect null

6. Reflection

The reflection foundation of Java core technology Volume I is not very detailed. Please refer to the notes of Shang Silicon Valley

7. Reflection and generics

Even if generics are erased at run time, reflection can still obtain generic information of a class or method. To express a generic Type declaration, you can use the interface Type in the reflection package, which contains the following subtypes:

  • Class, which describes the specific type

  • Typevariable interface, which describes type variables (such as T). Make sure that the generic method has a type parameter named T

  • WildcardType interface, which describes wildcards (such as "super T"). Determine that the qualified type has a wildcard parameter, and judge whether the wildcard has super type qualification

  • ParameterizedType interface, which describes the generic class or interface type (such as comparable <? Super T >). Make sure that the type parameter has a subtype qualification and is itself a generic type

  • GenericArrayType interface, which describes generic arrays (such as T []). Determines that the generic method has a generic array parameter

  • For the content of "type literal", refer to P359-363

Posted on Fri, 05 Nov 2021 22:35:17 -0400 by djdaz