为什么需要泛型
想象以下的场景,我们需要编写一个容器类,支持对数据的简单操作,例如增删改查,那么实现可以如下
public MyContainerOfString{
private String[] container = new String[10];
private int size;
public void add(String data){
container[size++] = data;
}
public String get(int index){
return container[index];
}
...
}
如上述代码所示,我们实现了一个可以存储String类型的容器,如果要实现一个Integer,Double等类型的容器,我们会发现需要写很多重复的代码,于是我们可以修改上述的代码,让其更灵活
public MyContainer{
private Object[] container = new Object[10];
private int size;
public void add(Object data){
container[size++] = data;
}
public Object get(int index){
return container[index];
}
...
}
上述的实现确实很灵活,可以支持任何类型,但是它也有一个问题,就是类型安全
MyContainer container = new MyContainer();
container.add(1);
container.add("A");
int value = (Interger)container.get(2) //运行失败
上述代码在编译期间不会报错,但是在运行时报错了,原因就是类型转换失败。
通过上述的场景我们不难看出使用泛型的好处
- 可以减少样本代码
- 可以保证类型安全
泛型类
泛型被用于类的定义则称之为泛型类,泛型类的通用写法
class 类名称<泛型标识>{
}
比较常见的泛型类可见于dao层,例如
class BaseDao<T> {
public void save(T t){
}
public void delete(T t){
}
}
泛型接口
泛型接口与泛型类相似,只不过用于定义接口,泛型类的通用写法
interface 接口名称<泛型标识>{
}
最常见的泛型接口即是各种容器接口,例如
public interface List<E> extends Collection<E>{
}
泛型方法
泛型方法相比于泛型类更为复杂,下面是泛型方法的通用写法
public <T> 返回类型 方法名称(泛型标识){
}
其中
public void save(T t) //不是泛型方法
public <T> void save(T t) // 泛型方法
下面是一个泛型方法的示例
public <T> List<T> fromArrayToList(T[] a) {
return Arrays.stream(a).collect(Collectors.toList());
}
一个泛型方法可以处理多个泛型类型,但是有几个泛型类型就需要声明几个,例如
public static <T, G> List<G> fromArrayToList(T[] a, Function<T, G> mapperFunction) {
return Arrays.stream(a)
.map(mapperFunction)
.collect(Collectors.toList());
}
泛型擦除(Type Erasure)
泛型擦除可以简单理解为泛型只在编译阶段生效,在编译阶段,正确检验泛型结果后,会将泛型的相关信息擦除,在运行阶段是看不到泛型信息的
List<Integer> list1 = new ArrayList<>();
List<String> list2 = new ArrayList<>();
Class<? extends List> list1Class = list1.getClass();
Class<? extends List> list2Class = list2.getClass();
if (list1Class.equals(list2Class)){
System.out.println("same class");
}
运行程序可以看到输出”same class”,由此可以证明泛型信息在运行间已被擦除。总结来说,泛型类型在逻辑上看以看成是多个不同的类型,实际上都是相同的基本类型。
那么我们有办法在运行阶段拿到泛型信息吗?答案是有的,但是前提是泛型是类声明的一部分,
public class CatCage implements Cage<Cat>
此时我们可以通过反射获取泛型
(Class<T>) ((ParameterizedType) getClass().getGenericSuperclass()).getActualTypeArguments()[0];
泛型边界
在声明泛型时,我们可以指定泛型的边界,所谓边界其实就是子类与父类的关系,例如
class Test<T extends Number> // T必须是Number的子类
class Test<T super Number> // T必须是Number的父类
我们再来看下定义边界有什么用处
class Cage<T> {
}
Cage<Animal> cage = new Cage<Cat>() //编译失败
可以发现装猫的笼子无法转换为装动物的笼子。因此可以得出结论,虽然cat和animal是存在继承关系,但是泛型Cage
class Cage<T extend Animal>{}
PECS
我们如何选择使用上边界
所谓的Producer extend是指当我们是”生产”泛型对象时,我们就用extend
public static void makeLotsOfNoise(List<? extends Animal> animals) {
animals.forEach(Animal::makeNoise);
}
对于上述例子中,我们不断的从animals中取出(生产)对象,执行makeNoise方法,因此使用extend
public static void addCats(List<? super Animal> animals) {
animals.add(new Cat());
}
对于上述例子中,我们不断的往animals中增加(消费)对象,因此使用super
泛型通配符
public void print(List<?> list){
list.forEach(Object::toString);
}
其中?就是通配符,表示未知的类型,?代表的一种具体的类型,例如Number,Integer
总结
- 使用泛型可以减少样本代码并保证类型安全
- 泛型方法一定需要用
进行声明 - 所谓泛型擦除是指泛型信息只在编译阶段生效,在运行阶段,已经看不到泛型信息
- 使用PECS原则来决定使用上边界还是下边界