文章目录
一、为什么要使用泛型
在增加泛型类以前
- 使用集合时,并不知道要装的对象是哪种类型,因此Collection及Map的实现类直接使用Object类型的数据集合来存放元素
- 对集合中添加元素时,任何类型元素都可以放入同一集合
- 对集合中的数据进行获取时,要进行强制类型转换
@Test
public void generictyTest1(){
// 集合中不使用泛型的问题:
List list = new ArrayList<>();
list.add(98);
list.add(77);
list.add(36);
// 问题一:类型不安全:
list.add("helloworld");
for (Object o : list) {
// 问题二:类型转换时可能出现异常
int score = (Integer)o;
}
}
- 泛型给这种问题提供了一种解决方法:类型参数。
List<Integer> list = new ArrayList<>();
(等式右边省略的类型参数可从变量的类型参数推导出)- 编译器也可以很好的利用这个信息,当调用get方法时,不需要进行强制类型转换
- 编译器还知道ArrayList中add方法有一个类型为String的参数,这将比使用Object类型的参数安全。并且这可以进行检查,避免插入错误类型的对象
// 使用集合时加上泛型
@Test
public void generictyTest2(){
// 集合中不使用泛型的问题:
List<Integer> list = new ArrayList<>();
list.add(98);
list.add(77);
list.add(36);
// list.add("helloworld"); // Error:;类型检查报错:
for (Integer integer : list) {
System.out.println(integer); // 不用对元素进行强制类型转换
}
}
二、泛型类的使用
1. 集合中使用泛型
- 集合接口或类在jdk5.0是都修改为带泛型的结构
- 在实例化集合类时,可以指明具体的泛型类型
- 指明完以后,在集合类或接口中凡是定义类或接口时,内部接口(比如:方法、 构造器、属性等)使用到类的泛型的位置,都指定为实例化的泛型类型
- 泛型类必须是类类型,而不能是基本数据类型,使用到基本数据类型的地方,需要使用对应的包装类
- 如果实例化时,没有指明泛型类型,默认类型为java.lang.Object类型
2. 自定义泛型类
- 首先列出将要定义的类中,需要用泛型实现的变量(即变量类型不确定)。
- 使用大写字母
T,E,K,V等
用来表示这些变量的类型(字母并不规定,则定义即可) - 将这些表示类型的字母添加一对<>中,放在类名后面
- 将成员变量中类型不确定的变量类型使用这些字母替带即可
// 将参数类型T放在尖括号中添加到类名之后
public class SelfDefineStack<T>{
private int MAXSIZE = 10 ;
// 泛型缺陷之一:不能之间创建泛型数组
private T[] stack = (T[])new Object[MAXSIZE] ; // 栈中要存储的元素的数据类型并不确定,因此使用参数类型T来表示
int top = 0 ;
}
- 在定义的类的方法中,凡是需要用到该变量类型的地方,均使用类型参数替换即可
// 插入元素类型不确定,使用类型参数替换,以下方法同理
public void push(T elem){
if(!isEmpty())
this.stack[top++] = elem;
}
public T peek(){
return this.stack[top-1];
}
public void pop(){
this.top-- ;
}
public boolean isEmpty(){
return this.top >= this.MAXSIZE;
}
3. 自定义泛型方法
- 自定义泛型方法和使用了泛型的方法并不是同一个概念
- 泛型方法的类型参数与类的类型参数毫无关系,即泛型方法所属的类是不是泛型类都没有关系
- 泛型方法可以声明为静态的,原因:泛型参数是在调用方法时确定的,并在实例化类时确定
// 泛型方法时示例: Collections工具类并不是泛型类,但它有很多泛型方法
public class Collections {
/* 其他的方法*/
public static <T extends Comparable<? super T>> void sort(List<T> list)
{
list.sort(null);
}
/* 其他的方法*/
}
// 自定义泛型方法:
public class DefineGenericityMethod {
// 使用<E>告诉编译器E是一个泛型而不是一个类
// public List<E> copyFormArrayToList(E[] arr):如果不在方法返回类型之间加<E>,编译器会认为E是一个类的类型而不是一个类型参数
public <E> List<E> copyFormArrayToList(E[] arr){
List<E> list = new ArrayList<>();
for(E elem:arr){
list.add(elem);
}
return list;
}
@Test
public void userGenericityMethod(){
DefineGenericityMethod stringDefineGenericityMethod = new DefineGenericityMethod();
Integer[] arr = new Integer[]{5,3,6,4,2};
// 泛型方法在调用时,与类并无任何关系
List<Integer> list = stringDefineGenericityMethod.copyFormArrayToList(arr);
System.out.println(list);
}
}
三、泛型类的继承
1. 继承规则:
- 子类不包有父类的泛型,按需实现
没有类型:擦除为Object
具体类型
// 上述情况说明:
// 父类:
class SuperClass<T,E>{
T value1 ;
E value2 ;
}
// 子类不包含父类的泛型,且没有指定类型,因此子类中的两个变量均为Object类型
class SubClass1 extends SuperClass{
// Object value1
// Object value2
}
// 子类不包含父类的泛型,并指定了类型
class SubClass2 extends SuperClass<String ,Integer>{
// String value1
// Integer value2
}
- 子类保留父类的泛型:泛型子类
全部保留
部分保留
- 父类的泛型类型有两个:子类在继承父类时,要么已经指明了部分类型,要么都没指明类型
// 子类保留父类的泛型(全部保留),此时子类也是一个泛型类
class SubClass3<T,E> extends SuperClass<T,E>{
// T value1 ;
// E value2 ;
}
// 子类保留父类的泛型(部分保留),此时子类也是一个泛型类
class SubClass4<E> extends SuperClass<String,E>{
// String value1 ;
// E value2 ;
}
class SubClass5<E,Z> extends SuperClass<String,E>{
Z race ; // 子类可以拥有自己的泛型类
// String value1 ;
// E value2 ;
}
- 结论:子类除了指定或保留父类的泛型,还可以增加自己的泛型
- 测试:
定义一个泛型父类:
// 定义一个角色类:
public class Role<T> {
String name ;
int damage ;
int blood ;
T race ; // 该角色的种族未知,因此使用这里使用泛型
public T getRace() {
return race;
}
public void setRace(T race) {
this.race = race;
}
public Role(String name, int damage, int blood, T race) {
this.name = name;
this.damage = damage;
this.blood = blood;
this.race = race;
}
}
定义两个泛型子类
// 定义一个人类英雄继承角色类,继承时指定参数类型为Person类(子类没有保留父类的泛型,指定了一个Person的类型参数)
public class Hero extends Role<Person>{
public Hero(String name, int damage, int blood, Person race) {
super(name, damage, blood, race);
}
}
class Person{
String name ;
int age ;
}
// 定义一个恶魔角色角色,继承自角色类(对父类的泛型进行保留,并拥有自己的泛型)
public class Ghost<E,T> extends Role<T>{
E source ; // 恶魔的来源:西方和东方
public Ghost(String name, int damage, int blood, T race) {
super(name, damage, blood, race);
}
public Ghost(String name, int damage, int blood, T race, E source) {
super(name, damage, blood, race);
this.source = source;
}
}
2. 泛型在继承方面的体现;
- 类A是类B的父类,G<A>和G<B>不具备子父类关系,二者是并列关系
List<Object> 和 List<String>是并列关系,进行赋值时会报错
- 类A和类B的父类,但是A
和B 两者具备子父类关系 List<String> list = new ArrayList<String>();
public void inheritProblem(){
String str = null ;
Object obj = null ;
obj = str ; // Object和String具有子父类关系
Object[] objs = null ;
String[] strs = null ;
objs = strs ; // Object数组和String数组具有子父类关系
List<String> strList = null ;
List<Object> objList = null ;
// objList = strList; // List<Object>和List<String>不具有子父类关系,因此不可以赋值
}
四、类型变量的限定
1. 类型参数不加以修饰所产生的问题
- 如果需要自定义一个对数组进行自然排序的类,调用了compareTo方法,如果该数组的元素所属的类并没有实现Comparable接口,则会出现错误
// 找出数组中的最小元素
class ArrayAlg{
public static <T>T min(T[] a){
if(a == null||a.length == 0) return null ;
T smallest = a[0] ;
for(int i = 1 ;i<a.length;i++)
if(smallest.compareTo(a[i]) >0 ) // Error
smallest = a[i] ;
return smallest ;
}
}
// 变量smallest类型为T,这意味着它可以是任何一个类的对象,这无法确保它实现了compareTo方法
对变量smallest的类型参数进行修饰:
public static <T extends Comparable>T min(T[] a)...
// 实际上, comparable接口本身就是一个泛型类型,现在暂时其复杂性和编译器产生的警告
2. 类型参数修饰的规则:
- 一个类型变量或通配符可以有多个限定,限定类型用&分隔,而逗号用来分隔类型变量。在Java中,可以根据需要拥有多个接口超类型,但限定中至多有一个类,如果用一个类作为限定,它必须是限定列表中的第一个
// 要求参数类型T继承了Comparable ,Serializable ,Iterator接口
// 参数类型E继承了Map接口
class MyDefineClass <T extends Comparable & Serializable & Iterator, E extends Map> {
/* */
}
五、类型参数通配符
1. 通配符的使用 ?
- 通配符类型中,允许类型参数变化。
// 如:该通配符类型表示任何泛型Pair类型
Pair<? extends Employee> // 它的参数类型是Employee的子类,如Pair<Manager>,但不是Pair<String>
{ // 类的变量和方法
}
- 类A和类B的父类,G<A>和G<B>不具备父子关系,但G<?>是G<A>和G<B>的公共父类
- 对于List<?>而言,就不能像向其内部添加数据,除了null之外
2. 有限制条件的通配符
- 通配符指定上限:上限extends:使用时指定的类型必须是继承某个类,或者实现某个接口,即<=
- 通配符指定下限:使用时指定的类型不能小于操作的类,即 >=
举例:
<? extends Number> // (无穷小,Number] 只允许泛型为Number及Number子类的引用调用
<? super Number> // [Number,无穷大) 只允许泛型为Number及Number父类的引用调用
<? extends Comparable> //只允许泛型为实现Comparable接口的实现类引用调用
// 有限制条件的通配符的测试:
@Test
public void userAllPowerfulChar2(){
class Person{}
class Student extends Person{}
List<? extends Person> list1 = null ; // 可以引用Person及其子类
List<? super Person> list2 = null ; // 可以引用Person及其父类
List<Student> list3 = new ArrayList<>();
List<Person> list4 = new ArrayList<>();
List<Object> list5 = new ArrayList<>();
list1 = list3 ;
list1 = list4 ;
// list1 = list5; // 编译不通过
// list2 = list3; // 编译不通过
list2 = list4 ;
list2 = list5 ;
// 读取数据
list1 = list3;
Person person = list1.get(0);
// Student student = list1.get(0); // 编译不通过:因为 ?extends Person所表示的类型必须是Person及其子类,因此必须使用Person及其父类来引用,
// 至少必须是Perosn,因为如果get(0)得到的元素类型Person,则Student类型无法引用其父类
list2 = list4;
// Person person1 = list2.get(0); // 编译不通过,因为如果限制类型为Person及其父类,则最高层次的父类为Object,只能使用Object来进行引用
Object obj = list2.get(0);
// 写入数据
list1 = list4;
// list1.add(new Student()); // 编译不通过,指定类型若是Student的子类,则无法将子类赋给父类
list2 = list4;
list2.add(new Person()); // 可以,
list2.add(new Student());
}
六、使用泛型类需要注意的点
- 泛型类可能有多个参数,此时应将多个参数一起放在尖括号中,比如<E1,E2,E3>
- 泛型类在声明构造器时,不应该添加泛型:
构造器定义:public GenerictyClass(){}
错误定义: public GenerictyClass(){} - 实例化后,操作原来反省位置的结构必须与指定的泛型类型一直
- 泛型不同的引用不能相互赋值
尽管编译时ArrayList和ArrayList是两种类型,但是,在运行时只有一个ArrayList被加载到JVM中(类型擦除) - 泛型如果不指定,则泛型对应的类型都按照Object处理,但不等价于Object
- 如果泛型结构是一个接口或抽象类,则不可创建泛型类的对象
- 泛型的指定中不能使用基本数据类型,需要用包装类进行替换
- 在类/接口中声明的泛型,在本类或本接口中即代表某种类型,可以作为非静态属性的类型、非静态方法的参数类型、非静态方法的返回值类型,但在静态方法中不能使用类的泛型
因为泛型类型的指定是在构造对象时指定,但静态方法可以直接通过类名调用,因此静态方法中不能使用泛型 - 异常类不能是泛型的
- 不能使用new E{},但可以先创建一个Object类型的数组,并肩齐强制转换成想要的类型数组
- 子类不包有父类的泛型,按需实现
没有类型:擦除为Object
具体类型 - 子类保留父类的泛型:泛型子类
全部保留
部分保留
父类的泛型类型有两个:子类在继承父类时,要么已经指明了部分类型,要么都没指明类型
结论:子类除了指定或保留父类的泛型,还可以增加自己的泛型
七、类型擦除
1. 类型擦除
- 虚拟机没有泛型类型对象--------所有的对象都属于普通类
- 无论何时定义一个泛型类型,都自动提供一个相应的原始类型。原始类型的名字就是删去类型参数的泛型类型名。擦除类型变量并替换为限定类型(无限定的变量用Object)
- 在程序中可以包含不同类型的Pair,而擦除类型后就变成原始的Pair类
- 原始类型用第一个限定的类型变量来替换,如果没有给定限定就用Object替换,
// 定义一个泛型类:对类型参数不进行修饰:
public MyClass<T>{
T value ;
}
// 在虚拟机中进行类型擦除之后:
public MyClass{
Object value ;
}
// 定义一个泛型类:对类型参数进行修饰:
public MyClass<T extends SuperClass>{
T value ;
}
// 在虚拟机中进行类型擦除之后:
public MyClass{
SuperClass value ;
}
2. 翻译泛型表达式
- 当程序调用泛型方法时,如果擦除返回类型,编译器插入强制类型转换。
// 示例:
Pair<Employee> buddies = ... ;
Employee buddy = buddies.getFirst() ;
// 在擦除getFirst的返回类型后将返回Object类型,编译器自动插入Employee的强制类型转换。
- 当存取一个泛型变量也要插入强制类型转换。
- 编译器把这个方法调用翻译为两条虚拟机指令:
- 对原始方法Pair.getFirst的调用
- 对返回的Object类型强制转换为Employee类型
3. 翻译泛型方法
- 类型擦除也会出现在泛型方法中。
public static <T extends Comparable> T min(T [] a)
// 以上是一个完整的方法族,擦除类型之后,只剩一个方法:
public static Comparable min(Comparable[] a)
// 类型参数已经被擦除,只留下了限定类型Comparable
- 类型擦除带来的复杂问题:
- 子类方法的返回类型冲突
- 子类方法的参数类型冲突(桥方法解决)
- 桥方法
- 泛型的类型擦除的补充