泛型的基本原理

泛型是 JDK1.5 的一个新特性,其实就是一个『语法糖』,本质上就是编译器为了提供更好的可读性而提供的一种小「手段」,虚拟机层面是不存在所谓『泛型』的概念的。程序员

在我看来,『泛型』的存在具备如下两点意义,这也是它被设计出来的初衷。数组

一是,经过泛型的语法定义,编译器能够在编译期提供必定的类型安全检查,过滤掉大部分由于类型不符而致使的运行时异常,例如:安全

ArrayList<Integer> list = new ArrayList<>();
list.add("ddddd"); //编译失败

因为咱们的 ArrayList 是符合泛型语法定义的容器,因此你能够在实例化的时候指定一个类型,限定该容器只能容纳 Integer 类型的元素。而若是你强行添加其余类型的元素进入,那么编译器是不会经过的。框架

二是,泛型可让程序代码的可读性更高,而且因为自己只是一个语法糖,因此对于 JVM 运行时的性能是没有任何影响的。性能

固然,『泛型』也有它与身俱来的一些缺点,虽然看起来好像只是提供了一种类型安全检查的功能,可是实际上这种语法糖的实现却没有看起来的那样轻松,理解好泛型的基本原理将有助于你理解各种容器集合框架。spa

类型擦除

『类型擦除』的概念放在最开始进行介绍是为了方便你们初步创建起对于『泛型』的一个基本认识,从而对于后续介绍的使用方式上会更容易理解。翻译

泛型这种语法糖,编译器会在编译期间「擦除」泛型语法并相应的作出一些类型转换动做。例如:设计

public class Caculate<T> {

    private T num;
}

咱们定义了一个泛型类,具体定义泛型类的细节待会会进行详细介绍,这里关注咱们的类型擦除过程。定义了一个属性成员,该成员的类型是一个泛型类型,这个 T 具体是什么类型,咱们也不知道,它只是用于限定类型的。指针

固然,咱们也能够反编译一下这个 Caculate 类:code

public class Caculate{

    public Caculate(){}

    private Object num;
}

会获得这样一个结果,很明显的是,编译器擦除 Caculate 类后面的两个尖括号,而且将 num 的类型定义为 Object 类型。

固然,有人可能就会问了,「是否是全部的泛型类型都以 Object 进行擦除呢?」

答案是:大部分状况下,泛型类型都会以 Object 进行替换,而有一种状况则不是。

public class Caculate<T extends String> {

    private T num;
}

这种状况的泛型类型,num 会被替换为 String 而再也不是 Object。

这是一个类型限定的语法,它限定 T 是 String 或者 String 的子类,也就是你构建 Caculate 实例的时候只能限定 T 为 String 或者 String 的子类,因此不管你限定 T 为何类型,String 都是父类,不会出现类型不匹配的问题,因而可使用 String 进行类型擦除。

那么不少人也会有这样的疑问,你类型擦除以后,全部泛型相关方法的返回值都是 Object,那我当初泛型限定的具体类型还有用吗?例如这样一个方法:

ArrayList<Integer> list = new ArrayList();
list.add(10);
Integer num = list.get(0);
//这是 ArrayList 内部的一个方法
public E get(int index) {
    .....
}

就是说,你类型擦除以后,方法 get 的返回值 E 会被擦除为 Object 类型,那么为何咱们看到的确实返回的 Integer 类型呢?

 

image

 

这是上述三行代码的一个反编译结果,能够看到,实际上编译器会正常的将 ArrayList 编译并进行类型擦除,而后返回实例。可是除此以外的是,若是构建 ArrayList 实例时使用了泛型语法,那么编译器将标记该实例并关注该实例后续全部方法的调用,每次调用前都进行安全检查,非指定类型的方法都不能调用成功。

其实还有一点可能你们都不多关注,大多数人只是知道编译器会类型擦除一个泛型类并对建立出来的实例进行必定的安全检查。可是实际上编译器不只关注一个泛型方法的调用,它还会为某些返回值为限定的泛型类型的方法进行强制类型转换,因为类型擦除,返回值为泛型类型的方法都会擦除成 Object 类型,当这些方法被调用后,编译器会限定这个结果只能赋值给 Integer 或者 Object。

其实这一个过程,咱们管它叫作『泛型翻译』。不得不感叹一下,编译器为了蒙骗虚拟机对程序员提供泛型服务但是没少费心思啊。

泛型的基本使用

泛型类与接口

定义一个泛型类或接口是容易的,咱们看几个 JDK 中的泛型类。

  • public class ArrayList
  • public interface List
  • public interface Queue

基本格式是这样的:

访问修饰符 class/interface 类名或接口名<限定类型变量名>

其中「限定类型变量名」能够是任意一个变量名称,你叫它 T 也好,E 也好,只要符合 Java 变量命名规范就能够。在这里至关于声明了一个泛型限定类型,该类中的成员属性或者方法均可以直接拿来用。

泛型方法

这里你们须要明确一点的是,泛型方法并不必定依赖其外部的类或者接口,它能够独立存在,也能够依赖外围类存在。例如:

public E get(int index) {
    rangeCheck(index);
    return elementData(index);
}

ArrayList 的这个 get 方法就是一个泛型方法,它依赖外围 ArrayList 声明的 E 这个泛型类型,也就是它没有本身声明一个泛型类型而用的外围类的。

固然,另外一种方式就是本身申明一个泛型类型并使用:

public class Caculate {

    public <T> T add(T num){
        return num;
    }
}

这是泛型方法的另外一种形态,其中 <T> 用于声明一个名称为 T 的泛型类型,第二个 T 是方法的返回值。

因此外部调用该方法都须要指定一个限定类型才能调用,像这样:

Caculate caculate = new Caculate();
caculate.<Integer>add(12);
caculate.<String>add("fadf");

使用泛型的目的就是为了限定类型,原本不使用泛型语法,那么全部的参数都是 Object 类型的,如今泛型容许咱们限定具体类型,这一点要明确。

固然,你们可能没怎么见过这样的调用语法,不管是平常写代码,或是看 JDK 源码实现里,基本上都省略了类型限定部分,也就是上述代码等效于:

Caculate caculate = new Caculate();
caculate.add(12);
caculate.add("fadf");

为何呢?由于编译会推断你的参数类型,因此容许你省略,但前提是你这个方法是有参数的,若是你这个方法的逻辑是不须要传参的,那么你依然须要显式指定限定的具体类型。例如:

public class Caculate {

    public <T> T add(){
        T num = null;
        return num;
    }
}
Caculate caculate = new Caculate();
caculate.add();

这样的 add 方法调用,就意味着你没有限定 T 的类型,那么这个 T 实际上就是 Object 类型,并无被限定。

泛型的类型限定

这里的类型限定其实指的是这么个语法:

<T extends String>

它既能够应用于泛型类或者接口的定义上,也能够应用在泛型方法的定义上,它声明了一个泛型的类型 T,而且 T 类型必须是 String 或者 String 的子类,也就是外部使用时所传入的具体限定类型不能是非 String 体系的类型。

使用这种语法时,因为编译器会确保外部使用时传入的具体限定类型不会超过 String,因此在编译期间将再也不使用 Object 作类型擦除,可使用 String 进行类型擦除。

通配符

通配符是用于解决泛型之间引用传递问题的特殊语法。看下面一段代码:

public static void main(String[] args){
    Integer[] integerArr = new Integer[2];
    Number[] numberArr = new Number[2];
    numberArr = integerArr;

    ArrayList<Integer> integers = new ArrayList<>();
    ArrayList<Number> numbers = new ArrayList<>();
    numbers = integers;//编译不经过
}

Java 中,数组是协变的,即 Integer extends Number,那么子类数组实例是能够赋值给父类数组实例的。那是因为 Java 中的数组类型本质上会由虚拟机运行时动态生成一个类型,这个类型除了记录数组的必要属性,如长度,元素类型等,会有一个指针指向内存某个位置,这个位置就是该数组元素的起始位置。

因此子类数组实例赋值父类数组实例,只不过意味着父类数组实例的引用指向堆中子类数组而已,并不会有所冲突,所以是 Java 容许这种操做的。

而泛型是不容许这么作的,为何呢?

咱们假设泛型容许这种协变,看看会有什么问题。

ArrayList<Integer> integers = new ArrayList<>();
ArrayList<Number> numbers = new ArrayList<>();
numbers = integers;//假设的前提下,编译器是能经过的
numbers.add(23.5);

假设 Java 容许泛型协变,那么上述代码在编译器看来是没问题的,但运行时就会出现问题。这个 add 方法实际上就将一个浮点数放入了整型容器中了,虽然因为类型擦除并不会对程序运行形成问题,但显然违背了泛型的设计初衷,容易形成逻辑混乱,因此 Java 干脆禁止泛型协变。

因此虽然 ArrayList<Integer> 和 ArrayList<Number>编译器类型擦除以后都是 ArrayList 的实例,可是起码在编译器看来,这二者是两种不一样的类型。

那么,假若有某种需求,咱们的方法既要支持子类泛型做为形参传入,也要支持父类泛型做为形参传入,又该怎么办呢?

咱们使用通配符处理这样的需求,例如:

public void test2(ArrayList<? extends Number> list){
        
}

ArrayList<? extends Number> 表示泛型类型具体是什么不知道,可是具体类型必须是 Number 及其子类类型。例如:ArrayList<Number>,ArrayList<Integer>,ArrayList<Double> 等。

可是,通配符每每用于方法的形参中,而不容许用于定义和调用语法中。例以下面的语句是不被支持的:

ArrayList<?> list = new ArrayList<>();

固然了,除了 <? extends xxx> 这种通配符,还有另外两种:

  • :通配任意一种类型
  • :必须是某个类型的父类

通配符至关于一个集合,符合通配符描述的类型都被框进集合中,方法调用时传入的实参都必须是这个集合中的一员,不然将不能经过编译。

细节与局限

通配符的只读性

考虑这样一段代码:

ArrayList<Number> list = new ArrayList<>();
ArrayList<?> arrayList = list;
arrayList.add(32);
arrayList.add("fadsf");
arrayList.add(new Object());

上述的三条 add 语句都不能经过编译,这就是通配符的一个局限点,通配符匹配出来的泛型类型只能读取,不能写。

缘由也很简单,? 表明不肯定类型,即你不知道你这个容器里面放的是什么类型的数据,因此你只能读取里面的数据,不能瞎往里面添加元素。

泛型不容许建立数组

咱们刚开始介绍通配符的时候说过,数组具备协变性,即子类数组实例能够赋值给父类数组实例。咱们也说过,泛型类型不具备协变性,即使两个泛型类实例的具体类型是父子关系,他们之间也不能相互转换。

具体缘由是什么,咱们也详细介绍了,大体意思就是,父类容器能够听任意类型的元素,而子类容器只能放某种特殊类型的元素,若是父类表明了某一个子类容器,那么父类容器就有可能放入非当前子类实例所容许的元素进入容器,这会致使逻辑上的混乱,因此 Java 不容许这么作。

那么,若是容许泛型建立数组,因为数组的协变性,泛型数组必然也具备协变性,而泛型自己又不容许协变,天然冲突,因此泛型数组也是不容许建立的。

相关文章
相关标签/搜索