关于Java泛型实现原理的思考与一般用法示例总结

时间:2023-03-09 02:52:16
关于Java泛型实现原理的思考与一般用法示例总结

面向对象的一个重要目标是对代码重用的支持。支持这个目标的一个重要机制就是泛型机制。在1.5版本之前,java并没有直接支持泛型实现,泛型编程的实现时通过使用继承的一些基本概念来完成的。

这种方式的局限性有:
1. 使用此种方式会不可避免地用到强制类型转换。
2.
不能使用基本类型,只有引用类型能和Object相容。(通过使用包装器类)

例如使用Comparable接口来暂时代表所有实现了该接口的类。

什么是协变性?

简而言之,如果A IS-A B,那么A[] IS-A B[]。
举例:现在有类型Person、Employee和Student。Employee
是一个(IS-A) Person,Student是一个(IS-A)Person。那么下面的语句可以通过编译:

但是上面的代码在运行时却会出错。因为arr[0]实际上是引用一个Employee,可是Student IS-NOT-A
Employee。这样就产生了混乱。这种错误正是由于Java数组的协变性而产生的。那么Java为什么不禁止数组协变呢?

因为SE5之前还没有泛型,但很多代码迫切需要泛型来解决问题。 例如:

Arrays.equals()方法的底层实现调用的是Object.equals()方法,和数组中元素的具体类型无关,这充分利用了Java中任何类型都继承自Object类的特性,避免了为每个类型都重新定义Arrays.equals()方法。而在没有泛型的时代,要让Object[]能接受所有数组类型,最简单的办法就是让数组接受协变,把String[],Integer[]都定义成Object[]的派生类,然后多态就起作用了。

为什么数组设计成”协变“不会有大问题呢?

这是基于数组的一个独有特性:

数组记得它内部元素的具体类型,并且会在运行时做类型检查。

因为arr[0]记得它内部的元素类型是Employee,所以运行时给它插入一个Student类型会报错。

这个特性使得Java数组协变带来的影响不会酿成大错——错误最终还是会被检测出来,只不过是从编译时推迟到了运行时。

正是有这个特性,Java当初才敢于把数组设计成协变的。虽然向上转型以后,编译期类型检查放松了,但因为数组运行时对内部元素类型的严格检查,不匹配的类型还是插不进去的。这也是为什么容器Collection不能设计成协变的原因——Collection不做运行时类型检查。

简单泛型类

简单泛型接口

如果一个int型量被传递到需要一个Integer对象的地方,那么,编译器将在幕后插入一个对Integer构造方法的调用以获得Integer对象。这就叫做自动装箱

反过来,如果一个Integer对象被放到需要int型量的地方,则编译器将在幕后插入一个对intValue方法的调用以获得int值,这就叫做自动拆箱

对于其他7对基本类型/包装类型,同样会发生类似的情形。

Java7增加了一种新特性,称作菱形运算符。使得下面的代码:

可以写成:

菱形运算符在不增加开发者负担的情况下简化了代码。

在Java中,数组是协变的,如果B IS-A C,那么B[] IS-A C[]
。但是集合Collection不是协变的,这就使得集合缺少灵活性。为了弥补这个不足,Java5引入了通配符。举例如下

既然有那么也有与之对应的,他们分别为泛型参数的上界和下界。

1.泛型类

2.泛型方法

泛型方法分为两种,区别在于是否带特定参数列表。

  • 一、 不带特定参数列表的泛型方法

因为它能接受不同类型的参数,所以,它是泛型方法。

  • 二、带特定参数列表的泛型方法

通过在返回类型前声明特定参数,能够获得以下好处:

  • 可以将T用作返回类型
  • 不止一个方法参数的声明需要用到T
  • 将T用于声明局部变量

泛型方法与泛型类很相似,因为参数列表使用相同的语法。在泛型方法中,泛型参数的声明在返回类型之前,而泛型类在类名之后。

一、什么是类型擦除

让我们看一个有趣的例子:

和很明显是不同的类型。但是上面的程序却认为它们是相同的类型。因此,存在一个残酷的现实:

在泛型代码内部,无法获得任何有关泛型参数类型的信息。

Java泛型是使用擦除来实现的,这意味着当你在使用泛型的时候,任何具体的类型信息都被擦除了,你唯一知道的就是你在使用一个对象。因此和在运行时事实上是相同的类型。这两种类型都被擦除成了他们的“原生”类型,即List。

再看另外一个例子:

类似的代码,在c++中能够正常运行。但是由于类型擦除,Java编译器无法将useF()能够在obj上调用f()这一需求映射到A拥有f()这一事实上。在类B中,由于T没有指定上下界,于是T只拥有默认的上界Object(等同于),因此在泛型方法中我们只能调用Object的方法。类似的,如果我们想要在泛型方法中使用泛型参数的方法,那么我们必须设定上下界。下面的代码就可以编译了:

边界声明T必须具有类型A或者从A导出的类型。

泛型参数将擦除到它的第一个边界。编译器实际上会把类型参数替换为它的擦除。就像上面的示例那样,T擦除到了A。.

二、怎么看待类型擦除

类型擦除不是一个语言特性,它只是一种折中。因为Java1中没有泛型,泛型是后来加入的。为了不影响现有的类库和已经在使用的代码,Java没有采用C++等语言那样彻底的泛型,而是使用类型擦除这种温和但有缺陷的方式类让Java拥有泛型。

擦除的代价是明显的。泛型不能用于显示地引用运行时类型的操作之中,例如转型、instanceof操作和new表达式。因为所有关于参数的类型信息都丢失了,无论何时,当你在编写泛型代码时,必须时刻提醒自己,雅思考试报名官网你只是看起来拥有有关参数的类型信息而已,你实际操作的只是一个Object。

  • 基本类型不能用作类型参数,类型参数必须是引用类型。必须使用包装类。
  • 不能用instanceof检测泛型,由于类型擦除的原因,任何泛型类型都会别擦除为它的原生类型。
  • 泛型类中,static方法和static域不能引用类的泛型变量。因为在类型擦除过后,类型参数就不存在了。同时,我们知道同一个类的所有实例共用类的static域和static方法,如果静态域接受泛型参数,那么这个参数到底是类型参数的哪一种实际类型就无法确定了。
  • 不能创建一个泛型类型的实例。是非法的。
  • 不能创建一个泛型数组,因为数组有严格的类型检查。通常用泛型容器,如来实现同样的需求。