泛型(Generics)总结
使用容易,理解难;掌握容易,精通难。花了大半天的时间从零整理泛型(上课讲的和没讲差不多QAQ),主要从泛型的概念、泛型的作用、泛型的使用(限定)、类型通配符及其三种限定、泛型jvm工作流程、泛型擦除、桥方法等方面进行学习。泛型和java中的容器类分不开,我觉得想了解泛型的应用重写容器类的代码是最好的方法。
泛型概念
什么是泛型?
- 泛型,即“参数化类型”,本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。
- 这种参数类型可以用在类、接口和方法的创建中,分别称为泛型类、泛型接口、泛型方法。
- 泛型是程序设计语言的一种特性。允许程序员在强类型程序设计语言中编写 体验泛型。代码时定义一些可变部份,那些部份在使用前必须作出指明。各种程序设计语言和其编译器、运行环境对泛型的支持均不一样。将类型参数化以达到代码复用提高软件开发工作效率的一种数据类型。
- 泛型类是引用类型,是堆对象,主要是引入了类型参数这个概念。
- 根据我个人的理解翻译一下:泛型就是允许类、方法、接口对类型进行抽象,在允许向目标中传递多种数据类型的同时限定数据类型,确保数据类型的唯一性。这在集合类型中非常常见。
- 在JVM中是没有泛型这个概念的,泛型在java中只存在于API层面,也就是编译器层次上,出现的错误也都是编译错误,编译时会进行类型擦除,编译形成的字节码文件中没有泛型,所以Java中的泛型被称为“伪泛型”。
泛型编程
泛型编程是一种通过参数化的方式将数据处理与数据类型解耦的技术,通过对数据类型施加约束(比如Java中的有界类型)来保证数据处理的正确性,又称参数类型或参数多态性。泛型最著名的应用就是容器,比如C++的STL、Java的Collection Framework。
泛型的作用
- JAVA泛型的初衷,是减少强制类型转换以及确保类型安全,建立具有类型安全的集合框架。
- > 在Java SE 1.5之前,没有泛型的情况的下,通过对类型Object的引用来实现参数的“任意化”,“任意化”带来的缺点是要做显式的强制类型转换,而这种转换是要求开发者对实际参数类型可以预知的情况下进行的。对于强制类型转换错误的情况,编译器可能不提示错误,在运行的时候才出现异常,这是一个安全隐患。
- 泛型的好处是安全简单。
- 泛型的好处是在编译的时候检查类型安全,并且所有的强制转换都是自动和隐式的,提高代码的重用率。
- 举个例子:
public class Practice_bb {
public static void main(String[] args) {
new Example<Character>('a').print();//改变尖括号里面的类型时,
}
}
class Example <T> {
private T t;
public Example(T t) {
this.t = t;
}
public void print() {
System.out.println(t.getClass() + "\t" + this.t.getClass());
System.out.println(this.t);
}
}
泛型使用
泛型声明
- 声明方式
Set<Student> set = new HashSet();//泛型只需声明,不需要再后面加泛型
Set<Student> set = new HashSet<>();//当然这样不会错
Set<Student> set = new HashSet<Student>();//当然这样也不会错
new HashSet()只是在内存中开辟一个存储空间,可以存储任何的类型对象。而真正涉及类型检查的是它的引用,因为我们是使用它引用set 来调用它的方法,比如说调用方法。所以set引用能完成泛型类型的检查。
2. 声明泛型类对象时泛型变量不能是基本数据类型
道理很简单,基本数据类型没有父类,在进行类型擦除的时候没法擦除。就比如,没有ArrayList,只有ArrayList。因为当类型擦除后,ArrayList的原始类型变为Object,但是Object类型不能存储double值,只能引用Double的值。
泛型分类
根据泛型应用的对象不同,泛型分为三类:
泛型类 | 泛型接口 | 泛型方法 |
泛型的限定
泛型类和普通类的最明显的区别在于类名之后有一个 “<>”,这个尖括号里面就是对变量类型的限定。换句话说,尖括号中的变量就是一个抽象的数据类型,在声明对象的时候才能确定它的类型。
限定泛型的符号
泛型类的限定总共有四种符号(占位符):
T | type |
K | key |
V | value |
E | enum |
* T,K,V,E并不是固定写法,jdk中之所以这么写是为了增加代码的可读性,举个例子:
把class Test <T>{...}
改成class Test <E> {...}
不会出现任何问题。
extends
直接举个例子:
class Example<T extends Number> {//允许传递数据类型为Number子类或Number,如Number,Integer,Double等
...
}
泛型中可以限定类型变量必须实现某几个接口或者继承某个类,多个限定类型用&分隔,类必须放在限定列表中所有接口的前面。
举个例子:
class D<T extends A & Serializable & Cloneable> {
...
}
注意
- 在自定义泛型类的时候,类内该泛型所拥有的方法和限定程度相关,泛型的具体类型是限定类型的公共父类。
举个例子:
class Example<T extends Number> {
...
}
上边代码中的Example类内的T可以实现的方法是Number和Number的父类所拥有的所有方法。
如果不加extends限定,那T能实现的方法就只有所有类的父类也就是Object类的方法。
- 泛型类型在逻辑上看以看成是多个不同的类型,实际上都是相同的基本类型。
举个例子:
public class Practice_bb {
public static void main(String[] args) {
Example<Integer> ex_i = new Example();
Example<Double> ex_d = new Example();
System.out.println(ex_i.getClass() + "\n" + ex_d.getClass() );
System.out.println(ex_i.getClass() == ex_d.getClass());
}
}
上述代码运行的结果是:
- 如果在方法前指定了,那么就是说,方法的这个泛型类型变量和类定义时的泛型类型无关,这个特性让泛型方法可以定义在普通类中而不是泛型类中。
泛型中没有泛型数组这一说
作为一个数组,必须牢记它的元素类型,也就是所有的元素对象都必须一个样(协变),而泛型类型恰恰做不到这一点。
举个例子: class Cls <? extends Number> {...}
中数据类型可以是Integer,Double等等多种类型,同一个容器内(比如List)可以存放他们所有,因为容器是以对象为单位的。
但是数组就不行,因为数组本身就是一个数据类型,它的所有元素都是协变的,如果其中一个元素是Integer类型,那么其他元素也必须是Integer类型,这时候想存放Double类型,不出错才怪。
通配符 “?”
为什么需要通配符
举个例子:
public class Practice_bb {
public static void main(String[] args) {
List<Double> lista = new ArrayList<>();
lista.add(1.1);
lista.add(2.2);
List<Integer> listb = new ArrayList<>();
listb.add(1);
listb.add(2);
traverseList(lista);//报错
traverseList(listb);//报错
}
private static void traverseList(List<Number> list) {
for (Number temp : list) {
System.out.println(temp);
}
}
}
上边的代码之所以报错是因为,方法traverseList()的形参List list中,虽然Number是Integer和Double包装类的共同父类,但是List却不是两个包装类的父类。
为了保证能够成功传参,保证代码的安全性和多态性,引入通配符“?”。更改代码为:
import java.util.*;
public class Practice_bb {
public static void main(String[] args) {
List<Double> lista = new ArrayList<>();
lista.add(1.1);
lista.add(2.2);
List<Integer> listb = new ArrayList<>();
listb.add(1);
listb.add(2);
traverseList(lista);
traverseList(listb);
}
private static void traverseList(List<? extends Number> list) {
for (Number temp : list) {
System.out.println(temp);
}
}
}
就能得出正确的运行结果:
通配符限定
和通配符同样可以对类型进行限定。可以分为子类型限定、超类型限定和无限定。
通配符不是类型变量,因此不能在代码中使用“?”作为一种类型。
通配符“?”相当于“T extends Object”。
通配符本身不是一种数据类型,因此限定方式和TKVE有很大不同。通配符总共有三种限定方式:
关键字 | 限定名称 | 作用 |
extends | 子类型限定,类型的上界 | 主要用来安全地访问数据,可以访问X及其子类型 可用于的返回类型限定,不能用于参数类型限定 |
super | 超类型限定,类型的下界 | 主要用来安全地写入数据,可以写入X及其子类型 可用于参数类型限定,不能用于返回类型限定 |
无限定 | 用于一些简单的操作比如不需要实际类型的方法,就显得比泛型方法简洁 |
* 之所以extends和无限定不能安全的写入,是因为限定之后类型不确定,举个例子: List<? extends Number> list = new ArrayList<>();
中,泛型是Number和Number的子类,可能是Number,也可能是Integer、Double类型,JVM不知道这个泛型究竟是类型。 List<? super Number> list = new ArrayList<>();
可以安全写入,泛型是Number和Number的超类,JVM会以最小的子类,也就是Number类为泛型。
* 唯一
* 关于通配符限定的详解请看:Java泛型通配符extends与super
类型通配符使用
带有super超类型限定的通配符可以向泛型对象写入,带有extends子类型限定的通配符可以向泛型对象读取。
先用集合类举几个栗子:
import java.util.*;
public class Practice_bb {
public static void main(String[] args) {
List<? super Number> list = new ArrayList<>();//用super来限定可以安全的写入,所以下边的add方法可以
list.add(15);
list.add(30);
list.add(55.2);
}
}
import java.util.*;
public class Practice_bb {
public static void main(String[] args) {
List<? extends Number> list = new ArrayList<>();
list.add(15);//报错
list.add(30);//报错
list.add(55.2);//报错
}
}
- null是在不符合条件的情况下唯一能写入或是读取的元素
泛型工作原理
工作流程
- 编译,进行类型检查
- 涉及类型检查的是创建的对象的引用,因为泛型类的方法基本都是该对象调用的。
- 进行类型擦除
- 存放在attributes域中
- 在类型参数出现的位置进行自动强制转换相关指令
与c++的对照
C++的模板会在编译时根据参数类型的不同生成不同的代码,泛型是随字节码文件一起存在的。
而Java的泛型是一种违反型,编译为字节码时参数类型会在代码中被擦除,单独记录在Class文件的attributes域内,而在使用泛型处做类型检查与类型转换。
类型擦除
- 正确理解类型擦除是正确理解泛型的前提,java中的泛型和其他语言最大的区别就在于它的泛型是假泛型。
- 所谓“假泛型”,上边也已经提到过,就是:Java中的泛型基本上都是在编译器这个层次来实现的。在生成的Java字节码中是不包含泛型中的类型信息的。
- 使用泛型的时候加上的类型参数,会在编译器在编译的时候去掉,把他们转化成他们的限定边界(普通的类和方法)。而这个过程就称为类型擦除。
比如我们使用<T>
来定义一个类,那么在类初始化的过程中把T转化为Object类型;
如果这个T有限定,比如class Cls<T extends Number>
,那么Number就是他的原始类型。 - 自动类型转换:
类型擦除会导致所有泛型类变量被替换为原始类型,之所以在获取的时候能够获得非原始类型,是因为容器在取得数据的时候进行了强制类型转换。
这个强制转换是在调用方法的地方进行转换的,比如调用List的get(int index)方法,其源代码如下。
public E get(int index) {
RangeCheck(index);
return (E) elementData[index]; //强转
}
桥方法
这里利用中的方法来引入桥方法,因为写的真的很棒:
比如现在有一对父子类。
class Pair<T> { //父类
private T value;
public T getValue() {
return value;
}
public void setValue(T value) {
this.value = value;
}
}
class DateInter extends Pair<Date> { //子类
@Override
public void setValue(Date value) {
super.setValue(value);
}
@Override
public Date getValue() {
return super.getValue();
}
}
这里子类想通过将T赋值为Date,限定父类的泛型,然后重写父类的setValue()方法,能成功吗?
仔细分析会发现,java编译过程中T变为了Object类,而此时的子类中T是Date方法,在擦除之后字节码文件中也变成了Object类型(因为它变成Date类型是在调用的时候强制转换的),确实重写了!但是能实现想要的功能吗?不能!
想要实现这个功能,就要借助java提供的桥方法。
反编译class文件,发现如下代码:
class com.tao.test.DateInter extends com.tao.test.Pair<java.util.Date> {
com.tao.test.DateInter();
Code:
0: aload_0
1: invokespecial #8 // Method com/tao/test/Pair."<init>"
:()V
4: return
public void setValue(java.util.Date); //我们重写的setValue方法
Code:
0: aload_0
1: aload_1
2: invokespecial #16 // Method com/tao/test/Pair.setValue
:(Ljava/lang/Object;)V
5: return
public java.util.Date getValue(); //我们重写的getValue方法
Code:
0: aload_0
1: invokespecial #23 // Method com/tao/test/Pair.getValue
:()Ljava/lang/Object;
4: checkcast #26 // class java/util/Date
7: areturn
public java.lang.Object getValue(); //编译时由编译器生成的桥方法
Code:
0: aload_0
1: invokevirtual #28 // Method getValue:()Ljava/util/Date 去调用我们重写的getValue方法
;
4: areturn
public void setValue(java.lang.Object); //编译时由编译器生成的桥方法
Code:
0: aload_0
1: aload_1
2: checkcast #26 // class java/util/Date
5: invokevirtual #30 // Method setValue:(Ljava/util/Date; 去调用我们重写的setValue方法
)V
8: return
}
桥方法内容正如其名,其内部实现,就是用JVM自动生成的桥方法,去调用我们重写的方法。顺序就是,子类字节码文件中利用桥方法(Object)覆盖父类的方法(Object),再利用桥方法(Object)调用我们想要重写的方法(Date)。
最后引用原博主的一段话,来理解桥方法中编译器和虚拟机的互相配合:
子类中的巧方法 Object getValue()
和Date getValue()
是同 时存在的,可是如果是常规的两个方法,他们的方法签名是一样的,也就是说虚拟机根本不能分别这两个方法。如果是我们自己编写Java代码,这样的代码是无法通过编译器的检查的,但是虚拟机却是允许这样做的,因为虚拟机通过参数类型和返回类型来确定一个方法,所以编译器为了实现泛型的多态允许自己做这个看起来“不合法”的事情,然后交给虚拟器去区别。