This article mainly introduces the use of Java generics and issues related to type erasure. Has very good reference value. Let’s take a look at it with the editor
Introduction
Java introduced the generic mechanism in 1.5. The essence of generics is parameterized types, that is to sayThe type of variable is a parameter, which is specified as a specific type when used. Generics can be used for classes, interfaces, and methods. By using generics, the code can be made simpler and safer. However, generics in Java use type erasure, so they are just pseudo-generics. This article summarizes the use of generics and existing problems, mainly referring to "Java Programming Thoughts".
The other two articles in this series:
Summary of Java Generics (2): Generics and Arrays
Summary of Java Generics (3): The use of wildcards
Basic usage
Generic classes
If there is a class Holder used to wrap a variable, the type of this variable may be arbitrary. How to write the Holder? Before there were generics, you could do this:
public class Holder1 { private Object a; public Holder1(Object a) { this.a = a; } public void set(Object a) { this.a = a; } public Object get(){ return a; } public static void main(String[] args) { Holder1 holder1 = new Holder1("not Generic"); String s = (String) holder1.get(); holder1.set(1); Integer x = (Integer) holder1.get(); } }
In Holder1, there is a variable referenced by Object. Because any type can be upcast to Object, this Holder can accept any type. When taking it out, Holder only knows that it saves an Object Object, so it must be forced to the corresponding type. In the main method, holder1 first saves a string, which is a String object, and then changes to save an Integer object (parameter 1 will be automatically boxed). Casting when taking variables out of the Holder is already troublesome. You also have to remember different types here. If you make the wrong conversion, a runtime exception will occur.
Let’s take a look at the generic version of Holder:
public class Holder2<T> { private T a; public Holder2(T a) { this.a = a; } public T get() { return a; } public void set(T a) { this.a = a; } public static void main(String[] args) { Holder2<String> holder2 = new Holder2<>("Generic"); String s = holder2.get(); holder2.set("test"); holder2.set(1);//无法编译 参数 1 不是 String 类型 } }
In Holder2, variable a is a parameterized type T. T is just an identifier, and other letters can also be used. When creating the Holder2 object, the type of parameter T is passed in angle brackets, then in this object, all occurrences of T are equivalent to being replaced with String. What get now takes out is not Object, but String object, so type conversion is not needed. In addition, when calling set, only the String type can be passed in, otherwise the compilation will not pass. This ensures type safety in holder2 and avoids accidentally passing in the wrong type.
Through the above example, we can see that pan makes the code simpler and safer. After the introduction of generics, some classes in the Java library, such as commonly used container classes, have also been rewritten to support generics. When we use them, we will pass in parameter types, such as: ArrayListc0f559cc8d56b43654fcbe4aa9df7b4a list = ArrayLista8093152e673feb7aba1828c43532094();.
Generic methods
Generics can not only target classes, but can also make a method generic individually. For example:
public class GenericMethod { public <K,V> void f(K k,V v) { System.out.println(k.getClass().getSimpleName()); System.out.println(v.getClass().getSimpleName()); } public static void main(String[] args) { GenericMethod gm = new GenericMethod(); gm.f(new Integer(0),new String("generic")); } } 代码输出: Integer String## The #GenericMethod class itself is not generic. There is no need to pass in generic parameters when creating its object, but its method f is a generic method. Before the return type is its parameter identifier b77a8d9c3c319e50d4b02a976b347910. Note that there are two generic parameters here, so there can be multiple generic parameters. You do not need to explicitly pass in generic parameters when calling a generic method, which is not the case in the above call. This is because the compiler uses parameter type inference to infer the types of K and V based on the types of the arguments passed in (here, integer and String).
Type erasure
What is type erasure
Java generics use The type erasure mechanism has caused great controversy, so much so that Java's generic functions have been limited and can only be said to be "pseudo-generic". What is type erasure? To put it simply, type parameters only exist at compile time. At runtime, the Java virtual machine (JVM) does not know the existence of generics. Let’s look at an example first:public class ErasedTypeEquivalence { public static void main(String[] args) { Class c1 = new ArrayList<String>().getClass(); Class c2 = new ArrayList<Integer>().getClass(); System.out.println(c1 == c2); } }The above code has two different ArrayLists: ArrayListc0f559cc8d56b43654fcbe4aa9df7b4a and ArrayListf7e83be87db5cd2d9a8a0b8117b38cd4. In our opinion, their parameterized types are different, one saves integers and the other saves strings. But by comparing their Class objects, the above code output is true. This shows that they are the same class in the JVM's view. In languages such as C++ and
C# that support true generics, they are different classes.
The generic parameter will be erased to its first boundary. For example, in the Holder2 class above, if the parameter type is a single T, then it will be erased to Object, which is equivalent to all places where T appears. Replace with Object. So from the perspective of the JVM, the saved variable a is still of type Object. The reason why it is automatically taken out is the parameter type we passed in. This is because the compiler inserts the type conversion code into the compiled bytecode file, so we do not need to manually convert it. If the parameter type has boundaries, then erase it to its first boundary, which will be discussed in the next section.擦除带来的问题
擦除会出现一些问题,下面是一个例子:
class HasF { public void f() { System.out.println("HasF.f()"); } } public class Manipulator<T> { private T obj; public Manipulator(T obj) { this.obj = obj; } public void manipulate() { obj.f(); //无法编译 找不到符号 f() } public static void main(String[] args) { HasF hasF = new HasF(); Manipulator<HasF> manipulator = new Manipulator<>(hasF); manipulator.manipulate(); } }
上面的 Manipulator 是一个泛型类,内部用一个泛型化的变量 obj,在 manipulate 方法中,调用了 obj 的方法 f(),但是这行代码无法编译。因为类型擦除,编译器不确定 obj 是否有 f() 方法。解决这个问题的方法是给 T 一个边界:
class Manipulator2<T extends HasF> { private T obj; public Manipulator2(T x) { obj = x; } public void manipulate() { obj.f(); } }
现在 T 的类型是 1f179c3e268e631bc7ed98c5289251b7,这表示 T 必须是 HasF 或者 HasF 的导出类型。这样,调用 f() 方法才安全。HasF 就是 T 的边界,因此通过类型擦除后,所有出现 T 的
地方都用 HasF 替换。这样编译器就知道 obj 是有方法 f() 的。
但是这样就抵消了泛型带来的好处,上面的类完全可以改成这样:
class Manipulator3 { private HasF obj; public Manipulator3(HasF x) { obj = x; } public void manipulate() { obj.f(); } }
所以泛型只有在比较复杂的类中才体现出作用。但是像 1f179c3e268e631bc7ed98c5289251b7 这种形式的东西不是完全没有意义的。如果类中有一个返回 T 类型的方法,泛型就有用了,因为这样会返回准确类型。比如下面的例子:
class ReturnGenericType<T extends HasF> { private T obj; public ReturnGenericType(T x) { obj = x; } public T get() { return obj; } }
这里的 get() 方法返回的是泛型参数的准确类型,而不是 HasF。
类型擦除的补偿
类型擦除导致泛型丧失了一些功能,任何在运行期需要知道确切类型的代码都无法工作。比如下面的例子:
public class Erased<T> { private final int SIZE = 100; public static void f(Object arg) { if(arg instanceof T) {} // Error T var = new T(); // Error T[] array = new T[SIZE]; // Error T[] array = (T)new Object[SIZE]; // Unchecked warning } }
通过 new T() 创建对象是不行的,一是由于类型擦除,二是由于编译器不知道 T 是否有默认的构造器。一种解决的办法是传递一个工厂对象并且通过它创建新的实例。
interface FactoryI<T> { T create(); } class Foo2<T> { private T x; public <F extends FactoryI<T>> Foo2(F factory) { x = factory.create(); } // ... } class IntegerFactory implements FactoryI<Integer> { public Integer create() { return new Integer(0); } } class Widget { public static class Factory implements FactoryI<Widget> { public Widget create() { return new Widget(); } } } public class FactoryConstraint { public static void main(String[] args) { new Foo2<Integer>(new IntegerFactory()); new Foo2<Widget>(new Widget.Factory()); } }
另一种解决的方法是利用模板设计模式:
abstract class GenericWithCreate<T> { final T element; GenericWithCreate() { element = create(); } abstract T create(); } class X {} class Creator extends GenericWithCreate<X> { X create() { return new X(); } void f() { System.out.println(element.getClass().getSimpleName()); } } public class CreatorGeneric { public static void main(String[] args) { Creator c = new Creator(); c.f(); } }
具体类型的创建放到了子类继承父类时,在 create 方法中创建实际的类型并返回。
总结
本文介绍了 Java 泛型的使用,以及类型擦除相关的问题。一般情况下泛型的使用比较简单,但是某些情况下,尤其是自己编写使用泛型的类或者方法时要注意类型擦除的问题。接下来会介绍数组与泛型的关系以及通配符的使用。
The above is the detailed content of Summary of Java Generics (1) - Detailed explanation of basic usage and type erasure. For more information, please follow other related articles on the PHP Chinese website!