Core Java 笔记(九)- 泛型

泛型

Java 中的泛型,就是在设计类、接口或方法时,不事先确定操作对象的具体类型,而是将其指定为参数,称为类型参数(type parameters),等到使用这些类、接口或方法时,才将具体类型传递。 使用泛型的好处在于:

  • 可重用性:实现代码的模板化

  • 可读性:无需进行强制类型转换

  • 安全性:在编译阶段发现错误

一、定义泛型

定义在类上

具有一个或多个类型变量的类就属于泛型类(generic class),用一个简单的 Pair 类作为例子:

public class Pair<T> {
    private T first;
    private T second;
    public Pair() {
        first = null;
        second = null;
    }
    public Pair(T first, T second) {
        this.first = first;
        this.second = second;
    }
    public T getFirst() {
        return first;
    }
    public T getSecond() {
        return second;
    }
    public void setFirst(T value) {
        first = value; 
    }
    public void setSecond(T value) {
        second = value;
    }
}

类型变量要用<>括起来,置于类名之后。可以有多个类型变量,为不同的域指定不同的类型,比如:

public class Pair<T, U> {
    private T first;
    private U second;
    // ...
}

使用时,将类型变量替换成具体的类型就可以:

Pair<Integer> pair = new Pair<>;

从 Java SE 7 开始,构造函数中可以省略泛型类型(又叫“菱形语法”)。

定义在方法上

定义一个带有类型参数的方法:

class ArrayAlg {
    public static <T> T getMiddle(T... a) {
        return a[a.length / 2];
    }
}

类型变量同样用<>括起来,置于修饰符的后面,返回类型的前面。 调用方式是在方法名前的<>中放入具体类型,不过,在编译器具备自行推断的条件的时候,也可以省略具体类型:

String middle = ArrayAlg.<String>getMiddle("hello", "here", "hero");
// String middle = ArrayAlg.getMiddle("hello", "here", "hero");

限定类型变量

有时,一个类或方法并不适用于任意的类型,比如一个计算最小(大)值的方法,必然要求传入的类型要实现 Comparable 接口,于是便要对类型变量加以约束。

class ArrayAlg {
    public static <T extends Comparable> T getMin(T[] a) {
        if (a == null || a.length == 0) return null;
        T min = a[0];
        for (int i = 1; i < a.length; i++) {
            min = min.compareTo(a[i]) > 0 ? a[i] : min;
        }
        return min;
    }
}

这种记法:

<T extends BoundingType>

表示 T 应该是限定类型的子类型,T 和限定类型可以是类,也可以是接口。另外,限定类型可以有多个,用 & 分隔:

<T extends Comparable & Serializable>

与继承的限制一样,限定类型中至多有一个类,并且(如果有的话)必须是限定列表中的第一个。

二、泛型代码和虚拟机

先来看一段简单的代码:

Pair<String> stringPair = new Pair(str1, str2);
Pair<Person> personPair = new Pair(person1, person2);
System.out.println(stringPair.getClass == personPair.getClass); // always true

为什么会出现这种情况?

编译器会检测泛型使用是否正确,但是,在编译阶段就会对泛型代码进行类型擦除(erased),在必要的地方再进行强制类型转换,就跟未引入泛型时的做法一样。从这个角度来说,Java 其实是“伪泛型”,基本是在编译器的层次中实现的,字节码文件中已不包含关于泛型的信息。在虚拟机的层次中,不存在泛型类型对象,所有对象都属于普通类。正因为这样,理解类型擦除成为弄懂 Java 泛型机制的首要前提。

类型擦除

任何一个泛型类型都有相对应的原始类型(raw type),原始类型的名字就是删去类型参数后的泛型类型名,比如 List / List<T>, Pair / Pair<T> ... 在类型参数被擦除后,如果没有指定上限,则会替换成 Object,如果指定了上限(如<T extends Comparable>)则会替换成第一个限定类型(Comparable)。

泛型类 Pair<T>中 T 是一个无限定的变量,直接用 Objec 替换:

public class Pair {  // not Pair<T>
    private Object first;
    private Object second;
    
    public Pair(Object first, Object second) {...}
    public Object getFirst() {...}
    public Object getSecond() {...}
    public void setFirst(Object value) {...}
    public void setSecond(Object value) {...}
}
与 C++ 模板的区别

C++ 模板的每一个实例化都会产生不同的类型,导致代码膨胀,而 Java 泛型机制不存在这个问题。

对于泛型方法,通常认为是一个完整的方法族:

public static <T extends Comparable> T min(T[] a)

而擦除类型后,就只剩下一个方法:

public static Comparable min(Comparable[] a)

翻译类型擦除后的泛型表达式

Pair 中的 get 方法返回泛型类型:

Pair<Employee> buddies = ...;
Employee buddy = buddies.getFirst();

编译器对 getFirst 方法进行类型擦除,将返回类型改为 Object 后,会自动插入 Employee 的强制类型转换,也就是说会翻译成两条虚拟机指令:

  • 对原始方法的调用

  • 将返回的 Object 类型或限定类型强制转换为语境需要的类型

类型擦除与多态的冲突

这里有一个继承了泛型实例的类:

class DateInterval extends Pair<LocalDate> {
    public void setSecond(LocalDate second) {
        if (second.compareTo(getFirst()) >= 0)  // ensure second >= first
            super.setSecond(second);
    }
}

在 Pair<LocalDate> 中 setSecond 方法的参数类型是 LocalDate,所以上述对 getSecond 方法的重写应该没有问题,但是,注意类型擦除后:

class DateInterval extends Pair {
    public void setSecond(LocalDate second) {
        ...
    }
}

这个时候存在另一个从 Pair 继承过来的 setSecond 方法:

public void setSecond(Object second)

这样一来,在 DateInterval 中定义的 setSecond 方法还是重写(override)吗?不,因为参数列表不同,所以变成了重载(overload),看看下面的方法调用:

DateInterval interval = new DateInterval(...);
Pair<LocalDate> pair = interval;
pair.setSecond(date);

pair 引用了子类对象 interval,多态性使得 interval.setSecond(Object) 被调用,而不是我们希望的 setSecond(LocalDate),现在,类型擦除与多态发生了冲突。

桥方法

对于上述问题,编译器会在 DateInterval 中生成一个桥方法(bridge method),顾名思义,就是起到一个联结的作用:

public void setSecond(Object second) {
    setSecond((LocalDate) second);
}

这个方法对继承自 Pair(类型已擦除)的 setSecond 进行了重写,其内部实现只是调用重载的 setSecond(LocalDate),建立联结,解决了冲突。

对于重写的 getSecond 方法,会稍微有一点不同。比如说是这样:

class DateInterval extends Pair<LocalDate> {
    ...
    public LocalDate getSecond() {
        return (LocalDate) super.getSecond().clone();
    }
}

编译器合成桥方法后,DateInterval 中会有两个 getSecond 方法:

LocalDate getSecond()
Object getSecond()
为什么编译器可以产生只有返回类型不同的方法?

这是因为在虚拟机中,通过参数类型和返回类型共同确定一个方法。

关于桥方法最后要说的是,它不是专用于处理泛型问题的,它也还用于实现前几章说过的具有协变的返回类型(子类在重写父类方法时可以指定一个更严格的返回类型)。

三、对泛型的限制

  • 不能用基本类型实例化类型参数

  • 运行时的类型查询(instanceof / getClass)只适用于原始类型

  • 不能实例化(但是可以声明)参数化类型的数组,比如 new Pair<String>[10]

  • 不能实例化类型变量,比如 new T(...)

  • 不能将类型参数用于静态域和静态方法

  • 不能抛出和捕获泛型类的实例

四、通配符

最后分享一篇关于泛型的好文章(点击此处访问,作者 frank909),其中一段话让我觉得很有意思:

我在文章开头将泛型比作是一个守门人,原因就是他本意是好的,守护我们的代码安全,然后在门牌上写着出入的各项规定,及“xxx 禁止出入”的提醒。但是同我们日常所遇到的那些门卫一般,他们古怪偏执,死板守旧,我们可以利用反射基于类型擦除的认识,来绕过泛型中某些限制,现实生活中,也总会有调皮捣蛋者能够基于对门卫们生活作息的规律,选择性地绕开他们的监视,另辟蹊径溜进或者溜出大门,然后扬长而去,剩下守卫者一个孤独的身影。

猜你喜欢

转载自www.cnblogs.com/zzzt20/p/11580897.html