Interview question series: after using Java generics for so many years, I only know its fur

Posted by yakk0 on Sat, 06 Nov 2021 00:07:31 +0100

Interview question: what is your understanding of generics?

Interview investigation point

Objective: to understand the job seekers' mastery of the basic knowledge of Java.

Scope of investigation: Java programmers working for 1-3 years.

background knowledge

Generics in Java is a new feature introduced by JDK5.

It mainly provides a security detection mechanism for compile time types. This mechanism allows the program to detect illegal types at compile time, so as to give an error prompt.

The advantage of this is to tell the developer the parameter types received or returned by the current method on the one hand, and to avoid type conversion errors when the program runs on the other hand.

Design deduction of generics

For a simple example, let's take a look at the ArrayList collection. Some of the code definitions are as follows.

public class ArrayList{
   transient Object[] elementData; // non-private to simplify nested class access
}

In ArrayList, the structure used to store elements is an Object [] Object array. This means that any type of data can be stored.

When we use this ArrayList to do the following operation.

public class ArrayExample {

    public static void main(String[] args) {
        ArrayList al=new ArrayList();
        al.add("Hello World");
        al.add(1001);
        String str=(String)al.get(1);
        System.out.println(str);
    }
}

After running the program, you will get the following execution results

Exception in thread "main" java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
	at org.example.cl06.ArrayExample.main(ArrayExample.java:11)

I believe you have encountered this type conversion error in development. Generally speaking, there will be two serious problems without generics

  1. Type needs to be cast
  2. Inconvenient and error prone

How to solve the above problem? To solve this problem, we have to think about what is the demand behind this problem?

Let me briefly summarize two points:

  1. Be able to support different types of data storage
  2. It is also necessary to ensure the uniformity of stored data types

Based on these two points, it is not difficult to find that the type of data to be stored in a data container is actually determined by the developer. Therefore, in order to solve this problem, the generic mechanism is introduced in JDK5.

Its definition form is: ArrayList < E >, which is equivalent to providing ArrayList with a template e for type input. E can be any type of object. Its definition method is as follows.

public class ArrayList<E>{
   transient E[] elementData; // non-private to simplify nested class access
}

In the definition of the ArrayList class, use the < > syntax and pass in an object E used to represent any type. This E can be defined arbitrarily. You can define it as A, B and C.

Next, set the type of the array elementData used to store elements to type E.

With this configuration, the type of data you want to store in the ArrayList container is up to the user. For example, if I want ArrayList to store only String types, it can be implemented in this way

public class ArrayExample {

    public static void main(String[] args) {
        ArrayList<String> al=new ArrayList();
        al.add("Hello World");
        al.add(1001);
        String str=(String)al.get(1);
        System.out.println(str);
    }
}

When defining ArrayList, a String type is passed in. This means that the elements added to the instance object al of ArrayList must be of String type, otherwise the following syntax errors will be prompted.

Similarly, if you need to save other types of data, you can write this:

  1. ArrayList<Integer>
  2. ArrayList<Double>

Summary: the so-called generic definition is essentially a type template. In actual development, we give the types of attributes to be saved in a container or an object to the caller through template definition, so as to ensure the safety of the type.

Definition of generics

Generic definitions can be described from two dimensions:

  1. Generic class
  2. generic method

Generic class

Generic class refers to adding one or more type parameters after the class name. A generic parameter, also known as a type variable, is an identifier used to specify a generic type name. Because they accept one or more parameters, these classes are called parameterized classes or parameterized types.

The commonly used representation marks of type variables are E(element), T (type), K(key), V(value), N(number), etc. This is only a representation symbol and can be any character without mandatory requirements.

The following code is about the definition of generic classes.

This class receives a type parameter of a t flag. There is a member variable in this class, which uses T type.

public class Response <T>{
    
    private T data;

    public T getData() {
        return data;
    }

    public void setData(T data) {
        this.data = data;
    }
}

The usage is as follows:

public static void main(String[] args) {
  Response<String> res=new Response<>();
  res.setData("Hello World");
}

generic method

Generic method refers to the type parameters at the specified method level. This method can receive different parameter types when calling. According to the parameter types passed to generic methods, the compiler handles each method call appropriately.

The following code represents the definition of generic methods and uses the reflection mechanism provided by JDK to generate dynamic proxy classes.

public interface IHelloWorld {

    String say();
}

Define the getProxy method, which is used to generate dynamic proxy objects, but the parameter type passed is T, that is, this method can complete the construction of dynamic proxy instances of any interface.

Here, we build a dynamic proxy instance for the IHelloWorld interface. The code is as follows.

public class ArrayExample implements InvocationHandler {

    public <T> T getProxy(Class<T> clazz){
        // clazz is not an interface and cannot use JDK dynamic proxy
        return (T) Proxy.newProxyInstance(clazz.getClassLoader(), new Class<?>[]{ clazz }, ArrayExample.this);
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        return "Hello World";
    }

    public static void main(String[] args) {
        IHelloWorld hw=new ArrayExample().getProxy(IHelloWorld.class);
        System.out.println(hw.say());
    }
}

Operation results:

Hello World

The definition rules of generic methods are briefly summarized as follows:

  1. All generic method definitions have a type parameter declaration represented by < >. This type parameter declaration part is before the method return type.
  2. Each type parameter declaration section contains one or more type parameters separated by commas. A generic parameter, also known as a type variable, is an identifier used to specify a generic type name.
  3. The type parameter can be used to declare the return value type and can be used as a placeholder for the actual parameter type obtained by the generic method
  4. The declaration of generic method body is the same as that of other methods. Note that type parameters can only represent reference types, not original types (such as int, double, char, etc.)##

Multi type variable definition

In fact, we only define one generic variable T. what if we need to pass in multiple generic variables?

We can write this:

public class Response <T,K,V>{
}

Each parameter declaration symbol represents a type.

Note that in the multivariable type definition, it is better to define generic variables as characters that can simply understand meaning, otherwise there are too many types, and callers are easy to confuse.

Bounded type parameter

In some scenarios, the parameter type we want to pass belongs to a certain type range. For example, a method that operates on numbers may only want to accept instances of Number or Number subclass. How to implement it?

Generic wildcard upper boundary

The upper boundary represents a limited range of type variables. Only a certain type or its subclasses can be passed in.

We can add an extends keyword on the generic parameter to indicate that the generic parameter type must be derived from an implementation class. The example code is as follows.

public class TypeExample<T extends Number> {
    private T t;

    public T getT() {
        return t;
    }

    public void setT(T t) {
        this.t = t;
    }

    public static void main(String[] args) {
        TypeExample<String> t=new TypeExample<>();
    }
}

The above code declares a generic parameter T, which must inherit the class Number, indicating that when TypeExample is instantiated later, the incoming generic type should be a subclass of Number.

Therefore, with this rule, the above test code will prompt Java: the type parameter java.lang.String is not within the range of type variable T.

Generic wildcard lower boundary

The lower boundary represents a limited range of type variables, and can only be passed in a type or its parent class.

We can add a super keyword to the generic parameter to set the upper boundary of the generic wildcard. The example code is as follows.

public class TypeExample<T> {
    private T t;

    public T getT() {
        return t;
    }
    public void setT(T t) {
        this.t = t;
    }
    public static void say(TypeExample<? super Number> te){
        System.out.println("say: "+te.getT());
    }
    public static void main(String[] args) {
        TypeExample<Number> te=new TypeExample<>();
        TypeExample<Integer> te2=new TypeExample<>();
        say(te);
        say(te2);
    }
}

Declare TypeExample <? Super Number > te, which indicates the generic type of the incoming TypeExample. It must be Number and the parent type of Number.

In the above code, the runtime will get the following error:

java: Incompatible types: org.example.cl06.TypeExample<java.lang.Integer>Cannot convert to org.example.cl06.TypeExample<? super java.lang.Number>

As shown in the following figure, the class diagram representing the class Number can only pass Number and the parent class Serializable after being qualified by the super keyword.

Type wildcard?

Type wildcards are generally used? Instead of specific type parameters. For example, list <? > Logically, it is the parent class of all list < concrete type arguments >, such as list < string >, list < integer >.

Look at the definition of the following code. In the say method, it accepts a parameter of TypeExample type, and the generic type is <? >, Represents a generic type parameter that receives any type.

public class TypeExample<T> {
    private T t;

    public T getT() {
        return t;
    }

    public void setT(T t) {
        this.t = t;
    }
    public static void say(TypeExample<?> te){
        System.out.println("say: "+te.getT());
    }
    public static void main(String[] args) {
        TypeExample<Integer> te1=new TypeExample<>();
        te1.setT(1111);
        TypeExample<String> te2=new TypeExample<>();
        te2.setT("Hello World");
        say(te1);
        say(te2);
    }
}

The operation results are as follows

say: 1111
say: Hello World

Similarly, parameters of type wildcards can also be qualified through extensions, such as:

public class TypeExample<T> {
    private T t;

    public T getT() {
        return t;
    }

    public void setT(T t) {
        this.t = t;
    }
    public static void say(TypeExample<? extends Number> te){ //Modify and add extensions
        System.out.println("say: "+te.getT());
    }
    public static void main(String[] args) {
        TypeExample<Integer> te1=new TypeExample<>();
        te1.setT(1111);
        TypeExample<String> te2=new TypeExample<>();
        te2.setT("Hello World");
        say(te1);
        say(te2);
    }
}

Due to the parameter TypeExample in the say method, the <? Extensions Number >, so when passing parameters later, the generic type must be a subtype of Number.

Therefore, when the above code runs, the following error will be prompted:

java: Incompatible types: org.example.cl06.TypeExample<java.lang.String>Cannot convert to org.example.cl06.TypeExample<? extends java.lang.Number>

Note: when building a generic instance, if the generic type is omitted, the default is the wildcard type, which means that any type of parameter can be accepted.

Generic inheritance

The definition of generic type parameters is allowed to be inherited, such as the following.

Indicates that the subclass SayResponse and the parent Response use the same generic type.

public class SayResponse<T> extends Response<T>{
    private T ox;
}

How does the JVM implement generics?

In the JVM, Type erasure generics is used to implement generics. In short, generics only exist in the. java source file. Once compiled, generics will be erased

Let's look at the ArrayExample class, the byte instruction after compilation.

public class ArrayExample implements InvocationHandler {

    public <T> T getProxy(Class<T> clazz){
        // clazz is not an interface and cannot use JDK dynamic proxy
        return (T) Proxy.newProxyInstance(clazz.getClassLoader(), new Class<?>[]{ clazz }, ArrayExample.this);
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        return "Hello World";
    }

    public static void main(String[] args) {
        IHelloWorld hw=new ArrayExample().getProxy(IHelloWorld.class);
        System.out.println(hw.say());
    }
}

View the byte instructions through javap -v ArrayExample.class as follows.

 public <T extends java.lang.Object> T getProxy(java.lang.Class<T>);
    descriptor: (Ljava/lang/Class;)Ljava/lang/Object;
    flags: ACC_PUBLIC
    Code:
      stack=5, locals=2, args_size=2
         0: aload_1
         1: invokevirtual #2                  // Method java/lang/Class.getClassLoader:()Ljava/lang/ClassLoader;

You can see that after getProxy is compiled, the generic T has been erased and the parameter type has been replaced with java.lang.Object

Not all types are converted to java.lang.Object. For example, if < T extensions string >, the parameter type is java.lang.String.

Meanwhile, to ensure IHelloWorld hw=new ArrayExample().getProxy(IHelloWorld.class); For the accuracy of this code, the compiler will also insert a type conversion mechanism here.

The following code is the rendering of ArrayExample.class after decompilation.

IHelloWorld hw = (IHelloWorld)(new ArrayExample()).getProxy(IHelloWorld.class);
System.out.println(hw.say());

Defects caused by generic type erasure implementation

There are still some defects in erasure to implement generics. Please give a few cases to illustrate.

Basic types are not supported

After the generic type is erased, it becomes a java.lang.Object type. This method is troublesome for eight basic types such as int/long/float, because Java cannot realize the forced conversion from basic type to Object type.

 ArrayList<int> list=new ArrayList<int>();

If you write this, you will get the following error

java: Unexpected type
  need: quote
  find:    int

Therefore, in a generic definition, only reference types can be used.

However, as a reference type, packing and unpacking will be involved when saving basic type data. such as

List<Integer> list = new ArrayList<Integer>();
list.add(10); // 1
int num = list.get(0); // 2

In the above code, a collection of list < integer > generic types is declared,

At the position marked 1, an int type number 10 is added. In this process, the boxing operation will be involved, that is, converting the basic type int to Integer

At Mark 2, the compiler first converts Object to Integer type, and then unpacks it to convert Integer to int. Therefore, the above code is equivalent to

List list = new ArrayList();
list.add(Integer.valueOf(10));
int num = ((Integer) list.get(0)).intValue();

Some execution steps are added, which will still have some impact on execution efficiency.

Cannot get generic actual type at run time

Since generics are erased after compilation, the Java virtual machine cannot obtain the actual type of generics during code execution.

In the following code, from the source code, the two lists appear to be collections of different types, but after generic erasure, the collections become Array List s. So the code in the if statement will be executed.

public static void main(String[] args) {
  ArrayList<Integer> li = new ArrayList<>();
  ArrayList<Float> lf = new ArrayList<>();
  if (li.getClass() == lf.getClass()) { // Generic erasure, the two List types are the same
    System.out.println("Same type");
  }
}

Operation results:

Same type

This makes it impossible for us to define overriding methods according to generic types when doing method overloading.

In other words, rewriting cannot be implemented in the following way.

public void say(List<Integer> a){}
public void say(List<String> b){}

In addition, it will bring us some restrictions in practical use. For example, we can't directly implement the following code

public <T> void say(T a){
  if(a instanceof T){

  }
  T t=new T();
}

There will be compilation errors in the above code.

Since there are so many defects in implementing generics by erasure, why do you design so?

To answer this question, you need to know the history of generics. Java generics were introduced in Jdk 1.5. Before that, container classes in JDK used Object to ensure the flexibility of the framework, and then forced it when reading. However, there is a big problem with this, that is, the type is unsafe. The compiler can't help us find type conversion errors in advance, which will bring this risk to the runtime. The introduction of generics is to solve the problem of type insecurity. However, since Java has been widely used at that time, it is necessary to ensure the forward compatibility of the version. Therefore, in order to be compatible with the old version of JDK, the designer of generics chose the implementation based on erasure.

Problem solving

Interview question: what is your understanding of generics?

Answer: generics is a new feature provided by JDK5. It mainly provides a security detection mechanism for compile time types. This mechanism allows the program to detect illegal types at compile time, so as to give an error prompt.

Problem summary

An in-depth understanding of Java generics is the most basic and necessary skill for programmers. Although the interview is very voluminous, strength is still very important.

Focus on [Mic learning architecture] official account, get more original works.

Topics: JDK