Introduction to Java generics

1 what is generics

1.1 problems when not using generics

Before JAVA introduced generics, you can store any data type Object by building a collection with the element type of Object. However, in the process of using the collection, you need to know the data type of each element. Otherwise, it is easy to cause ClassCastException exceptions.
The following code is shown:

        ArrayList list = new ArrayList();
        list.add("java");
        list.add(90);
        list.add(false);

        for (int i = 0; i < list.size(); i ++){
            Object o = list.get(i);
            String str = (String) o;
            System.out.println(str);
        }

Error message after operation:

1.2 concept of generics

Java generics is a new feature introduced in JDK5. Generics provides a compile time type safety monitoring mechanism, which enables us to detect illegal type data structures at compile time.
The essence of generics is parameterized type, that is, the data type operated on is specified as a parameter.

		/*
        Generic:
        Check type during compilation
        Reduced data type conversion
         */
        ArrayList<String> arrayList = new ArrayList<String>();
        arrayList.add("hello");
        arrayList.add("java");
        arrayList.add("world");

        for (int i = 0; i < arrayList.size(); i++) {
            String s = arrayList.get(i);
            System.out.println(s);
        }

1.3 benefits of generics

  • Type safety
  • Elimination of cast type conversion

2. Generic classes, interfaces

2.1 generic classes

  • Definition syntax
class Class name <Generic ID, generic ID,...>{
 	private Generic identification variable name:
 	...
}

Common generic identifiers: T, E, K, V

  • Use syntax
Class name<Specific data types> Object name = new Class name<Specific data types>();

        Java1. After 7, the specific data types in the following < > can be omitted.

Class name<Specific data types> Object name = new Class name<>();

Generic class definition and usage example:

/**
 * Definition of generic class
 * @param <T> Generic identifier - type parameter
 *           T Specify specific data types when creating objects
 */
public class Generic<T> {
    //T is specified when the class is used externally
    private T key;

    public Generic(T key) {
        this.key = key;
    }

    public T getKey() {
        return key;
    }

    public void setKey(T key) {
        this.key = key;
    }

    @Override
    public String toString() {
        return "Generic{" +
                "key=" + key +
                '}';
    }
}
public class MainClass {
    public static void main(String[] args) {
        //When creating an object, a generic class specifies the specific data type of the operation
        Generic<String> strGeneric = new Generic<>("abc");
        String key1 = strGeneric.getKey();
        System.out.println("key1:"+key1);

        Generic<Integer> intGeneric = new Generic<>(100);
        int key2 = intGeneric.getKey();
        System.out.println("key2:"+key2 );

        //When creating an Object, a generic class does not specify a type and will operate according to the Object type
        Generic generic = new Generic("ABC");
        Object key3 = generic.getKey();
        System.out.println("key3:"+key3);

        //Generic classes do not support basic data types
        //Generic<int> generic1 = new Generic<int>(100);

        //For the same generic class, the objects created according to different data types are essentially the same type
        System.out.println(strGeneric.getClass());
        System.out.println(intGeneric.getClass());
        System.out.println(strGeneric.getClass() == intGeneric.getClass());
    }
}

Operation results:

  • Generic class considerations
    1. Generic class. If no specific data type is specified, the operation type is Object.
    2. The type parameter of a generic type can only be a class type, not a basic data type.
    3. A generic class can logically be regarded as multiple different types, but they are actually the same type.

2.2 subclasses derived from generic classes

  • If the subclass is also a generic class, the generic types of the subclass and the parent class should be consistent
class ChildGeneric<T> extends Generic<T>
  • If the subclass is not a generic class, the parent class should specify the data type of the generic class
class ChildGeneric extends Generic<String>

Examples of subclasses derived from generic classes:

public class Parent<E> {
    private E value;

    public E getValue() {
        return value;
    }

    public void setValue(E value) {
        this.value = value;
    }
 }
/**
 * A generic class generates subclasses. If the subclass is also a generic class, the generic ID of the subclass should be consistent with that of the parent class.
 * @param <T>
 */
public class ChildFirst<T> extends Parent<T> {

    @Override
    public T getValue() {
        return super.getValue();
    }
}
/**
 * Generic classes generate subclasses. If the subclass is not a generic class, the parent class should specify the data type
 */
public class ChildSecond extends Parent<Integer>{

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

    @Override
    public void setValue(Integer value) {
        super.setValue(value);
    }
}
public class Test {
    public static void main(String[] args) {
        ChildFirst<String> childFirst = new ChildFirst<>();
        childFirst.setValue("abc");
        String valueFirst = childFirst.getValue();
        System.out.println(valueFirst);

        ChildSecond childSecond = new ChildSecond();
        childSecond.setValue(100);
        Integer valueSecond = childSecond.getValue();
        System.out.println(valueSecond);
    }
}

2.3 generic interface

  • Definition syntax of generic interface
interface Interface name<Generic ID, generic ID,...>{
	Generic identity method name();
	.....
}
  • Use of generic interfaces
    1. The implementation class is not a generic class, and the interface should specify the data type.
    2. Implementation classes are also generic classes. The generic types of implementation classes and interfaces should be consistent.

Definition and use examples of generic interfaces:

/**
 * generic interface 
 * @param <T>
 */
public interface Generator<T> {
    T getKey();
}
/**
 * The class that implements the generic interface. If it is not a generic class, you need to specify the data type that implements the generic interface
 */
public class Apple implements Generator<String> {
    @Override
    public String getKey() {
        return "apple";
    }
}
/**
 * If the implementation class of the generic interface is a generic class, ensure that the generic ID of the generic class implementing the interface includes the generic ID of the generic interface
 * @param <T>
 * @param <E>
 */
public class Pear<T,E> implements Generator<T>{

    private T key;
    private E value;

    public Pear(T key,E value) {
        this.key = key;
        this.value = value;
    }

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

    public E getValue(){
        return value;
    }
}
public class Test {
    public static void main(String[] args) {
        Apple apple = new Apple();
        String key = apple.getKey();
        System.out.println(key);

        Pear<String,Integer> pear = new Pear<>("count",100);
        String pearKey = pear.getKey();
        Integer pearValue = pear.getValue();
        System.out.println(pearKey+"="+pearValue);
    }
}

Operation results:

3. Generic method

3.1 generic method syntax

Modifier  <T,E,...> Return value type method name(parameter list ){
	Method body...
}
  • < T > between public and return value is very important, which can be understood as declaring this method as a generic method.
  • Only methods that declare < T > are generic methods. Member methods that use generics in generic classes are not generic methods.
  • <T> Indicates that the method will use generic type T. only then can generic type t be used in the method.
  • Like the definition of generic class, t here can be arbitrarily written as any identification. Common parameters in the form of T, E, K, V, etc. are often used to represent generics.

Generic method definition and usage example:

public class ProductGetter<T> {
    static Random random = new Random();

    /**
     * Defining generic methods
     * @param list parameter
     * @param <T> The generic identifier and the specific type are specified when calling the method.
     * @return
     */
    public <T> T getProduct(ArrayList<T> list){
        return list.get(random.nextInt(list.size()));
    }

    /**
     * Static generic method, using multiple generic types
     * @param t
     * @param e
     * @param k
     * @param <T>
     * @param <E>
     * @param <K>
     */
    public static <T,E,K> void printType(T t,E e,K k){
        System.out.println(t+"\t"+t.getClass().getSimpleName());
        System.out.println(e+"\t"+e.getClass().getSimpleName());
        System.out.println(k+"\t"+k.getClass().getSimpleName());
    }

	/**
     * Definition of generic variable parameters
     * @param e
     * @param <E>
     */
    public static <E> void print(E... e){
        for (int i = 0; i < e.length; i++) {
            System.out.println(e[i]);
        }
    }
}
public class Test {
    public static void main(String[] args) {
        ProductGetter<Integer> productGetter = new ProductGetter<>();
        ArrayList<String> strList = new ArrayList<>();
        strList.add("product01");
        strList.add("product02");
        strList.add("product03");
        //The call type of a generic method is specified when the method is called
        String product1 = productGetter.getProduct(strList);
        System.out.println(product1+"\t"+product1.getClass().getSimpleName());

        ArrayList<Integer> intList = new ArrayList<>();
        intList.add(100);
        intList.add(200);
        intList.add(300);
        Integer product2 = productGetter.getProduct(intList);
        System.out.println(product2+"\t"+product2.getClass().getSimpleName());

        //Calling static generic methods of multiple generic types
        System.out.println("--------------");
        ProductGetter.printType(100,"java",true);
        
		//Invocation of generic methods with variable parameters
        System.out.println("--------------");
        ProductGetter.print(1,2,3,4,5);
        System.out.println("--------------");
        ProductGetter.print("a","b","c");
    }
}

Operation results:

3.2 generic method summary

  • Generic methods enable methods to change independently of classes.
  • If static methods are to use generic capabilities, they must be made generic.

4 type wildcard

4.1 what are type wildcards

  • Type wildcards are generally used? Replace specific type arguments.
  • Type wildcards are type arguments, not type parameters.

Examples are as follows:

public class Box<E> {
    private E first;

    public E getFirst() {
        return first;
    }

    public void setFirst(E first) {
        this.first = first;
    }
}
/**
 * Type wildcard
 */
public class Test {
    public static void main(String[] args) {
        Box<Number> box1 = new Box<>();
        box1.setFirst(100);
        showBox(box1);

        Box<Integer> box2 = new Box<>();
        box2.setFirst(200);
        showBox(box2);
    }

    public static void showBox(Box<?> box){
        Object first = box.getFirst();
        System.out.println(first);
    }
}

Operation results:

4.2 upper limit of type wildcard

  • grammar
class/Interface<? extends actual argument >

The upper limit of type wildcard requires that the type of this generic type can only be argument type or subclass type of argument type.

Type wildcard upper limit example:

public class Animal {
}
public class Cat extends Animal{
}
public class MiniCat extends Cat {
}
/**
 * Type wildcard
 */
public class Test {
    public static void main(String[] args) {
        ArrayList<Animal> animals = new ArrayList<>();
        ArrayList<Cat> cats = new ArrayList<>();
        ArrayList<MiniCat> miniCats = new ArrayList<>();

        //In the error reporting cats, you can only pass in Cat or Cat subclasses through addAll
//        cats.addAll(animals);
        cats.addAll(cats);
        cats.addAll(miniCats);

//        showAnimal(animals);
        showAnimal(cats);
        showAnimal(miniCats);
    }

    /**
     * Generic upper limit wildcard. The collection type passed can only be Cat or Cat subclass type
     * @param list
     */
    public static void showAnimal(ArrayList<? extends Cat> list){

        //Error list cannot fill in elements
//        list.add(new Animal());
//        list.add(new Cat());
//        list.add(new MiniCat());
//        list.addAll(new ArrayList<Cat>());

        for (int i = 0; i < list.size(); i++) {
            Cat cat = list.get(i);
            System.out.println(cat);
        }
    }
}

In the example above, the showAnimal method is invoked in the main method, and the parameter passed can only be the subclass type MiniCat of Cat or Cat, and the incoming Animal type will be wrong.
The parameter addAll passed in Cat ArrayList and the error message passed in Cat ArrayList can be found at the same time. The reason is the same. You can view the source code of addAll method:

4.3 lower limit of type wildcard

  • grammar
class/Interface<? super actual argument >

The lower limit of type wildcard requires that the type of the generic type can only be the argument type or the parent type of the argument type.

Examples are as follows:

/**
 * Type wildcard lower limit
 */
public class Test {
    public static void main(String[] args) {
        ArrayList<Animal> animals = new ArrayList<>();
        ArrayList<Cat> cats = new ArrayList<>();
        ArrayList<MiniCat> miniCats = new ArrayList<>();

        showAnimal(animals);
        showAnimal(cats);
//        showAnimal(miniCats);
    }

    /**
     * The lower limit of type wildcard requires that the collection can only be Cat or Cat's parent type
     * @param list
     */
    public static void showAnimal(ArrayList<? super Cat> list){

        //The list can be populated with Cat or Cat subclass types
        list.add(new Cat());
        list.add(new MiniCat());

        for (Object o : list){
            System.out.println(o);
        }
    }
}

In the above example, the showAnimal method is invoked in the main method, and the parameter passed is only the Animal of the parent class Animal of Cat or Cat.

Another example:

public class Animal {

    public String name;

    public Animal(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "Animal{" +
                "name='" + name + '\'' +
                '}';
    }
}
public class Cat extends Animal{

    public int age;
    
    public Cat(String name,int age) {
        super(name);
        this.age = age;
    }

    @Override
    public String toString() {
        return "Cat{" +
                "age=" + age +
                ", name='" + name + '\'' +
                '}';
    }
}
public class MiniCat extends Cat {

    public int level;

    public MiniCat(String name, int age,int level) {
        super(name, age);
        this.level = level;
    }

    @Override
    public String toString() {
        return "MiniCat{" +
                "level=" + level +
                ", age=" + age +
                ", name='" + name + '\'' +
                '}';
    }
}
/**
 * Use of type wildcard lower limit
 */
public class Test {
    public static void main(String[] args) {
        TreeSet<Cat> treeSet1 = new TreeSet<>(new Comparator1());
        treeSet1.add(new Cat("liu",35));
        treeSet1.add(new Cat("zhang",30));
        treeSet1.add(new Cat("wang",29));
        treeSet1.add(new Cat("sun",34));
        for (Cat cat : treeSet1) {
            System.out.println(cat);
        }

        System.out.println("--------------------------------");

        TreeSet<Cat> treeSet2 = new TreeSet<>(new Comparator2());
        treeSet2.add(new Cat("liu",35));
        treeSet2.add(new Cat("zhang",30));
        treeSet2.add(new Cat("wang",29));
        treeSet2.add(new Cat("sun",34));
        for (Cat cat : treeSet2) {
            System.out.println(cat);
        }

        //An error is reported. The parameter can only be Cat or Cat's parent type
//        TreeSet<Cat> treeSet3 = new TreeSet<>(new Comparator3());

    }
}
class Comparator1 implements Comparator<Animal>{
    @Override
    public int compare(Animal o1, Animal o2) {
        return o1.name.compareTo(o2.name);
    }
}

class Comparator2 implements Comparator<Cat>{
    @Override
    public int compare(Cat o1, Cat o2) {
        return o1.age - o2.age;
    }
}

class Comparator3 implements Comparator<MiniCat>{
    @Override
    public int compare(MiniCat o1, MiniCat o2) {
        return o1.level - o2.level;
    }
}

The operation results are as follows:

It can be seen that when the parameter of TreeSet is Comparator1 comparator, Cat objects are sorted by name. When the parameter of TreeSet is Comparator2 comparator, Cat objects are sorted by age. That is, the comparator parameter of TreeSet can be passed into Cat and the parent type of Cat, but an error will be reported when the subclass type of Cat is passed into MiniCat. The reason is that the parameter type of this construction method of TreeSet uses the limit of the lower limit of wildcard. The following figure is the source code:

5 type erase

5.1 concept

Genericity is a concept introduced in Java version 1.5. Before that, there was no genericity. However, the generic code can be well compatible with the code of the previous version. That is because the generic information only exists in the code compilation stage. Before entering the JVM, the information related to genericity will be erased, which is called type erasure.
Example:

/**
 * Generic erase
 */
public class Test {
    public static void main(String[] args) {
        ArrayList<Integer> intList = new ArrayList<>();
        ArrayList<String> strList = new ArrayList<>();

        System.out.println(intList.getClass().getSimpleName());
        System.out.println(strList.getClass().getSimpleName());

        System.out.println(intList.getClass() == strList.getClass());
    }
}

Operation results:
It can be seen that although two different types of lists are created in the compilation stage, the type information of intList and strList are completely erased after compilation. They are both Array List types.

5.2 several cases of type erasure

  • Unlimited type erase
    As shown in the following figure, T is replaced by Object type during compilation.
    Examples are as follows:
public class Erasure<T> {
    private T key;

    public T getKey() {
        return key;
    }

    public void setKey(T key) {
        this.key = key;
    }
}
public class Test {
    public static void main(String[] args) {
        Erasure<Integer> erasure = new Erasure<>();
        //Use reflection to obtain the class object of the bytecode file of the error class
        Class<? extends Erasure> clz = erasure.getClass();
        //Get all member variables
        Field[] declareFields = clz.getDeclaredFields();
        for (Field field : declareFields) {
            //Prints the name and type of the member variable
            System.out.println(field.getName() + ":" + field.getType().getSimpleName());
        }
    }
}

Operation results:

  • Limited type erase

As shown in the figure below, the generic class specifies the upper limit of type. During compilation, T is replaced by the upper limit Number of type.
Examples are as follows:

public class Erasure<T extends Number> {
    private T key;

    public T getKey() {
        return key;
    }

    public void setKey(T key) {
        this.key = key;
    }
}
public class Test {
    public static void main(String[] args) {
        Erasure<Integer> erasure = new Erasure<>();
        //Use reflection to obtain the class object of the bytecode file of the error class
        Class<? extends Erasure> clz = erasure.getClass();
        //Get all member variables
        Field[] declareFields = clz.getDeclaredFields();
        for (Field field : declareFields) {
            //Prints the name and type of the member variable
            System.out.println(field.getName() + ":" + field.getType().getSimpleName());
        }
    }
}

Operation results:

  • Parameters defined by type in erase method

As shown in the figure below, the generic method specifies the upper limit of type. During compilation, T is replaced by the upper limit Number of type.
Examples are as follows:

public class Erasure<T extends Number> {
    private T key;

    public T getKey() {
        return key;
    }

    public void setKey(T key) {
        this.key = key;
    }

    /**
     * generic method 
     * @param t
     * @param <T>
     * @return
     */
    public <T extends List> T show(T t){
        return t;
    }
}
public class Test {
    public static void main(String[] args) {
        Erasure<Integer> erasure = new Erasure<>();
        //Use reflection to obtain the class object of the bytecode file of the error class
        Class<? extends Erasure> clz = erasure.getClass();
        //Get all methods
        Method[] declareMethods = clz.getDeclaredMethods();
        for (Method method : declareMethods) {
            //Print method name and return value type
            System.out.println(method.getName() + ":" + method.getReturnType().getSimpleName());
        }
    }
}

Operation results:

  • Bridging method
    Examples are as follows:
/**
 * generic interface 
 * @param <T>
 */
public interface Info<T> {
    T info(T t);
}
public class InfoImpl implements Info<Integer> {

    @Override
    public Integer info(Integer value) {
        return value;
    }
}
public class Test {
    public static void main(String[] args) {
        Class<InfoImpl> infoClass = InfoImpl.class;
        //Get all methods
        Method[] infoImplMethods = infoClass.getDeclaredMethods();
        for (Method method : infoImplMethods) {
            //Print method name and return value type
            System.out.println(method.getName() + ":" + method.getReturnType().getSimpleName());
        }
    }
}

Operation results:
It can be seen that the implementation class has more bridging methods to maintain the implementation relationship between the interface and the class, and the return value type is Object

6 generics and arrays

6.1 creation of generic array

  • Array references with generics can be declared, but array objects with generics cannot be created directly.
    Example:
public class Test {
    public static void main(String[] args) {
        ArrayList<String>[] listArr = new ArrayList[5];

        ArrayList<String> strList = new ArrayList<>();
        strList.add("abc");
        listArr[0] = strList;
        String s = listArr[0].get(0);
        System.out.println(s);
    }
}

Operation results:

  • You can use Java lang.reflect. newInstance(Class,int) of array creates T [] array.
    Example:
public class Fruit<T> {
    private T[] array;

    public Fruit(Class<T> clz,int length){
        //Through array Newinstance creates a generic array
        array = (T[])Array.newInstance(clz,length);
    }

    /**
     * Fill array
     * @param index
     * @param item
     */
    public void put(int index,T item){
        array[index] = item;
    }

    /**
     * Get array elements
     * @param index
     * @return
     */
    public T get(int index){
        return array[index];
    }

    public T[] getArray(){
        return array;
    }
}
public class Test1{
    public static void main(String[] args) {
        Fruit<String> fruit = new Fruit<>(String.class,3);
        fruit.put(0,"Apple");
        fruit.put(1,"watermelon");
        fruit.put(2,"Banana");
        System.out.println(Arrays.toString(fruit.getArray()));
        String s = fruit.get(2);
        System.out.println(s);
    }
}

Operation results:

7 generics and reflection

7.1 common generic classes for reflection

  • Class<T>
  • Constructor<T>

Example:

public class Person {
    private String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

When generics are not used, the Object type is obtained through reflection, and type conversion is required for subsequent use:

public class Test {
    public static void main(String[] args) throws Exception {
        Class personClass = Person.class;
        Constructor constructor = personClass.getConstructor();
        Object o = constructor.newInstance();
    }
}

The Person class can be obtained directly through generics and reflection, which makes it more convenient to write reflection programs:

public class Test {
    public static void main(String[] args) throws Exception {
        Class<Person> personClass = Person.class;
        Constructor<Person> constructor = personClass.getConstructor();
        Person person = constructor.newInstance();
    }
}

Keywords: Java Back-end

Added by Niccaman on Mon, 07 Mar 2022 14:17:53 +0200