Network Security Internet Technology Development Database Servers Mobile Phone Android Software Apple Software Computer Software News IT Information

In addition to Weibo, there is also WeChat

Please pay attention

WeChat public account

Shulou

Example Analysis of java generic Type erasure

2025-03-29 Update From: SLTechnology News&Howtos shulou NAV: SLTechnology News&Howtos > Internet Technology >

Share

Shulou(Shulou.com)06/01 Report--

In this issue, the editor will bring you an example analysis of java generic type erasure. The article is rich in content and analyzed and described from a professional point of view. I hope you can get something after reading this article.

Implementation of 1.Java generics: type erasure

As we all know, Java generics are pseudo-generics, because all generics information is erased during compilation of Java. The first prerequisite for correctly understanding the concept of generics is to understand type erasure. Java generics are basically implemented at the compiler level, the generated bytecode does not contain type information in generics, type parameters are added when generics are used, and will be removed when the compiler compiles, and this process is called type erasure.

For example, defining types such as List and List in your code will become List,JVM after compilation. All you can see is List, but the type information attached by generics is not visible to JVM. The Java compiler will find possible errors as much as possible during compilation, but it is still unable to achieve type conversion exceptions at run time. Type erasing is also an important difference between Java generics and C++ template mechanism implementation.

One-two. Using two examples to prove the type erasure of Java type

Example 1. The original type is equal

Public class Test {public static void main (String [] args) {ArrayList list1 = new ArrayList (); list1.add ("abc"); ArrayList list2 = new ArrayList (); list2.add (123); System.out.println (list1.getClass () = = list2.getClass ());}}

In this example, we define two ArrayList arrays, but one is of ArrayList generic type, which can only store strings; the other is of ArrayList generic type, which can only store integers. Finally, we get the information about their classes through the getClass () method of the list1 object and list2 object, and find that the result is true. Indicates that the generic types String and Integer have been erased, leaving only the original type.

Example 2. Add other types of elements by reflection

Public class Test {public static void main (String [] args) throws Exception {ArrayList list = new ArrayList (); list.add (1); / / calling the add method in this way can only store shaping, because instances of generic types are Integer list.getClass (). GetMethod ("add", Object.class) .invoke (list, "asd"); for (int I = 0; I < list.size ()) ) {System.out.println (list.get (I));}

In the program, an ArrayList generic type is instantiated as an Integer object. If the add () method is called directly, then only integer data can be stored, but when we use reflection to call the add () method, we can store strings, which shows that the Integer generic instance is erased after compilation, leaving only the original type.

two。 The original type retained after type erasure

Above, the primitive type is mentioned twice. What is the primitive type?

Primitive type: the true type of the type variable in the bytecode is erased, and whenever a generic type is defined, the corresponding primitive type is automatically provided, the type variable is erased, and replaced with its qualified type (unqualified variable with Object).

Example 3. Primitive type Object

Class Pair {private T value; public T getValue () {return value;} public void setValue (T value) {this.value = value;}}

After the original type of Pair is compiled to bytecode:

Class Pair {private Object value; public Object getValue () {return value;} public void setValue (Object value) {this.value = value;}}

Because T is an unqualified type variable in Pair, replace it with Object and the result is a normal class, just as generics were implemented before they were added to the Java language. You can include different types of Pair in your program, such as Pair or Pair, but after erasing the type, theirs becomes the original Pair type, all of which are Object. Mainly to cater to the pre-JDK 1.5.

From example 2 above, we can also see that after the ArrayList is erased, the original type becomes Object, so we can store strings through reflection.

If the type variable is qualified, the original type is replaced with the type variable class of the first boundary.

For example, if Pair declares like this

Public class Pair {}

Then the primitive type is Comparable.

To distinguish between primitive types and generic variable types.

When you call a generic method, you can specify generics or no generics.

Without specifying a generic type, the type of the generic variable is the smallest level of the same parent class of several types in the method (where the inheritance graph intersects earliest from the bottom to the top) until Object

In the case of specifying a generic type, several types of the method must be the type of the instance of the generic type or its subclasses

Public class Test {public static void main (String [] args) {/ * * when generics are not specified * / int I = Test.add (1,2); / / both parameters are Integer, so T is the Integer type Number f = Test.add (1,1.2) / / one of these two parameters is Integer, and the style is Float, so take the minimum level of the same parent class as Number Object o = Test.add (1, "asd"); / / one of these two parameters is Integer, so the style is Float, so take the minimum level of the same parent class and specify * / int a = Test.add (1, 2) for Object / * * / / Integer is specified, so only Integer type or its subclass int b = Test.add (1,2.2); / / compilation error, Integer is specified, not Float Number c = Test.add (1,2.2) / / is specified as Number, so it can be Integer and Float} / / this is a simple generic method public static T add (T XJ T y) {return y;}}

In fact, in generic classes, the same is true when generics are not specified, except that generics are Object at this time, as in ArrayList, if generics are not specified, then this ArrayList can store arbitrary objects.

Example 4.Object generics

Public static void main (String [] args) {ArrayList list = new ArrayList (); list.add (1); list.add ("121"); list.add (new Date ());} 3. Problems caused by type erasure and their solutions

For a variety of reasons, Java can not achieve true generics, can only use type erasure to achieve pseudo-generics, so although there will be no problem of type inflation, but it also gives rise to many new problems, so SUN has made a variety of restrictions on these problems to avoid all kinds of mistakes.

3-1. Check, compile, and compile object and reference delivery problems first

Q: since it is said that type variables will be erased at compile time, why do we report an error by adding integers to the object created by ArrayList? Doesn't it mean that the generic variable String becomes Object at compile time? Why can't there be other types? Now that the types are erased, how can we ensure that we can only use types qualified by generic variables?

A: the Java compiler compiles by checking the types of generics in your code and then erasing them.

For example:

Public static void main (String [] args) {ArrayList list = new ArrayList (); list.add ("123"); list.add (123); / / compilation error}

In the above program, use the add method to add an integer, in IDE, will directly report an error, indicating that this is the check before compilation, because if it is checked after compilation, after the type is erased, the original type is Object, any reference type should be allowed to be added. In fact, this is not the case, which means that the use of generic variables is checked before compilation.

So, who is this type of check aimed at? Let's first look at the compatibility of parameterized types with primitive types.

Take ArrayList as an example, it was written in the previous way:

ArrayList list = new ArrayList ()

The way it is now written:

ArrayList list = new ArrayList ()

If it is compatible with previous code, the following must occur between the values passed by various references:

ArrayList list1 = new ArrayList (); / / the first case ArrayList list2 = new ArrayList (); / / the second case

There are no errors, but there will be a compile-time warning.

In the first case, however, you can achieve the same effect as fully using generic parameters, while the second has no effect.

Because type checking is done at compile time, new ArrayList () only opens up a storage space in memory to store objects of any type, while what the real design type checks is its reference, because we use it to reference list1 to call its methods, such as calling the add method, so the list1 reference can complete the generic type checking. The reference list2 does not use generics, so it is not possible. Follow up the type when the variable is declared for type checking.

Examples are as follows:

Public class Test {public static void main (String [] args) {ArrayList list1 = new ArrayList (); list1.add ("1"); / / compiled through list1.add (1); / / compilation error String str1 = list1.get (0); / / return type is String ArrayList list2 = new ArrayList (); list2.add ("1") / / compiled by list2.add (1); / / compiled by Object object = list2.get (0); / / the return type is Object new ArrayList (). Add ("11"); / / compiled by new ArrayList (). Add (22); / / compilation error String str2 = new ArrayList (). Get (0); / / return type is String}}

The conclusion is super important: type checking in generics is for references. Who is a reference and calling a generic method with this reference will check the type of the method called by that reference, regardless of the object it actually references.

Why don't parameterized types consider inheritance relationships in generics?

Reference passing in the form of the following is not allowed in Java:

ArrayList list1 = new ArrayList (); / / compilation errors these are two different types ArrayList list2 = new ArrayList (); / / compilation errors these are two different types

Let's first look at the first situation and expand the first situation into the following form:

ArrayList list1 = new ArrayList (); list1.add (new Object ()); list1.add (new Object ()); ArrayList list2 = list1; / / compilation error

In fact, in line 4 of the code, there will be compilation errors. So, let's assume it compiled correctly. So when we use the list2 reference to take the value with the get () method, we return objects of type String (as mentioned above, type checking is based on references), but we have actually stored objects of type Object in it, so there will be ClassCastException. So in order to avoid this highly prone error, Java does not allow such reference passing. (this is also the reason for generics, that is, in order to solve the problem of type conversion, we can't go against its original intention.) one is to set the rules before playing (after JDK1.5), and the other is to set the rules after putting them (before JDK1.5). Obviously, it's better after 1.5.

Let's take a look at the second case, and expand the second situation into the following form:

ArrayList list1 = new ArrayList (); list1.add (new String ()); list1.add (new String ()); ArrayList list2 = list1; / / compilation error

Yes, this situation is much better than the first case, at the very least, there is no ClassCastException when we use list2, because it is a conversion from String to Object. However, what's the point of doing this? the reason for generics is to solve the problem of type conversion. We use generics, but in the end, we still have to make strong changes on our own, which goes against the original intention of generic design. So java doesn't allow it. Besides, if you use list2 to add () a new object, how do I know if it's of type String or type Object when I get it?

Therefore, pay special attention to the problem of reference passing in generics.

3-2. Low-level automatic type conversion

Because of the problem of type erasure, all generic type variables are eventually replaced with the original type.

Since they are all replaced with primitive types, why do we not need to cast when we get them?

Look at the ArrayList.get () method:

Public E get (int index) {RangeCheck (index); return (E) elementData [index];}

As you can see, before return, strong turns are made based on generic variables. Assuming that the generic type variable is Date, the generic information will be erased, but (E) elementData [index] will be compiled to (Date) elementData [index]. So we don't have to do it ourselves. A cast is also automatically inserted when accessing a generic field. Assuming that the value field of the Pair class is public, the expression:

Date date = pair.value

A cast is also automatically inserted in the resulting bytecode.

3-3. Conflicts and Solutions between Type erasure and Polymorphism

There is now a generic class:

Class Pair {private T value; public T getValue () {return value;} public void setValue (T value) {this.value = value;}}

Then we want a subclass to inherit it.

Class DateInter extends Pair {@ Override public void setValue (Date value) {super.setValue (value);} @ Override public Date getValue () {return super.getValue ();}}

In this subclass, we set the generic type of the parent class to Pair. In the subclass, we override the two methods of the parent class. Our original intention is to limit the generic type of the parent class to Date, then the arguments of both methods in the parent class are of type Date.

Public Date getValue () {return value;} public void setValue (Date value) {this.value = value;}

So we have no problem overriding these two methods in subclasses. In fact, as you can see from their @ Override tag, there is no problem at all. Is that really the case?

Analysis: in fact, after the type is erased, all the generic types of the parent class become the original type Object, so the parent class will look like this after compilation:

Class Pair {private Object value; public Object getValue () {return value;} public void setValue (Object value) {this.value = value;}}

Then look at the types of two overridden methods of the subclass:

Override public void setValue (Date value) {super.setValue (value);} @ Override public Date getValue () {return super.getValue ();}

First of all, let's analyze the setValue method. The type of the parent class is Object, while the type of the subclass is Date, and the parameter type is different. If it is really a common inheritance relationship, it is not overridden at all, but overloaded.

Let's test it in a main method:

Public static void main (String [] args) throws ClassNotFoundException {DateInter dateInter = new DateInter (); dateInter.setValue (new Date ()); dateInter.setValue (new Object ()); / / compilation error}

If it is overloaded, then there are two setValue methods in the subclass, one is the parameter Object type and the other is the Date type, but we found that there is no such subclass inheriting from the Object type parameter of the parent class. So, it is indeed rewritten, not reloaded.

What causes it?

The reason is that we pass in that the generic type of the parent class is Date,Pair, and our intention is to change the generic class to the following:

Class Pair {private Date value; public Date getValue () {return value;} public void setValue (Date value) {this.value = value;}}

Then override the two methods with parameter type Date in the subclass to implement polymorphism in inheritance.

However, for a variety of reasons, the virtual machine can not change the generic type to Date, but can only erase the type and become the original type Object. In this way, our original intention is to rewrite and achieve polymorphism. However, after the type is erased, it can only become overloaded. In this way, type erasure conflicts with polymorphism. Does JVM know what you mean? Yes! But can it be realized directly? no! If we really can't, how can we rewrite the method of the Date type parameter we want?

So JVM uses a special method to accomplish this function, that is, the bridge method.

First, we decompile the bytecode of the DateInter subclass using javap-c className. The result is as follows:

Class com.tao.test.DateInter extends com.tao.test.Pair {com.tao.test.DateInter (); Code: 0: aload_0 1: invokespecial # 8 / / Method com/tao/test/Pair. "": () V 4: return public void setValue (java.util.Date) / / our rewritten setValue method Code: 0: aload_0 1: aload_1 2: invokespecial # 16 / / Method com/tao/test/Pair.setValue: (Ljava/lang/Object;) V 5: return public java.util.Date getValue () / / our rewritten getValue method Code: 0: aload_0 1: invokespecial # 23 / / Method com/tao/test/Pair.getValue: () Ljava/lang/Object; 4: checkcast # 26 / / class java/util/Date 7: areturn public java.lang.Object getValue () / / the clever method Code: 0: aload_0 1: invokevirtual # 28 / / Method getValue: () Ljava/util/Date generated by the compiler at compile time to call our rewritten getValue method; 4: areturn public void setValue (java.lang.Object) / / ingenious method Code: 0: aload_0 1: aload_1 2: checkcast # 26 / / class java/util/Date 5: invokevirtual # 30 / / Method setValue: (Ljava/util/Date; to call our rewritten setValue method) V 8: return}

From the result of the compilation, we intended to rewrite the subclass of the setValue and getValue methods, there are actually four methods, in fact, not surprisingly, the last two methods are the bridge methods generated by the compiler itself. You can see that the parameter type of the bridge method is Object, that is, what really overrides the two methods of the parent class in the subclass are the two bridge methods that we can't see. The @ Oveerride typing on top of our own defined setvalue and getValue methods is just an illusion. The internal implementation of the bridge method is simply to call the two methods that we rewrite.

Therefore, the virtual machine skillfully uses the bridge method to solve the conflict between type erasure and polymorphism.

However, it is important to mention that the meaning of the two bridge methods setValue and getValue are different.

The purpose of the setValue method is to resolve the conflict between type erasure and polymorphism. Because the input parameters are different.

On the other hand, getValue has a universal meaning, how to put it, if this is an ordinary inheritance relationship:

Then the setValue method of the parent class is as follows:

Public Object getValue () {return super.getValue ();}

The method of subclass rewriting is:

Public Date getValue () {return super.getValue ();}

In fact, this is also a common rewriting in ordinary class inheritance, which is called covariance.

About covariation:

Also, it may be doubtful that the clever methods Object getValue () and Date getValue () in the subclass exist at the same time, but if they are two regular methods, their method signatures (method name and parameter type) are the same, which means that the virtual machine cannot distinguish the two methods at all. If we write our own Java code, such code cannot be checked by the compiler, but the virtual machine is allowed to do so, because the virtual machine determines a method by parameter type and return type, so the compiler allows itself to do what seems to be "illegal" in order to achieve generic polymorphism, and then give it to the virtual machine to distinguish.

3-4. Generic type variables cannot be basic data types

You cannot replace a base type with a type parameter. For example, there is no ArrayList, only ArrayList. Because when the type is erased, the original type of ArrayList becomes Object, but the Object type cannot store the Double value and can only reference the value of the type. Because the parent of the basic type is not Ojbect, there is no relationship between the two.

3-5. Runtime type query ArrayList arrayList = new ArrayList ()

Because after the type is erased, only the original type Object is left in the ArrayList, and the generic information String no longer exists.

Then, it is wrong to use the following method when doing type queries at run time

If (arrayList instanceof ArrayList) 3-6. Problems of generics in static methods and static classes

Static methods and static variables in a generic class cannot use generic type parameters declared by a generic class

Examples are as follows:

Public class Test2 {public static T one; / / compilation error public static T show (T one) {/ / compilation error return null;}}

Because the instantiation of generic parameters in a generic class is specified when the object is defined, static variables and static methods do not need to be called with an object. Objects have not been created, how to determine the type of this generic parameter, so of course it is wrong.

But be careful to distinguish one of the following situations:

Public class Test2 {public static T show (T one) {/ / this is the correct return null;}}

Because this is a generic method, the T used in the generic method is the T defined in the method, not the T in the generic class.

The above is the example of java generic type erasure shared by Xiaobian. If you happen to have similar doubts, you might as well refer to the above analysis to understand. If you want to know more about it, you are welcome to follow the industry information channel.

Welcome to subscribe "Shulou Technology Information " to get latest news, interesting things and hot topics in the IT industry, and controls the hottest and latest Internet news, technology news and IT industry trends.

Views: 257

*The comments in the above article only represent the author's personal views and do not represent the views and positions of this website. If you have more insights, please feel free to contribute and share.

Share To

Internet Technology

Wechat

© 2024 shulou.com SLNews company. All rights reserved.

12
Report