看泛型篇总结
一,为什么使用泛型
我们假设要编写一个Stack类用于存储不同的类型数据,如果不用泛型此时有两种写法:
1)定义不同的Stack类
如Integer的IntegerStack类
public class IntegerStack{
private Integer[] arr;
private int size;
private int top;
public IntegerStack(int size){
this.arr = new Integer[size];
this.size = size;
this.top = 0;
}
public void push(Integer elem){
if(top == size) return ;
arr[top++] = elem;
}
public Integer pop(){
if(top == 0) return null;
return arr[--top];
}
}
如果要存储Long则需要创建一个LongStack类
public class LongStack{
private Long[] arr;
private int size;
private int top;
public LongStack(int size){
this.arr = new Long[size];
this.size = size;
this.top = 0;
}
public void push(Long elem){
if(top == size) return ;
arr[top++] = elem;
}
public Long pop(){
if(top == 0) return null;
return arr[--top];
}
}
此时每增加一种存储的数据类型,就需要多创建一个类,会导致类型太多,且大量的重复代码。
2)抽象出统一的父类或接口
按这种方式,我们只需要创建一个类就可以存储所有的子类。比如我们创建一个Number的NumberStack,那么Short,Int,Long,Float,Double等都可以存储了
public class NumberStack{
private Number[] arr;
private int size;
private int top;
public NumberStack(int size){
this.arr = new Number[size];
this.size = size;
this.top = 0;
}
public void push(Number elem){
if(top == size) return ;
arr[top++] = elem;
}
public Number pop(){
if(top == 0) return null;
return arr[--top];
}
}
上面的如果不够或者子类不确定,可以将父类设置为Object,这样就可以存储所有的类了。
public class Stack{
private Object[] arr;
private int size;
private int top;
public Stack(int size){
this.arr = new Object[size];
this.size = size;
this.top = 0;
}
public void push(Object elem){
if(top == size) return ;
arr[top++] = elem;
}
public Object pop(){
if(top == 0) return null;
return arr[--top];
}
}
但是此时有个问题,编译器没有办法帮我们做类型检查了,如下:
Stack stack = new Stack(3);
stack.push(3);
stack.push("34");
Integer elem = (Ingeger)stack.pop(); //编译期不会报错,运行会报错
此代码编译器并不会报错,但是在运行的时候会报错,因为栈的最上面是String,没有办法转换为Integer。
另外还有一个缺陷,从父类转换为子类是需要显式强制转换的,最后会存在大量的显式转换。
此时在jdk1.5加入了泛型,刚好继承了上面两种方式的优点,又解决了其缺陷
使用泛型实现的Stack类
public class Stack{
private Object[] arr; //因为类型擦除,此处需要使用Object,不使用Object那么就需要在构造器传入定义好的数组
private int size;
private int top;
public Stack(int size){
this.arr = new Object[size]; //不在构造器中定义,那么可以改为传入,int size, T[] arr
this.size = size;
this.top = 0;
}
public void push(E elem){
if(top == size) return ;
arr[top++] = elem;
}
public E pop(){
if(top == 0) return null;
return (E)arr[--top];
}
}
3)泛型与类型参数
通过上面的可以发现,泛型本质是对类型的参数化,在类的定义中,我们可以把类型作为参数,在使用类时,我们把具体的类型传入到类型参数。
二,泛型的基本用法
1)基本用法
泛型的用法有三种:泛型接口,泛型类,泛型方法;代码如下:

上面尖括号中的T,E表示类型参数,可以是任意的大写字母,但我们一般使用T,E,K,V,N来表示各种类型参数
1,E是element的首字母,一般用来表示容器中的元素的类型参数。
2,T是type的首字母,一般用来表示非容器元素的数据类型参数。
3,K,V分别是key和value的首字母,一般用来表示键值对中键和值的类型参数。
4,N是number的首字母,一般用来表示数字类型参数。
当然一个泛型接口,类,方法中可以有多个类型参数,如HashMap:

2)上界限定符
除了上面的基本用法以外,还可以使用上界限定符,限定参数的具体取值范围。
例如<T extends Number> 限定了传入的类型参数必须是Number或Number的子类。
另外在泛型中只有extends没有implements,而extends即值继承,也指实现接口。
如<T extends Comparable<T>> 和 <T extends Closeable> 分别指传入的参数得实现接口Comparable或Closeable。
举例,设计一个泛型方法,用来比较两个类的大小,只支持实现了Comparable接口的两个类进行比较
public class Util{
public <T extends Comparable<T>> T max(T a, T b){
if(a.compareTo(b) >= 0) return a;
return b;
}
}
3)注意项
1,泛型接口和泛型类,要求每个方法中的泛型使用相同的数据类型,只能是声明时定义的,即整个类或接口要使用相同的类型参数。
如List<String> list = new ArrayList<>();
在定义类时类型参数定义为String了,那么list的所有方法里的参数也是String了。
2,泛型方法则没有上面的要求,两个不同的方法可以完全使用不同的数据类型。
如下:如果不是泛型类或接口,可以一个方法是Number或其的子类,另一个方法则是别的的子类。
public <T extends Number> void method1(T a);
public <T extends Student> void method2(T a);
3,使用泛型方法时,前面要记得声明。如不想声明可以使用通配符 ?
三,泛型中的通配符
1)定义
除了类型参数以外还有另一种常用语法,通配符 "?"
通配符与类型参数不太一样,类型参数一般用于泛型接口,类,方法,而通配符则与Integer,Number这种具体的类型无异,用来具体化类型参数,可以看作是比较特殊的具体类型。
2)使用
通配符常用于方法中,假如某个方法传入的是泛型接口或泛型类,但我们又没法确认具体类型时,可以使用通配符。

当然上面的方法也可以使用如下泛型方法替代

3)只能使用通配符的地方
1,下界限定符 super
除了上界限定符extends外,还有下界限定符super,如:
<? super People> 指传的类型必须是People或其父类,此时只能使用通配符?
2,<? super T> 和<? extends T>
通配符可以extends或super类型参数,返过来则不行

四,泛型的类型擦除
1)定义
在编译时,编译器会对泛型做类型检查,但是当编译成字节码时,泛型中的类型参数统统会替换为其上界,比如
<T> 会替换成 Object,<T extends String> 会替换为String
2)类型擦除带来的问题:
1,另外因为类型擦除,所以泛型没有办法支持基本类型,基本类型并不继承自Object。
同时针对基本类型的方法没法使用泛型,那么就像最开始的情况,针对int,double,char等都需要单独定义其方法。
2,而因为类型擦除,无法使用new T()来创建对象,因为jvm没法知道T的具体类型,也没有办法知道其是否有无参构造器。
3,编译错误,如下因为不确定T里面有什么方法,可以通过T extends HasF来解决,因为不加时变成了Object,加了类型擦除后为Hasf,还是存在f()方法的
class HasF {
public void f() {
System.out.println("HasF.f()");
}
}
public class Manipulator<T> {
private T obj;
public Manipulator(T obj) {
this.obj = obj;
}
public void manipulate() {
obj.f(); //无法编译 找不到符号 f()
}
public static void main(String[] args) {
HasF hasF = new HasF();
Manipulator<HasF> manipulator = new Manipulator<>(hasF);
manipulator.manipulate();
}
还不错的别的方章,上面的HasF就是下面看到的
本文详细介绍了Java泛型的使用,包括为何使用泛型、基本用法(接口、类、方法)、上界和下界限定符、通配符的应用以及类型擦除的原理及其带来的问题。通过实例展示了泛型在避免代码冗余和类型安全方面的优势。

被折叠的 条评论
为什么被折叠?



