Implementation Principles of Java generics
Generic is a parameterized class or interface. Raw type removes the type parameter information. To maintain compatibility with Java, the implementation of generics is not as thorough as in C #. Let's look at the specific generic class below,
public class Node<T, U extends Number> {private T data;private List<U> list;private Node<T> next;public Node(T data, Node<T> next) {this.data = data;this.next = next;} public void setData2(List<U> l) {list = l; }public U getFirstData(){return list.get(0); }public T getData() { return data; }// ... }
When compiling this Generic type, the compiler will perform a process called Erasure of Generic Types. The specific processing content is as follows:
[1] generic type parameters are removed directly and all types parameters are replaced. For a parameter with Bounded, the corresponding boundary type is used. For example, if the generic parameter is <U extends Number>, the parameter is directly converted to Number. If the type is <T> like above, <T> is replaced with an Object.
[2] perform necessary type replacement to ensure type security.
[3] generate a bridge method to ensure that the polymorphism feature still works properly when integrating generic types.
Erasure of Generic Types and related forced type conversion
This is mainly related to [1] and [2]. According to article [1], the Java compiler will compile this generic class as follows:
// The type parameter is directly removed from public class Node {// type parameter T is replaced with Object private Object data; // U is replaced with Number private List data2; // The type parameter is directly removed from the private Node next; // The type parameter is removed directly, and the type parameter T is replaced with the Object public Node (Object data, Node next) {this. data = data; this. next = next;} // U is replaced with Number public void setData2 (List l) {list = l;} // U is replaced with Number. After necessary type conversion, it will actually change to public Number getData2 () {return (Number) list. get (0);} public Number getData2 () {return list. get (0);} // type parameter T is replaced with Object public Object getData () {return data ;}//...}
After this process, let's take a look at the 23rd lines of processed Code. There is actually a problem here. Because list. get (0) returns an Object, while the getData2 method returns a Number type. The Object type cannot be directly assigned to the Number type. Therefore, a forced update is required. This is also the meaning of article [2] mentioned above. After the rule of article [2], row 23rd will actually be compiled:
public Number getData2() { return (Number)list.get(0); }
In this way, no matter whether we are using generic
When the specific type is used, the above Code can ensure the type security. For example,
Node<String, Integer> node1 = new Node<String, Integer>(); Node<List, Short> node2 = new Node<List, Short>();
Node <String, Integer> and Node <List, Short> are converted to Node for use, and Node <String, Integer> and Node <List are not available, short> these two types exist. The compiler performs type conversion to ensure the type security when calling related methods. This requires forced type conversion. For example, we write the following code:
Node<String, Integer> node1 = new Node<String, Integer>(); Integer somenumber = node1.getData2()
This code will be compiled as follows, because getData2 returns a Number type and a forced conversion is required here:
Node<String, Integer> node1 = new Node<String, Integer>(); Integer somenumber = (Integer)node1.getData2();
Bridge Method)
Let's take a look at the following example (copy the relevant Oracle documents ):
public class Node<T> { private 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); } }
As mentioned above, this code changes to the following after the de-Generic type (Erasure of Generic Types:
public class Node { private Object data; public Node(Object data) { this.data = data; } public void setData(Object data) { System.out.println("Node.setData"); this.data = data; } } public class MyNode extends Node { public MyNode(Integer data) { super(data); } public void setData(Integer data) { System.out.println(Integer data); super.setData(data); } }
Note: After Erasure of Generic Types, consider the following code:
MyNode mn = new MyNode(5);Node n = (MyNode)mn; n.setData("Hello"); mn.data //??
Pay special attention to the 3rd rows, because the base class and the subclass have two setData (Object) and setData (Integer) methods respectively. Because the parameters of the methods are different, the two are actually overloaded methods, so the third line goes back to call Node's setData (Object ). So what is data at this time? Obviously there is a problem here, And the inherited polymorphism is not saved. Here we need to do the previously mentioned Article [3]. The compiler will add a setData (Object) method to the subclass. This method is called the bridge method as follows:
Class MyNode extends Node {// The Bridge Method automatically generated by the compiler public void setData (Object data) {setData (Integer) data);} public void setData (Integer data) {System. out. println ("MyNode. setData "); super. setData (data );}//...}
Now let's look at the following code:
MyNode mn = new MyNode(5);Node n = (MyNode)mn;n.setData("Hello");
Here, setData will call the method of the base class. Of course, there will be errors during running, because the string cannot be converted into an integer. The purpose is to maintain the polymorphism of generic inheritance.