Read with questions

1. What are Java generics and what do they do

2. What is the implementation mechanism of Java generics

What are the limitations and limitations of Java generics

Introduction to Java generics

Before the introduction of generics, imagine writing an adder that overloads different type parameters to handle different numeric types, but the implementation is exactly the same, and a more complex approach would be repetitive.

public int add(int a, int b) {returna + b; }public float add(float a, float b)  {returna + b; }public double add(double a, double b)  {returna + b; }Copy the code

For general classes and methods, you can only use specific types, either primitive types or custom classes. This rigid constraint can be very restrictive if you want to write code that can be applied to multiple types. Ideas for Java Programming

Java introduced generics in version 1.5, and the addition code implemented through generics can be simplified to:

public <T extends Number> double add(T a, T b) {
    return a.doubleValue() + b.doubleValue();
} 
Copy the code

The core concept of generics is parameterized typing, using parameters to specify method types rather than hard coding. The advent of generics has brought us many benefits, the most important of which is the improvement of the collection class, which avoids the unreliable problem that any type can be thrown into the same collection.

However, the collection of Python and Go can accommodate any type, which is a step forward or backward

Introduction to the use of Java generics

Generics are generally used in three ways: generic classes, generic interfaces, and generic methods.

A generic class

public class GenericClass<T> {
    privateT member; }...// Specify a generic type at initialization
GenericClass<String> instance = new GenericClass<String>();
Copy the code

A 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) {...}
}
Copy the code

Generic method

As in the previous article, the implementation of the addition code is a generic method.

// Add 
      
        before the method. Generic types can be used for return values as well as parameters
      
public <T> T function(T param); . function("123"); // The compiler automatically recognizes T as String
Copy the code

Dive into Java generics

Java’s pseudo-generics and type erasure

List<String> strList = new ArrayList<>();
List<Integer> intList = new ArrayList<>();
System.out.println(strList.getClass() == intList.getClass()); //true
Copy the code

As for the above code, I believe that the first time most people encounter generics, they think that these are two different types. Decompressing the bytecode yields the following code:

ArrayList var1 = new ArrayList();
ArrayList var2 = new ArrayList();
System.out.println(var1.getClass() == var2.getClass());
Copy the code

We see that both lists are of type ArrayList, and if you remember anything from Jdk1.5, this decompiled piece of code is the original use of Java collections. Thus, Java generics are pseudo-generics implemented at compile time by erasing the actual type of the generic to the original type (usually Object).

Pseudo-generics are the “true generics” of C++ (heterogeneous extensions, see article 3). In Java, because specific types are erased after compilation, there is no information about generic parameter types inside the generic code, and all the code holds at runtime is the original type erased. This means that any primitive type parameter can be passed to the generic class at run time by reflection.

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));   // Prints the GenericTest variable address
        int number = test.ints.get(0);  // Type conversion throws an exception}}// Inside generic code means inside a generic class or method.
public class Generic<T> {
    public Class getTClass(a) {  // can't get}
}
public <T> Class getParamClass(T param) { // can't get}
Copy the code

The specified generic parameter types can be obtained outside of the generic type. Using javap -v to view the Constant Pool, you can see that the specific types are recorded in Signature.

public class Outer {
    private List<String> list = new ArrayList<>();  // We can get the specific type of list
}
Copy the code

In fact, by the time Java introduced generics, template generics in C++ were quite mature, and designers were not without the ability to implement generics containing concrete types. The most important reason for using type erasure was to maintain compatibility. If ArrayList

and ArrayList are compiled as different classes, then in order for the old code to run properly, a set of generic collections must be added in parallel and maintained at the same time in later versions, and the collection classes are the basic utility classes that are used extensively. Developers have to risk a lot of code switching (see the legacy of Vector and HashTable), so using type erasers to implement generics is a compromise over compatibility.

Can the following classes compile

public class Test {
void test(List<String> param) {}
void test(List<Integer> param) {}}Copy the code

Upper and lower bounds for Java generics

As mentioned earlier, generics are erased to primitive types, usually Object. If the generic is declared
is erased to Number.

List<Number> numbers = new ArrayList<>();
List<Integer> integers = new ArrayList<>();
numbers = integers;	// compile error
Copy the code

Considering the code above, numbers can add elements of type Integer, and it is intuitive that integers should also be able to assign values to numbers. Due to type erasures, Java establishes at compile time that only generic instances of the same type can be assigned to each other. However, this violates Java’s polymorphism. To solve the problem of generic conversions, Java introduced lower and upper bounds
and
two mechanisms.

If the generic is declared
, which declares that the upper bound of the generic is A, and that instances of the generic class can refer to generic instances of subclasses of A.

// The upper bound guarantees that the fetched element must be Number, but does not constrain the put type
List<Integer> integers = new ArrayList<>();
List<Float> floats = new ArrayList<>(); 
List<? extends Number> numbers = integers;	// numbers = floats; Can also be
numbers.get(0);	// select * from 'Number'
numbers.put(1);	// compile error, which cannot be guaranteed if the constraints are put
Copy the code

If the generic is declared
, that is, declare that the lower bound of the generic is B, the original type is still Object, and the strength of the generic class can reference the generic instance of B’s parent class.

Child -> Father -> GrandFather
// The lower bound guarantees that the element to be written is Child, but does not determine the type to be fetched
List<Father> fathers = new ArrayList<>();
List<GrandFather> grandFathers = new ArrayList<>();
List<? super Child> childs = fathers;	// childs = grandFathers; Can also be
numbers.put(new Child());	//ok, always ensure that the actual container accepts Child
Child ele = (Child) numbers.get(0);	// Runtime error, cannot determine the specific type obtained
Copy the code

In Java, an upward transition is legal by default, while a downward transition requires a cast. If it cannot be cast, an error is reported. In the put scenario of extends GET and super, it is guaranteed that elements read/put are uptransitable. In the PUT scenario of extends and Super, there is no way to recognize the type that can be transitable, so extends can only read and super can only write.

Of course, if you use super, the removed Object is stored as Object, there is no problem, because the original type of super after erasing is Object.

See The PECS recommendations 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, the wildcard type doesn’t do you much good.

The producer writes and the consumer reads. Extends is used to read and super is used to write.

I think the correct way to read this paragraph is to start with generics, using extends when generic types themselves provide functionality as producers (to be read) and super when generic types are written (to be written). In the unconventional sense, producers write to containers using extends and consumers read containers using super.

// producer, in which case the return value is provided to consumers as the result of productionList<? extends A> writeBuffer(...) ;// consumer, in which case the return value is provided to the producer as the result of consumption
List<? superB> readBuffer(...) ;Copy the code

Polymorphism of Java generics

Generic classes can also be inherited, and there are two main ways to inherit generic classes.

public class Father<T> { public void test(T param){}}// Child is still a generic class
public class Child<T> extends Father<T> { 
    @Override
    public void test(T param){}}// Specify the generic type. StringChild is the concrete class
public class StringChild extends Father<String> { 
    @Override
    public void test(String param){}}Copy the code

Void test(Object param); void test(Object param); void test(Object param); In StringChild, the method signature is void test(String param). At this point the reader may realize that this is not overwriting at all but overloading.

Look at the bytecode for StringChild.

. #3 = Methodref
...
public void test(java.lang.String); . invokespecial #3 // Method StringChild.test:(Ljava/lang.Object;) V.public void test(java.lang.Object);
Copy the code

You can see that it actually contains two methods, one with a String parameter and the other with an Object parameter. The latter is a rewrite of the parent method, and the former is invoked to call the latter. This method is automatically added by the JVM at compile time and is also called a bridge method. It should also be mentioned that the code in the example takes a generic parameter as an input. As a return type, it produces two methods, Object test() and String test(). These methods would not compile in normal coding, but the JVM’s implementation of generic polymorphism allows for this non-compliance.

Limitations of Java generics use

  • A primitive type cannot be erased to a primitive typeObject, so the stereotype does not support primitive types
List<int> intList = new ArrayList<>(); // Failed to compile
Copy the code
  • Because of type erasure, concrete types cannot be obtained internally at run time in generic code
T instance = new T();   // You cannot use generic initialization directly
if (t instanceOf T);    // Generic types cannot be judged
T[] array = new T[]{};  // Generic arrays cannot be created
Copy the code
  • Because static resource loading precedes type instantiation, generics cannot be referenced in static code blocks
// Error
public class Generic<T> {
    public static T t;
    public static T test(a) {return t;}
}
Copy the code
  • A derived class of an exception type cannot add generics
// Suppose inheritance implements a generic exception
class SomeException<T> extends Exception.try {... }catch(SomeException<Integer> | SomeException<String> ex) {
    // Unable to catch more than one generic exception due to type erasure. }Copy the code

reference

  • Ideas for Java Programming
  • “Effective Java”
  • Why Java cannot implement true generics
  • Java generics in detail