Java泛型

泛型出现以前,泛型程序设计主要通过继承实现,例如ArrayList中维护一个Object数组,即可接受所有类型的对象。
这种设计存在两个问题:

  1. 当从ArrayList中取出一个元素时,需要进行强制类型转换才能给子类对象。
  2. 当实例化一个ArrayList对象后,可以向其中加入任意类的对象,没有错误检查。

泛型的出现解决了以上两个问题,通过类型参数来指定元素的类型。

定义一个String链表
ArrayList<String> list = new ArrayList<String>();
后一个String可以省略,编译器能够通过前一个String推断出泛型类型。
当使用ArrayList.get()方法时,不需要再类型转换,直接赋值给String对象

类型参数的魅力在于:使得程序具有更好的可读性和安全性

一个简单的泛型类Pair

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class Pair<T> {
    private T first;
    private T second;

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

    public T getFirst() {
        return first;
    }

    public void setFirst(T first) {
        this.first = first;
    }

    public T getSecond() {
        return second;
    }

    public void setSecond(T second) {
        this.second = second;
    }
}

泛型类中,类型变量用<>括起来放在类型名的后面。在Java库中,使用E表示集合的元素类型,K、V分别表示Key、Value,T表示任意类型。

泛型方法

可以将方法声明为泛型方法,类型变量放在修饰符之后,返回类型之前。

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

调用

1
String middle = ArrayAlg.<String>getMiddle("1","2","4");

<String>可以省略,编译器能够自动识别类型为String,但是参数列表需要都为String,否则会报错。

限定类型变量


当我尝试在泛型类中调用String.length()方法时,会出现编译错误,这是显然的,因为目前这个类是未知类型,未来可能赋给它的是任意类型,无法保证这个属性中有哪些方法(但是一定包含Object的方法)。
为了解决这个问题,可以对类型变量加以约束,使用这样的语法<T extends BoundingType>表示T应该是绑定类型的子类型,这个类型可以是类也可以是接口。
如果想要限定为 继承了某个类、实现了某些接口的话,需要使用&分隔多个限定例如:
T extends Comparable & Serializable
如果限定中有类,那么类名必须是列表中的第一个。
修改Pair类,编译通过。

1
2
3
4
5
6
7
8
public class Pair<T extends String> {
    private T first;
    private T second;

    public int compareLength(T t1,T t2){
        return t1.length() - t2.length();
    }
}

泛型代码和虚拟机

其实泛型类只是在编码阶段优化了传统的基于继承的泛型程序设计,在编译之后,虚拟机中,是不存在泛型类型对象的,所有的对象都属于普通类。

类型擦除

对任意的泛型类型,都自动提供了一个相应的原始类型,就是泛型类型去掉类型参数之后的泛型类型名,对尖括号和其中的类型参数,直接删除,对其他的类型参数,用其限定类型代替,没有限定类型就用Object
如上面的Pair<T extends String>泛型类型的原始类型是:

1
2
3
4
5
6
7
8
public class Pair {
    private String first;
    private String second;

    public int compareLength(String t1,String t2){
        return t1.length() - t2.length();
    }
}

这样显然是合理的,这个泛型类型中,类型参数被限定为String的子类,在编译前编译器借助泛型保证了传入的任何对象都可以赋给String对象的引用,所以在编译后不会出现问题。

思考一个问题,假如有多个类型限定,在泛型类中都使用了这多个类型的方法,那么类型擦除机制会选用那种类型呢?

比如T extends ArrayAlg & Comparable & ActionListener,在泛型类中同时使用了:

1
2
3
4
5
6
7
8
9
10
public class Pair<T extends ArrayAlg & Comparable & ActionListener> {
    private T first;
    private T second;

    public int compareLength(T t1,T t2){
        t1.test();
        t1.compareTo(t2);
        t1.actionPerformed(new ActionEvent(new Object(),0,""));
        return 0;
    }

首先,限定中的类一定写在第一个,否则编译不通过:

然后,编译器一定选择第一个类名或者接口名,这里是ArrayAlg
之后,T类型参数被擦除成为ArrayAlg,但是因为其中有Comparable & ActionListener 接口的方法,所以报错:

对此,编译器会进行必要的类型转换:

1
2
3
4
5
6
public int compareLength(ArrayAlg t1,ArrayAlg t2){
        ((ActionListener)t1).actionPerformed(new ActionEvent(new Object(),0,""));
        ((Comparable)t1).compareTo(t2);
        t1.test();
        return 0;
    }

这样是合理的,在这个泛型类型中,类型参数被限定为ArrayAlg的子类,并且实现了Comparable & ActionListener 接口,这也就是说,赋值给Pair对象中first、second属性的对象一定有以下方法:

  1. test() –来自ArrayAlg
  2. compareTo(T) –来自Comparable
  3. actionPerformed(ActionEvent) –来自ActionListener

所以即使编译后类型Pair中的类型参数被擦除为ArrayAlgPair对象中这个ArrayAlg类对象的引用指向的对象一定包含上述三个方法域。所以,对这个ArrayAlg对象进行强制类型转换,使其变成Comparable & ActionListener 接口,是没有问题的。

翻译泛型表达式

在擦除返回类型后,编译器会自动插入强制类型转换。
在使用泛型类型,如:

1
2
3
ArrayList<String> list = new ArrayList<>();
list.add("hello");
String string = list.get(0);

这里编译器根据ArrayList<String>识别出类型参数是String类型,当删除这个类型参数后,编译器就会使用原始类型,于是出现类型不兼容:

加上类型转换即可解决:

1
2
3
4
5
public static void main(String[] args) {
        ArrayList list = new ArrayList<>();
        list.add("hello");
        String string = (String) list.get(0);
    }

所以说,泛型只存在于编码阶段,编译完成之前,泛型的意义就是增加了代码的可读性、安全性,以及减少了一部分代码量。

翻译泛型方法

类型擦除对泛型方法同样适用。看下面这个例子:

看似右边的方法是重写了父类的方法,但是在类型擦除后,父类方法其实变成了:
public void setSecond(Object second)
这个方法理所应当的继承到了子类中,
而上面个这个子类方法是:
public void setSecond(LocalDate localDate)
参数类型不一样,所以我们写的方法不是真正的重写,但是为了保持setSecond的多态性和代码的可读性,编译器会在子类中加上一个桥方法

1
2
3
4
5
6
7
8
9
10
public class DateInterval extends Pair<LocalDate> {
//    桥方法
    public void setSecond(Object localDate){
        setSecond((LocalDate)localDate);
    }
//    这是我们写的方法
    public void setSecond(LocalDate localDate){

    }
}

第一个方法才是真正重写的方法,同时也是父子类的桥方法。在使用子类对象去调用如dateInterval.setSecond(date)时,依据多态特性,调用的是桥方法,编译器给我们套了一层,让我们看起来是直接调用了我们写的方法。

对Setter来说,出现了返回类型相同,参数不同的方法,而对Getter来说,则是出现了返回类型不同,参数类型相同,在写Java代码时,这是不允许的。
但是在JVM中,用参数类型和返回类型确定一个方法,所以编译器能够正确处理这一情况。
Getter桥方法实现了,先调用我们”重写”的getter,然后在这个里面调用子类对象中从父类对象继承的Getter,获得Object对象,然后进行类型转换为DateInterval返回。

约束与局限性

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

例如,没有Pair<double>,只有Pair<Double>,当然其原因是类型擦除,Pair类中包含Object类型的域,而Object不能存储double值。

运行时类型查询仅适用于原始类型

1
2
3
4
Pair<String> pair = ...;
Pair<Employee> employeePair = ...;

pair.getClass() == employeePair.getClass(); //true

不能创建参数化类型的数组

这样创建后,pairs的类型是Pair[],可以将其转换为Object[]
数组会记住他的元素类型,如果向其添加一个String,就会报错。
对数组来说,它存储的类型就是Pair,这也就意味着,向其中放入一个别的参数类型的Pair对象如Pair<Employee>那么就会通过数组的参数类型检查,因为类型擦除后类型都是Pair,但是在编译时会造成类型转换错误,因为如果这个对象中使用了Employee类中的方法,那么编译器会尝试使用类型转换

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值