1. Why - the reason for introducing generic mechanisms
If we want to implement a String array and require it to dynamically change the size, we will all think of using ArrayList to aggregate String objects. However, after a while, we want to implement an array of Date objects whose size can be changed. At this time, we certainly hope to be able to reuse the ArrayList implementation for String objects I wrote before.
Prior to Java 5, the implementation of ArrayList is roughly as follows:
public class ArrayList { public Object get(int i) { ... } public void add(Object o) { ... } ... private Object[] elementData;}From the above code, we can see that the add function used to add elements to the ArrayList receives an Object-type parameter. The get method that obtains the specified element from the ArrayList also returns an Object-type object. The Object object array elementData stores the object in the ArrayList. That is to say, no matter what type of type you put into the ArrayList, it is an Object object inside it.
Generic implementation based on inheritance will bring two problems: the first question is about the get method. Every time we call the get method, we will return an Object object, and each time we have to cast the type to the type we need, which will appear very troublesome; the second question is about the add method. If we add a File object to the ArrayList that aggregates the String object, the compiler will not generate any error prompts, which is not what we want.
Therefore, starting from Java 5, ArrayList can be used to add a type parameter (type parameter) when using it. This type parameter is used to indicate the element type in ArrayList. The introduction of type parameters solves the two problems mentioned above, as shown in the following code:
ArrayList<String> s = new ArrayList<String>();s.add("abc");String s = s.get(0); //No need to cast s.add(123); //Compilation error, you can only add String objects to it...In the above code, after the compiler "knows" the type parameter String of the ArrayList, it will complete the casting and type checking for us.
2. Generic Classes
The so-called generic class is a class with one or more type parameters. For example:
public class Pair<T, U> { private T first; private U second; public Pair(T first, U second) { this.first = first; this.second = second; } public T getFirst() { return first; } public U getSecond() { return second; } public void setFirst(T newValue) { first = newValue; }}In the above code, we can see that the type parameters of the generic class Pair are T and U, and are placed in angle brackets after the class name. Here, T means the first letter of Type, which represents type. Commonly used are E (element), K (key), V (value), etc. Of course, it is also perfectly fine to not use these letters to refer to type parameters.
When instantiating a generic class, we only need to replace the type parameter with a specific type, such as instantiating a Pair<T, U> class, we can do this:
Pair<String, Integer> pair = new Pair<String, Integer>();
3. Generic methods
The so-called generic method is a method with type parameters. It can be defined in a generic class or a normal class. For example:
public class ArrayAlg { public static <T> T getMiddle(T[] a) { return a[a.length / 2]; }}The getMiddle method in the above code is a generic method, and the format defined is that the type variable is placed after the modifier and before the return type. We can see that the above generic methods can be called for various types of arrays. When the types of these arrays are known to be limited, although they can also be implemented with overload, the encoding efficiency is much lower. The example code for calling the above generic method is as follows:
String[] strings = {"aa", "bb", "cc"};
String middle = ArrayAlg.getMiddle(names);
4. Limitation of type variables
In some cases, generic classes or generic methods want to further limit their type parameters. For example, if we want to define type parameters that can only be subclasses of a certain class or only classes that implement a certain interface. The relevant syntax is as follows:
<T extends BoundingType> (BoundingType is a class or interface). There can be more than 1 BoundingType, just use "&" to connect.
5. Understand the implementation of generics
In fact, from the perspective of virtual machines, there is no concept of "generics". For example, the generic class Pair we defined above looks like this in the virtual machine (that is, after being compiled into bytecode):
public class Pair { private Object first; private Object second; public Pair(Object first, Object second) { this.first = first; this.second = second; } public Object getFirst() { return first; } public Object getSecond() { return second; } public void setFirst(Object newValue) { first = newValue; } public void setSecond(Object newValue) { second = newValue; }}The above class is obtained by type erasing and is the raw type corresponding to the Pair generic class. Type erasing means replacing all type parameters with BoundingType (replace it with Object if no restrictions are added).
We can simply verify that after compiling Pair.java, type "javap -c -s Pair" to get:
The line with "descriptor" in the above figure is the signature of the corresponding method. For example, from the fourth line, we can see that the two formal parameters of the Pair constructor have become Object after type erasing.
Since the generic class Pair becomes its raw type in the virtual machine, the getFirst method returns an Object object, and from the compiler's perspective, this method returns an object of the type parameter specified when we instantiate the class. In fact, it is the compiler that helps us complete the casting work. In other words, the compiler will convert the call to the getFirst method in the Pair generic class into two virtual machine instructions:
The first is a call to the raw type method getFirst, which returns an Object object; the second instruction casts the returned Object object to the type parameter type we specified.
Type erasure also occurs in generic methods, such as the following generic methods:
public static <T extends Comparable> T min(T[] a)
After compilation, it will become like this after type erasing:
public static Comparable min(Comparable[] a)
Type erasing of methods can cause some problems, consider the following code:
class DateInterval extends Pair<Date, Date> { public void setSecond(Date second) { if (second.compareTo(getFirst()) >= 0) { super.setSecond(second); } } ...}After the above code is erased by type, it becomes:
class DateInterval extends Pair { public void setSecond(Date second) { ... } ...}In the DateInterval class, there is also a setSecond method inherited from the Pair class (after type erasing) as follows:
public void setSecond(Object second)
Now we can see that this method has different method signatures (different formal parameters) from the setSecond method overridden by DateInterval, so it is two different methods, however, these two methods should not be different methods (because it is override). Consider the following code:
DateInterval interval = new DateInterval(...);Pair<Date, Date> pair = interval;Date aDate = new Date(...);pair.setSecond(aDate);
From the above code, we can see that pair actually refers to the DateInterval object, so the setSecond method of DateInterval should be called. The problem here is that type erasing conflicts with polymorphism.
Let's sort out why this problem occurs: pair was previously declared as type Pair<Date, Date>, and this class seems to have only one "setSecond(Object)" method in the virtual machine. Therefore, when running, the virtual machine discovers that the pair actually refers to the DateInterval object, it will call the "setSecond(Object)" of DateInterval, but there is only the "setSecond(Date)" method in the DateInterval class.
The solution to this problem is to generate a bridge method in DateInterval by the compiler:
public void setSecond(Object second) { setSecond((Date) second);}6. Things to note
(1) The type parameters cannot be instantiated with basic types
That is, the following statement is illegal:
Pair<int, int> pair = new Pair<int, int>();
However, we can use the corresponding packaging type instead.
(2) Cannot throw or capture generic class instances
Generic class extension Throwable is illegal, so generic class instances cannot be thrown or captured. But it is legal to use type parameters in exception declarations:
public static <T extends Throwable> void doWork(T t) throws T { try { ... } catch (Throwable realCause) { t.initCause(realCause); throw t; }}(3) Parameterized array is illegal
In Java, an Object[] array can be the parent class of any array (because any array can be transformed upward to an array of the parent class that specifies the element type when defined). Consider the following code:
String[] strs = new String[10];Object[] objs = strs;obj[0] = new Date(...);
In the above code, we assign the array element to an object that satisfies the parent class (Object) type, but unlike the original type (Pair), it can pass at compile time, and an ArrayStoreException exception will be thrown at runtime.
Based on the above reasons, suppose Java allows us to declare and initialize a generic array through the following statement:
Pair<String, String>[] pairs = new Pair<String, String>[10];
Then after the virtual machine performs type erasing, pairs actually become Pair[] arrays, and we can transform it upwards into an Object[] array. At this time, if we add Pair<Date, Date> objects to it, we can pass compile-time checks and run-time checks. Our original intention is to just let this array store Pair<String, String> objects, which will cause difficult to locate errors. Therefore, Java does not allow us to declare and initialize a generic array through the above statement form.
A generic array can be declared and initialized using the following statement:
Pair<String, String>[] pairs = (Pair<String, String>[]) new Pair[10];
(4) Type variable cannot be instantiated
Type variables cannot be used in forms such as "new T(...)", "new T[...]", "T.class". The reason why Java prohibits us from doing this is simple. Because there is type erasure, statements like "new T(...)" will become "new Object(...)", which is usually not what we mean. We can replace the call to "new T[...]" with the following statement:
arrays = (T[]) new Object[N];
(5) Type variables cannot be used in the static context of generic classes
Note that we emphasize generic classes here. Because static generic methods can be defined in ordinary classes, such as the getMiddle method in the ArrayAlg class mentioned above. For reasons such a rule, please consider the following code:
public class People<T> { public static T name; public static T getName() { ... }}We know that at the same time, there may be more than one People<T> class instance in memory. Suppose there is a People<String> object and People<Integer> object in memory now, and the static variables and static methods of the class are shared by all class instances. So the question is, is the name String type or Integer type? For this reason, type variables are not allowed in Java to be used in static contexts of generic classes.
7. Type wildcard
Before introducing the type wildcard, we first introduce two points:
(1) Suppose Student is a subclass of People, but Pair<Student, Student> is not a subclass of Pair<People, People>, and there is no "is-a" relationship between them.
(2) There is an "is-a" relationship between Pair<T, T> and its original type Pair. Pair<T, T> can be converted to Pair type in any case.
Now consider this method:
public static void printName(Pair<People, People> p) { People p1 = p.getFirst(); System.out.println(p1.getName()); //Suppose that the People class defines the getName instance method}In the above method, we want to be able to pass parameters of Pair<Student, Student> and Pair<People, People> at the same time, but there is no "is-a" relationship between the two. In this case, Java provides us with a solution: use Pair<? extends People> as the type of formal parameter. That is to say, Pair<Student, Student> and Pair<People, People> can both be regarded as subclasses of Pair<? extends People>.
The code that looks like "<? extends BoundingType>" is called the subtype limitation of wildcard characters. Corresponding to this is the super type limitation of wildcard characters, the format is as follows: <? super BoundingType>.
Now let's consider the following code:
Pair<Student> students = new Pair<Student>(student1, student2);Pair<? extends People> wildchards = students;wildchards.setFirst(people1);
The third line of the above code will report an error because wildchards is a Pair<? extends People> object, and its setFirst method and getFirst method are as follows:
void setFirst(? extends People)? extends People getFirst()
For the setFirst method, the compiler will not know what type of formal parameters are (only known to be a subclass of People). When we try to pass in a People object, the compiler cannot determine whether the People and formal parameters are "is-a", so calling the setFirst method will report an error. It is legal to call wildchards' getFirst method because we know it will return a People subclass, and People's subclass "always is a People". (You can always convert subclass objects to parent objects)
In the case of wildcard supertype limitation, calling getter method is illegal, while calling setter method is legal.
In addition to subtype limitations and supertype limitations, there is also a wildcard called the infinite wildcard, which looks like this: <?>. When will we use this thing? Consider this scenario. When we call a method, we will return a getPairs method, which will return a set of Pair<T, T> objects. Among them are Pair<Student, Student>, and Pair<Teacher, Teacher> objects. (There is no inheritance relationship between the Student class and the Teacher class) Obviously, in this case, both the subtype limitation and the supertype limitation cannot be used. At this time, we can use this statement to solve it:
Pair<?>[] pairs = getPairs(...);