[JavaSE series] generics and wildcards in Java

Posted by onekoolman on Mon, 28 Feb 2022 11:07:54 +0100

⭐ ♪ previous words ⭐ ️

This article introduces you to Java syntax - generics and wildcards. Generics and wildcards are very abstract concepts. In short, both can pass types as "parameters". However, generics are used when you know what type to pass in, while wildcards are used when you are not sure what type to pass in, This article will introduce the use of generics and wildcards and the differences between them.

📒 Blog home page: Blog home page without flower smell
🎉 Welcome to pay attention 🔎 give the thumbs-up 👍 Collection ⭐ Leave a message 📝
📌 This article is original by Huawen, CSDN first!
📆 Starting time: 🌴 February 28, 2022 🌴
✉️ Persistence and hard work will surely bring poetry and distance!
💭 Reference books: 📚 Java core technology, 📚 Java programming ideas, 📚 <Effective Java>
💬 Refer to the online programming website: 🌐 Niuke network🌐Force buckle
The blogger's code cloud gitee is usually the program code written by the blogger.
The github of the blogger is usually the program code written by the blogger.
🙏 The author's level is very limited. If you find an error, you must inform the author in time! Thank you, thank you!

Aside: generics and wildcards are two difficult grammars in Java syntax. The main purpose of learning generics and wildcards is to understand the source code, which is not used much in practice.

1. Generic

1.1 usage of generics

1.1.1 concept of generics

There is a sentence in Java programming thought: general classes and methods can only use specific types: either basic types or custom classes. If you want to write code that can be applied to many types of code, this rigid restriction will have a great constraint on the code.
So the generic mechanism has been introduced since Java 5. What does this generic mean? Because general classes and methods can only use one specific type, the code is greatly constrained. For example, a method for finding the maximum value of three numbers. Assuming that the type of parameter list in the method is Integer at the beginning, there is no problem finding a maximum value from three Integer data. This program can run perfectly, However, when you want to find the maximum value of the three floating-point numbers, the program cannot be compiled. At this time, you can choose to write another overloaded method to implement the parameter list and implementation functions based on Double again, which can also solve the problem. However, do you think there is a problem? If there are 10000 or even one million types that need to find the largest of the three objects, What should I do, write a million overloaded methods? This is impossible. In order to solve the problem of this type, generics are introduced. Generics implement the concept of parameterized types, so that code can apply multiple types. Generally speaking, generics means "applicable to many types". Using generics, types can be passed as "parameters" to classes, interfaces and methods, so that classes and methods can have the most extensive expression ability, and there is no need to create another type because of different parameters.

1.1.2 generic classes

Let's understand generics through a piece of code. First, let's look at the following piece of code that does not use generics:

/**
 * Do not use generics
 */
class A {
}
class Print {
    private A a;

    public Print(A a) {
        setA(a);
        System.out.println(this.a);
    }
    public void setA(A a) {
        this.a = a;
    }
    public A getA() {
        return this.a;
    }
}

public class Generic {
   public static void main(String[] args) {
        Print print = new Print(new A());
    }
}
//output:A@1b6d3586

There is no problem in creating a class without using generics, but the reusability of this class is not very good. It can only hold the objects of class A, not the objects of any other class. We don't want to write a new class for each type we encounter, which is unrealistic. When we study classes, we know that the Object class is the parent class of all classes, so the Object class can accept all type references. We can let the Print class hold objects of Object type.

/**
 * Using the Object class
 */
class B{ }
class Print1 {
    private Object b;

    public Print1(Object b) {
        setB(b);
        System.out.println(this.b);
    }

    public void print(Object b) {
        setB(b);
        System.out.println(this.b);
    }
    
    public void setB(Object b) {
        this.b = b;
    }
}
public class Generic1 {
    public static void main(String[] args) {
        Print1 print1 = new Print1(new B());//Print type B
        int i = 2022;
        print1.print(i);//Print integer type
        print1.print("This is a string object!");//Print string type
    }
}
//output:
//B@1b6d3586
//2022
//This is a string object!

Print1 can receive and print any type, but this is not the result we want. Think about it. If the implementation is a sequential table class, which is implemented through an array, if the array can receive any type, it will be very chaotic. When taking out the data, we can't determine what type of data we are taking out, Moreover, the extracted data is an Object class, which requires forced type conversion. Can you realize what type of Object the specified class holds, and the compiler can check the correctness of the type.
We have to rewrite the above generic syntax to achieve the following purpose:

class Class name<Generic parameter list> {
	Permission modifier generic parameter variable name;//Generic member variable
	Permission modifier return value type method name (parameter list){}//Parameter lists and return value types can be generic
}

For example:

class Print2<T> {
    private T c;

    public void print(T c) {
        setC(c);
        System.out.println(this.c);
    }

    public void setC(T c) {
        this.c = c;
    }
}

The usage syntax of generic classes is as follows:

Generic class<type argument > Variable name; // Define a generic class reference 
new Generic class<type argument >(Construction method arguments); // Instantiate a generic class object

For example:

Print2<Integer> print3 = new Print2<Integer>();

Implement a class using generics and use it:

/**
 * Use Generics 
 */
class C{ }
class Print2<T> {
    private T c;

    public void print(T c) {
        setC(c);
        System.out.println(this.c);
    }

    public void setC(T c) {
        this.c = c;
    }
}
public class Generic2{
    public static void main(String[] args) {
        Print2<C> print2 = new Print2<>();//Print C type
        print2.print(new C());
        Print2<Integer> print3 = new Print2<>();//Print integer type
        print3.print(2022);
        Print2<String> print4 = new Print2<>();//Print string type
        print4.print("This is a string object!");
    }
}
/**
 * output:
 *C@1b6d3586
 * 2022
 * This is a string object!
 */

The < T > after the class name represents a placeholder, indicating that the current class is a generic class.
[Specification] type parameters are generally represented by a capital letter. Common names include:
E stands for Element
K means Key
V means Value
N stands for Number
T means Type
S. U, V, etc. - second, third, fourth type

//A generic class
class ClassName<T1, T2, ..., Tn> { }

When using a generic class, if the type held by the Object of this class is specified, the Object can only receive objects of this type. If other types of objects are passed in, the compiler will report an error. When receiving the return value of the generic method in the generic class, forced type conversion (downward conversion) is not required, but forced type conversion is required when using the Object class.

1.1.3 type derivation

When using a generic class, you can deduce the type parameters required to instantiate the generic class through the types passed in the generic type. In other words, when defining a generic object, you must specify the type in the front angle brackets and not in the later instantiation. For example:

Print2<Integer> print3 = new Print2<>();//The following angle brackets can be omitted

1.2 bare type

In fact, a bare type is a generic class. You do not specify the type held by the generic object. Such a type is a bare type.
For example:

    public static void main(String[] args) {
        Print2 print2 = new Print2();
        print2.print(2022);
        print2.print("character string");
    }
    //output:
    //2022
	//character string

We should not use naked types by ourselves. Naked types are mechanisms reserved for compatibility with older versions of the API.

1.3 erasure mechanism

1.3.1 about generic arrays

Before introducing the erasure mechanism of generics, let's first understand the generic array. First, let's conclude that instantiating a generic array is not allowed in Java. If you must establish a generic array, the correct way can only be realized through reflection. Of course, there is a "shortcut" to create a generic array without reflection. The created code is as follows:
1. Create by shortcut, and there will be no error in most cases.

public class MyArrayList<T> {
    public T[] elem ;
    private int usedSize;

    public MyArrayList(int capacity) {
        this.elem = (T[])new Object[capacity];
    }
}

2. Create through reflection. Now just give the code. Why do you do this? We'll talk about reflection later.

public class MyArrayList<T> {
    public T[] elem ;
    private int usedSize;
    
    public MyArrayList(Class<T> clazz, int capacity) { 
        this.elem = (T[]) Array.newInstance(clazz, capacity); 
    }
}

1.3.2 compilation and erasure of generics

Let's first implement a simple generic sequence table. We don't consider the expansion problem, but only implement simple addition and deletion operations. Let's take a look at the disassembly after partial compilation of the construction method.

import java.lang.reflect.Array;

public class MyArrayList<T> {
    public T[] elem ;
    private int usedSize;

    public MyArrayList(int capacity) {
        this.elem = (T[])new Object[capacity];
    }
    public MyArrayList(Class<T> clazz, int capacity) {
        this.elem = (T[]) Array.newInstance(clazz, capacity);
    }
}


We found that all generic placeholders T were erased and replaced with Object, which shows that Java's generic mechanism is implemented at compile time, and the implementation of generic mechanism is realized through such erasure mechanism, and the type check is completed during compile time.

We print MyArrayList classes with different types to see whether the generic mechanism will not appear during operation. If so, the printed types should be MyArrayList.

    public static void main(String[] args) {
        MyArrayList<Integer> list1 = new MyArrayList<>(10);
        MyArrayList<String> list2 = new MyArrayList<>(10);

        System.out.println(list1);
        System.out.println(list2);
    }
    /**
     * output:
     * MyArrayList@1b6d3586
     * MyArrayList@4554617c
     */

We found that the types of printing are the same, all MyArrayList, so we can draw a conclusion that generics occur at compile time, the type check of generics is completed at compile time, the implementation of generics is realized through erasure mechanism, the placeholders behind the class will be erased, and other placeholders will be replaced with Object. Of course, this is when the generic parameter does not specify an upper bound. If there is an upper bound, the placeholder will be erased into the type or interface of the upper bound. In fact, no upper bound is specified. The upper bound defaults to Object. What is the Generic upper bound? Shh, wait a minute.

According to the erasure mechanism, it can also explain why the generic array cannot be instantiated in Java, because the placeholder in front of the generic array will be erased into an Object. In fact, an Object array can be created, and any type in the Object array can be placed, which makes it unsafe to get data, because you can't be sure that all the elements stored in the array are of the type you expect, Therefore, for security reasons, Java does not allow instantiation of generic arrays.

1.4 upper bound of generics

1.4.1 upper bound of generics

When defining generic classes, you sometimes need to make certain constraints on the incoming type variables, which can be constrained by type boundaries.

class Generic class name<type parameter  extends Type boundary> {
	...
}

For example, Number is the parent of Integer,Float,Double and other related numeric types.

public class MyArrayList<T extends Number> {
	
}

Then, the MyArrayList generic class can only specify the Number class and the subclass of Number. Like this, it restricts the type transfer parameters of the generic. This constraint is the upper bound of the generic. When the generic class is constrained by the type boundary, it can only specify that the generic class holds the type boundary, this class and its subclasses.

        MyArrayList<Integer> list1 = new MyArrayList<>(10);//correct
        MyArrayList<Double> list2 = new MyArrayList<>(10);//correct
        MyArrayList<String> list3 = new MyArrayList<>(10);//Error because String is not a subclass of Number

1.4.2 special Generic upper bound

Suppose you need to design a generic class that can find the largest element in the array.

class MaxVal<T extends Comparable<T>> {
    public T max(T[] data) {
        T max = data[0];
        for (int i = 0; i < data.length; i++) {
            if (max.compareTo(data[i]) < 0) max = data[i];
        }
        return max;
    }
}

Because the comparison of reference types needs to use the Comparable interface to determine the size, the passed in class needs to implement the Comparable interface. The upper bound of the type parameter of the generic type above is a special upper bound, which means that the passed in type must implement the Comparable interface, but the class that implements the Comparable interface, that is, the subclass of the Comparable, to sum up, Like this, for a type that needs to achieve the expected function by implementing an interface, the upper bound of the generic needs to be specified when using the generic, and the incoming type must implement the upper bound interface.

1.4.3 generic methods

If there are generic classes, there must be generic interfaces and generic methods. The creation and use of generic interfaces and generic classes are the same, so we focus on the creation and use of generic methods.
Basic syntax for creating generic methods:

Method qualifier <Type parameter list> Return value type method name(parameter list ) { ... }

For example, the above method to find the generic version of the largest element in the array is as follows:

class MaxVal<T extends Comparable<T>> {
    public <T extends Comparable<T>> T max(T[] data) {
        T max = data[0];
        for (int i = 0; i < data.length; i++) {
            if (max.compareTo(data[i]) < 0) max = data[i];
        }
        return max;
    }
}

For static methods without static modification, < type parameter list > can be omitted, and the above code can become:

class MaxVal<T extends Comparable<T>> {
    public T max(T[] data) {
        T max = data[0];
        for (int i = 0; i < data.length; i++) {
            if (max.compareTo(data[i]) < 0) max = data[i];
        }
        return max;
    }
}

However, if it is a static method decorated with static, the type parameter list cannot be omitted, because the static method does not depend on the object, and its use does not need to instantiate the object, so there must be a separate type parameter list to specify the object type held.

class MaxVal<T extends Comparable<T>> {
    public static <T extends Comparable<T>> T max(T[] data) {
        T max = data[0];
        for (int i = 0; i < data.length; i++) {
            if (max.compareTo(data[i]) < 0) max = data[i];
        }
        return max;
    }
}

1.4.4 type derivation

Like generic classes, generic methods also have a type derivation mechanism. If type derivation is not used, then generic methods are used as follows:

The circled part in the use type derivation diagram can be omitted.

There is no parent-child relationship in the generic class as follows:

public class MyArrayList<E> { ... }
 // Myarraylist < Object > is not the parent type of myarraylist < number > 
 // Myarraylist < number > is also not the parent type of myarraylist < integer >

However, there is a subclass relationship between the two kinds of wildcards.

2. Wildcard

2.1 concept of wildcard

? It is a wildcard, which is different from the use of generics. Generic T is a certain type. After passing in the type argument, it will be determined, and the wildcard is more like a regulation, specifying a range, indicating which parameters you can pass.

    //Print order using generic tables
    public static<T> void printList1(ArrayList<T> list) {
        for (T x:list) {
            System.out.println(x);
        }
    }
    //Print sequence table using wildcards
    public static void printList2(ArrayList<?> list) { 
        for (Object x:list) { 
            System.out.println(x); 
        }
    }

Using generic type T can determine that the type passed in is type T, so use variables of type T to receive instead of wildcards? When no boundary is set, the default upper bound is that Object has no lower bound. To ensure safety, only Object type variables can be used to receive.

Wildcards are used to solve the problem that generics cannot be covariant. Covariance means that if Student is a subclass of Person, then list < Student > should also be a subclass of list < Person >. However, generics do not support such parent-child relationship.

2.2 upper bound of wildcard

Wildcards also have upper bounds, which can restrict the type passed in to be the upper bound of this class or a subclass of this class.

Basic syntax:

<? extends upper bound> 
<? extends Number>//The argument type that can be passed in is Number or a subclass of Number

For example:

    public static void printAll(ArrayList<? extends Number> list) {
        for (Number n: list) {
            System.out.println(n);
        }
    }

We limit the upper bound Number of the type to a formal parameter of the printAll method, so when traversing the sequence table, we need to use Number to receive the objects in the sequence table, and when using this method, we can only traverse the objects of the output Number and its subclasses.

    public static void main(String[] args) {
        printAll(new ArrayList<Integer>());//ok
        printAll(new ArrayList<Double>());//ok
        printAll(new ArrayList<Float>());//ok

        printAll(new ArrayList<String>());//error
    }


Suppose there are the following categories:

class Animal{}
class Cat extends Animal{}
class Dog extends Animal{}
class Bird extends Animal{}

Animal is the parent of cat, dog and bird. Let's take a look at the difference between using generics and wildcards in printing object results? We set upper bounds on both of them. When printing different objects, whose toString method will be called.

	//generic paradigm
    public static <T extends Animal> void printAnimal1(ArrayList<T> list) {
        for (T animal: list) {
            System.out.println(animal);
        }
    }
    //wildcard
        public static void printAnimal2(ArrayList<? extends Animal> list) {
        for (Animal animal: list) {
            System.out.println(animal);
        }
    }

Let's first look at generics. After specifying the type with generics, it will output what type of object it specifies. For example, if you specify that the type in the sequence table is Cat, it calls the toString method of Cat object.

    public static void main(String[] args) {
        Cat cat = new Cat();
        Dog dog = new Dog();
        Bird bird = new Bird();

        //generic paradigm
        ArrayList<Cat> list1 = new ArrayList<>();
        ArrayList<Dog> list2 = new ArrayList<>();
        ArrayList<Bird> list3 = new ArrayList<>();
        list1.add(cat);
        list2.add(dog);
        list3.add(bird);
        printAnimal1(list1);//Cat
        printAnimal1(list2);//Dog
        printAnimal1(list3);//Bird
    }


Let's take another look at wildcards. Using wildcards stipulates that Animal and its subclasses can be used. It doesn't matter which subclass object you pass in, it is the reference of the parent class, but it's not clear which subclass.

    public static void main(String[] args) {
        Cat cat = new Cat();
        Dog dog = new Dog();
        Bird bird = new Bird();

        //wildcard
        ArrayList<Cat> list1 = new ArrayList<>();
        ArrayList<Dog> list2 = new ArrayList<>();
        ArrayList<Bird> list3 = new ArrayList<>();
        list1.add(cat);
        list2.add(dog);
        list3.add(bird);
        printAnimal2(list1);//Cat
        printAnimal2(list2);//Dog
        printAnimal2(list3);//Bird
    }


The parent class reference receives the subclass object. When printing the subclass object referenced by the parent class, the toString method of the subclass will be used first. This problem was also mentioned when introducing polymorphism, so the output result is the same as using generics, but the effect of generics and wildcards is different. Generics is what type you pass in, Then this class will hold what type of object, and wildcards specify a range and which types you can pass.

The upper bound of wildcards supports the following parent-child relationship, while the upper bound of generics does not support:

MyArrayList<? extends Number> yes MyArrayList <Integer>perhaps MyArrayList<Double>Parent type of 
MyArrayList<?> yes MyArrayList<? extends Number> Parent type of

There is a characteristic of the upper bound of wildcards. Let's start with the conclusion. Using the upper bound of wildcards can read data, but it is not suitable for writing data, because it is not sure what the object held by the class is.

    public static void main(String[] args) {
        ArrayList<Integer> arrayList1 = new ArrayList<>();
        ArrayList<Double> arrayList2 = new ArrayList<>();
        arrayList1.add(10);
        List<? extends Number> list = arrayList1;
        System.out.println(list.get(0));//ok
        Integer = list.get(0);//error because it is not sure what the object held by the list is
        list.add(2);//error because the object held by the list cannot be determined. For security reasons, Java does not allow inserting elements
    }


Because the object type obtained from the list must be Number or a subclass of Number, you can use the Number reference to obtain the element, but you can't determine which type it is when inserting the element. For safety, the list with the upper bound of wildcard is not allowed to insert the element.

2.3 lower bound of wildcard

Unlike generics, wildcards can have lower bounds. The difference between the syntax level and the upper bound of wildcards is that the keyword extends is changed to super.

<? super Lower bound> 
<? super Integer>//Represents that the type of the argument that can be passed in is Integer or the parent type of Integer

Since it is a lower bound, the wildcard lower bound and the upper bound have opposite regulations on the incoming class, that is, a generic class can only pass in the class type of the lower bound or the parent type of the class. Like <? Super Integer > indicates that the type of the argument that can be passed in is Integer or the parent type of Integer (such as Number, Object)

    public static void printAll(ArrayList<? super Number> list) {
        for (Object n: list) {			//Only Object can be used here, because the incoming class is Number or the parent class of Number
            System.out.println(n);
        }
    }
    public static void main(String[] args) {
        printAll(new ArrayList<Number>());//ok
        printAll(new ArrayList<Object>());//ok

        printAll(new ArrayList<Double>());//error
        printAll(new ArrayList<String>());//error
        printAll(new ArrayList<Integer>());//error
    }


Similarly, the lower bound of wildcards also satisfies the parent-child relationship like the following.

MyArrayList<? super Integer> yes MyArrayList<Integer>Parent type of 
MyArrayList<?> yes MyArrayList<? super Integer>Parent type of

Summary:
? Yes? extends .... And? super .... The most important thing is to look at the "specified" range of wildcards. Parent and child classes are judged according to this range.

The lower bound of wildcards also has a feature, that is, it can allow writing data. Of course, the data objects that can be written are the lower bound and subclasses of the lower bound, but they are not good at reading data, which is opposite to the upper bound of wildcards.

    public static void main(String[] args) {
        ArrayList<? super Animal> list = new ArrayList<Animal>(); 
        ArrayList<? super Animal> list2 = new ArrayList<Cat>();//Compilation error. list2 can only refer to list of Animal or Animal parent type
        list.add(new Animal());//When adding an element, you only need to add an element whose type is Animal or a subclass of Animal
        list.add(new Cat());
        Object s2 = list.get(0);//sure
        
        ArrayList<? super Animal> list3 = new ArrayList<Object>();
        Cat s1 = list3.get(0);//error because the ArrayList of the parent type of Animal can be constructed when constructing an object, the object taken out may not be Animal or a subclass of Animal
    }


When adding an element to this chestnut, you can only add an element whose type is Animal or a subclass of Animal. When obtaining an element, you can only use Object reference to receive it, not other references, because when constructing an Object, you can construct an ArrayList of Animal's parent type, although you can insert Animal and its subclass objects, However, the extracted Object cannot be guaranteed to be Animal or a subclass of Animal.

That's all about generics and wildcards. These two concepts are very abstract and difficult to understand, and they are not used much, because you basically don't have a chance to use them in the future, because generic wildcards can only be used when writing source codes similar to Java or other languages. The main purpose of learning generic wildcards is to be able to read the source code and understand the code written by others.

Hey! I'm really confused when writing about the usage of java syntax! Finally, the liver is over. It's not too much to ask for three consecutive times!

The old fellow who felt that the article was well written, praised the comments and paid attention to a wave. Thank you!

Topics: Java Back-end JavaSE