Detailed explanation of Java generic mechanism

Posted by releasedj on Sat, 20 Nov 2021 20:04:06 +0100

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> {
    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> { 
    public void test(T param){} 
// Specify the generic type, and StringChild is the concrete class
public class StringChild extends Father<String> { 
    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

Topics: Java