深入理解泛型

引入泛型的意义何在?

  • 泛型的提出是为了编写重用性更好的代码。
  • 泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数

  在未引入泛型之前,需要用Object来实现通用、不同类型的处理。

  缺点如下:

  • 每次使用时都需要强制转换成想要的类型。
  • 在编译时编译器并不知道类型转换是否正常,运行时才知道,不安全。

实际上引入泛型的主要目标有以下几点:

类型安全 :

  • 泛型的主要目标是提高 Java 程序的类型安全
  • 编译时期就可以检查出因 Java 类型不正确导致的 ClassCastException 异常
  • 符合越早出错代价越小原则

消除强制类型转换 :

  • 泛型的一个附带好处是,使用时直接得到目标类型,消除许多强制类型转换
  • 所得即所需,这使得代码更加可读,并且减少了出错机会潜在的性能收益 

潜在的性能收益:

  • 由于泛型的实现方式,支持泛型(几乎)不需要 JVM 或类文件更改
  • 所有工作都在编译器中完成
  • 编译器生成的代码跟不使用泛型(和强制类型转换)时所写的代码几乎一致,只是更能确保类型安全而已

泛型的使用

  泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。

这种参数类型可以用在类、接口和方法的创建中,分别称为泛型类、泛型接口、泛型方法

泛型类:泛型类最常见的用途就是作为容纳不同类型数据的容器类,比如 Java 集合容器类。

泛型接口:实现类在实现泛型接口时需要指明具体的参数类型,不然默认类型是 Object类型。

泛型方法:如果所在的类是泛型类,则直接使用类声明的参数,如果不是,则需自己声明参数类型。

 

泛型通配符

<?>:无限制通配符,表示可以持有任何类型。

<?>和<Object>不一样,<?>表示未知类型,<Object>表示任意类型。

 

<? extends E>:

在类型参数中使用 extends 表示这个泛型中的参数必须是 E 或者 E 的子类,这样有两个好处:

  • 如果传入的类型不是 E 或者 E 的子类,编辑不成功。
  • 泛型中可以使用 E 的方法,要不然还得强转成 E 才能使用。

 

<? super E>:

在类型参数中使用 super 表示这个泛型中的参数必须是 E 或者 E 的父类。

 

小结:

  • 无限制通配符<?> 和 Object 有些相似,用于表示无限制或者不确定范围的场景
  • 两种有限制通配形式 < ? super E> 和 < ? extends E> 也比较容易混淆,我们再来比较下。
  • 它们的目的都是为了使方法接口更为灵活,可以接受更为广泛的类型
  • < ? super E> 用于灵活写入或比较,使得对象可以写入父类型的容器,使得父类型的比较方法可以应用于子类对象。
  • < ? extends E> 用于灵活读取,使得方法可以读取 E 或 E 的任意子类型的容器对象。

使用通配符的基本原则:

  • 如果参数化类型表示一个 T 的生产者,使用 < ? extends T>;
  • 如果它表示一个 T 的消费者,就使用 < ? super T>;
  • 如果既是生产又是消费,那使用通配符就没什么意义了,因为你需要的是精确的参数类型。
  • T 的生产者的意思就是结果会返回 T,这就要求返回一个具体的类型,必须有上限才够具体;
  • T 的消费者的意思是要操作 T,这就要求操作的容器要够大,所以容器需要是 T 的父类,即 super T;

 

泛型的类型擦除

用泛型编写的 Java 程序和普通的 Java 程序基本相同,只是多了一些参数化的类型同时少了一些类型转换

实际上泛型程序也是首先被转化成一般的、不带泛型的 Java 程序后再进行处理的,编译器自动完成了从 Generic Java 到普通 Java 的翻译,Java 虚拟机运行时对泛型基本一无所知。

当编译器对带有泛型的java代码进行编译时,它会去执行类型检查和类型推断,然后生成普通的不带泛型的字节码,这种普通的字节码可以被一般的 Java 虚拟机接收并执行,这在就叫做 类型擦除(type erasure)

总之,泛型就是一个语法糖,它运行时没有存储任何类型信息。

 

泛型的情况称为不可变性,与之对应的概念是协变、逆变:

  • 协变:如果 A 是 B 的父类,并且 A 的容器(比如 List<A>) 也是 B 的容器(List<B>)的父类,则称之为协变的(父子关系保持一致)。
  • 逆变:如果 A 是 B 的父类,但是 A 的容器 是 B 的容器的子类,则称之为逆变(放入容器就篡位了)。
  • 不可变:不论 A B 有什么关系,A 的容器和 B 的容器都没有父子关系,称之为不可变。

Java 中数组是协变的,泛型是不可变的。

  如果想要让某个泛型类具有协变性,就需要用到边界

 

  • 我们知道,泛型运行时被擦除成原始类型,这使得很多操作无法进行。
  • 如果没有指明边界,类型参数将被擦除为 Object。
  • 如果我们想要让参数保留一个边界,可以给参数设置一个边界,泛型参数将会被擦除到它的第一个边界(边界可以有多个),这样即使运行时擦除后也会有范围。

 

泛型的规则

  • 泛型的参数类型只能是类(包括自定义类),不可以是简单类型。
  • 同一种泛型可以对应多个版本(因为参数类型是不确定的),不同版本的泛型类实例是不兼容的。
  • 泛型的类型参数可以有多个
  • 泛型的参数类型可以使用 extends 语句,习惯上称为“有界类型”。
  • 泛型的参数类型还可以是通配符类型,例如 Class。

泛型的使用场景

当类中要操作的引用数据类型不确定的时候,过去使用 Object 来完成扩展,JDK 1.5后推荐使用泛型来完成扩展,同时保证安全性

 

Java中List<Object>和原始类型List之间的区别?

原始类型和带参数类型之间的主要区别是:

  • 在编译时编译器不会对原始类型进行类型安全检查,却会对带参数的类型进行安全检查。
  • 通过使用Object作为类型,可以告知编译器该方法可以接受任何类型的对象,比如String或Integer。
  • 你可以把任何带参数的类型传递给原始类型 List,但却不能把List<String> 传递给接受List<Object>的方法,因为泛型的不可变性,会产生编译错误。

 

 

posted @ 2018-07-10 17:51  要死要活的程序猿啊  阅读(193)  评论(0编辑  收藏  举报