In-depth analysis of Java generics: understanding generic principles and practical application methods

Java added a generic mechanism in 1.5, and it is said that experts spent about 5 years on this (sounds rather difficult). With generics, especially the use of collection classes, it becomes more standardized.

Take a look at the simple code below.

ArrayList<String> list = new ArrayList<String>();
list.add("Silence King Two");
String str = list. get(0);

We can use Object array to design Arraylist class.

class Arraylist {
    private Object[] objs;
    private int i = 0;
    public void add(Object obj) {
        objs[i++] = obj;
    }
    
    public Object get(int i) {
        return objs[i];
    }
}

Then, we access data to Arraylist.

Arraylist list = new Arraylist();
list.add("Silence King Two");
list.add(new Date());
String str = (String) list. get(0);

Found these two issues:

  • Arraylist can store any type of data (either strings or mixed dates), because all classes inherit from the Object class.
  • Type conversion is required when taking data out of Arraylist, because the compiler cannot determine whether you are taking a string or a date.

By comparison, you can clearly feel the excellence of generics: use type parameters to solve the uncertainty of elements – the collection whose parameter type is String is not allowed to store other types of elements Yes, there is no need for mandatory type conversion when fetching data.

Design a generic type by hand

First, let’s redesign the Arraylist class according to generic standards.

class Arraylist<E> {
    private Object[] elementData;
    private int size = 0;

    public Arraylist(int initialCapacity) {
        this. elementData = new Object[initialCapacity];
    }
    
    public boolean add(E e) {
        elementData[size + + ] = e;
        return true;
    }
    
    E elementData(int index) {
        return (E) elementData[index];
    }
}

A generic class is a class with one or more type variables. The type variable introduced by the Arraylist class is E (Element, the first letter of the element), which is enclosed in angle brackets <> and placed after the class name.

We can then instantiate the generic class by replacing the type variable with a concrete type such as a string.

Arraylist<String> list = new Arraylist<String>();
list.add("Silent King Three");
String str = list. get(0);

Date type is also possible.

Arraylist<Date> list = new Arraylist<Date>();
list.add(new Date());
Date date = list. get(0);

Second, we can also define generic methods in a non-generic class (or generic class).

class Arraylist<E> {
    public <T> T[] toArray(T[] a) {
        return (T[]) Arrays. copyOf(elementData, size, a. getClass());
    }
}

To be honest, though, the definition of a generic method looks a little cryptic. Let’s take a picture (note: at least one method return type and method parameter type are required).

?

Now, let’s call the generic method.

Arraylist<String> list = new Arraylist<>(4);
list.add("Shen");
list.add("黑");
list.add("King");
list.add("two");

String[] strs = new String[4];
strs = list.toArray(strs);

for (String str : strs) {
    System.out.println(str);
}

generic qualifier

Then, let’s talk about the qualifier extends for generic variables.

Before explaining this qualifier, we assume that there are three classes, and the definitions between them are like this.

class Wanglaoer {
    public String toString() {
        return "Wang Lao Er";
    }
}

class Wanger extends Wanglaoer{
    public String toString() {
        return "Wang Er";
    }
}

class Wangxiaoer extends Wanger{
    public String toString() {
        return "Wang Xiaoer";
    }
}

Let’s redesign the Arraylist class with the qualifier extends .

class Arraylist<E extends Wanger> {
}

When we add Wanglaoer element to Arraylist, the compiler will prompt an error: Arraylist only allows adding Wanger and its subclass Wangxiaoer object, its parent class Wanglaoer is not allowed to be added.

Arraylist<Wanger> list = new Arraylist<>(3);
list.add(new Wanger());
list.add(new Wanglaoer());
// The method add(Wanger) in the type Arraylist<Wanger> is not applicable for the arguments
// (Wanglaoer)
list.add(new Wangxiaoer());

That is to say, the qualifier extends can narrow the type range of the generic.

Type erasure

“As long as we decompile the bytecode of the generic class, we will see it!” After decompiling the class file with a decompilation tool (jad was used when I wrote this article, you can also use other tools) as follows. “

// Decompiled by Jad v1.5.8g. Copyright 2001 Pavel Kouznetsov.
// Jad home page: http://www.kpdus.com/jad.html
// Decompiler options: packimports(3)
// Source File Name: Arraylist.java

package com.cmower.java_demo.fanxing;

import java.util.Arrays;

class Arraylist
{

    public Arraylist(int initialCapacity)
    {
        size = 0;
        elementData = new Object[initialCapacity];
    }

    public boolean add(Object e)
    {
        elementData[size + + ] = e;
        return true;
    }

    Object elementData(int index)
    {
        return elementData[index];
    }

    private Object elementData[];
    private int size;
}

The type variable is gone, replaced by Object !

That being the case, what happens if the generic class uses the qualifier extends?

Take a look at this code.

class Arraylist2<E extends Wanger> {
    private Object[] elementData;
    private int size = 0;

    public Arraylist2(int initialCapacity) {
        this. elementData = new Object[initialCapacity];
    }

    public boolean add(E e) {
        elementData[size + + ] = e;
        return true;
    }

    E elementData(int index) {
        return (E) elementData[index];
    }
}

The result after decompilation is as follows.

// Decompiled by Jad v1.5.8g. Copyright 2001 Pavel Kouznetsov.
// Jad home page: http://www.kpdus.com/jad.html
// Decompiler options: packimports(3)
// Source File Name: Arraylist2.java

package com.cmower.java_demo.fanxing;


// Referenced classes of package com.cmower.java_demo.fanxing:
// Wanger

class Arraylist2
{

    public Arraylist2(int initialCapacity)
    {
        size = 0;
        elementData = new Object[initialCapacity];
    }

    public boolean add(Wanger e)
    {
        elementData[size + + ] = e;
        return true;
    }

    Wanger elementData(int index)
    {
        return (Wanger)elementData[index];
    }

    private Object elementData[];
    private int size;
}

“The type variable is missing, E is replaced by Wanger“, “Through the above two examples, the Java virtual machine takes the generic type variable Erase and replace with a qualified type (if not limited, use Object)”

public class Cmower {
    
    public static void method(Arraylist<String> list) {
        System.out.println("Arraylist<String> list");
    }

    public static void method(Arraylist<Date> list) {
        System.out.println("Arraylist<Date> list");
    }

}

In a superficial sense, we will take it for granted that Arraylist list and Arraylist list are two different types, because String and Date are different the type.

But because of type erasure, the above code will not compile – the compiler will prompt an error (this is exactly those “problems” caused by type erasure):

>Erasure of method method(Arraylist<String>) is the same as another method in type
 Cmower
>
>Erasure of method method(Arraylist<Date>) is the same as another method in type
 Cmower

Roughly speaking, the parameter types of these two methods are the same after erasure.

That is to say, method(Arraylist list) and method(Arraylist list) are methods of the same parameter type and cannot exist at the same time. The type variables String and Date will disappear automatically after being erased, and the actual parameter of the method method is Arraylist list.

There is a saying: “Seeing is better than hearing a hundred times”, but even if you see it, it may not be true-the erasure problem of generics can well support this point of view.

Generic wildcard

Wildcards are represented by English question marks (?). When we create a generic object, we can use the keyword extends to qualify the subclass, or use the keyword super to qualify the parent class.

Let’s look at the following code.

// Define a generic class Arraylist<E>, E represents the element type
class Arraylist<E> {
    // Private member variable, storing element array and number of elements
    private Object[] elementData;
    private int size = 0;

    // Constructor, pass in the initial capacity initialCapacity, create an Object array with the specified capacity
    public Arraylist(int initialCapacity) {
        this. elementData = new Object[initialCapacity];
    }

    // Add elements to the end of the array, return whether the addition is successful or not
    public boolean add(E e) {
        elementData[size + + ] = e;
        return true;
    }

    // Get the element with the specified subscript
    public E get(int index) {
        return (E) elementData[index];
    }

    // Find the subscript of the first occurrence of the specified element, if not found, return -1
    public int indexOf(Object o) {
        if (o == null) {
            for (int i = 0; i < size; i ++ )
                if (elementData[i]==null)
                    return i;
        } else {
            for (int i = 0; i < size; i ++ )
                if (o. equals(elementData[i]))
                    return i;
        }
        return -1;
    }

    // Determine whether the specified element appears in the array
    public boolean contains(Object o) {
        return indexOf(o) >= 0;
    }

    // Convert the elements in the array to string output
    public String toString() {
        StringBuilder sb = new StringBuilder();
        
        for (Object o : elementData) {
            if (o != null) {
                E e = (E)o;
                sb.append(e.toString());
                sb.append(',').append(' ');
            }
        }
        return sb.toString();
    }

    // returns the number of elements in the array
    public int size() {
        return size;
    }

    // Modify the element with the specified subscript and return the element before modification
    public E set(int index, E element) {
        E oldValue = (E) elementData[index];
        elementData[index] = element;
        return oldValue;
    }
}

1) Add the indexOf(Object o) method to determine the position of the element in Arraylist. Note that the parameter is Object instead of generic E.

2) Add the contains(Object o) method to determine whether the element is in the Arraylist. Note that the parameter is Object instead of generic E.

3) Add the toString() method to facilitate printing of Arraylist.

4) Add the set(int index, E element) method to facilitate the modification of the Arraylist element.

Because of generic erasure, a statement like Arraylist list = new Arraylist(); cannot be compiled, even though Wangxiaoer is a subclass of Wanger. But what if we really need this “upward transformation” relationship? At this time, wildcards are needed to play a role.

Using wildcards in the form of can realize the upward transformation of generics. Let’s see an example.

Arraylist<? extends Wanger> list2 = new Arraylist<>(4);
list2. add(null);
// list2. add(new Wanger());
// list2.add(new Wangxiaoer());

Wanger w2 = list2. get(0);
// Wangxiaoer w3 = list2. get(1);

The type of list2 is Arraylist, in translation, list2 is an Arraylist whose type is Wanger and its subclasses.

Attention, here comes the “key”! list2 does not allow adding Wanger or Wangxiaoer objects to it through the add(E e) method, the only exception is null.

Although you cannot add elements to list2 through the add(E e) method, you can assign values to it.

Arraylist<Wanger> list = new Arraylist<>(4);

Wanger wanger = new Wanger();
list. add(wanger);

Wangxiaoer wangxiaoer = new Wangxiaoer();
list.add(wangxiaoer);

Arraylist<? extends Wanger> list2 = list;

Wanger w2 = list2. get(1);
System.out.println(w2);

System.out.println(list2.indexOf(wanger));
System.out.println(list2.contains(new Wangxiaoer()));

The Arraylist list2 = list; statement assigns the value of list to list2, at this time list2 == list. Since list2 does not allow other elements to be added to it, it is safe at this point – we can do get(), indexOf() and >contains(). Think about it, if you can add elements to list2, these 3 methods will become less safe, and their values may change.

Using wildcards in the form of , elements whose parent class is Wanger can be stored in the Arraylist, see an example.

Arraylist<? super Wanger> list3 = new Arraylist<>(4);
list3. add(new Wanger());
list3.add(new Wangxiaoer());

// Wanger w3 = list3. get(0);

It should be noted that data cannot be retrieved from list3 of type Arraylist.

Summary

In Java, generics are a strong type constraint mechanism that can check type safety during compilation and can improve code reusability and readability.

1) type parameterization

The essence of generics is a parameterized type, that is, when defining a class, interface or method, one or more type parameters can be used to represent the parameterized type.

For example, you can define a generic class like this.

public class Box<T> {
    private T value;

    public Box(T value) {
        this.value = value;
    }

    public T getValue() {
        return value;
    }

    public void setValue(T value) {
        this.value = value;
    }
}

In this example, represents a type parameter, and you can use T instead of a specific type anywhere in the class that requires a type. By using generics, we can create a box that can store any type of object.

Box<Integer> intBox = new Box<>(123);
Box<String> strBox = new Box<>("Hello, world!");

Generics are widely used in actual development. For example, container classes such as List, Set, and Map in the collection framework, and tool classes such as Future and Callable in the concurrent framework all use generics.

2) type erasure

In Java’s generic mechanism, there are two important concepts: type erasure and wildcards.

Generics will erase the generic type at compile time and replace the generic type with the Object type. This is for backward compatibility and avoids affecting legacy Java code.

For example, for the code below:

List<Integer> intList = new ArrayList<>();
intList.add(123);
int value = intList. get(0);

At compile time, the Java compiler will replace the generic type List with List, replace the return value type Integer of the get method with Object, and generate The bytecode equivalent of the following code:

List intList = new ArrayList();
intList.add(Integer.valueOf(123));
int value = (Integer) intList. get(0);

Java generics only work at compile time, and generic type information is not preserved at runtime.

3) wildcard

Wildcards are used to represent an unknown type. For example, List represents a List that can store objects of any type, but cannot add elements to it. Wildcards can be used to resolve untyped situations, such as in method parameters or return values.

Using wildcards can make a method more generic while maintaining type safety.

For example, define a generic method:

public static void printList(List<?> list) {
    for (Object obj : list) {
        System.out.print(obj + " ");
    }
    System.out.println();
}

This method can accept any type of List, such as List, List and so on.

Upper limit wildcard

Generics also provide an upper bound wildcard , which means that the wildcard can only accept T or a subclass of T. Using capped wildcards can improve the type safety of your program.

For example, define a method that accepts only Lists of Number and its subclasses:

public static void printNumberList(List<? extends Number> list) {
    for (Number num : list) {
        System.out.print(num + " ");
    }
    System.out.println();
}

This method can accept List, List, etc.

lower limit wildcard

Lower bound wildcards (Lower Bounded Wildcards) are declared with the super keyword, and its syntax is , where T represents a type parameter. It means that the type parameter must be a superclass of a specified class (including the class itself).

When we need to add elements to a generic collection, if the upper limit wildcard is used, the type of elements in the collection may be restricted, so that certain types of elements cannot be added. However, if we use lower-bound wildcards, subtypes of the specified type can be added to the collection, ensuring the integrity of the elements.

For example, suppose there is a class Animal, and two subclasses Dog and Cat. Now we have a List collection whose type parameter must be Dog or its superclass type. We can add elements of type Dog to this collection, as well as subclasses of it. However, an element of type Cat cannot be added to it because Cat is not a subclass of Dog.

Here’s an example using a lower bound wildcard:

List<? super Dog> animals = new ArrayList<>();

// You can add elements of type Dog and its subtype elements
animals. add(new Dog());
animals. add(new Bulldog());

// cannot add elements of type Cat
animals.add(new Cat()); // compile error

It should be noted that although some subtype elements can be added by using the lower limit wildcard, when reading the element, we can only ensure that it is of type Object, but cannot ensure that it is of the specified type or its parent type. Therefore, type conversion is required when reading elements, as follows:

List<? super Dog> animals = new ArrayList<>();
animals. add(new Dog());

// Type conversion is required when reading elements
Object animal = animals. get(0);
Dog dog = (Dog) animal;

In general, Java’s generic mechanism is a very powerful type constraint mechanism that can check type safety at compile time and improve code reusability and readability. However, you also need to pay attention to issues such as type erasure and wildcards when using generics to ensure the correctness of the code.

The knowledge points of the article match the official knowledge files, and you can further learn relevant knowledge. Java skill treeHomepageOverview 125806 people are studying systematically

syntaxbug.com © 2021 All Rights Reserved.