Read with questions
1. What are Java generics and what are their uses
2. What is the implementation mechanism of Java generics
3. What are the limitations and limitations of Java generics
Introduction to Java generics
Before introducing generics, imagine writing an adder. In order to handle different numeric types, you need to overload different type parameters, but its implementation content is exactly the same. If it is a more complex method, it will undoubtedly cause duplication.
public int add(int a, int b) {return a + b;} public float add(float a, float b) {return a + b;} public double add(double a, double b) {return a + b;}
General classes and methods can only use specific types, either basic types or custom classes. If you want to write code that can be applied to many types of code, this rigid restriction will have a great constraint on the code. Java programming ideas
Java introduces generics in version 1.5. The addition code realized through generics can be simplified as follows:
public <T extends Number> double add(T a, T b) { return a.doubleValue() + b.doubleValue(); }
The core concept of generics is parameterized types, which use parameters to specify method types rather than hard coding. The emergence of generics brings us many benefits, the most important of which is the improvement of set classes, which avoids the unreliability that any type can be thrown into the same set.
However, the collection of Python and Go can accommodate any type. Is this a step forward or a step backward
Introduction to using Java generics
Generics are generally used in three ways: generic classes, generic interfaces, and generic methods.
Generic class
public class GenericClass<T> { private T member; } ... // Specifies the generic type when initializing GenericClass<String> instance = new GenericClass<String>();
generic interface
public interface GenericInterface<T> { void test(T param); } // The implementation class specifies the generic type public class GenericClass implements GenericInterface<String> { @Override public void test(String param) {...} }
generic method
As in the previous article, the implementation of addition code is a generic method.
// Add < T > before the method. Generic types can be used for return values and parameters public <T> T function(T param); ... function("123"); // The compiler automatically recognizes T as String
Deep Java generics
Pseudo generics and type erasure in Java
List<String> strList = new ArrayList<>(); List<Integer> intList = new ArrayList<>(); System.out.println(strList.getClass() == intList.getClass()); //true
For the above codes, I believe that most people think they are two different types when they come into contact with generics. Decompile their bytecode to obtain the following codes:
ArrayList var1 = new ArrayList(); ArrayList var2 = new ArrayList(); System.out.println(var1.getClass() == var2.getClass());
We found that both lists have become ArrayList type. If you are still impressed with the version before Jdk1.5, you can see that this decompiled code is the original use form of Java collection. Therefore, the of Java generics is pseudo generics implemented by erasing the actual type of generics to the original type (usually Object) at compile time.
The so-called pseudo generics are relative to the "true generics" of C + + (heterogeneous extensions, see article 3). In Java, because the specific types are erased after compilation, no information about the generic parameter types can be obtained inside the generic code, and the code only holds the erased original types at run time, This means that the parameters of any original type can be passed into the generic class by reflection at run time.
public class GenericTest { public List<Integer> ints = new ArrayList<>(); public static void main(String[] args) { GenericTest test = new GenericTest(); List<GenericTest> list = (List<GenericTest>) GenericTest.class.getDeclaredField("ints").get(test); list.add(new GenericTest()); System.out.println(test.ints.get(0)); // Print GenericTest variable address int number = test.ints.get(0); // Exception thrown by type conversion } } // Generic code internals refer to generic classes or generic methods. public class Generic<T> { public Class getTClass() { //Cannot get} } public <T> Class getParamClass(T param) { //Cannot get}
You can get the specified generic parameter type outside the generic. Check the Constant Pool through javap -v to see that the specific type is recorded in the Signature.
public class Outer { private List<String> list = new ArrayList<>(); //You can get the specific type of list }
In fact, when Java introduced generics, the template generics of C + + were quite mature, and the designers were not unable to implement generics containing specific types. The most important reason for using type erasure was to maintain compatibility. Assuming that ArrayList < string > and ArrayList are different classes after compilation, in order to be compatible with the normal operation of the old code, a set of generic collections must be added in parallel and maintained in subsequent versions. As a basic tool class widely used, the developer has to bear a lot of code switching risks (refer to the legacy problems caused by Vector and HashTable), Therefore, compared with the choice of compatibility, using type erasure to implement generics is a compromise.
Think about it. Can the following classes be compiled
public class Test { void test(List<String> param) {} void test(List<Integer> param) {} }
Upper and lower bounds of Java generics
As mentioned earlier, generics will be erased as primitive types, usually Object. If the generic declaration is <? Extensions Number > will be erased as Number.
List<Number> numbers = new ArrayList<>(); List<Integer> integers = new ArrayList<>(); numbers = integers; // compile error
Considering the above code, numbers can add elements of Integer type, and intuitively integers should also be assigned to numbers. Due to type erasure, only generic instances of the same type can be assigned to each other during the compilation period of Java, but this violates Java's polymorphism. In order to solve the problem of generic conversion, Java introduces the upper and lower limits <? Extensions a > and <? Super b > two mechanisms.
If the generic declaration is <? Extends A >, that is, declare the upper bound of the generic, that is, the original type after erasure is A, and the instance of the generic class can refer to the generic instance of subclass A.
// The upper bound guarantees that the extracted element must be Number, but it cannot constrain the type put in List<Integer> integers = new ArrayList<>(); List<Float> floats = new ArrayList<>(); List<? extends Number> numbers = integers; // numbers = floats; it's fine too numbers.get(0); // ok, I can always guarantee that the Number must be taken out numbers.put(1); // compile error, it is impossible to guarantee whether the put meets the constraints
If the generic declaration is <? Super b >, that is, declare that the lower bound of the generic is B, and the original type is still Object. At the same time, the strength of the generic class can reference the generic instance of the B parent class.
// Suppose three inheritance classes child - > father - > grandfather // The lower bound guarantees that the element to be written must be Child, but the type of extraction cannot be determined List<Father> fathers = new ArrayList<>(); List<GrandFather> grandFathers = new ArrayList<>(); List<? super Child> childs = fathers; // childs = grandFathers; it's fine too numbers.put(new Child()); //ok, it can always be guaranteed that the actual container is acceptable, Child Child ele = (Child) numbers.get(0); // runtime error, unable to determine the specific type
In Java, according to the Chinese substitution principle, the upward transformation is legal by default, and the downward transformation requires forced conversion. If it cannot be converted, an error is reported. In the get of extensions and put of super, it is guaranteed that the elements read / put can be transformed upward. However, in the put of extensions and get of super, the types that can be converted cannot be confirmed. Therefore, extensions can only read and super can only write.
Of course, if you use super, there is no problem if the extracted Object is stored as Object, because the original type after super erasure is Object.
Refer to the PECS usage suggestions given in Effective Java.
For maximum flexibility, use wildcard types on input parameters that represent producers or consumers.
If the parameterized type represents a t producer, use <? extends T>. producer-extends
If the parameterized type represents a t consumer, use <? super T>. consumer-super
If an input parameter is both a producer and a consumer, wildcard types are not good for you.
The author believes that this paragraph is somewhat confusing. Producers write and consumers read. As mentioned earlier, extends is used for reading and super is used for writing. On the contrary.
Personally, I think the correct understanding of this paragraph is to take generics as the first perspective, that is, when the generic type itself provides functions as a producer (read), use extends, and vice versa (written), use super. In an unconventional sense, the container to be written by the producer uses extends, and the container read by the consumer uses super.
// producer. At this time, the return value is provided to the consumer as the result after production List<? extends A> writeBuffer(...); // consumer. At this time, the return value is provided to the producer as the result of consumption List<? super B> readBuffer(...);
Polymorphism of Java generics
Generic classes can also be inherited. There are two main ways to inherit generic classes.
public class Father<T> { public void test(T param){} } // Generic inheritance, Child is still a generic class public class Child<T> extends Father<T> { @Override public void test(T param){} } // Specify the generic type, and StringChild is the concrete class public class StringChild extends Father<String> { @Override public void test(String param){} }
We know that @ Override keeps the signature unchanged and rewrites the parent method. Check the parent bytecode, where the test method is erased as void test(Object param); In StringChild, the method signature is void test(String param). At this point, readers may realize that this is not rewriting at all, but overloading.
View the bytecode of StringChild.
... #3 = Methodref ... public void test(java.lang.String); ... invokespecial #3 // Method StringChild.test:(Ljava/lang.Object;) V ... public void test(java.lang.Object);
You can see that it actually contains two methods. One parameter is String and the other is Object. The latter is the override of the parent method. The former goes to the call to the latter through invoke. This method is automatically added by the JVM at compile time, also known as the bridge method. At the same time, it should be mentioned that the code in the example takes generic parameters as input parameters. If they are used as return types, Object test() and String test() methods will be generated. These two methods cannot be compiled in conventional coding, but the JVM's implementation of generic polymorphism allows this non-compliance.
Limitations of using Java generics
- The basic type cannot be erased as the original type Object, so the template does not support basic types
List<int> intList = new ArrayList<>(); // Unable to compile
- Due to type erasure, the generic code cannot obtain the specific type at run time
T instance = new T(); // Cannot use generic initialization directly if (t instanceOf T); // Cannot determine generic type T[] array = new T[]{}; // Cannot create generic array
- Generic types cannot be referenced in static code blocks because static resource loading precedes type instantiation
// Error public class Generic<T> { public static T t; public static T test() {return t;} }
- Derived classes of exception types cannot add generics
// Suppose inheritance implements a generic exception class SomeException<T> extends Exception... try { ... } catch(SomeException<Integer> | SomeException<String> ex) { //Cannot catch multiple generic exceptions due to type erasure ... }
reference resources
- Java programming ideas
- <Effective Java>
- The reason why Java can't implement true generics
- Detailed explanation of Java generic mechanism