一、概述
什么是泛型(Generic)
本质:参数化类型(类型,参数化)
提到参数,最熟悉的就是定义方法时有形参,然后调用此方法时传递该类型实参。
那么参数化类型怎么理解呢?顾名思义,就是将类型指定为参数。类似于方法中的变量参数,此时类型也定义成参数形式(类型形参),然后在使用时传入具体的类型(类型实参)。
类型形参、类型实参
重点!后面的形参extends、实参通配符都是在此基础上讨论的!
//类型形参: T、V extends Teach
public class School<T>{
public void manage(T t){}
public <V extends Teach> void hasTeacher(V v){}
}
//类型实参: Student、Teacher(不是teacher对象哦)
School<Student> school = new School();
Teacher teacher = new Teacher();
school.hasTeacher(teacher);
为什么要引入泛型
- 类型安全
编译时期就可以检查出因 Java 类型不正确导致的 ClassCastException 异常,提高 Java 程序的类型安全 - 消除强制类型转换
使用时直接得到目标类型,消除许多强制类型转换
泛型命名
任意字母,通常情景下提高可读性代表含义:
- E — Element,常用在java Collection里,如:List,Iterator,Set
- K,V — Key,Value,代表Map的键值对
- N — Number,数字
- T — Type,类型,如String,Integer等等
... - 任何泛型变量都派生自Object,而不能使用原生类型如int、float、true
二、泛型作用位置
接口、类、方法中,分别被称为泛型接口、泛型类、泛型方法
泛型接口
类型形参 写在接口名后
interface Info <T>{
T get();
void set(T t);
}
泛型类:
类型形参 写在类名后
class StuInfo<T>{
private T t;
public T get(){ return t;}
public void set(T t){}
}
//泛型类实现接口:未填充接口的泛型,是泛型类
class StuInfo<T> implement Info<T>{
public T get(){}
public void set(T t){}
}
//非泛型类实现接口:填充接口的泛型,类已不是泛型类
class StuInfo implement Info<String>{
public Stirng get(){}
public void set(String str){}
}
//多泛型类实现接口,是泛型类
class StuInfo<T,V> implement Info<T>{
public T get(){}
public void set(T t){}
public V get(){}
public void set(V v){}
}
//多泛型类实现接口,是泛型类
class StuInfo<V> implement Info<String>{
public Stirng get(){}
public void set(String str){}
public V get(){}
public void set(V v){}
}
泛型方法
类型形参 写在返回值前
class Info<V>{
//返回V,该方法类型形参在类上定义,所以不用再返回值前加类型形参
public V getValue(V v){
return v;
}
// 返回T,该方法类型形参在方法上定义,需要类型形参在返回值前
public <T> T setValue(T t){
return t;
}
}
// 1,2写法都可以,推荐写法2
Info info = new Info();
info.setValue("string");//1
info.<String>setValue("string");//2
三、通配符
有时候希望传入的类型不在局限于某个单一类型,而是有一个指定的范围,从而可以进行一些特定的操作,这时候就需要使用通配符边界了
注意:通配符作用位置是,类型实参!
- :无限制通配符
- <? extends E> :声明类型的上界
表示参数化的类型是指定类型,或者指定类型的子类 - <? super E> :声明了类型的下界
表示参数化的类型是指定类型,或者指定类型的父类
定义以下继承关系:
Food
|--Fruit
|--Apple
|--RedApple
|--GreenApple
|--Banana
|--Meat
|--Pork
|--Beef
public class Food {}
public class Fruit extends Food{}
public class Meat extends Food {}
public class Banana extends Fruit {}
public class Apple extends Fruit {}
public class RedApple extends Apple {}
public class GreenApple extends Apple {}
public class Pork extends Meat {}
public class Beef extends Meat {}
public class Plate<T>{
private T mT;
public void add(T t){
this.mT = t;
}
public T get(){
return mT;
}
}
3.1 泛型类型
/**
* Plate<Fruit>不是Plate<Apple>的父类,它们的泛型类没有继承关系
* 是完全不同的两种类型
*/
Plate<Fruit> plateFruit = new Plate<Fruit>();
Plate<Fruit> plateApple = new Plate<Apple>();//error
/**
* 使用通配符,让两者泛型类之间有继承关系
* 通配符作用位置是,类型实参
*/
Plate<?> plate1 = new Plate<Apple>();
Plate<? extends Fruit> plate2 = new Plate<Apple>();
Plate<? super Fruit> plate3 = new Plate<Food>();
3.2 无限制通配符 < ?>
能取,不能存
因为不确定类型,所以取出放到Object中
Plate<?> plate1 = new Plate<Apple>();
plate1.add(new Apple());//error
Object g1 = plate1.get();
?与 T关系
- ?作用在类型实参,表示不确定的类型
- T 作用在类型形参,表示某一确定类型
3.3 PECS原则
为了获得最大限度的灵活性,要在表示 生产者或者消费者 的输入参数上使用通配符,使用的规则就是:生产者有上限、消费者有下限:
PECS: producer-extends, costumer-super
生产者
有返回参数(T get())
- 所以要求有上限,可以返回一个具体类型T(基类)
- 因为不确定具体类型是子类中哪个,所以不能操作,即“能取不能存”
- 用 <? extends T>
消费者
有传入参数(add(T t))
- 所以要求有下限,最起码是T类型,T的子类都能存入进行操作
注意:存入的是T的子类,而不是 T的父类 - 因为不确定具体类型是T父类的哪一个,所以不能取出具体类型,只能放入object中,即“能存不能取”(也能取出,只是Object类型)
- 用<? super T>
3.4 上界通配符 < ? extends E>
/**
* ? extends Fruit 可以是 Fruit及其子类Plate类型的基类,如下图篮框范围
*/
Plate<? extends Fruit> plate2 = new Plate<Apple>();
//Plate<? extends Fruit> plate2 = new Plate<RedApple>();
//Plate<? extends Fruit> plate2 = new Plate<Banana>();
Plate<? extends Fruit> plate2 = new Plate<Apple>();
plate2.add(new Food());//error
plate2.add(new Fruit());//error
plate2.add(new Apple());//error
Fruit g2 = plate2.get();
根据PECS原则,该类为生产者,能取不能存
下界通配符 < ? super E>
/**
* ? super Fruit 可以是 Fruit及其父类Plate类型的基类,如下图红框范围
*/
Plate<? super Fruit> plate3 = new Plate<Food>();
//Plate<? super Fruit> plate3 = new Plate<Apple>();//error
Plate<? super Fruit> plate3 = new Plate<Food>();
plate3.add(new Food());//error
plate3.add(new Fruit());
plate3.add(new Apple());
plate3.add(new RedApple());
Object g3 = plate3.get();
根据PECS原则,该类为消费者,能存不能取
泛型与数组
不能创建一个确切的泛型类型的数组
//创建List数组
List<String>[] ls = new ArrayList<String>[10]; //error
List<?>[] ls = new ArrayList<?>[10];
List<String>[] ls = new ArrayList[10];
Plate[] p1 = new Plate[10];
Plate<?>[] p2 = new Plate<?>[10];
Plate<Fruit>[] p3 = new Plate<Fruit>[10];//error
Plate<Fruit>[] p4 = new Plate[10];
Object g1 = p1[0].get();
Object g2 = p2[0].get();
Fruit g3 = p3[0].get();
Fruit g4 = p4[0].get();
四、类型擦除
编译器对带有泛型的java代码进行编译时,执行类型检查和类型推断,生成普通的不带泛型的字节码,这就叫做 类型擦除(type erasure)。
- Java 的泛型在编译器有效,在运行期被删除,也就是说所有泛型参数类型在编译后都会被清除掉。编译器自动完成了从 Generic Java 到普通 Java 的翻译,Java 虚拟机运行时对泛型基本一无所知
- 在泛型类被类型擦除的时候,之前泛型类中的类型参数部分如果没有指定上限,如 则会被转译成普通的 Object 类型,如果指定了上限如 则类型参数就被替换成类型上限
/**
* 泛型的 class 对象是相同的
* 每个类都有一个 class 属性,泛型化不会改变 class 属性的返回值
* List<String> 和 List<Integer> 擦除后的类型都是 List
**/
List<String> ls = new ArrayList<String>();
List<Integer> li = new ArrayList<Integer>();
System.out.println(ls.getClass() == li.getClass()); //true
问题和解决
类型擦除以及类型擦除带来的问题
Java泛型擦除的缺陷及补救措施
面试官问我:“泛型擦除是什么,会带来什么问题?”
List<Integer> ls = new ArrayList<>();
ls.add(23);
//ls.add("text");
try {
Method method = ls.getClass().getDeclaredMethod("add",Object.class);
method.invoke(ls,"test");
method.invoke(ls,42.9f);
} catch (NoSuchMethodException e) {
e.printStackTrace();
}
//这样的写法是正确的,但是会转型失败,报错
int i = ls[1];
问题和解决【TODO】
资料
泛型中 extends 和 super 的区别?
深入理解 Java 泛型
Java 泛型,你了解类型擦除吗?
深入理解Java泛型
Java中泛型 类型擦除
类型擦除以及类型擦除带来的问题
Java泛型擦除的缺陷及补救措施
面试官问我:“泛型擦除是什么,会带来什么问题?”