Why do I introduce generics?
Bug is a part of programming. We can only do our best to reduce the chance of a bug, but no one can guarantee that the program we write will not have any problems.
There are two types of errors: compile-time errors and runtime errors. Compile-time errors can be found and eliminated during compilation, while runtime errors are subject to great uncertainty and can be found only when the program is running. The consequences may be disastrous.
With generics, errors can be detected during compilation to increase program robustness.
Let's look at an example:
public class Box{ private Object object; public void set(Object object) { this.object= object; } public Object get() { return object; }}
According to the Declaration, the set () method can accept any java Object as the parameter (any Object is a subclass of the Object). If this class is used somewhere, set () the expected input object of the method is Integer type, but the actual input is String type. A runtime error is thrown, which cannot be detected during compilation. For example:
Box box = new Box; box. set ("abc"); Integer a = (Integer) box. get (); // No error is reported during compilation, but ClassCastException is reported during runtime.Transform the above Code Using Generics:
public class Box<T>{ private T t; public void set(T t) { this.t= t; } public T get() { return t; }}
When we use this class, we will specify the specific type of T, which can be a class, interface, array, but not a basic type.
For example:
Box <Integer> box = new Box <Integer>; // The specified type is Integer // box. set ("abc"); this sentence will return an error box during compilation. set (new Integer (2); Integer a = box. get (); // no conversion type required
As you can see, the use of generics also removes the need for conversion operations.
Before introducing the generic mechanism, to support multiple data types in the method, you need to overload the method. After introducing the paradigm, you can solve this problem more concisely, furthermore, you can define the relationship between multiple parameters and return values.
For example
public void write(Integer i, Integer[] ia);public void write(Double d, Double[] da);public void write(Long l, Long[] la);
The specified model version is
public <T> void write(T t, T[] ta);
In general, the generic mechanism can use "type" as a parameter when defining classes, interfaces, and methods. It is a bit similar to the formal parameter in method declaration, in this way, different input parameters can be used to reuse the program. The difference is that the input of a formal parameter is a value, while the input of a generic parameter is a type.
Naming rules
There is a set of default naming rules for type parameters. To improve code maintainability and readability, we strongly recommend that you follow these rules. The application of these naming rules is everywhere in JDK.
E-Element (usually the Element in the Collection class)
K-Key
N-Number
T-Type
V-Value
S, U, VEtc.-second, third, and fourth type parameters ......
Note: The type parameters defined by the parent class cannot be inherited by the quilt class.
You can also declare multiple types of variables and use commas to separate them. For example:
public interface Pair<K, V> { public K getKey(); public V getValue();} public class OrderedPair<K, V> implements Pair<K, V> { private K key; private V value; public OrderedPair(K key, V value) { this.key = key; this.value = value; } public K getKey() { return key; } public V getValue() { return value; }}
The following two lines of code create two instances of the OrderedPair object.
Pair <String, Integer> p1 = new OrderedPair <String, Integer> ("Even", 8); Pair <String, String> p2 = new OrderedPair <String, string> ("hello", "world"); // You can also omit the type parameter after new, abbreviated as: // Pair <String, integer> p1 = new OrderedPair <> ("Even", 8); // you can also use type variables with type variables in angle brackets, such as OrderedPair <String, box <Integer> p = new OrderedPair <> ("primes", new Box <Integer> (...));Generics are introduced only after JDK 5.0. For compatibility, generic parameters are not allowed to be specified. However, the compiler cannot perform type checks. During programming, it is best to specify generic parameters explicitly.
Similarly, generic parameters are also used in methods, and the scope of use of this parameter is limited to the method body. For example:
Public class Util {// This method is used to compare whether two Pair objects are equal. // The Generic parameters must be written before the return type boolean of the method. public static <K, V> boolean compare (Pair <K, V> p1, Pair <K, V> p2) {return p1.getKey (). equals (p2.getKey () & p1.getValue (). equals (p2.getValue () ;}} Pair <Integer, String> p1 = new Pair <> (1, "apple"); Pair <Integer, string> p2 = new Pair <> (2, "pear"); boolean same = Util. <Integer, String> compare (p1, p2); // In fact, the compiler can infer the type to be used by compare using the Pair type, so it can be abbreviated: // boolean same = Util. compare (p1, p2 );Sometimes we want to limit the type parameter to a certain range, we need to use the extends keyword (extends can be followed by an interface, here extends can represent inheriting a class, it can also be used to implement an interface. For example, we want the parameter to be of the numeric type:
Class Box <T extends Number> {// subclass of the type parameter limited to Number private T t; public Box (T t) {this. t = t;} public void print () {System. out. println (t. getClass (). getName ();} public static void main (String [] args) {Box <Integer> box1 = new Box <Integer> (new Integer (2); box1.print (); // print the result: java. lang. integer Box <Double> box2 = new Box <Double> (new Double (1.2); box2.print (); // print the result: java. lang. double Box <String> box2 = new Box <String> (new String ("abc"); // an error is returned because the String type is not a subclass of Number box2.print ();}}If multiple restrictions are added, you can connect them with "&". However, because java is a single inheritance Only one class is allowed at most, and must be placed in the first position. For example:
Class Box <T extends Number & Cloneable & Comparable> {// This type must be a subclass of Number and implement the Cloneable interface and Comparable interface .......}
Inheritance of generic classes
Java is an object-oriented high-level language. It is allowed to input A subclass of A where class A parameters are accepted. For example:
Object someObject = new Object (); Integer someInteger = new Integer (10); someObject = someInteger; // because Integer is a subclass of the Object
This feature also applies to type parameters, such:
Box <Number> box = new Box <Number> (); box. add (new Integer (10); // Integer is the subclass box of Number. add (new Double (10.1); // Double is also a subclass of Number
However, there is a situation that can easily cause confusion, for example:
// The parameter type accepted by this method is Box <Number> public void boxTest (Box <Number> n ){......} // BoxTest (Box <Integer>) and boxTest (Box <Double>) are returned for the following two types of calls );
Although Integer and Double are both subclasses of NumberBox <Integer> and <Double> are not subclasses of Box <Number>., There is no inheritance relationship. The common parent class of Box <Integer> and Box <Double> is Object.
Taking the Collection class in JDK as an example, ArrayList <E> implements the List <E> interface, and the List <E> interface inherits the Collection <E> interface. Therefore, arrayList <String> is a subclass of List <String>, rather than a subclass of List <Integer>. The inheritance relationships are as follows:
Type Inference)
Let's look at an example:
public class Demo{ static <T> T pick(T a1, T a2) { return a2; } }
The static method pick () uses generics in three places, which respectively limits the types of two input parameters and the return types. The code for calling this method is as follows:
Integer ret = Demo. <Integer> pick (new Integer (1), new Integer (2); // As mentioned earlier, the above Code can be abbreviated as Integer ret = Demo. pick (new Integer (1), new Integer (2 ));
Because the java compiler will infer that the type returned by the method should be Integer Based on the parameter type in the method, this mechanism is called Type inference.
So the question is, add two input parameters to different types. What type should be returned?
For example:
pick("d", new ArrayList<String>());
The first parameter is of the String type and the second parameter is of the ArrayList type. The java compiler will deduce the two parameter types and try to make the return type the most explicit one. In this example, both String and ArrayList implement the same interface-Serializable. Of course, they are also subclasses of objects. The Serializable type is clearly clearer than the Object type, because its range is smaller and more subdivided, the final return type should be Serializable:
Serializable s =pick("d", new ArrayList<String>());
This mechanism can also be used to simplify code During generic class instantiation. Note that the angle brackets "<>" cannot be omitted at this time. For example:
Map <String, List <String> myMap = new HashMap <> (); // the compiler can deduce the following types, so it can be simplified to Map <String, list <String> myMap = new HashMap <> (); // However, it cannot be simplified to Map <String, List <String> myMap = new HashMap <> (); // because HashMap () is the constructor of the original HashMap Type (Raw Type), rather than the constructor of HashMap <String, List <String>, if "<>" is not added, the compiler does not check the type.
Wildcard
We mentioned an example above:
public void boxTest(Box<Number> n){ ……}
This method can only accept Box <Number>This type of parameter, when we enter a Box <Double>Or Box <Integer>An error is reported even though Integer and Double are subclasses of Number. But what if we want this method to accept Number and any of its sub-classes?
At this time, wildcards will be used. Rewrite as follows:
public void boxTest(Box<? extends Number> n){ ……}
"? Extends Number"This means that the Number and its subclass can be accepted as parameters. This declaration method is calledUpper Limit wildcard(Upper boundedwildcard ).
On the contrary, what if we want this method to accept parameters of the Integer, Number, and Object types? Should be usedLower limit wildcard(Lower bounded wildcard ):
public void boxTest(Box<? super Integer> n){……}
"? Super Integer"It indicates that Integer and its parent class can be accepted as parameters.
If there is neither the extends keyword nor the super keyword in the type parameter, there is only one ?, RepresentativeUnlimited wildcard(Unbounded Wildcards ).
In two cases, an unlimited wildcard is used:
(1) If you are writing a method, you can use the functions provided in the Object class to implement it.
(2) The functions implemented by the Code are irrelevant to the type parameters. For example, the List. clear () method and the List. size () method have frequently used classes. <?> Method, and its implementation functions are irrelevant to the type parameter.
Let's look at an example:
public static void printList(List<Object> list) { for (Object elem : list) System.out.println(elem + ""); System.out.println();}
This method can only accept List <Object> parameters, but does not accept any other types of parameters. However, the function implemented by this method has nothing to do with the parameter type in the List, so we want it to accept List parameters containing any type. The code is changed as follows:
public static void printList(List<?> list) { for (Object elem : list) System.out.println(elem + " "); System.out.println();}
Note that List <?> It is different from List <Object>. No matter what type A is, List <A> is List <?> But List <A> is not A subclass of List <Object>.
For example:
List <Number> lb = new ArrayList <> ();
List <Integer> la = lb; // a compilation error is returned. Although Integer is a subclass of Number, List <Integer> is not a subclass of List <Number>.
The relationship between List <Integer> and List <Number> is as follows:
Therefore, the following code is correct:
List <? Extends Integer> intList = new ArrayList <> (); List <? Extends Number> numList = intList; // No error is returned. List <? Extends Integer> Yes List <? Subclass of extends Number>
The following figure shows the relationship between upper limit wildcard, lower limit wildcard, and unlimited fixed wildcard:
The compiler can determine the type of the wildcard through the type inference mechanism, which is calledWildcard character capture. Most of the time, we don't have to worry about wildcard capturing unless the compiler reports an error that contains "capture. For example:
Public class WildcardError {void foo (List <?> Ii) {I. set (0, I. get (0); // a compilation error will be reported }}
In the preceding example, when the List. set (int, E) method is called, the compiler cannot infer the type of I. get (0), and an error is returned.
We can solve this problem by using a private helper method that can capture wildcard characters:
Public class WildcardFixed {void foo (List <?> I) {fooHelper (I);} // This method ensures that the compiler uses wildcard capture to deduce the parameter type private <T> void fooHelper (List <T> l) {l. set (0, l. get (0 ));}}
According to the Conventions, the naming method of the helper method is "original method" + "helper". In the above example, the original method is "foo", so it is named "fooHelper"
When to use the upper limit wildcard and when to use the lower limit wildcard should follow several guiding rules.
First, the variables are dividedIn-VariableAndOut-Variable: The in-variable holds the data for the current Code Service, and the out-variable holds the data needed elsewhere. For example, if the copy (src, dest) method copies data from the src Source to the dest destination, src is the in-variable, and dest is the out-variable. Of course, in some cases, a variable may be both an in-variable and an out-variable.
(1) Use the upper limit wildcard for in-variables;
(2) The out-variable uses the lower limit wildcard;
(3) When an in-variable can be accessed by methods in the Object class, use an infinite set wildcard;
(4) When a variable is both in-and out-variables, wildcards are not used.
Note that the preceding rule does not apply to the return type of the method.
Type Erasure)
When processing generics, the java compiler will do the following:
(1) Replace unspecified type parameters with objects to ensure that the class file contains only normal classes, interfaces, and methods;
(2) type conversion when necessary to ensure type security;
(3) Use of generic inheritanceBridge Method(Bridge methods) maintains polymorphism.
Such operations are calledType Erasure.
For example:
public class Node<T> { private T data; private Node<T> next; public Node(T data, Node<T> next) } this.data = data; this.next = next; } public T getData() { return data; } // ...}
T in this class is not limited by extends or super and will be replaced by Object by the compiler:
public class Node { private Object data; private Node next; public Node(Object data, Node next) { this.data = data; this.next = next; } public Object getData() { return data; } // ...}
If T is qualified, the compiler will replace it with the appropriate type:
public class Node<T extends Comparable<T>> { private T data; private Node<T> next; public Node(T data, Node<T> next) { this.data = data; this.next = next; } public T getData() { return data; } // ...}
Transformed:
public class Node { private Comparable data; private Node next; public Node(Comparable data, Node next) { this.data = data; this.next = next; } public Comparable getData() { return data;} //...}
The Type erasure in the method is similar.
Sometimes the type erasure will produce some unexpected situations. The following uses an example to analyze how it is generated.
public class Node<T> { public T data; public Node(T data) { this.data = data; } public void setData(T data) { System.out.println("Node.setData"); this.data = data; }} public class MyNode extends Node<Integer>{ public MyNode(Integer data) { super(data);} public void setData(Integer data) { System.out.println("MyNode.setData"); super.setData(data); }}
The above Code defines two classes. The MyNode class inherits the Node class and then runs the following code:
MyNode mn = new MyNode (5); Node n = mn; n. setData ("Hello"); Integer x = mn. data; // throws a ClassCastException
The above code is converted to the following form after the type is erased:
MyNode mn = new MyNode (5); Node n = (MyNode) mn; n. setData ("Hello"); Integer x = (String) mn. data; // throw a ClassCastException
Let's see how the code is executed:
(1)N. setData ("Hello ")Actually called by the MyNode classSetData (Object)Method (inherited from the Node class );
(2) n the data field in the referenced object is assigned a String variable;
(3) mn-referencedSame objectData in is expected to be of the Integer type (mn is of the Node <Integer> type );
(4) The fourth line of code tries to assign a String value to an Integer type variable, which causes a ClassCastException.
When compiling a class or excuse that inherits parameterized generics, the compiler creates a bridge method called bridge method as needed, which is part of the type erasure.
In the above example, MyNode inherits the Node <Integer> class. After the type is erased, the Code becomes:
Class MyNode extends Node {// The bridge method added by the compiler public void setData (Object data) {setData (Integer) data );} // This method of MyNode does not overwrite the setData (Objectdata) method of the parent class, because the parameter type is different from public void setData (Integer data) {System. out. println ("MyNode. setData "); super. setData (data );}//...}
Notes
To use generics efficiently, pay attention to the following aspects:
(1) Basic Type instantiation type parameters cannot be used
For example
class Pair<K,V> { private K key; private V value; public Pair(K key, V value) { this.key = key; this.value = value; } // ...}
When creating a Pair class, you cannot replace K and V with the basic type.
Pair <int, char> p = new Pair <> (8, 'A'); // compilation error Pair <Integer, Character> p = new Pair <> (8, 'A'); // correct statement
(2) Non-instantiation type parameters
For example:
Public static <E> void append (List <E> list) {E elem = new E (); // compilation Error list. add (elem );}
However, we can instantiate objects with type parameters through reflection:
Public static <E> void append (List <E> list, Class <E> cls) throws Exception {E elem = cls. new Instance (); // correct list. add (elem) ;}list <String> ls = new ArrayList <> (); append (ls, String. class); // Class Object of the input type parameter
(3) You cannot use generics in static fields.
ThroughCounterexampleTo describe:
Public class MobileDevice <T> {private static T OS; // suppose we have defined a static field with generics //...} mobileDevice <Smartphone> phone = new MobileDevice <> (); MobileDevice <Pager> pager = new MobileDevice <> (); MobileDevice <TabletPC> pc = new MobileDevice <> ();
Because static variables are class variables shared by all instances, what is the real type of the static variable OS? Obviously, it cannot be both Smartphone, Pager, and TabletPC.
This is why generic type cannot be used on static fields.
(4) classes with parameterized types cannot be used. CastOr InstanceofMethod
Public static <E> void rtti (List <E> list) {if (list instanceof ArrayList <Integer>) {// compilation error //...}}
The set of parameterized types passed to this method is:
S = {ArrayList <Integer>, ArrayList <String> Character List <Character> ,...}
The runtime environment does not track the type parameters, so we cannot identify the ArrayList <Integer> and ArrayList <String>. What we can do is to use an infinite number of wildcards to verify whether the list is an ArrayList:
Public static void rtti (List <?> List) {if (list instanceof ArrayList <?>) {// Correct //...}}
Likewise, you cannot convert a parameter to an object with a parameterized type, unless its parameterized type is an infinitely set wildcard (<?>) :
List <Integer> li = new ArrayList <> (); List <Number> ln = (List <Number>) li; // compilation Error
Of course, if the compiler knows that the parameterized type must be valid, this conversion is allowed:
List <String> l1 =...; ArrayList <String> l2 = (ArrayList <String>) l1; // The type parameter is not changed.
(5) arrays with parameterized types cannot be created.
For example:
List <Integer> [] arrayOfLists = new List <Integer> [2]; // compilation Error
The following two pieces of code are used to explain why it is not feasible. First, let's look at a normal operation:
Object [] strings = new String [2]; string s [0] = "hi"; // insert string s [1] = 100; // an error is returned, because 100 is not of the String type
In the same operation, if a generic array is used, a problem occurs:
Object [] stringLists = new List <String> []; // The Code returns an error, but let's assume that it can execute string Lists [0] = new ArrayList <String> (); // insert normal string Lists [1] = new ArrayList <Integer> (); // This code should report an ArrayStoreException exception, but the runtime environment cannot detect it
(6) You cannot create or capture generic exceptions.
Generic classes cannot directly or indirectly inherit the Throwable class.
Class MathException <T> extends Exception {/*... */} // compilation error class QueueFullException <T> extends Throwable {/*... */} // compilation Error
Methods cannot capture generic exceptions:
Public static <T extends Exception, J> void execute (List <J> jobs) {try {for (J job: jobs )//...} catch (T e) {// compilation error //...}}
However, we can use the type parameter in the throw clause:
Class Parser <T extends Exception> {public void parse (File file) throws T {// correct //...}}
(7) methods that convert form parameters to the same original type after type erasure cannot be reloaded.
Let's take a look at a piece of code:
List<String> l1 = new ArrayList<String>();List<Integer> l2 = new ArrayList<Integer>();System.out.println(l1.getClass()== l2.getClass());
The printed result may be different from our guess. The printed result is true rather than false, because all instances of a generic class have the same runtime class at runtime ), regardless of their actual type parameters.
In fact, generics are called generics because they have the same behavior for all their possible type parameters. The same class can be treated as many different types.
Now, let's take a look at the following example:
Public class Example {public void print (Set <String> strSet) {}// compilation error public void print (Set <Integer> intSet) {}// compilation error}
Because Set <String> and Set <Integer> are essentially the same runtime class, after the type is erased, the above two methods will share a method signature, which is equivalent to a method, therefore, an error occurred while reloading.
Copyright Disclaimer: This article is an original article by the blogger and cannot be reproduced without the permission of the blogger.