一.什么是泛型(What)
泛型就是参数化类型,即我们在定义的时候,将具体的类型进行参数化,在调用或者使用的时候,再传入具体的参数类型,我们可以将泛型用在类、接口和方法中,分别被称为泛型类、泛型接口、泛型方法。
二.为什么使用泛型(Why)
泛型的好处:
- 适用于多种数据类型执行相同的代码
- 类型安全:我们使用泛型的时候,指定了特定的参数类型,这样对其类型进行限定,可以在编译期间对我们传入的参数类型进行判断,增加了类型的安全性
- 取消强制类型转换:我们指定明确的类型参数后,由于在编译阶段就会对类型进行约束,泛型会自动且隐式的给我们做类型转换,转换成我们指定的类型,我们不再需要关心类型的转换
三.泛型的使用(How)
泛型可以定义在类、接口、方法上,分别被称为泛型类、泛型接口、泛型方法。
3.1、泛型类
通过 <> 将类型变量T(大写字母都可以,不过常用的就是T,E,K,V等等)括起来,放在类名后面,泛型类可以有多个类型变量。
一个类型变量的泛型类:
/**
*desc:一个类型变量的泛型类
**/
public class NormalGeneric<T>{
private T data;
public NormalGeneric(){
}
public T getData(){
return data;
}
public void setData(T data) {
this.data=data;
}
}
多个类型变量的泛型类:
/**
* 多个类型变量的泛型类
**/
public class NormalGeneric<T,K>{
private T data;
private K result;
public NormalGeneric(T t,K k){
this.data = t;
this.result = k;
}
public T getData(){
return data;
}
public void setData(T data){
this.data = data;
}
public T getResult(){
return result;
}
public void setReuslt(K result){
this.result = result;
}
}
3.2、泛型接口
泛型接口与泛型类的定义基本相同,在泛型接口名称后面加上<>并指定泛型类型
/**
* 泛型接口
**/
public interface Genertor<T>{
T getData();
void setData(T data);
}
我们在实现泛型接口可以使用下面两种方式:
1.未传入泛型实参
在 new 出类的实例时,需要指定具体类型:
public ImplGenerator<T> implements Genertor<T>{
@Override
T getData(){
return null;
}
@Override
void setData(T data){
}
}
//使用的时候需要指定具体类型
ImplGenerator<String> i = new ImplGenerator<String>("Impl");
2.传入泛型实参
在 new 出类的实例时,和普通的类没区别。
public ImplGenerator implements Genertor<String>{
@Override
String getData(){
return "";
}
@Override
void setData(String data){
}
}
//使用的时候传入对应类型即可
ImplGenerator i = new ImplGenerator("Impl");
3.3、泛型方法
泛型方法,是在调用方法的时候指明泛型的具体类型 ,泛型方法可以在任何地方和任何场景中使用,包括普通类和泛型类。注意泛型类中定义的普通方法和泛型方法的区别。
泛型类中定义的普通方法和泛型方法的区别
泛型类中的普通方法:
// 虽然在方法中使用了泛型,但是这并不是一个泛型方法。
// 这只是类中一个普通的成员方法,只不过他的返回值是在声明泛型类已经声明过的泛型。
// 所以在这个方法中才可以继续使用 T 这个泛型。
public T getKey(){
return key;
}
泛型方法:
/**
* 这才是一个真正的泛型方法。
* 首先在 public 与返回值之间的 <T> 必不可少,这表明这是一个泛型方法,并且声明了一个泛型 T
* 这个 T 可以出现在这个泛型方法的任意位置,泛型的数量也可以为任意多个。
*/
public <T,K> K showKeyName(Generic<T> container){
// ...
}
3.4 字母规范
在定义泛型时,指定泛型的变量可以是任意一个大写字母,意义完全相同;但为了提高可读性,通常使用有意义的字母:
- E- Element,常用在java集合中,如:List,Iterator,Set
- K,V-Key,Value,代表Map的键值对
- N-Number,数字
- T-Type,类型,如String,Integer等
四.泛型类型变量的限制(限定类型变量)
public class ComparableNum<T extends Comparable>{
}
public class ComparableList<T extends ArrayList&Comparable>{
}
- 「T表示应该绑定类型的子类型,Comparable表示绑定类型,子类型和绑定类型可以使类也可以是接口」。
- 「extends 左右都允许多个,如T,V extends Comparable&Serializable」。
- 「限定类型变量既可以用在泛型方法上也可以用在泛型类上」。
五.泛型通配符
- ? extends X:表示类型的上界,类型参数是X的子类。
- ? super X: 表示类型的下界,类型参数是X的超类。
?extends X
如果其中提供了 get 和 set类型参数变量的方法的话,set方法是不允许被调用的,会出现编译错误,而 get 方法则没问题。
?extends X 表示类型的上界,类型参数是 X 的子类,那么可以肯定的说,get方法返回的一定是个 X(不管是 X 或者 X 的子类)编译器是可以确定知道的。但是set方法只知道传入的是个X,至于具体是 X 的哪个子类,是不知道的。
因此,? extends X主要用于安全地访问数据,可以访问X及其子类型,并且不能写入非null的数据。
? super X
如果其中提供了 get 和 set类型参数变量的方法的话,set方法可以被调用,且能传入的参数只能是 X 或者 X 的子类。而 get 方法只会返回一个 Object 类型的值。
? super X 表示类型的下界,类型参数是 X 的超类(包括 X 本身),那么可以肯定的说,get 方法返回的一定是个 X 的超类,那么到底是哪个超类?不知道,但是可以肯定的说,Object一定是它的超类,所以 get 方法返回Object。编译器是可以确定知道的。对于set方法来说,编译器不知道它需要的确切类型,但是 X 和X 的子类可以安全的转型为 X。
因此,?super X主要用于安全地写入数据,可以写入X及其子类型。
无限定的通配符?
表示对类型没有任何限制,可以把?看成所有类型的父类,如ArrayList<?>。
泛型中的约束和局限性
- 1、不能用基本类型实例化类型参数。
- 2、运行时类型查询只适用于原始类型。
- 3、泛型类的静态上下文中类型变量失效:不能在静态域或方法中引用类型变量。因为泛型是要在对象创建的时候才知道是什么类型的,而对象创建的代码执行先后顺序是 static 的部分,然后才是构造函数等等。所以在对象初始化之前 static 的部分已经执行了,如果你在静态部分引用泛型,那么毫无疑问虚拟机根本不知道是什么东西,因为这个时候类还没有初始化。
- 4、不能创建参数化类型的数组,但是可以定义参数化类型的数组。
- 5、不能实例化类型变量。
- 6、不能使用 try-catch 捕获泛型类的实例。
泛型类型的继承规则
泛型类可以继承或者扩展其它泛型类,比如List和ArrayList:
public static class ExtendsPair<T> extends Pair<T>{
}
六.自定义泛型
返回值中存在泛型
public static <T> List<T> parseArray(String response,Class<T> object){
List<T> modelList = JSON.parseArray(response,object);
return modelList;
}
使用Class传递泛型类Class对象
我们用Class object来传递类的class对象,因为Class也是一泛型,它是用来装载类的class对象的,它的定义如下:
public final class Class<T> implements Serializable{
}
定义泛型数组
public static <T> T[] fun(T...arg){
return arg;
}
七.泛型的擦除
Java 语言使用类型擦除机制实现了泛型,类型擦除机制,如下:
- 编译器会把所有的类型参数替换为其边界(上下限)或 Object,因此,编译出的字节码中只包含普通类、接口和方法。
- 在必要时插入类型转换,已保持类型安全
- 生成桥接方法以在扩展泛型类时保持多态性
1.泛型类型的擦除
Java 编译器在擦除过程中,会擦除所有类型参数,如果类型参数是有界的,则替换为第一个边界,如果是无界的,则替换为 Object。 例如,我们定义一个无界泛型类如下:
public class Node<T>{
private T data;
private Node<T> next;
public Node(T data,Node<T> next){
this.data = data;
this.next = next;
}
public T getData(){
return data;
}
}
由于类型参数T是无界的,Java编译器会将其替换为Object,如下:
public class Node{
private Object data;
private Node next;
public Node(Object data,Node next){
this.data = data;
this.next = next;
}
public Object getData(){
return data;
}
}
我们在定义一个有界泛型类如下:
public class ComparableNum<T extends Comparable>{
private T data;
private ComparableNum<T> next;
public ComparableNum(T data,ComparableNum<T> next){
this.data = data;
this.next = next;
}
public T getData(){
return data;
}
}
Java编译器会将其替换为第一个边界Comparable,如下:
public class ComparableNum{
private Comparable data;
private ComparableNum next;
public ComparableNum(Comparable data, ComparableNum next) {
this.data = data;
this.next = next;
}
public Comparable getData() {
return data;
}
}
2.泛型方法的擦除
Java 编译器同样会擦除泛型方法中的类型参数,例如:
public static <T> int index(T[] array){
int index = 0;
...
return index;
}
由于T是无界的,因此Java编译器将其替换为Object,如下:
public static int index(Object[] array){
int index = 0;
...
return index;
}
同样,有界泛型方法:
public static<T extends Shape> void draw(T shape){
...
}
编译器会将其替换为Shape,如下:
public static void draw(Shape shape){
...
}
3.桥接方法
为了解决泛型擦除后继承类型的多态性,Java编译器会生成一个桥接方法,如下:
public class Node<T>{
public T data;
public Node(T data){
this.data = data;
}
public void setData(T data){
this.data = data;
}
}
public class ChildNode extends Node<Integer>{
public ChildeNode(Integer data){
super(data);
}
public void setData(Integer data){
super.setData(data);
}
}
Java编译器会在子类中生成一个桥接方法,保留泛型类型的多态性,ChildNode中的setData(Object data)和Node中的setData(Object data)可以完成方法覆盖
public class ChildNode extends Node{
//生成的桥接方法
public void setData(Object data){
setData((Integer)data);
}
public void setData(Integer data){
super.setData(data);
}
}
八.什么时候使用泛型
当接口、类及方法中操作的引用数据类型不确定的时候,以前用Object来进行扩展的,现在可以使用泛型来表示。这样可以避免强转的麻烦,而且将运行问题转移到编译时期。
九.Java类库中的泛型有哪些
所有的标准集合接口都是泛型化的—— Collection、List、Set 和 Map<K,V>。类似地,集合接口的实现都是用相同类型参数泛型化的,所以HashMap<K,V> 实现 Map<K,V> 等。
除了集合类之外,Java 类库中还有几个其他的类也充当值的容器。这些类包括 WeakReference、SoftReference 和 ThreadLocal。