Java, as a strongly typed programming language, must be a definite type where type declarations are required, which places great limitations on the code that can be reused by different types.

Declaring a type as a superclass or interface allows code to be reused to a certain extent, but this only extends the scope to superclasses and their subclasses or classes that implement the interface. In some cases, this scope is still not enough for us, especially since Java is single-root inherited. We want “non-specific” encoding, not a specific class or interface.

The introduction of generics at the beginning of Java 5 allows us to write “untyped” code. Generics parameterize types, declaring type parameters when defining a class, interface, or method, and then determining their specific types when they are used.

Generics are a compile-time feature, where the compiler pairs type check them and add extra conversion code at the ‘bounds (input and return)’ to ensure type safety at the generic runtime. When used, it looks like we’ve replaced our declared type argument with a specific type, but the implementation loses the parameter type information after compilation, and the specific type we specified is erased at runtime.

Java’s use of type erasure, rather than type substitution like c++, was also a choice of type erasure because Java 5 did not have generics before, so it was a choice of type erasure for compatibility with pre-java 5 code.

Noun explanation:

  • Type arguments: Type arguments declared in Angle brackets when declaring generic classes, interfaces, or methods, such as E of List
  • Generic classes: Classes, interfaces, or methods that declare type parameters are called generic classes, generic interfaces, and generic methods, respectively.
  • Parameterized types: When using generic classes, a specific type is specified, such as List
  • Primitive: The Class of a parameterized generic Class. For example, the primitive type of List is List, and the primitive type of List[] is List

Generics definition and Use:

1.1. Generic Classes

Type arguments are declared with Angle brackets after the class name. The declared type arguments can be used in the type declaration just like normal types. When used, the specific type is determined.

public class Holder<T> {
    T val;

    public Holder(T val) {
        this.val = val;
    }

    public T getVal(a) {
        return val;
    }
    
    public void setVal(T val) {
        this.val = val;
    }
    
    public static void main(String[] args) {
        Holder<String> strHolder = new Holder<String>("abc"); String s = h.getVal(); }}Copy the code

When used, the Holder type parameter specified is String. You can assign the return value of getVal() directly to a String variable without the explicit transformation. You must also pass in String or a subclass of setVal. If the input parameter is not String or a subclass of setVal, an error will be reported at compile time.

Before Java7, new parameterized types needed to specify the type, but after Java7, new operations can not display the specified type, and the compiler will deduce it automatically:

 Holder<String> h = new Holder<>("abc");
Copy the code

Multiple type arguments are separated by commas:

public class Holder<A.B.C> {

    public A v1;
    public B v2;
    public C v3;

    public Holder(A v1, B v2, C v3) {
        this.v1 = v1;
        this.v2 = v2;
        this.v3 = v3;
    }

    public static void main(String[] args) {
        Holder<String, Integer, Float> h = new Holder<>("abc".1.2.5); }}Copy the code

An inner class can use the type arguments of an outer class:

class A<T> {
    class B { T a; }}Copy the code

Anonymous inner classes can also be parameterized

interface A<T> {
    T next(a);
}
new A<String>() {
    @Override
    public String next(a) {
        return null; }};Copy the code

Static properties, static methods, and static inner classes cannot use class generic parameters. If you want to make static methods generic, you can use generic methods.

public class Calculate<T> {
    // Static methods cannot use T, compile time error
    public static T add(T a, T b) { T c = a + b; }}Copy the code

1.2 Generic interfaces

Interfaces can also be declared generic in the same way as generic classes.

public interface Generator<T> {
    T next(a);
}
Copy the code

When implementing generic classes, you need to specify specific types for type parameters:

public interface Bottle<T> {
    void pourInto(T t);
    T pourOut(a);
}

// When implementing Bottle, specify the type argument as Juice
public class GlassBottle implements Bottle<Juice> {
    public void pourInto(Juice juice) {}public Juice pourOut(a) {
        return null; }}Copy the code

1.3 Generic methods

You can declare generics for a method alone, and the class need not be a generic class. To define a generic method, simply place the generic parameter list before the return value. Declared type parameters are used like normal classes in methods where types are defined.

public class Test {
    public static <T> void t(T x) {
        System.out.println(x.getClass().getName());
    }
    
    public static <K,V> Map<K, V> newMap(a) {
        return new HashMap<K, V>();
    }

    public static void main(String[] args) {
        t(11);  // java.lang.Integer
        t("abc"); // java.lang.String

        Map<String, Date> m = newMap();
        m.put("now".newDate()); }}Copy the code

When using generic methods, the type is not explicitly specified. The compiler will infer the type from the input parameter of the method type argument or the type of the return assignment. However, the compiler does not do type inference when the result of the call is passed directly to another method as an argument. If it is a basic type, it is automatically boxed as a package type.

public static <T> String className(T v) {
    return v.getClass().getSimpleName();
}

public static void main(String[] args) {
    // Output an Integer, which is automatically inferred as an Integer
    System.out.println(Test.className(11));
}
Copy the code

The specified type can also be displayed when a generic method is called. Angle brackets are inserted between the dot operator and the method name, and the type is placed between them.

Test.<String, Date>newMap();
Copy the code

Variable-length argument lists can also use generic arguments:

public static <T> List<T> toList(T... args) {
    List<T> l = new ArrayList<T>(args.length);
    for (T e : args) {
        l.add(e);
    }
    return l;
}
Copy the code

When a mutable parameter method is called, an array is created to hold the mutable parameters. If the parameter type is generic, then a generic array is created, but Java allows arrays to be created using generics directly. Java has made some compromises here to allow you to create a generic array for mutable arguments.

But variadic lists can have different types of input arguments, so sometimes compilations can’t determine the exact type of a generic variadic argument, so they have to choose the most generic type.

public class Test {

    public static void main(String[] args) {
        System.out.println(toArray(Integer.valueOf(11),  Double.valueOf(13)).getClass());
    }

    public static <T> T[] toArray(T... args) {
        returnargs; }}Copy the code

Output:

class [Ljava.lang.Number;

Inherit generic classes/implement generic interfaces

2.1. Specify the type when inheriting

When you inherit a generic class or implement a generic interface, you need to specify a specific type. If you specify a specific type, for a subclass its parent class or the interface that implements it is parameterized. GetGenericSuperclass returns the type of ParameterizedType for the parent Class.

public class Holder<T> {

    private T val;

    public Holder(T val) {
        this.val = val;
    }

    public T getVal(a) {
        return val;
    }

    public void setVal(T val) {
        this.val = val; }}class Apple {
    public void  show(a) { System.out.println(getClass().getSimpleName()); }}public class AppleHolder extends Holder<Apple> {

    public AppleHolder(Apple apple) {
        super(apple);
    }

    public static void main(String[] args) {
        AppleHolder appleHolder = new AppleHolder(new Apple());
        Apple apple = appleHolder.getVal();
        apple.show();

        System.out.println(appleHolder.getClass().getGenericSuperclass() instanceofParameterizedType); }}Copy the code

Output:

Apple

true

2.2. Do not specify the type when inheriting

If you inherit a Class or implement an interface without specifying a type, the parent Class or interface is an ordinary Class or interface for a subclass, and the type argument is erased as Object. The type returned by Class’s getGenericSuperclass is Class.

public class CommonHolder extends Holder {

    public CommonHolder(Object val) {
        super(val);
    }

    public static void main(String[] args) {
        System.out.println(CommonHolder.class.getGenericSuperclass() instanceofClass); }}Copy the code

Output:

true

2.3. Specify a type parameter in a subclass

You can also pass type parameters declared in a subclass to the parent class, which then gets the same type when you specify a type for a subclass. For subclasses its parent Class is still parameterized and the return type of getGenericSuperclass through Class is still ParameterizedType.

public class CommonHolder<T> extends Holder<T> {

    public CommonHolder(T val) {
        super(val);
    }

    public static void main(String[] args) {
        System.out.println(CommonHolder.class.getGenericSuperclass() instanceofParameterizedType); }}Copy the code

Output:

true

The boundaries of generics

Because of type erasure, we cannot directly use specific properties or methods for type parameters. The following calls will fail to compile:

import java.sql.DriverManager;
import java.util.*;;

public class Test<T> {
    public T val;

    public void show(a) {
        // Failed at compile time
        val.show();
    }

    public static class Apple {
        public void show(a) {}}public static void main(String[] args) throws ClassNotFoundException {
        Test<Apple> t = newTest<>(); t.show(); }}Copy the code

In the example above, even though we know that val’s type will be followed by Show, the compiler forbade it because it was not safe to do so after the type was erased.

However, it is possible to declare an upper bound on a type parameter through extends; otherwise, the upper bound is Object. After declaring an upper bound on a class, the type specified when using the generic class can only be the upper bound or a subclass of it.

public class Show {
    public void show(a) {}}public class Test<T extends Show> {
    public T val;

    public void show(a) {
        // can be called
        val.show();
    }
    
    public static void main(String[] args)  {
        Test<Show> t = newTest<>(); t.show(); }}Copy the code

The default upper bound is Object if no upper bound is declared, so we can call the method Object if no upper bound is declared.

public class Test<T> {
    public T val;
    public void show(a) { val.getClass(); val.toString(); val.hashCode(); }}Copy the code

4. Wildcard:

// Drink -> Juice -> AppleJuice
public class Drink {}
public class Juice extends Drink {}
public class AppleJuice extends Juice {}

public class Bottle<T> {
    private T drink;

    public Bottle(T drink) {
        drink = drink;
    }

    public T getDrink(a) {
        return drink;
    }

    public void setDrink(T drink) { drink = drink; }}Copy the code

For ordinary classes, objects of the same class can be assigned to each other, or subclass objects can be assigned to superclass objects.

Juice juice = new Juice();
juice = new AppleJuice();
Copy the code

However, as long as the specified type parameters are different for generic classes, even if they are the same generic class, they are different parameterized types and cannot be assigned to each other directly:

// Error
Bottle<Juice> b1 = new Bottle<AppleJuice>(new AppleJuice());
Copy the code

Although they are both bottles after type erasers, at compile time the compiler inserts different type handling code at the boundary of the generic class. Obviously, you can’t use AppleJuice code to handle other types, so they are different types from the compiler’s point of view and will report errors at compile time.

To solve the assignment problem between generic instances whose type parameters have inherited relationships, Java provides wildcards.

4.1. Upper bound wildcards

When defining generic variables, you can use the extends key to specify an upper bound on a type so that declared variables can be assigned to generic parameterized types of an upper bound class and its subclasses with type parameters, provided, of course, that the generic classes are identical or parent classes.

Bottle<? extends Juice> b = new Bottle<AppleJuice>(new AppleJuice());
Copy the code

B, whose upper bound is Juice, can be assigned to Bottle. However, the use of upper bound wildcards limits the use of generic instances.

Although the extends wildcard is used, the compiler still does not know whether b is of type AppleJuice or a subclass of OrangeJuice, so the compiler cannot guarantee the safety of input arguments to methods whose argument types have type arguments, such as:

Bottle<? extends Juice> bottle = new Bottle<AppleJuice>(new AppleJuice());
// error
bottle.setDrink(new OrangeJuice());
Copy the code

SetDrink is defined as:

void setDrink(T drink)
Copy the code

So clearly the actual type of the bottle variable is bottle, so setDrink will compile to:

setDrink((AppleJuice) val)
Copy the code

Obviously, casting between sibling types is not safe, so instances declared with upper bound wildcards are not allowed to call methods with type parameters. But it can be done if the input parameter is null, because null has no specific type. But it is safe to return, and it is safe to assign a subclass to a parent class, so methods that return type type parameters are not affected.

Bottle<? extends Juice> b = new Bottle<AppleJuice>(new AppleJuice());
Juice juice = b.getDrink();
Copy the code
4.2. Lower bound wildcards

Generic variables that specify a lower bound using the super keyword. Variables that specify a lower bound can only be assigned to the type argument of the specified lower bound or the type of the parent class.

Bottle<? super Juice> b = new Bottle<Drink>(new AppleJuice());
Copy the code

The input is converted to the actual type Drink at compile time:

setDrink((Drink) val)
Copy the code

It is safe to use a parent type to operate on a child type, so it is safe for instances of a lower-bound wildcard declaration to use methods that take type arguments. But since a parent class cannot be assigned to a subclass, an instance of a lower-bound wildcard declaration cannot assign the return value of a method whose return type is a parameter type to another variable.

Bottle<? super Juice> b = new Bottle<Drink>(new AppleJuice());
// Error
Drink drink = b.getDrink();
Copy the code

4.3. Unbounded wildcards

The parameter type is? Number, indicating that any type can be used.

Bottle<? > b =new Bottle<>(new AppleJuice());
Drink drink = (Drink) b.getDrink();

// ERROR
b.setDrink(new AppleJuice());
Copy the code

Using unbounded wildcard looks much the same and the original raw type, but the meaning of the unbounded wildcard is the use of any type we know here, and unbounded wildcard type checks, because don’t know the exact type of unbounded wildcard cannot guarantee the security, the unbounded wildcard variables cannot be called into the parameter type for the type parameter method.

Fifth, type erasure

When using generics, the type specified only takes effect at compile time. After compilation, all type arguments are erased to its first boundary, or Object if no boundary is specified.

Because of type erasure, type parameters are no longer available at runtime, so you cannot explicitly use generic type operations such as instanceof, new, and t.class at runtime, but you can use pre-type conversions:

public class Test<T> { Class<? > type;public Test(Class
        type) {
        this.type = type;
    }

    public T[] newArray(int size) {
        return (T[]) Array.newInstance(type, size);
    }

    public static void main(String[] args) {
       Test<String> t = new Test<>(String.class);
       String[] strArr = t.newArray(10); }}Copy the code

Although we can specify different type arguments, they all point to the same type after erasure. For example, the Class of List and List are the same: list.class.

Because in addition to specific types of information, at compile time for the class to ensure runtime type behavior correctly, the compiler of generic ‘border’ at compile time, namely the generic into the reference in the class and back on the way to do the type checking and code insertion force transformation, when the method is called to type conversion into arguments, return to return values.

Six, advice,

6.1. Specify type information

Because of type erasers, we cannot get the specific type information at runtime. If we need the specific type information, we can display the Class object passing the type.

public class Test<T> { private Class<T> kind; public T val; public Test(Class<T> kind) { this.kind = kind; } public boolean isType(Object o) { return kind.isInstance(o); }}Copy the code

6.2. Do not use generic classes if you can use generic methods

If you can replace a generic class with a generic method, you should try to replace the generic class with a generic method.

6.3. Use parameterized generics whenever possible:

If a class or interface is generic, try to use its parameterized type, so that the compiler does some type checking for us at compile time to avoid errors at run time.

Wildcards are also recommended if there is no specific type, such as List<? >. Using wildcards checks at compile time and prevents us from calling methods that have type arguments.

Directly using the original type of generic risk from time to tome, primitive types will not at compile time type checking, and type parameters be erased as the Object, the Object can accept any type of instance, if the instance is given to the class of the different types of, so to manipulate these instances in the class there is a safety hazard and exposed these hazards may be in luck. Java supports the use of generic primitives only for compatibility with pre-Java5 code.

6.4. Do not assign parameterized types to primitive types

For compatibility, Java does not prohibit converting a parameterized variable to a primitive type; doing so only raises an alarm at compile time. However, after assigning a parameterized type to a primitive type, the compiler no longer performs type checking on the operation of the primitive instance, which can cause runtime errors.

class Calculator<T> {
    public int intAdd(T v1, T v2) {
        return((Number) v1).intValue() + ((Number) v1).intValue(); }}public class Test {
    public static void main(String[] args) {
        Calculator<Integer> intCal= new Calculator<>();
        Calculator cal = intCal;
        cal.intAdd("a"."b"); }}Copy the code

Exception in thread “main” java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Number

at com.test.java.Calculator.intAdd(Test.java:10)  
at com.test.java.Test.main(Test.java:20)
Copy the code

Assigning the intCal of the parameterized type Calculator to the CAL of the Calculator will not be type-checked by the compiler until runtime.

public static void main(String[] args) {
    List<String> strList = new ArrayList<>();
    List list = strList;
    list.add(Integer.valueOf(11));
    String s = strList.get(0);
}
Copy the code

Exception in thread “main” java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String

at com.test.java.Test.main(Test.java:27)  
Copy the code

The code above generates only a warning at compile time, but a fatal error at run time. Because strList is assigned to a List of type List, the lock is compiled without type checking for operations on the List variable. List.add (integer.valueof (11)) works fine because the String is erased as Object at runtime because of type erasings. But because strList is a List, compiler time inserts String conversion code for it, and converting an Integer to a String is illegal.

Try not to use generic mutable argument lists

The mutable parameter of a generic type is sometimes a generic type that can only be positioned as the array type of the mutable parameter. With arrays of mutable argument lists, we can do more than just pass values; we can manipulate them, which creates type-safety risks. You should avoid using generic variables or replacing them with parameterized types of List.

Effective Java has a classic example where three objects are passed in and two of the most anticipated arrays are randomly selected:

public class Test { public static void main(String[] args) { String[] strArr = pickTwo("a", "b", "c"); } public static <T> T[] toArray(T... args) { return args; } public static <T> T[] pickTwo(T a, T b, T c) { switch (ThreadLocalRandom.current().nextInt(3)) { case 0: return toArray(a, b); case 1: return toArray(a, c); case 2: return toArray(b, c); } throw new AssertionError(); }}Copy the code

Exception in thread “main” java.lang.ClassCastException: [Ljava.lang.Object; cannot be cast to [Ljava.lang.String;

at cn.ly.test.java.Test.main(Test.java:21)
Copy the code

This class does not report an error when compiled, but throws a ClassCastException when run. PickTwo’s arguments are type arguments. The compiler cannot determine type arguments when passing them to the toArray method. Instead, it creates an Object[] array to hold mutable arguments. The return compiler for pickTwo inserts a conversion to String[], but the actual type Object[] cannot be converted to String[].

6.6. Cast generics should be cast to wildcard types

When casting, if the destination type is a generic type, it should be cast to the wildcard parameterized type of that type, not the primitive type. After this transformation, the variable is checked by the compiler.

if (o instanceof List) { List<? > l = (List<? >) o; }Copy the code