美文网首页工作生活
Java:泛型的理解

Java:泛型的理解

作者: EricTao2 | 来源:发表于2019-07-04 15:01 被阅读0次
image

本文源自参考《Think in Java》,多篇博文以及阅读源码的总结

前言

Java中的泛型每各人都在使用,但是它底层的实现方法是什么呢,为何要这样实现,这样实现的优缺点有哪些,怎么解决泛型带来的问题。带着好奇,我查阅资料进行了初步的学习,在此与诸位探讨。

一 类型参数

学过JAVA的人都知道泛型,明白大概怎么使用。在类上为:class 类名<T> {},在方法上为:public <T> void 方法名 (T x){}。泛型的实现使得类型变成了参数可以传入,使得类功能多样化。

具体可分为5种情况:

  1. T是成员变量的类型
  2. T是泛型变量(无论成员变量还是局部变量)的类型参数,常见如Class<T>,List<T>
  3. T是方法抛出的Exception(要求<T extends Exception>
  4. T是方法的返回值
  5. T是方法的参数

1.1 泛型的实现

JAVA的泛型是基于编译器实现的,使用了擦除的方法实现,这是因为java1.5之后才出现了泛型,为了保持向后兼容而做出的妥协。

所谓擦除就是JAVA文件在编译成字节码时类型参数会被擦除掉,单独记录在其他地方。并且用类型参数的父类代替原有的位置。
假设参数类型的占位符为T,擦除规则如下:

  1. <T>擦除后变为Obecjt
  2. <? extends A>擦除后变为A
  3. <? super A>擦除后变为Object

这种规则叫做保留上界

编译器擦除类型参数后,通过JAVA的强制转换保证了类型参数在使用时的正确。如:在类型参数T中传入了类A,那么编译器会在所有类A将返回(抛出)类型参数T的代码处加上(A)进行强转.

举个栗子:

        ArrayList<String> list = new ArrayList<String>();
        list.add("123");
        String b = list.get(0);

在编译后会变成

        ArrayList list = new ArrayList();//没有参数即默认为Object
        list.add("123");
        String b = (String) list.get(0);

并且会在带有类型参数类的子类中形成桥方法保证了多态性。
具体参考官方解释如下

  • Replace all type parameters in generic types with their bounds or Object if the type parameters are unbounded. The produced bytecode, therefore, contains only ordinary classes, interfaces, and methods.
  • Insert type casts if necessary to preserve type safety.
  • Generate bridge methods to preserve polymorphism in extended generic types.

二 通配符?

在带有类型参数的类内部,代码仍然按照参数类型擦除后的父类来处理。但是擦除存在一个问题,在这种机制下泛型是不变的,而没有逆变和协变。

2.1 逆变与协变


协变和逆变网上有很多解释,显得模糊不清,我参考几个编程语言的官方解释后给出一个比较宽泛的定义。协变指能够使用比原始声明类型的派生程度更大(更具体的)的类型,逆变指能够使用比原始声明类型的派生程度更小(不太具体的)的类型。
如:
Object obj = new String("123");
这就是协变,将String这个更具体的(子类)类型赋给了原本较宽泛定义(父类)的类型Object。
JAVA不允许将父类赋给子类,自然Java不支持逆变。

网上很多博文说JAVA泛型也有逆变,我是不赞同的,那只是一种模拟的逆变,即有部分逆变的特性而且看起来像逆变,具体分析后文会给出


2.2 Java中的逆变与协变

在JAVA中,

List<Integer> b = new ArrayList<Integer>()
List<NumFber> a = b;

是无法通过编译器检查的。不允许这样做有一个很充分的理由:这样做将破坏要泛型的类型安全。如果能够将List<Integer> 赋给List<Number>。那么下面的代码就允许将非Integer的内容放入 List<Integer>

List<Integer> b = new ArrayList<Integer>(); 
List<Number> a = b; // illegal 
a.add(new Float(3.1415));

因为aList<Number>,所以向其添加Float似乎是完全可行的。但是如果a实际是List<Integer>,那么这就破坏了蕴含在b中定义的类型声明 —— 它是一个整数列表,这就是泛型类型不能协变的原因。但也因此使得泛型失去了多态的拓展性。

2.3 通配符解决协变

Java官方通过加入了通配符?来解决泛型协变的问题。这样就能通过编译了:

List<Integer> b = new ArrayList<Integer>(); 
List<? extends Number> a = b;

可以解读为a是一种带有NumberList集合类,在从a中取出数据的时候统一当做Number处理就行了。同时这也是符合里氏替换原则的

但是编译器会禁止你将将类Integer放入a,即a.add(new Integer(1))//illegal
这也很合理,因为你声明的a本来就没有限定a包含的具体是哪个Number子类,因此不准任何变量的添加保证了泛型的安全性。
解决往a添加对象的方法也很简单

List<Object> b = new ArrayList<Object>(); 
List<? super Number> a = b;

a是某种Number父类的List集合类,将ArrayList<Object>赋给a也是合情合理的,Object确实是Number的父类。这也符合里氏替换原则的

(网上大部分博文说这就是逆变,但是仔细想想逆变的官方定义,在JAVA中可以理解为:类T是类S的子类,而类A<T>是类A<S>的父类。仔细看看List<? super Number>List<Object>的关系,在这里TNumber,而SObject,但是List<? super Number>从逻辑上来看真的是List<Object>的子类吗,如果单纯从字面上来看List<? super Number>是带有Number父类的集合类,根据保留上界的擦除方法,应该擦除为List<Object>,将一个List<Object>赋给另一个List<Object>是不存在任何逆变的。我在疑惑之下进行了科学上网查阅资料,也没有英文资料说明JAVA泛型里这属于逆变)

相关文章

  • 泛型琐碎之泛型上下限

    泛型的命名规范 为了更好地去理解泛型,我们也需要去理解java泛型的命名规范。 为了与java关键字区别开来,ja...

  • Java 泛型

    java 泛型 很多朋友对java的泛型不是很理解,很多文章写的已不是很清楚,这篇博客对java泛型进行 一个总结...

  • java泛型理解及应用

    内容: java泛型理解及应用

  • Java泛型教程

    Java泛型教程导航 Java 泛型概述 Java泛型环境设置 Java泛型通用类 Java泛型类型参数命名约定 ...

  • 泛型

    java 泛型详解-绝对是对泛型方法讲解最详细的,没有之一Java泛型深入理解加泛型面试数组的协变性与范型的不可变性

  • 反射获取泛型类型

    参考:步步理解 JAVA 泛型编程(一)

  • Java 泛型擦除原理

    问:请比较深入的谈谈你对 Java 泛型擦除的理解和带来的问题认识? 答:Java 的泛型是伪泛型,因为在编译期间...

  • java 泛型

    很多朋友对Java的泛型不是很理解,很多文章写的已不是很清楚,这篇博客对java泛型进行 一个总结。 泛型的转换:...

  • JDK1.5后增加了泛型,那么为什么要有泛型呢?我们该如何自定义

    本篇主要讲解java泛型的理解、集合中简单使用泛型、自定义泛型结构(包括类、接口、方法)。 一、什么是泛型? 通俗...

  • [Java菜鸟系列] 「林彪」教你"通配符泛型

    J007- [Java菜鸟系列] 「林彪」教你"通配符泛型" 菜鸟:Java方法参数中的泛型通配符要怎么理解?老湿...

网友评论

    本文标题:Java:泛型的理解

    本文链接:https://www.haomeiwen.com/subject/mpqrhctx.html