Java Generics

2021-05-25

泛型的概念

泛型,也就是把类型参数化。比如定义一个方法,他需要对几种不同的数据类型做相同的处理,如果不用泛型,那我们就需要定义好几个方法,每个方法对应每种数据类型。这几个方法只是参数类型不同,方法体都是一样的,那么就显得很冗余。

拿比较常用的 List 举例,假设他不支持泛型,那我们就需要一个 StringList 来存放字符串,一个 IntegerList 来存放整型,一个 DoubleList 来存放双精度浮点型等等。另外,对于我们自己定义的类,也需要再定义很多 List 来分别存放他们。而这些 List 提供的功能都是一样的:添加数据、读取数据。

List 里面可以存放任意类型的数据,这里我们创建一个 List,并添加一个字符串和一个整型数据。

List myList = new ArrayList();
myList.add("hello");
myList.add(23);

在需要从 myList 中读取数据时,对于不同的数据类型,就要做对应的类型转换了。

String strElem = (String) myList.get(0);
Integer intElem = (Integer) myList.get(1);

显然,我们需要记住数组中每个元素是什么类型的,如果在读取数据时搞乱了,比如第 0 个元素是字符串,我们却记成了整型

Integer intElem = (Integer) myList.get(0);

自然就会抛出一个类型转换的错误,导致程序异常。泛型的出现很好地解决了这个问题。

List<String> myStrList = new ArrayList<>();
List<Integer> myIntList = new ArrayList<>();

myStrList.add("hello");
myIntList.add(23);

这样就规定了 myStrList 中只能存放字符串类型,myIntList 中只能存放整型。如果你在 myStrList 中添加了一个非字符串的类型,是不会编译通过的。

类型擦除

泛型信息只存在于编译阶段,编译完成后,所有的泛型信息都会被擦除,也就是说在 JVM 执行代码阶段,是不存在泛型这个概念的,这就是类型擦除。

System.out.println(myStrList.getClass() == myIntList.getClass());
System.out.println(myStrList.getClass());

执行这段代码,会输出 true,表示在 JVM 中,myStrListmyIntListClass 是同一个,并且在输出的第二行打印出了 class java.util.ArrayList,这里并不包含泛型信息。

既然泛型信息被擦除了,那么 JVM 在执行阶段是怎么确定不同的 List 中存放的是什么数据类型呢?

我们定义一个泛型类,并通过反射看一下其中的泛型在编译后是什么样的

class MyGeneric<T> {
    T object;

    public MyGeneric(T object) {
        this.object = object;
    }

    public static void main(String[] args) {
        MyGeneric<String> myGeneric = new MyGeneric<>("hello");

        Class myClz = myGeneric.getClass();
        Field[] fields = myClz.getDeclaredFields();

        for (Field f : fields) {
            System.out.println("field: " + f.getName() + ", type: " + f.getType().getName());
        }
    }
}

输出为:

field: object, type: java.lang.Object

可以看到,其中的泛型类型变成了 Object。我们改一下 MyGeneric 的泛型部分

class MyGeneric<T extends String> {
    // ...省略
}

再次编译运行,输出为:

field: object, type: java.lang.String

这样我们就可以确定,在对泛型做类型擦除时,如果没有指定类型的上限,那么这个类型会被编译为 Object 类型;如果指定了上限,那么他会被编译为这个上限的类型。

既然在编译后类型会被擦除,那么我们就可以通过反射来绕过在泛型中的类型限制。比如定义了一个 List<Integer> 类型,就可以通过反射调用他的 add() 方法向其中添加非 Integer 类型的数据。

还有一个问题:

List<Integer>[] myList = new ArrayList<>[];

这里 myList 是一个数组,他的每个元素是 List<Integer> 类型的。由于会有类型擦除,导致程序无法分辨元素的类型,这行代码是无法编译通过的。

泛型的使用

泛型有三种使用方式:泛型类,泛型方法,泛型接口。

泛型类

我们来看泛型类的写法:

class 类名 <泛型标识> {
    private 泛型标识 变量;

    public 类名(泛型标识 形参) {

    }

    public void 方法名(泛型标识 形参) {

    }
}

其中泛型标识可以是一个,比如 <T>,也可以是多个,比如 <T, E>。上文的 MyGeneric 就是一个泛型类,再比如 HashMap 使用了两个泛型标识,第一个指定键的类型,第二个指定值的类型,用法 new HashMap<String, Integer>

实例化泛型类:

泛型类<具体类型> 变量名 = new 泛型类<>(具体类型 参数);

第二个 <> 中的具体类型是可以省略的,因为编译器从第一个 <> 中已经获得了具体类型。

当然,实例化泛型类时,也可以不用指定具体类型,比如我们在 List 中可以添加任意类型的数据。

泛型方法

public <泛型标识> void method1(泛型标识 参数) {}

public <泛型标识> 泛型标识 method2() {
    // value 是泛型标识限定的类型
    return value;
}

比如在上文的 MyGeneric 类中,添加一个获取 object 数据的方法:

public <T> T getObject() {
    return object;
}

泛型方法与泛型类可以共存,也可以只定义其中一个。

泛型接口

泛型接口的定义跟泛型类的定义类似:

public interface 接口名<泛型标识> {
    泛型标识 方法名();
}

实现泛型接口时,可以指定具体的泛型类型,也可以不指定:

public interface MyInterface<T> {
    T method1();
}

// 不指定具体类型
class MyClassA implements MyInterface<T> {
    @Override
    T method1() {

    }
}

// 指定具体类型
class MyClassB implements MyInterface<String> {
    @Override
    String method1() {

    }
}

泛型通配符

常用的泛型通配符有:T, E, K, V, ?

其中:

  • T (type) 表示具体的一个 Java 类型
  • K, V (key, value) 表示键和值
  • E (element) 表示元素
  • ? 表示任意的类型

? 通配符可以指定类型范围,有三种形式:

  • <?> 任意类型
  • <? extends T> 指定类型上限,表示类型 T 及其子类
  • <? super T> 指定类型下限,表示类型 T 及其超类(父类)
Java

Jeff Liu

Java Object Class

vim 配置 Golang 开发