Briefly describe the basic use and function of generics

foreword

In the last article, I explained the concept, function, usage scenarios of generics, and the usage of generic collections, generic interfaces, and generic classes, but due to space limitations, I did not explain the content of generics complete. So today we will continue to learn about generic methods, generic erasure, and wildcards, etc. I hope everyone will continue to prepare for learning.


The full text is about [ 4600] words, no nonsense, just pure dry goods that allow you to learn techniques and understand principles! This article has a wealth of cases and videos with pictures, so that you can better understand and use the technical concepts in the article, and can bring you enough enlightening thinking...

1. Generic method

1 Introduction

We can use generics when defining interfaces and classes, so that all methods and member variables in the interfaces and classes can also use the generics. But in fact, generics can be applied to the entire class, or only to a method in the class. That is to say, the class in which the method resides may or may not be a generic class. Whether a method has generics has nothing to do with whether the class it is in has generics.

A generic method is a method whose type is determined when the method is called. Generics can make the method change independently of the class . In addition, static static methods cannot access the type parameters of generic classes. Therefore, if you want a static method to have generic capabilities, you must make the static method a generic method.

2. Grammar

When we define a generic method, we need to add type parameters in front of the method name. The syntax for defining a generic method is as follows:

[访问权限修饰符] [static] [final] <类型参数列表> 返回值类型 方法名([形式参数列表])

For example:

public static <T> List showInfo(Class<T> clazz,int userId){
    
    }

In general, when we write a generic method, we must declare the generic type to be used before the name of the method, and multiple generic types can be declared at the same time, separated by commas. Next, define a generic method, and give you a specific introduction to the creation and use of generic methods.

3. Code example

Here we define a generic method for sorting the array and then traversing the element output, the code is as follows:

import java.util.Arrays;

public class Demo04 {
    
    
	
	//定义了一个静态的泛型方法,遍历数组中的每个元素
	public static <T> void printArray(T[] arr) {
    
    
		//先对数组进行排序
		Arrays.sort(arr);
		//再遍历数组元素
	    for (T t : arr) {
    
    
	        System.out.print(t + " ");
	    }
	    System.out.println();
	}
	
	public static void main(String[] args) {
    
    
		Integer[] nums= {
    
    100,39,8,200,65};
		//调用泛型方法
		printArray(nums);
	}
}

In the above code, printArray()it is a generic method in which the type parameter T is used. And we define a generic array T[] using the type parameter T in the parameters of the method, and then sort and traverse the array. In this way, no matter what type of array we pass in, we can easily implement functions such as sorting without type conversion, so that our previous requirements can be easily realized.

2. Wildcards

In addition to the above usages, there is also a very important wildcard function in generics. Next, let's take a look at how it works.

1 Introduction

Wildcards in generics are actually a special generic type , also known as wildcard type parameters . Taking advantage of wildcard type parameters allows us to write more generic code, even using them without knowing the actual type. We generally use ? to replace specific type parameters. For example, List<?> can be logically equivalent to all List<concrete type arguments> classes such as List and List .

In this regard, some friends may be curious, why do we need wildcards? In fact, the reason why wildcards appear is mainly during development. Sometimes we need a generic type, but we don't know which specific type to use. In this case, we can use wildcard type parameters to make the code more generic. For example, when we want to write a collection that can accept any type and return the largest element, we may not be sure which specific collection to pass in at this time, then it is better to use wildcards.

2. The form of wildcards

When generic wildcards are used specifically, there are three implementation forms as follows:

  • Unqualified wildcard (?) : ? represents a wildcard of unknown type ;
  • Upper limit wildcard (? extends T) : ? Indicates the wildcard of the upper limit of the type, T is a class or interface ;
  • Lower limit wildcard (? super T) : ? represents the wild card of the lower limit of the type, T is a class or interface .

Next, for the above three forms, we will explain their usage through several cases.

3. Unqualified wildcard (?)

The unqualified wildcard (?) is a wildcard for an unknown type that can be used when a type parameter is required. But since there is no restriction, it can only be used in simple cases, such as iterators in collections or methods whose return type is generic, etc. Here is a simple example:

import java.util.ArrayList;
import java.util.List;

public class Demo05 {
    
    

	public static void main(String[] args) {
    
    
		List<String> names = new ArrayList<String>();
		List<Integer> ages = new ArrayList<Integer>();
		List<Number> numbers = new ArrayList<Number>();

		names.add("一一哥");
		names.add("秦始皇");
		ages.add(28);
		ages.add(50);
		ages.add(28);
		numbers.add(100);
		numbers.add(800);

		printElement(names);
		printElement(ages);
		printElement(numbers);
	}

	//未限定通配符的使用
	public static void printElement(List<?> data) {
    
    
		for(int i=0;i<data.size();i++) {
    
    
			//data.getClass().getSimpleName():用于获取某个类的类名
			System.out.println(data.getClass().getSimpleName()+"--data: " + data.get(i));
		}
	}

}

In this example, the printElement() method accepts a List collection of unknown type, so names, ages, and numbers can all be used as actual parameters of this method, which is the role of unrestricted wildcards.

4. PECS principles

PECS is the abbreviation of Producer Extends Consumer Super , which is a design principle about Java generics. This principle means that if we need to return T, it is the producer (Producer), and we must use the extends wildcard; if we need to write T, it is the consumer (Consumer), and we must use the super wildcard. This principle can guide us how to define the upper and lower bounds of type parameters when using generics.

When we use generics, we may need to define upper and lower bounds for type parameters.

For example, we want to write a method to handle some collection types, but we don't know what type of elements are in these collections, then we can define a type parameter to handle all collection types. Generally, we can use extends to set the upper limit of generics, and use super to set the lower limit of generics. Next, in the fifth and sixth summary below, I will explain to you how to realize the upper and lower limits of generics. Please continue to study.

5. Upper limit wildcard (? extends T)

The upper limit wildcard (? extends T) is a wildcard that indicates the upper limit of the type, where T is a class or interface, and the type of the generic class must implement or inherit the T interface or class. It specifies the upper limit of the type that can be used, mainly used to limit the input parameter type.

import java.util.ArrayList;
import java.util.List;

/**
 * @author 一一哥Sun 
 */
public class Demo06 {
    
    

	public static void main(String[] args) {
    
    
		List<String> names = new ArrayList<String>();
		List<Integer> ages = new ArrayList<Integer>();
		List<Number> numbers = new ArrayList<Number>();

		names.add("一一哥");
		names.add("秦始皇");
		ages.add(28);
		ages.add(50);
		ages.add(28);
		numbers.add(100);
		numbers.add(800);
		
		//String等非Number类型就不行
		//printElementUpbound(names);
		
		//泛型值只能是Number及其子类类型,所以Integer/Double/Float等类型都可以,但String就不行
		printElementUpbound(ages);
		printElementUpbound(numbers);
	}

	//上限通配符的使用,这里的泛型值只能是Number及其子类类型
	public static void printElementUpbound(List<? extends Number> data) {
    
    
		for(int i=0;i<data.size();i++) {
    
    
			//data.getClass().getSimpleName():用于获取某个类的类名
			System.out.println(data.getClass().getSimpleName()+"--data: " + data.get(i));
		}
	}
}

In this example, the collection generic type in the printElementUpbound method can be the Number class or its subclasses, other types are not allowed . That is, we can only use Number or its subclasses as a type parameter, and the upper bound of a generic type is Number , which is what the upper bound wildcard means.

6. Lower limit wildcard (? super T)

A lower bound wildcard (? super T) is a wildcard that represents a lower bound of a type, where T is a class or interface. It specifies the lower limit of the type that can be used, mainly used to limit the output parameter type . Here is a simple example:

import java.util.ArrayList;
import java.util.List;

public class Demo07 {
    
    

	public static void main(String[] args) {
    
    
		List<String> names = new ArrayList<String>();
		List<Integer> ages = new ArrayList<Integer>();
		List<Double> numbers = new ArrayList<Double>();

		names.add("一一哥");
		names.add("秦始皇");
		ages.add(28);
		ages.add(50);
		ages.add(28);
		numbers.add(100.0);
		numbers.add(800.9);
		
		//String等非Number类型就不行
		//printElementUpbound(names);
		
		//此时Double类型也不行
		//printElementDownbound(numbers);
		
		//泛型值只能是Integer及其父类类型,所以Double/Float/String等类型都不可以
		printElementDownbound(ages);
		
	}

	//下限通配符的使用,这里的泛型值只能是Integer及其父类类型
	public static void printElementDownbound(List<? super Integer> data) {
    
    
		for(int i=0;i<data.size();i++) {
    
    
			System.out.println(data.getClass().getSimpleName()+"--data: " + data.get(i));
		}
	}
}

In this example, printElementDownboundthe collection generic type in the method can be Integer or its parent type, that is, the lower limit of the type is Integer, and other types are not allowed . That is, we can only use Integer or its superclass as a type parameter, and the lower bound of the generic type is Integer , which is what the lower bound wildcard means.

7. The difference between <? extends T> and <? super T>

Here, I want to give you a summary <? extends T>和<? super T>of the differences:

  • <? extends T> Allows calling a read method such as T get() to obtain a reference to a T object , but does not allow a write method such as set(T) to pass in a reference to T (except passing in null );
  • <? super T> It is allowed to call a write method such as set(T) to pass in a reference to the T object , but it is not allowed to call a read method such as T get() to obtain a reference to the T object ( except for obtaining Object ).
  • That is, <? extends T> allows reading and does not allow writing, and <? super T> allows writing and does not allow reading.

Please note that whether it is an unrestricted wildcard, an upper limit wildcard or a lower limit wildcard, we can use it in a method or in a class or interface.

3. Generic erasure

When we learn generics, in addition to mastering the above generic classes, generic interfaces, generic methods, and wildcards, we also need to learn about generic erasure. So what is generic erasure? Let's move on.

1 Introduction

The so-called type erasure (Type Erasure) means that at compile time, the JVM compiler will erase all the generic information and turn it into the original type. Generally, the generic type parameters are replaced with specific types. Upper or lower bound (defaults to Object if no upper bound is specified).

In other words, although we use generics in the code, after compilation, all generic types will be erased and their corresponding primitive types will be used instead. This is the underlying implementation principle of Java generics. The purpose of this design is to be compatible with the old JDK version, so that Java has better backward compatibility, and the old non-generic code can directly use the generic class library without any modification. At the same time, Java also provides a reflection mechanism to manipulate generic types, so that generic types can still be obtained in some cases, so even if there is generic erasure, it will not affect the runtime of the Java virtual machine too much efficiency.

For example, when we define a generic class, we will use generic type parameters instead of specific types, such as the following example:
insert image description here

2. Restrictions brought about by generic erasure

After compilation, the type parameter T of this generic class will be erased and become its corresponding primitive type Object.

This also means that we cannot get the actual type parameters of the generic at runtime, so there will be some restrictions on the use of generic erasure. First of all, since the generic type parameter is erased, we cannot obtain the information of the generic type parameter at runtime. For example, if we have a variable of type List, we cannot obtain that the element type in this List collection is Integer at runtime. Another limitation is that when using generic types, you also need to pay attention to type safety. At the compile stage, the compiler will check the type safety of the generic type; but at the run stage, since the generic type parameters are erased, the type safety cannot be guaranteed. The limitations of generic erasure are mainly manifested in the following aspects:

Type parameters cannot be instantiated with primitive types;

Unable to get generic type information at runtime;

Generic type parameters cannot be used in static variables or static methods;

The type T cannot be instantiated.

Next, we will analyze these limitations in detail.

2.1 Type parameters cannot be instantiated with primitive types

Type parameters in Java generics cannot be primitive types, only class or interface types. For example, the following code fails to compile due to an error during compilation:

List<int> list = new ArrayList<int>();

The correct way to write it is to use the wrapper type corresponding to the basic type, as shown below:

List<Integer> list = new ArrayList<Integer>();

2.2 Unable to get generic type information at runtime

Due to the existence of generic erasure, we cannot obtain the information of the generic type when the program is running. For example, the following code cannot get the element type of List at runtime:

List<String> list = new ArrayList<String>(); 
Class<?> clazz = list.getClass(); 
Type type = clazz.getGenericSuperclass(); 
// 输出:class java.util.ArrayList<E>
System.out.println(type); 

In the output result, we can only get the type information of ArrayList, but cannot get the specific generic type information in the collection, that is, we cannot get the information of String. But if we just want to get the actual type parameter of the generic at runtime, we can actually refer to the following ways to achieve it:

public class Box<T> {
    
    
    private T content;

    public Box(T content) {
    
    
        this.content = content;
    }

    public T getContent() {
    
    
        return content;
    }

    public void setContent(T content) {
    
    
        this.content = content;
    }

    public Class<?> getContentType() {
    
    
        return content.getClass();
    }
}

In the above code, we added a new method getContentType(), which is used to return the actual bytecode type of the generic type parameter. Later, we can indirectly obtain the information of the generic type by calling this method up.

2.3 Generic type parameters cannot be used in static variables or static methods

Since generic type parameters are determined when an object is instantiated, generic type parameters cannot be used in static variables or static methods. For example, the following code will not compile:

public class MyClass<T> {
    
       
    //这样的代码编译不通过
    private static T value;   
    
    public static void setValue(T value) {
    
            
        MyClass.value = value;     
    } 
}

The correct way to write it is to use an actual type instead of the generic type parameter:

public class MyClass {
    
         
    private static String value;          
    public static void setValue(String value) {
    
             
        MyClass.value = value;     
    } 
}

2.4 The T type cannot be instantiated

For example in the following case:

public class MyClass<T> {
    
    
    private T first;
    private T last;
    
    public MyClass() {
    
    
        first = new T();
        last = new T();
    }
}

The above code cannot be compiled because of the two lines of the constructor:

first = new T(); 
last = new T(); 

After wiping it actually becomes:

first = new Object(); 
last = new Object(); 

In this way, creating new MyClass<String>()and creating new MyClass<Integer>()becomes Object, and the compiler will prevent this type of wrong code. If we want to instantiate the generic T, we need to use the Class<T> parameter and set the reflection technology to achieve it, and we must also pass in the Class<T> when using it.


4. Conclusion

However, despite some limitations of generic erasure, generics are still a powerful programming tool that can improve the readability and maintainability of code. By properly using generics, we can perform type checking at compile time, avoiding type conversion errors and runtime exceptions, thereby improving the security and reliability of the code. At the same time, we also need to understand the limitations of Java generic erasure in order to make correct decisions in practical applications.

Video tutorial: video tutorial, click me directly

Guess you like

Origin blog.csdn.net/GUDUzhongliang/article/details/131001245