泛型 Generics

196 阅读11分钟

泛型(Generics)是在 JDK 5 版本中引入的。泛型的引入解决了许多类型安全的问题,同时减少了类型转换的需求。

⨳ JDK 5 引入的泛型,允许在定义类、接口和方法时使用类型参数(即泛型参数),从而可以在不指定具体类型的情况下进行编程。泛型保证了在编译时进行类型检查,而不是在运行时进行强制转换。

⨳ JDK 7 引入了钻石操作符(Diamond Operator),使得泛型的使用更加简洁。例如,List list = new ArrayList<>();,这里的 <> 是钻石操作符,它允许编译器根据右侧的类型推断出泛型类型。

⨳ JDK 8 引入了类型注解(Type Annotations),并提供了更强大的类型推断功能,特别是在 lambda 表达式和方法引用中。

⨳ ...

总体而言,Java 泛型自引入以来在语法简化和类型推断方面不断得到改进,特别是在简化代码和提升可读性方面。

泛型基本使用

泛型(Generics)允许在类、接口和方法中使用类型参数化。这意味着你可以编写可以适应多种不同类型的代码,而不必为每种类型重复编写相似的代码。

泛型定义

泛型的核心思想是 类型参数化,即你可以在定义类、接口或方法时不指定具体的类型,而是使用一个符号(通常是大写字母如 T, E, K, V, N 等)作为类型的占位符,在使用该类或方法时再指定具体的类型。

如下,定义一个简单的容器 Box,这个容器 可以存储任意类型的元素 Item:

package com.cango.generics;

public class Box<T> { // 在类上定义泛型 T
    private T item;

    public void setItem(T item) { // 泛型作为方法参数类型
        this.item = item;
    }

    public T getItem() {// 泛型作为方法返回值类型
        return item;
    }
}

泛型可以定义在类上、接口上、属性上或方法参数与返回值上,再使用该类的时候只要指定T是什么类型,就可以避免类型转换,如下:

public static void main(String[] args) {
    Box<String> stringBox = new Box<String>();
    stringBox.setItem("Hello, Generics");
    stringBox.setItem(123);  // 编译错误,防止错误的类型插入集合
    System.out.println(stringBox.getItem());

    Box<Integer> integerBox = new Box<Integer>();
    integerBox.setItem(123);
    System.out.println(integerBox.getItem());
}

这里主要注意,Java 泛型只支持引用类型,不能使用基本类型(如 int, char)作为泛型类型参数。

泛型推断

在 Java 7 及以后的版本中,泛型推断在对象创建时得到了简化。这就是所谓的“钻石语法”(Diamond Syntax),它允许你在实例化泛型类时省略右侧的泛型类型参数,编译器会根据左侧的声明自动推断类型。

Box<String> stringBox = new Box<>();
stringBox.setItem("Hello, Generics");
System.out.println(stringBox.getItem());    

Java 8 及以后的版本改进了泛型推断的能力,特别是在 Lambda 表达式和方法引用的使用上。编译器可以在更复杂的上下文中进行类型推断。

Function<String, Integer> stringLength = s -> s.length(); // 推断 s 为 String 
System.out.println(stringLength.apply("Hello")); // 输出 5

关于 Lambda 表达式和方法引用,可以参考《Lambda篇》。

泛型上下界

Java 的泛型支持上下界的定义,允许开发者对泛型类型参数进行约束,以限制它们可以接受的类型。这些上下界主要通过关键字 extendssuper 来实现,分别用于指定泛型的上界和下界。

? extends T:表示类型参数必须 TT 的子类型。 ⨳ ? super T:表示类型参数必须是 TT的父类型。


    // 使用上界通配符 `? extends Number` 处理任意 Number 类型的子类
    public static void printNumbers(List<? extends Number> list) {
        for (Number number : list) {
            System.out.println(number);
        }
    }

    public static void main(String[] args) {
        List<Integer> intList = Arrays.asList(1, 2, 3);
        List<Double> doubleList = Arrays.asList(1.1, 2.2, 3.3);

        printNumbers(intList);  // 输出 1, 2, 3
        printNumbers(doubleList);  // 输出 1.1, 2.2, 3.3
    }

这里 printNumbers 方法能够处理所有 Number 类型的子类,如 IntegerDouble

泛型上下界为 Java 泛型提供了灵活的类型约束机制:

⨳ 当你只想从一个泛型对象中读取数据,而不对其进行修改时,可以使用上界。例如,在处理从列表中读取数据的操作时,可以使用 ? extends

⨳ 当你需要向泛型对象中写入数据时,可以使用下界。例如,当向列表中添加数据时,可以使用 ? super 来确保写入的安全性。

泛型擦除

Java 泛型是在 编译时 实现的,在编译过程中,所有的泛型信息都会被擦除,这就是所谓的“类型擦除”(Type Erasure)。类型擦除机制保证了泛型在 Java 中的向后兼容性,使得泛型代码能够与非泛型代码(如 Java 1.5 之前的代码)无缝集成。

要了解泛型擦除,得看看泛型类编译后变成了什么。使用 JAD 反编译 Box.class,得到下述代码:

package com.cango.generics;
public class Box
{

    public Box()
    {
    }

    public void setItem(Object item)
    {
        this.item = item;
    }

    public Object getItem()
    {
        return item;
    }

    private Object item;
}

可以看到在编译后的字节码中,泛型类型 T 会被擦除,被替换为了 Object,难怪可以使用任意类型替换泛型。

其实类型擦除并不只是使用 Object 替换,在 Java 中,类型擦除的过程有两种情况:

未指定上界:如果泛型类型参数没有指定上界(如 <T>),编译器会将泛型类型擦除为 Object

指定了上界:如果泛型类型参数指定了上界(如 <T extends Number>),编译器会将泛型类型擦除为上界类型(如 Number)。

由于类型擦除的存在,Java 的泛型在运行时并没有类型参数的具体信息。例如,不能在运行时判断某个对象是否是某个特定的泛型类。

List<String> stringList = new ArrayList<>();
List<Integer> integerList = new ArrayList<>();

System.out.println(stringList.getClass() == integerList.getClass());  // true    

因为所有的泛型信息都会在运行时被擦除替换,所以无法在运行时判断一个 List 实例是否是 List<String> 还是 List<Integer>,因为它们都会被擦除为 List。在运行时,stringListintegerList 的类型实际上是相同的(都是 ArrayList),因为泛型信息被擦除。

虽然List<String>List<Integer>在运行时都是 List,但编译时仍然会进行类型检查,因此不能将 List<String> 赋值给 List<Integer>,也不能赋值给 List<Object>

而且无法通过 new T() 创建泛型类型的实例。

class Box<T> {
    T item = new T();  // 编译错误
}

尽管类型擦除在运行时丢失了泛型信息,还是可以在一定程度上规避这个限制,比如使用反射结合类型参数。

反射操作泛型

要想使用反射操作泛型,需要了解泛型的相关类。

image.png

java.lang.reflect.Type 表示 Java 中的所有类型,包括类、接口、基本类型、参数化类型、数组类型、类型变量和通配符类型。

public interface Type {
    default String getTypeName() {
        return toString();
    }
}

《反射》那篇文章主要讲解了表示类和接口的Class类,下面详解介绍一下 Type 的其他子接口或实现类。

ParameterizedType 参数化类型

java.lang.reflect.ParameterizedTypeType 接口的一个子接口,它表示带有实际类型参数的泛型类型。

例如List<String>中的String就是一个类型参数。

ParameterizedType有两个重要的方法,分别获取List<String>中的 List 和 String:

Type[] getActualTypeArguments() : 返回参数化类型的实际类型参数数组。对于 List<String>,该方法将返回包含 String 类型的数组。

Type getRawType() : 返回不带类型参数的原始类型。对于 List<String>getRawType() 将返回 List.class

import java.lang.reflect.Field;
import java.lang.reflect.Type;
import java.lang.reflect.ParameterizedType;
import java.util.Map;

public class ParameterizedTypeDemo {
    public Map<String,Integer> map;


    public static void main(String[] args) throws NoSuchFieldException {
        // 获取 ParameterizedTypeDemo 类中的字段 "map"
        Field field = ParameterizedTypeDemo.class.getField("map");
        // 获取字段的泛型类型
        Type genericFieldType = field.getGenericType();
        // 检查是否为 ParameterizedType
        if (genericFieldType instanceof ParameterizedType) {
            ParameterizedType parameterizedType = (ParameterizedType) genericFieldType;
            // 获取实际的类型参数
            Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();
            // 打印原始类型
            System.out.println("Raw Type: " + parameterizedType.getRawType());
            // 打印类型参数信息
            for (Type typeArgument : actualTypeArguments) {
                System.out.println("Type argument: " + typeArgument.getTypeName());
            }

        }
    }
}

输出结果如下:

Raw Type: interface java.util.Map
Type argument: java.lang.String
Type argument: java.lang.Integer

通过 ParameterizedType 可以在运行时获取被擦除的泛型类型信息,使得更动态和灵活的代码成为可能。

TypeVariable 类型变量

java.lang.reflect.TypeVariable<D> 也是 Type 接口的一个子接口,表示类型变量。

类型变量通常是在泛型类、接口或方法中定义的占位符,用于表示未知的类型。

例如 class GenericClass<T> {} 中的 T 就是类型变量。

TypeVariable 也提供了一些方法用于获取类型变量的相关信息:

Type[] getBounds() : 返回类型变量的上界数组。默认情况下,类型变量的上界是 Object,但如果类型变量被显式地限制为某些类或接口,那么该方法将返回这些限制的类型。

D getGenericDeclaration() : 返回声明该类型变量的实体(类、接口或方法)。D 是泛型参数,通常是 ClassMethodConstructor

String getName() : 返回类型变量的名称。例如,对于 T,该方法将返回 "T"

package com.cango.generics;

import java.lang.reflect.TypeVariable;
import java.lang.reflect.Type;

public class TypeVariableDemo <T extends Number & Comparable<T>> {

    public static void main(String[] args) {
        // 获取 TypeVariableDemo 类的类型变量
        TypeVariable<Class<TypeVariableDemo>>[] typeParameters = TypeVariableDemo.class.getTypeParameters();

        for (TypeVariable<Class<TypeVariableDemo>> typeVariable : typeParameters) {
            // 打印类型变量的名称
            System.out.println("Type Variable Name: " + typeVariable.getName());
            // 获取类型变量的上界
            Type[] bounds = typeVariable.getBounds();
            for (Type bound : bounds) {
                System.out.println("Bound: " + bound.getTypeName());
            }
        }
    }
}

输出结果如下:

Type Variable Name: T
Bound: java.lang.Number
Bound: java.lang.Comparable<T>

这么看类型变量和参数化类型还是不同的:TypeVariable 代表的是泛型声明中的类型变量,即泛型的占位符(如TEKV),而 ParameterizedType 代表的是一个具体的参数化类型(如 List<String>),即带有实际类型参数的泛型类型。

GenericArrayType: 泛型数组类型

java.lang.reflect.GenericArrayType 也是Type 接口的一个子接口,表示泛型数组类型。

例如,T[]List<String>[] 这样的类型在运行时就被表示为泛型数组类型

它只有一个方法,用于获取数组中的组件类型:

Type getGenericComponentType() : 返回数组的组件类型。这意味着它返回的是数组中每个元素的类型,可能是普通类型或泛型类型。

package com.cango.generics;

import java.lang.reflect.Field;
import java.lang.reflect.GenericArrayType;
import java.lang.reflect.Type;
import java.util.List;

public class GenericArrayTypeDemo<T> {
    public T[] genericArray;
    public List<String>[] listArray;
    public static void main(String[] args) throws NoSuchFieldException {
        // 获取泛型数组字段
        Field genericArrayField = GenericArrayTypeDemo.class.getField("genericArray");
        Type genericArrayType = genericArrayField.getGenericType();

        if (genericArrayType instanceof GenericArrayType) {
            GenericArrayType arrayType = (GenericArrayType) genericArrayType;
            Type componentType = arrayType.getGenericComponentType();
            System.out.println("Generic Array Component Type: " + componentType.getTypeName());
        }

        // 获取带有泛型的数组字段
        Field listArrayField = GenericArrayTypeDemo.class.getField("listArray");
        Type listArrayType = listArrayField.getGenericType();

        if (listArrayType instanceof GenericArrayType) {
            GenericArrayType arrayType = (GenericArrayType) listArrayType;
            Type componentType = arrayType.getGenericComponentType();
            System.out.println("List Array Component Type: " + componentType.getTypeName());
        }
    }
}

输出结果如下:

Generic Array Component Type: T
List Array Component Type: java.util.List<java.lang.String>

T[] 是一个带有泛型组件类型的数组,其中组件类型是类型变量 T;:List<String>[] 是一个带有泛型组件类型的数组,其中组件类型是 List<String>

WildcardType 通配符类型

java.lang.reflect.WildcardType 同样是Type 接口的一个子接口,用于表示带有通配符的泛型类型。

通配符类型就是前文讲的上下边界,例如 ? extends Number? super Integer

通常用在泛型中,表示一种不确定的类型边界,例如 ? extends Number? super Integer

WildcardType 有两个方法分别用于获取获取类型的上下边界:

Type[] getUpperBounds() : 返回通配符的上界数组。

Type[] getLowerBounds() : 返回通配符的下界数组。

import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.WildcardType;
import java.util.List;

public class WildcardTypeDemo {
    public List<? extends Number> numbers;

    public static void main(String[] args) throws NoSuchFieldException {
        // 获取带有 ? extends Number 的字段
        Field numbersField = WildcardTypeDemo.class.getField("numbers");
        Type numbersType = numbersField.getGenericType();

        if (numbersType instanceof ParameterizedType) {
            ParameterizedType parameterizedType = (ParameterizedType) numbersType;
            Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();

            for (Type typeArgument : actualTypeArguments) {
                if (typeArgument instanceof WildcardType) {
                    WildcardType wildcardType = (WildcardType) typeArgument;

                    // 打印上界
                    for (Type upperBound : wildcardType.getUpperBounds()) {
                        System.out.println("Upper bound: " + upperBound.getTypeName());
                    }

                    // 打印下界
                    for (Type lowerBound : wildcardType.getLowerBounds()) {
                        System.out.println("Lower bound: " + lowerBound.getTypeName());
                    }
                }
            }
        }
    }
}

输出结果如下:

Upper bound: java.lang.Number

总结

泛型使得 Java 代码更加灵活、通用,并且提供了类型安全和更好的代码重用性。泛型的引入减少了代码的冗余性,特别是在处理集合类和算法时非常有用。

代码的可重用性:泛型提供了编写通用代码的能力,使得类、接口和方法可以适应不同的数据类型,而无需重复编写相同功能的代码。这显著提高了代码的可重用性。

类型安全:使用泛型后,编译器会强制类型检查,从而防止类型转换异常(ClassCastException)。

消除强制类型转换:在没有泛型的情况下,通常需要手动进行类型转换,而泛型通过类型推断可以消除这些强制类型转换,使代码更简洁,避免错误。

// 没有泛型之前的代码:     
List list = new ArrayList();
list.add("Hello");
String item = (String) list.get(0);  // 需要进行强制类型转换

使用泛型后的代码:

List<String> list = new ArrayList<>();
list.add("Hello");
String item = list.get(0);  // 不需要类型转换

泛型虽然很好,但要注意由于类型擦除等机制,但这也没事,在编译时擦除的泛型可以通过反射获取,这就需要我们了解这几个与泛型相关的类,才能更加得心应手。