Java中的泛型

218 阅读5分钟

泛型作用

泛型的本质是为了参数化类型(在不创建新的类型的情况下,通过泛型指定的不同类型来控制形参具体限制的类型)。也就是说在泛型使用过程中,操作的数据类型被指定为一个参数,这种参数类型可以用在类、接口和方法中,分别被称为泛型类、泛型接口、泛型方法。

引入泛型的意义在于:

适用于多种数据类型执行相同的代码(代码复用)

泛型使用

泛型类

多元泛型

泛型接口

泛型方法

泛型类要在实例化的时候就指明类型,如果想换一种类型,不得不重新new一次,可能不够灵活;而泛型方法可以在调用的时候指明类型,更加灵活。

泛型上下限

在使用泛型的时候,我们可以为传入的泛型类型实参进行上下边界的限制,如:类型实参只准传入某种类型的父类或某种类型的子类。

<?> 无限制通配符
<? extends E> extends 关键字声明了类型的上界,表示参数化的类型可能是所指定的类型,或者是此类型的子类
<? super E> super 关键字声明了类型的下界,表示参数化的类型可能是指定的类型,或者是此类型的父类
​
// 使用原则《Effictive Java》
// 为了获得最大限度的灵活性,要在表示 生产者或者消费者 的输入参数上使用通配符,使用的规则就是:生产者有上限、消费者有下限
1. 如果参数化类型表示一个 T 的生产者,使用 < ? extends T>;
2. 如果它表示一个 T 的消费者,就使用 < ? super T>;
3. 如果既是生产又是消费,那使用通配符就没什么意义了,因为你需要的是精确的参数类型。
  
    

泛型原理

Java泛型这个特性是从JDK 1.5才开始加入的,因此为了兼容之前的版本,Java泛型的实现采取了“伪泛型”的策略,即Java在语法上支持泛型,但是在编译阶段会进行所谓的“类型擦除”(Type Erasure),将所有的泛型表示(尖括号中的内容)都替换为具体的类型(其对应的原生态类型)

Java的类型擦除式泛型无论在使用效果上还是运行效率上,几乎是全面落后于C#的具现化式泛型,而它的唯一优势是在于实现这种泛型的影响范围上:擦除式泛型的实现几乎只需要在Javac编译器 上做出改进即可,不需要改动字节码、不需要改动Java虚拟机,也保证了以前没有使用泛型的库可以直接运行在Java 5.0之上。

擦除原则

泛型的类型擦除原则是:

  • 消除类型参数声明,即删除<>及其包围的部分。
  • 根据类型参数的上下界推断并替换所有的类型参数为原生态类型:如果类型参数是无限制通配符或没有上下界限定则替换为Object,如果存在上下界限定则根据子类替换原则取类型参数的最左边限定类型(即父类)。
  • 为了保证类型安全,必要时插入强制类型转换代码。
  • 自动产生“桥接方法”以保证擦除类型后的代码仍然具有泛型的“多态性”。

编译期检查

Java编译器是通过先检查代码中泛型的类型,然后在进行类型擦除,再进行编译。

类型检查就是针对引用的,谁是一个引用,用这个引用调用泛型方法,就会对这个引用调用的方法进行类型检测,而无关它真正引用的对象

泛型中参数话类型不考虑继承关系

违背了泛型的意义

桥接方法

类型擦除会造成多态的冲突,而JVM解决方法就是桥接方法。

编译器自己生成桥方法,桥方法的参数类型都是Object,所以,子类中覆盖父类两个方法的就是这两个JVM内部的桥方法。而自己定义@Oveerride只不过是假象。而桥方法的内部实现,就只是去调用我们自己重写的那两个方法。

所以,虚拟机巧妙的使用了桥方法,来解决了类型擦除和多态的冲突。

局限性

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

因为八种基本数据类型不能被Object保存

泛型类型不能实例化

因为在 Java 编译期没法确定泛型参数化类型,也就找不到对应的类字节码文件,所以自然就不行了,此外由于T 被擦除为 Object,如果可以 new T() 则就变成了 new Object(),失去了本意。

但可以在运行期间通过反射实例化泛型类型

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

不能创建参数化类型数组

可以进行声明,但不能初始化实例

需要特殊语法构建泛型数组

使用反射来初始化泛型数组算是优雅实现,因为泛型类型 T在运行时才能被确定下来,或使用minmax

泛型类的静态上下文中类型变量无效

泛型的具体类型无法在编译器确定,因此禁止使用带有类型变量的静态字段和方法

异常中的泛型

不能抛出也不能捕获泛型类的对象。事实上,泛型类扩展Throwable都不合法

不能再catch子句中使用泛型变量,否则可能违背异常捕获的类型顺序