文章目录
- 泛型-概述
- 基础使用
- 泛型类的使用
- 泛型类派生子类
- 泛型接口
- 泛型方法
- 类型通配符
- 类型通配符上限
- 类型通配符下限
- 常用泛型标识符
- 类型擦除
- 使用注意
- 泛型与数组
- 泛型和反射
- 其他
泛型-概述
Java 泛型(generics)是 JDK 5 中引入的一个新特性,泛型提供了编译时类型安全检测机制,该机制允许程序员在编译时检测到非法的类型。
泛型的本质是将类型进行参数化,也就是说所操作的数据类型被指定为一个参数。编译时根据泛型参数指定的类型完成检查和转换为Object。
为什么需要泛型?
- 编译期类型检查,避免类转换异常
- 减少类型抽象情况下的数据类型转换编码工作
JDK1.5之前没有泛型,以前使用Object或公共基础父类完成泛型功能,但是这种方式在静态代码层面是无法检查它的正确性的,发现不了转换错误,引入泛型是程序的转换错误能在编译阶段被发现。(但如果用反射的话是在运行时做转换的,无法完成检查)。
基础使用
泛型类的使用
-
定义泛型类
calss 类名称 <泛型标识, 泛型标识, ……> { private 泛型标识 变量名; }
-
使用泛型类
类名<具体的数据类型> 对象名 = new 类名<>();
-
使用注意:
- 在没有指定具体数据类型时,将按照Object类型来操作
- 不支持基本数据类型(因为泛型实质是将对象转换为Object类型,基本数据类型不继承自Object无法完成转换)
- 同一泛型类根据不同数据类型创建的对象,本质是同一个类型(Class对象是同一个)
泛型类派生子类
从泛型类派生子类时,需遵守如下规则:
-
子类也是泛型类时,可将子类的泛型标识指定给父类表示父类泛型使用和子类泛型一样的数据类型,如
class ChildGeneric<T> extends Generic<T>
-
子类不是泛型时,要在继承父类时明确指定父类泛型的数据类型
class ChildGeneric extends Generic<String>
-
如果不给父类指定泛型类型,此时父类泛型的数据类型为
Object
。(不推荐使用这样做),如class ChildGeneric<T> extends Generic
,class ChildGeneric extends Generic
泛型接口
- 定义方式和泛型类一样
- 使用方式和派生子类一样
泛型方法
定义泛型方法:
修饰符 <T, E, ……> 返回值类型 方法名(形参列表) {
方法体……
}
使用泛型方法:泛型方法的泛型类型是通过调用方法时指定的
使用注意:
-
方法的泛型标识可以和类的泛型标识一样但使用时完全没有关系,独立指定。
-
方法的泛型必须在形参中体现,否则在调用的地方无法指定泛型的类型
-
static的使用:泛型类的泛型给类方法使用时,不能给类方法加
static
修饰符;泛型方法的泛型给方法使用时则可以加static
修饰符。原因是泛型类的泛型必须在创建对象时指定,使用static方法时无法指定泛型的实际数据类型,而泛型方法的泛型类型是在使用方法时指定的。 -
泛型可以搭配可变参数使用(参数的数据类型必须相同,至少要有公共父类),如:
public static <E> void print(E... e) { for(int i = 0; i < e.length; i++) { System.out.println(e[i]); } }
类型通配符
在使用泛型类/接口/方法修饰变量时必须指定泛型的数据类型,但如果是方法的形成且还不能确定,可以使用 ?
代替具体的类型实参。如:
public void test(ArrayList<?> list) {
list.add(new Person()); // 这里会编译报错
}
类型通配符上限
定义类型通配符上限语法: 泛型类/泛型接口<? extend 实参类型>
作用:使可以通过泛型调用继承的实参类型的方法(因为通配符上限限定了传入的数据一定会是实参类型或其子类型本身)
上限的使用限定:
-
可以限定传递的类型必须是实参类型(语法中extend后面的数据类型)或其子类类型
-
使用通配符上限修饰的变量时是不允许做复制操作或者对泛型集合做添加操作的(因为无法确定传进来的到底是什么类型的数据,即使添加实参类型的数据也不行),如下面写法是错误的
public void test(ArrayList<? extends Person> list) { list.add(new Person()); // 这里会编译报错 }
类型通配符下限
定义类型通配符下限语法: 泛型类/泛型接口<? super 实参类型>
下限的使用限定:
- 具体使用时传入的类型必须是实参类型(语法中super后面的数据类型)或实参类型的父类类型
- 使用通配符下限修饰的变量可以填充元素,但是只能填充实参类型和其子类类型
- 使用通配符下限修饰的变量在方法中使用时,类型都是
Object
的
使用案例:JDK中使用sort方法时可以传入一个 Comparator
比较器,Comparator
就使用了通配符且使用的是通配符下限,
常用泛型标识符
- E - Element (在集合中使用,因为集合中存放的是元素)
- T - Type(Java 类)
- K - Key(键)
- V - Value(值)
- N - Number(数值类型)
- ? - 表示不确定的 java 类型
类型擦除
泛型是Java 1.5版本才引进的概念,在这之前是没有泛型的。但是泛型代码能够很好地和之前版本的代码兼容,那是因为泛型信息只存在于代码编译阶段,在进入JVM之前,与泛型相关的信息会被擦除掉,我们称之为类型擦除。
类型擦除后的代码:
-
无限定的泛型:擦除为Object类型
-
有上限的泛型:擦除为上限类型
桥接方法:当实现接口时必须实现接口中的方法,泛型接口的方法使用到泛型编译后泛型会转换为 Object
,而如下图中的 InfoImp
在实现 Info<Integer>
接口时重写方法时泛型变为了具体的 Integer
, InfoImp
编译后和 Info
对不上(因为 InfoImpl
没有重写 Object info(Object var)
,而是重写的 Integer info(Integer var)
,为了解决这个问题,引入桥接方法保持泛型接口和其子类的关系
使用注意
泛型与数组
泛型数组的使用限制规则:可以声明带泛型的数组引用,但是不能直接创建带泛型的数组对象。如下写法会编译报错:Pair<String>[] p=new Pair<String>[10];
为什么不能直接创建带泛型的数组对象?
数组必须在定义时就知道它的所有元素的数据类型且检查所有元素的数据类型都是一致的,而泛型具有擦除机制,每个元素的类型需要到运行时使用时才具体确定,两者的特性冲突。
如何曲线救国?
-
尽量不要使用泛型数组,可以使用泛型集合代替
-
对间接依赖的泛型可以不指定,如
List<String>[] list = new List[3]
-
对直接依赖泛型的数组(如
T[]
数组)可以通过java.lang.reflect.Array的newInstance(Class<T>, int)
创建
泛型和反射
-
反射中主要用到
Class<T>
和Constructor<T>
两个泛型类。如:Class<Person> personClass = Person.class; Construct<Person> construct = personClass.getConstructor(); Person person = construct.newInstance();
-
Java的泛型是“伪泛型”,只在编译期间做类型检查,当搭配反射使用进行运行时类型擦除可能导致实际的类型并不是定义的类型。如:
public static void main(String[] args) { List list = new ArrayList(); list.add("hello"); list.add("world"); List<Integer> intlist = list; for (int i = 0; i < intlist.size(); i++) { // 此时报错,转换异常 System.out.println(intlist.get(i)*100); } }
其他
实参和形参的区别
参考资料:
黑马程序员——JavaSE强化教程泛型,由点到面的讲解了整个泛型体系