Writing high-quality code: 151 suggestions for improving Java programs (Chapter 1: generics and reflection ___ suggestion 93 ~ 97), java151
Generics can reduce the forced type conversion, regulate the element types of the Set, and improve the Code security and readability. Because of these advantages, since Java introduced generics, there is one more project encoding rule: the generic type is preferred.
Reflection can "see through" the running status of a program, so that we can know the running status of a class or instance at runtime, and can dynamically load and call it. Although there is a certain performance worry, however, traversal brings us far greater than its performance defects.
Recommendation 93: Java generics can be erased.
The introduction of Java Generic (Generic) enhances the security of parameter types and reduces type conversion. It is similar to the template (Temeplates) in C ++, but there is a difference: java generics are valid in the compiler and deleted at runtime. That is to say, all generic parameter types will be cleared after compilation. Let's take an example. The Code is as follows:
1 public class Foo {2 // arrayMethod receives array parameters and loads 3 public void arrayMethod (String [] intArray) {4 5} 6 7 public void arrayMethod (Integer [] intArray) {8 9} 10 // listMethod receives the generic List parameter, and reload 11 public void listMethod (List <String> stringList) {12 13} 14 public void listMethod (List <Integer> intList) {15 16} 17}
The program is very simple. Four methods are compiled. The arrayMethod method receives String arrays and Integer arrays. This is a typical overload. listMethod receives list variables of String and Integer element types. The question is, can this program be compiled? If not? What is the problem?
In fact, this program cannot be compiled. The error message is as follows:
This error indicates that the method signature is repeated. In fact, the listMethod (List <Integer> intList) method erased at compilation is listMethod (List <E> intList) it is the same as another method. This is the problem caused by Java generic Erasure: After compilation, all generic types will be converted accordingly. The conversion rules are as follows:
- List <String>, List <Integer>, and List <T> are erased as List
- List <String> [] The erased type is List [].
- List <? Extends E>, List <? Super E> The erased type is List <E>.
- List <T extends Serializable & Cloneable> The erased type is List <Serializable>.
After understanding these rules, let's look at the following code:
public static void main(String[] args) {
List<String> list = new ArrayList<String>();
list.add("abc");
String str = list.get(0);
}
After compilation, the above Code is consistent with the following code:
public static void main(String[] args) {
List list = new ArrayList();
list.add("abc");
String str = (String) list.get(0);
}
After Java compilation, the bytecode does not contain any information about generics. That is to say, a generic class and a general class point to the same bytecode after compilation, for example, the Foo <T> class has only one Foo after compilation. class. Both Foo <String> AND Foo <Integer> reference the same bytecode. There are two reasons why Java does this:
- Avoid JVM failover. The C ++ generic life cycle continues to the runtime, while Java is erased during the compilation. Let's think about it. If JVM also extends the generic type to the runtime, therefore, JVM requires a lot of refactoring work.
- Version compatibility: the native Type (Raw Type) can be better supported during compilation. 6... on the platform, even if you declare a native type such as List, it can be compiled normally, but only generates warning information.
Once we understand that Java generics are type erased, we can explain the following problems:
public static void main(String[] args) { List<String> list = new ArrayList<String>(); List<Integer> list2 = new ArrayList<Integer>(); System.out.println(list.getClass()==list2.getClass()); }
The above code returns true for a simple reason. The List <String> and List <Integer> types after erasure are all List, no difference.
2. The Generic Array cannot be declared during initialization. The following code is not used during compilation:
List<String>[] listArray = new List<String>[];
The reason is very simple. You can declare an array with generic parameters, but You Cannot initialize the array because the type erasure operation is performed, list <Object> [] is the same as List <String> []. The Compiler rejects such declaration.
3. instanceof does not allow wildcard Parameters
The following code cannot be compiled for the same reason. The generic type is erased:
List<String> list = new ArrayList<String>(); System.out.println(list instanceof List<String>);
Recommendation 94: the generic parameters and arrays cannot be initialized.
The generic type is erased during compilation. During class initialization, we cannot obtain the specific parameters of the generic type, such as the Code:
class Test<T> { private T t = new T(); private T[] tArray = new T[5]; private List<T> list = new ArrayList<T>();}
Is there any problem with this code? T, tArray, and list are all class variables. They all declare a type through new and look very similar! However, this Code cannot be compiled, because the compiler needs to obtain the T type during compilation, but the generic type has been erased during compilation, all new T () and new T [5] will report an error (some people may have doubts that the generic type can be erased as a top-level Object. can T be compiled if it is erased as an Object? This does not work. generics are only part of the Java language. After all, Java is a strongly typed and compiled Security language, to ensure the stability and security at runtime, you must strictly check the compiler ). But why is there no error in the new ArrayList <T>?
This is because the ArrayList surface is generic and has already been converted to an Object during compilation. Let's take a look at the source code of ArrayList. The Code is as follows:
1 public class ArrayList <E> extends actlist <E> implements List <E>, 2 RandomAccess, Cloneable, java. io. serializable {3 // array containing elements 4 private transient Object [] elementData; 5 6 // constructor 7 public ArrayList () {8 this (10 ); 9} 10 11 // obtain an element 12 public E get (int index) {13 rangeCheck (index); 14 // force type conversion before return 15 return elementData (index ); 16} 17/* Other Code omitted */18 19}
Pay attention to the definition of elementData, which contains all the elements of ArrayList. Its type is the Object array, because the Object is the parent class of all classes, and the array allows Covariant ), therefore, the elementData array can accommodate all instance objects. When an element is added, it is converted to the Object type (the E type is converted to the Object type). When the element is removed, it is converted to the E type.
In some cases, we need generic arrays. What should we do? The Code is as follows:
1 class Test <T> {2 // no longer initialized. The constructor initializes 3 private T; 4 private t [] tArray; 5 private List <T> list = new ArrayList <T> (); 6 7 // constructor initializes 8 public Test () {9 try {10 Class <?> TType = Class. forName (""); 11 t = (T) tType. newInstance (); 12 tArray = (T []) Array. newInstance (tType, 5); 13} catch (Exception e) {14 e. printStackTrace (); 15} 16} 17}
At this time, there is no problem in running. The problem is how to obtain the T type, that is, the tType parameter, at runtime. Generally, the generic type cannot be obtained. However, when the client calls an extra T-type class, the problem will be solved.
Class member variables are initialized before class initialization. Therefore, it is required to have a clear type before class initialization. Otherwise, it can only be declared and cannot be initialized.
Suggestion 95: Force declare the actual type of the generic
The Arrays tool class has a method asList that can convert a variable-length parameter or array into a list, but it has a drawback: the length of the list generated by it is immutable, this is sometimes inconvenient in our project development. If you want to generate a variable list length, you need to write an array tool class by yourself. The Code is as follows:
1 class ArrayUtils {2 // converts a variable-length parameter to a List with a variable length of 3 public static <T> List <T> asList (T... t) {4 List <T> list = new ArrayList <T> (); 5 Collections. addAll (list, t); 6 return list; 7} 8}
This is simple. It is called in the same way as Arrays. asList. We pass in a generic object and return the corresponding List. The Code is as follows:
Public static void main (String [] args) {// normal usage List <String> list1 = ArrayUtils. asList ("A", "B"); // The parameter is empty. List list2 = ArrayUtils. asList (); // The parameter is a mix of integer and floating-point list3 = ArrayUtils. asList (1, 2, 3.1 );}
Three variables need to be described here:
(1). the list1 variable: The list1 variable is a common usage and has no problem. The actual parameter type of the generic type is String, and the returned result is a List object containing the String element.
(2) list2: What elements does list2 contain? We cannot deduce from the code what elements the list2 list actually contains (because the parameters it passes are empty, and the compiler does not know what the actual parameter type is). However, the compiler will cleverly infer that the top-level class Object is its generic type, that is, the complete definition of list2 is as follows:
List<Object> list2 = ArrayUtils.asList();
In this way, the compiler will not give an "unchecked" Warning. Now the new problem arises again: if list2 is expected to be an Integer-type list instead of an Object list, because the subsequent logic will add the Integer type to list2, what should we do?
Forced type conversion (forcibly convert asList to List <Integer> )? It does not work. Although Java generics are erased during compilation, List <Object> and List <Integer> do not have an inheritance relationship and cannot be forcibly converted.
Redeclare a List <Integer>, read the List <Object> element, and transform it down one by one? It is troublesome and inefficient.
The best solution is to forcibly declare the generic type. The Code is as follows:
List<Integer> intList = ArrayUtils.<Integer>asList();
In this simple way, the asList method requires a generic parameter, So we define this parameter as an Integer type before input. Of course, the output is also a set of Integer types.
(3) The list3 variable has two types of elements: integer and floating-point. What are the List generic parameters generated by list3? Is the Number of the parent class of Integer and Float? If you look at the compiler too much, it won't be so inferred. When it finds that the actual types of multiple elements are inconsistent, it will directly confirm that the generic type is Object, instead of tracing what is the common parent class of an element, but for list3, we expect its generic parameter Number to be a Number. Refer to the list2 variable and modify the Code as follows:
List<Number> list3 = ArrayUtils.<Number>asList(1, 2, 3.1);
Number is the parent class of Integer and Float. First, the three input parameters and output parameters are of the same type. The question is, when should we define the generic type? One sentence: If the generic type cannot be pushed out from the code, the generic type can be forcibly declared.
Recommendation 96: use different wildcard characters in different scenarios
Java generics support wildcards (Wildcard). You can use a single "?" Represents any class. You can also use the extends keyword to represent the child type of a class (interface). You can also use the super keyword to represent the parent type of a class (interface, but the question is, when should extends and super be used?
(1) If the generic structure only participates in the "read" operation, the upper limit (extends keyword) is limited)
Read the following code to see if our business logic operations can continue:
Public static <E> void read (List <? Super E> list) {for (Object obj: list) {// business logic operation }}
The operation of reading elements from the List (such as the sum calculation in the number List). Do you think the method of read can continue to be written?
The answer is: No. We don't know what elements are stored in the list. We can only infer that the E type is the parent class, but the question is, what is the parent class of the E type? It cannot be inferred that the encoder cannot be operated only during the runtime. Of course, you can treat it as an Object class and convert it to the E type when necessary-this completely violates the original intention of the generic type. In this case, if you want to read data from the List set, you need to use the extends keyword, that is, to define the upper bound of the generic type. The Code is as follows:
Public static <E> void read (List <? Extends E> list) {for (E: list) {// business logic operation }}
In this case, we have inferred the E-type element when the elements are extracted from the List set. The specific type of element is determined only at runtime, but it must be a definite type, such as read (Arrays. asList ("A") when calling this method, you can infer that the element type in the List is String, and then you can operate on the elements in the List. Such as adding to another List <E> or as the Map <E, V> key.
(2) If the generic structure only participates in the "write" operation, the lower bound is limited (the super keyword is used)
Check whether the following code can be compiled:
Public static <E> void write (List <? Extends Number> list) {// add an element list. add (123 );}
Compilation failed because the element type in the list is uncertain, that is, the compiler cannot infer what the generic type is. Is it an Integer type? Is Double? Or Byte? These all comply with the definition of the extends keyword. The Compiler rejects this operation because the actual generic type cannot be determined.
In this case, when there is only one element, the null value can be added. This is because null is of the 10 thousand type and can be an instance object of all classes, so it can be added to any list.
Can an Object be used? No, because it is not a subclass of Number, and even if you change the List variable to List <? The extends Object> type cannot be added either. The reason is very simple. The compiler cannot infer the generic type, and any element added is invalid.
In this case of "write" operation, the upper and lower bounds of the generic type are defined by using the super keyword. The Code is as follows:
Public static <E> void write (List <? Super Number> list) {// add the element list. add (123); list. add (3.14 );}
Both Integer 123 and floating point 3.14 can be added to the list, because they are of the Number type, which ensures the reliability of generic classes.
Therefore, we need to limit the upper or lower bound, JDK's Collections. the copy method is a good example. It copies all the elements in the source list to the corresponding index location in the target list. The Code is as follows:
1 public static <T> void copy(List<? super T> dest, List<? extends T> src) { 2 int srcSize = src.size(); 3 if (srcSize > dest.size()) 4 throw new IndexOutOfBoundsException("Source does not fit in dest"); 5 6 if (srcSize < COPY_THRESHOLD || 7 (src instanceof RandomAccess && dest instanceof RandomAccess)) { 8 for (int i=0; i<srcSize; i++) 9 dest.set(i, src.get(i));10 } else {11 ListIterator<? super T> di=dest.listIterator();12 ListIterator<? extends T> si=src.listIterator();13 for (int i=0; i<srcSize; i++) {14 di.next();15 di.set(si.next());16 }17 }18 }
The source list is used to provide data. Therefore, the src variable must define the upper bound and have the extends keyword. The target list is used to write data. Therefore, the dest variable must define the lower bound with the super keyword.
If a generic structure is used for both read and write operations, how can we limit it? Not limited. Use a certain generic type, such as List <E>.
Recommendation 97: Exercise caution that generic variables cannot be changed together with inverters.
What is covariant and inverter?
In the type framework of programming languages, covariant and invert refer to the features that replace or exchange wide and narrow types under certain circumstances (such as parameters, generics, and return values, in short, covariant is a narrow type replacement width type, while the inverter uses the wide type to cover the narrow type. In fact, we have been using covariant and inverter in Java for a long time, but we didn't find it. See the following code:
class Base { public Number doStuff() { return 0; }}class Sub extends Base { @Override public Integer doStuff() { return 0; }}
The Return Value Type of the doStuff method of the subclass is narrower than that of the parent class method. In this case, the doStuff method is a covariant method. In addition, according to the Java override definition, this is a overwriting method. So what is inverter? The Code is as follows:
class Base { public void doStuff(Integer i) { }}class Sub extends Base { @Override public void doStuff(Number n) { }}
The parameter type of the doStuff method of the subclass is wider than that of the parent class. This is an inverter method. The subclass expands the input parameters of the parent class method. However, according to the overwrite definition, doStuff is not overwrite, but a heavy load. Since the doStuff method has nothing to do with the parent class, it is only an action that the subclass expands independently. Therefore, it is of little significance to declare it as the doStuff method name, the inverter is no longer of special significance. Let's focus on the covariant. Let's first check whether the following code is covariant:
public static void main(String[] args) { Base base = new Sub(); }
Has the base variable undergone a covariant? Yes, a covariant occurs. The base variable is of the Base type. It is the parent class, but its value is assigned to a subclass instance, that is, it overwrites the width type with a narrow type. This is also known as polymorphism.
After talking about this, let's look at whether generics support covariant and inverter. The answer is: Generics do not support covariant or inverter. Why not?
(1) Generics do not support covariant: arrays and generics are very similar. One is brackets and the other is angle brackets. Then we take arrays as the reference object. See the following code:
Public static void main (String [] args) {// The array supports covariant Number [] n = new Integer [10]; // compilation fails, the wildcard type does not support the covariant List <Number> list = new ArrayList <Integer> ();}
ArrayList is a sub-type of List, and Integer is a sub-type of Number. The reason is that Java must ensure that the type of generic parameters is fixed to ensure runtime security, therefore, it does not allow a generic parameter to contain two types at the same time, even the parent-child relationship does not work.
Generic variables do not support covariant, but wildcard characters can be used to simulate the covariant. The Code is as follows:
// The Number subtype (including the Number type) can all be a List of generic parameter types <? Extends Number> list = new ArrayList <Integer> ();
"? Extends Number indicates that all subclasses of Number (including itself) can be used as generic parameter types, but can only be one specific type, Integer type, or Double type at runtime, or the Number type, that is, the wildcard is valid only during the encoding period, and the runtime must be a definite type.
(2) Generic instances do not support Inverter
Although java allows the inverter, the inverter is not allowed in assigning values to types. You cannot assign a parent class instance object to a subclass type variable, this situation is naturally not allowed for generics. However, it can be simulated using the super keyword. The Code is as follows:
// The Integer parent type (including Integer) can all be a List of generic parameter types <? Super Integer> list = new ArrayList <Number> ();
"? Super Integer indicates that all Integer parent types (themselves, parent classes, or interfaces) can be used as generic parameters, here it looks like assigning a Number-type ArrayList to an Integer-type List. Its appearance is similar to overwriting a narrow type with a wide type, which simulates the implementation of the inverter.
Generics neither support covariant nor invert. The subtypes with generic parameters are also different from the class types we often use. The basic type relationships are shown in the following table:
Wildcard QA |
Question |
Answer: |
Is Integer a subtype of Number? |
Correct |
ArrayList <Integer> is the subtype of List <Integer>? |
Correct |
Is Integer [] A subtype of Number? |
Correct |
List <Integer> is the subtype of List <Number>? |
Error |
List <Integer> is List <? Child type of extends Integer>? |
Error |
List <Integer> is List <? Subtype of super Integer>? |
Error |
The generic Java model does not support covariant and invert, but only implements the inverter and collaborative change. |