Java 泛型

文章介绍了Java中泛型的概念和作用,包括为何使用泛型以避免类型转换和安全问题,泛型类和泛型方法的定义,类型参数的限定,以及类型擦除的原理和编译器如何处理。同时,讨论了泛型的限制、继承规则,通配符的子类限定和超类限定,并提到了自限定类型的特殊用法。
摘要由CSDN通过智能技术生成

一、为什么使用泛型

泛型之前ArrayList的使用:

public class Main {
    public static void main(String[] args) {
        ArrayList list = new ArrayList();
        list.add("Hello");
        String s = (String)list.get(0);
        list.add(new File("..."));
    }
}

当我们希望 list 只存储 String 类型的对象时,原始的 ArrayList 存在两个问题:

  • 当获取一个值时,必须进行强制类型转换
  • 编译器不能进行类型检查,可以向数组列表中添加任何类的值

而泛型可以解决这两个问题,下面介绍泛型的使用。

二、泛型类和泛型方法

1. 泛型类

定义泛型类,类型参数用尖括号括起来,放在类名之后:

class Pair<T> {...}

创建对象时,可以使用菱形语法省略构造器中的类型参数:

Pair<String> p = new Pair<>();

2. 泛型方法

定义泛型方法,类型参数放在修饰符之后,返回类型之前

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

当调用一个泛型方法时,可以把具体类型包围在尖括号中,放在方法名之前:

String middle = Main.<String>getMiddle("John", "Q.", "Public");

不过,在大多数情况下,方法调用可以省略类型参数:

String middle = Main.getMiddle("John", "Q.", "Public");

三、类型参数的限定

Java 库使用参数 E 表示集合的元素类型,K 和 V 分别表示表的键和值的类型,T(或者 U 和 S)表示"任意类型"。

我们可以使用extends关键字对类型参数进行限定:

<T extends BoundingType>

以上写法,表示 T 应该是限定类型(bounding type)的子类型(subtype),T 和限定类型可以是类或接口。

一个类型参数或通配符可以有多个限定,限定类型用&分隔,而类型参数用逗号分隔:

<T extends Comparable & Serializable>

需要注意的是,如果有一个类作为限定,它必须是限定列表中的第一个限定

四、类型擦除

在虚拟机中没有泛型类型对象,所有对象都属于普通类。

无论何时定义一个泛型类型,都会自动提供一个相应的原始类型,这个原始类型的名字就是去掉类型参数后的泛型类型名。类型参数会在编译时被擦除,并替换为其第一个限定类型(对于无限定的类型参数则替换为Object)。

1. 擦除泛型类

下面我们定义一个泛型类:

class Interval<T extends Comparable & Serializable> implements Serializable {
    private T lower;
    private T upper;
    ...
    public Interval(T first, T second) {
        if(first.compareTo(second) <= 0) { lower = first;upper = second; }
        else { lower = second;upper = first; }
    }
}

该泛型类的原始类型如下所示:

class Interval implements Serializable {
    private Comparable lower;
    private Comparable upper;
    ...
    public Interval(Comparable first, Comparable second) {...}
}

需要注意,如果将限定换为<T extends Serializable & Comparable>,会用 Serializable 替换 T,而这可能导致在调用 compareTo 方法时需要进行强制类型转换。为了提高效率,应该将标签接口放在限定列表的末尾

2. 编译器的工作

① 当调用一个泛型方法时,如果擦除了返回类型,编译器会插入强制类型转换

示例代码:

Pair<Employee> pair = new Pair<>(...);
Employee first = pair.getFirst();

对字节码文件进行反编译:

Code:
      ...
        32: invokespecial #22                 // Method com/company/Pair."<init>":(Ljava/lang/Object;Ljava/lang/Object;)V
        35: astore_3
        36: aload_3
        37: invokevirtual #25                 // Method com/company/Pair.getFirst:()Ljava/lang/Object;
        40: checkcast     #7                  // class com/company/Employee
        43: astore        4
        45: return

可以看到在调用 getFirst 方法后,进行了checkcast强制类型转换的检查。

② 类型擦除与多态存在冲突,为了解决这个问题,编译器会生成桥方法

示例代码:

class Pair<T> {
    private T first;
    private T second;

    public Pair() {};
    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 first) { this.first = first; }
    public void setSecond(T second) { this.second = second; }
}

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

DateInterval 类描述一个日期区间,我们覆盖了 Pair 类的 setSecond 方法来确保第二个值不小于第一个值。

类型擦除后变成:

class Pair {
    private Object first;
    private Object second;

    public Pair() {};
    public Pair(Object first, Object second) {
        this.first = first;
        this.second = second;
    }

    public Object getFirst() { return first; }
    public Object getSecond() { return second; }

    public void setFirst(Object first) { this.first = first; }
    public void setSecond(Object second) { this.second = second; }
}

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

这时,我们发现 DateInterval 类中 setSecond 方法的签名为setSecond(LocalDate second),而 Pair 类中的签名为setSecond(Object second),不构成覆盖方法

但是,考虑以下代码:

Pair pair = new DateInterval(...);
pair.setSecond(...);

setSecond 的调用应该具有多态性,pair.setSecond 应该调用 DateInterval 的 setSecond 方法。但是,类型擦除与多态产生了冲突,实际上会调用 Pair 的 setSecond 方法。 为了解决这个问题,编译器在 DateInterval 类中生成了一个桥方法

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

这时,桥方法覆盖了 Pair 类中的 setSecond 方法,并且会调用 DateInterval 的 setSecond(LocalDate) 方法,保证了 Java 的多态。

我们对 DateInterval 类文件进行反编译,进行验证:

class com.company.DateInterval extends com.company.Pair<java.time.LocalDate> {
  com.company.DateInterval();
  public void setSecond(java.time.LocalDate);
  public void setSecond(java.lang.Object);
}

可以看到,确实多出了一个 setSecond(Object) 方法。

五、泛型的限制(待续)

六、泛型类型的继承规则

继承规则:无论 S 与 T 有什么关系,通常 Pair<S> 与 Pair<T> 都没有任何关系。

在这里插入图片描述

如上所示,Manager 是 Employee 的子类,但是 ArrayList<Manager> 和 ArrayList<Employee> 没有任何关系。

七、通配符的限定

子类限定

子类限定<? extends Type>,表示将泛型对象的类型参数限制为 Type 类型或其子类型。类型参数为子类限定的泛型对象引用,可以读取,但不能写入。

示例代码:

Pair<Manager> p1 = new Pair<>(m1, m2);
Pair<? extends Employee> p2 = p1;

对象变量 p2 引用的对象的类型为 Pair<Manager>,但是 p2 本身被声明为 Pair<? extends Employee> 类型。从 p2 角度看,我们只知道 p2 引用的 Pair 类型的对象存储的是 Employee 类或其子类的对象,可以读取这个对象并向上转型为 Employee 类型;但是不能进行写入,因为不知道子类的具体类型。

超类限定

超类限定<? super Type>,表示将泛型对象的类型参数限制为 Type 类型或其超类型。类型参数为超类限定的泛型对象引用,可以写入,但只能读取为 Object。

示例代码:

Pair<Employee> p1 = new Pair<>(e1, e2);
Pair<? super Manager> p2 = p1;

对象变量 p2 引用的对象的类型为 Pair<Manager>,但是 p2 本身被声明为 Pair<? super Manager> 类型。从 p2 角度看,我们只知道 p2 引用的 Pair 类型的对象存储的是 Manager 类或其超类的对象,可以写入 Manager 类型或其子类的对象,它会自动向上转型为特定的超类类型;但是因为不知道读取到的超类的具体类型,只能将读取结果赋值为 Object。

无限定

无限定<?>,表示不限定泛型对象的类型参数。类型参数为无限定的泛型对象引用,只能读取为 Object,不能写入。

补充:PECS( Producer Extends, Consumer Super )原则,即读取数据使用子类限定,写入数据使用超类限定。

八、自限定的类型

在 Java 泛型中,有一种经常出现的写法:

class SelfBound<T extends SelfBound<T>> {...}

我们称这种类型为自限定的类型,selfBound 接收类型参数 T,而 T 限定为 selfBound<T> 的子类。

示例代码:

class A extends SelfBound<A> {...}
class B extends SelfBound<A> {...}
class C extends SelfBound<B> {...} // Error

A 继承 selfBound<A> ,使得 A 可以作为 selfBound 的类型参数;而 B 没有继承 selfBound<B>,所以不能将 B 作为 selfBound 的类型参数。

自限定类型的主要使用方式为class A extends SelfBound<A> {...},它的目的是保证 SelfBound 类的类型参数为当前定义的类。你可能会说,B 继承的 SelfBound 类的类型参数是 A 而不是当前定义的 B 类,但是一般情况下并不会这样使用。

如果看不懂,可以考虑没有自限定类型的情况:

class SelfBound<T> {...}

class A extends SelfBound<Any-Type> {...}

如果 selfBound 没有自限定,A 类在继承 selfBound 时,类型参数可以是任意类型。


如有错误,欢迎指正。.... .- ...- . .- -. .. -.-. . -.. .- -.-- -.-.--

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值