一、泛型概述和定义
泛型就是将 数据类型参数化,例如我们实现一个存储对象的容器时,并不是直接定义好只能存储一种类型,好的办法就是定义时将类型参数化,也就是泛型,在真实使用时再传入真正的参数,这种参数化类型可以用在类、接口和方法中,分别被称为 泛型类、泛型接口、泛型方法。
1、泛型类
类中成员为泛型的类称为泛型类。最典型的就是各种容器类,如:List、Set、Map等。定义语法如下:
class 类名称 <泛型标识> {
private 泛型标识 变量名;
.....
}
}
- 其中的T表示类型参数,可以表示任何包装类型(基本类型不涉及泛型),这里的 T 就是泛型标识可以任意设置
- Java 常见的泛型标识以及其代表含义如下:
T :代表一般的任何类。
E :代表 Element 元素的意思,或者 Exception 异常的意思。
K :代表 Key 的意思。
V :代表 Value 的意思,通常与 K 一起配合使用。
S :代表 Subtype 的意思,文章后面部分会讲解示意。
public class FanXing <T> {
private T value;
public void setValue(T value) {
this.value = value;
}
public T getValue() {
return this.value = value;
}
}
- 当创建一个 Generic< T > 类对象时,会向尖括号 <> 中传入具体的数据类型。
public static void main(String[] args){
FanXing fanXing1 = new FanXing<>(); // 不传时相当于默认 Object 类型
FanXing fanXing2 = new FanXing(); // 同上
FanXing fanXing3 = new FanXing<String>();
FanXing fanXing4 = new FanXing<Integer>();
}
2、泛型接口
public interface 接口名<泛型标识> {
...
}
// 接口定义
public interface FanXingInterface<T> {
int n = 10;
// T value;// 报错! 接口中的属性默认是静态的,因此不能使用类型参数声明
T show(T value);
// 在jdk8 中,可以在接口中使用默认方法, 默认方法可以使用泛型接口的类型参数
default T method(T value) {
return null;
}
}
第一种: 接口实现类1,实现类中继承了泛型参数
public class FanXingInterfaceImpl1<T> implements FanXingInterface <T>{
@Override
public T show(T value){
return value;
}
}
第二种:接口实现类2,实现类中定义了明确的类型
public class FanXingInterfaceImpl2 implements FanXingInterface<String>{
@Override
public String show(String value){
return value;
}
}
第三种:接口子类接口,必须明确的类型
public interface FanXingInterface3 extends FanXingInterface<String>{
@Override
default String show(String value) {
return null;
}
@Override
default String method(String value) {
return null;
}
}
3、泛型方法
- 方法签名中声明了< 泛型标识>的方法称为泛型方法。
- 只有在方法签名中声明了的方法才是泛型方法,仅使用了泛型类定义的类型参数的方法并不是泛型方法,如下show1()
public <泛型标识> 返回类型 方法名(类型参数 变量名) {
...
}
public class FanXingMethod <E>{
// 泛型方法
public <T> void show(T value){
return;
}
// 非泛型方法
public void show1(E value){
return;
}
// 泛型方法,使用泛型类中定义的泛型参数
public <E> E show2(E value){
return value;
}
}
二、使用场景
在 jDk 1.5之前没有泛型的概念,例如支持容器参数多样化时,是定义为Object类型,但是存在下面这样的问题:
public static void main(String[] args){
List list1 = new ArrayList();
list1.add("aa");
list1.add(11);
log.info(list1.get(0).toString());
// 下一行会运行时报错:java.lang.ClassCastException
// log.info((String)list1.get(1));
}
在没有泛型时会向上转型为 Object 类型处理,存储了 String 类型和 Integer 类型的数据后,运行时报错了,我们正常期望越早暴露问题比较好,最好在在编译时期就将问题暴露出来,而不是运行失败。我们可以使用< String>泛型,则限定list 中存储非 String 类型数据时编译失败。
List<String> list1 = new ArrayList();
list1.add("aa");
list1.add(11);// 编译时报错
- 灵活性、可读性。泛型使得数据类型的类别可以像参数一样由外部传递进来,比传统由Object代替一切引用数据类型对象要更优雅,由于需要显示的定义类型,也提高了程序代码的可读性。
- 类型安全检查。在真正使用定义的类型的时候,泛型提供了类型检查机制,将类型不匹配问题前置在编译时期。
注意事项
1、泛型类 中的静态方法和静态变量不可以使用泛型类所声明的类型参数
因为静态变量和静态方法在类加载时已经初始化,直接使用类名调用。但是泛型替换为真正类型创建泛型类对象,即类型未确定时静态方法外部已经可用,所以静态方法不能使用泛型。
2、泛型方法可以是静态的,这一点容易和上面的点混淆。
3、使用了泛型类中的泛型标识的方法不一定时泛型方法,泛型方法必须必须由< T>泛型标识声明。即使泛型类中定义的类型参数标识和泛型方法中定义的类型参数标识都为< T > ,但它们彼此之间是相互独立的。也就是说,泛型方法始终以自己声明的类型参数为准,所以,在泛型类中定义泛型方法时最好用不同的标识来区分
public class FanXingMethod<E> {
// 1、静态变量不可以使用泛型参数
// 普通变量
public E num1;
// 报错:静态变量
// 'apractice.fanxingtest.FanXingMethod.this' cannot be referenced from a static context
public static E num2;
// 2、静态方法不可以使用泛型参数
// 普通方法、非泛型方法
public void show1( E value) { return; }
// 报错:静态非泛型方法
public static void show2( E value) { return; }
// 3、泛型方法可以是静态的
// 普通泛型方法
public <T> void show3(T value){ return; }
// 静态泛型方法
public static <T> void show4(T value){ return; }
}
三、泛型类型擦除
泛型的本质是类型参数化,定义了泛型标识,实际使用时,泛型和真正的数据类型类型之间有一系列动作。其实泛型信息只存在编译时期,代码编译之后,会完全擦除所有泛型信息,这称为 类型擦除。
举例:我们定义的
List<String> list1 = new ArrayList();
- 在创建一个泛型类的对象时,会将< String> 泛型信息记录下来
- 编译的时候将< String> 进行擦除
- 在使用的时候再将< String> 拿出来使用(例如:添加元素的时候需识别只能添加 String 类型的的数据,添加 Integer 类型数据会编译报错)
可以把泛型的**类型安全检查机制**和**类型擦除**想象成演唱会的验票机制,以 List< String> 泛型集合为例
//List 相当于一个演唱会场,< String> 相当于验票系统,验票系统标识了持非String票类均无法入场。
List<String> list1 = new ArrayList();
//一个String 类型对象为 持有演唱会门票的听众 这里相当于持有演唱会门票听众入场验票过程,验票成功的用户门票会被收走(String类型擦除),此时场内的听众并没有纸质票了(与未购票的用户一致 Object 类型),但场馆后台(JVM)会记录下观众信息(泛型 String 类型信息)
list1.add("aa");
//一个Integer 类型对象为 持有不合格门票的听众 这里相当于持有不合格门票入场验票过程,验票失败的用户闸机关闭无法入场。
list1.add(11);
// 获取容器中对象理解为场内需要二次检查是否有票。 场内的用户会正常通过(编译器自动进行类型向下转换,拿到 String 泛型信息)
log.info(list1.get(0));
上面的list1.get() 方法的返回值将返回 Object 类型,但编译器会自动插入 String 的强制类型转换。也就是说,编译器把 get() 方法调用翻译为两条字节码指令
list1.get(0);// 相当于下面2条
Object object = get(int index);
String str = (String)object;
四、泛型通配符
观察下面代码,我们知道 Number 是 Integer 的父类,根据多态的性质,是否可以向上转型,但实际第二行会编译报错,可以看出ArrayList< Integer> 并不是ArrayList< Number>的子类,两者之间没有继承关系,即一般的泛型< T>不支持向上转型。
ArrayList<Integer> arrayInteger = new ArrayList<Integer>();
ArrayList<Number> arrayNumber = new ArrayList<Integer>(); // 编译错误
如果我们想创建一个只能存储数字类型的list 容器。例如,一个list, 能Integer 类型和 Float 类型数据,所以需要一个支持这两种类型的父类引用类型,可以使用泛型通配符,能够处理某一类型范围内的类型参数。
泛型通配符有 3 种形式:
- <?> :被称作无限定的通配符。
- <? extends T> :被称作有上界的通配符。
- <? super T> :被称作有下界的通配符。
1、上界通配符 <? extends T>
类型参数的范围为泛型T及其子类
的所有类型。需要注意的是: <? extends T> 也是一个数据类型实参,它和 Number、String、Integer 一样都是一种实际的数据类型
ArrayList<? extends Number> arrayNumber = new ArrayList<Integer>();
ArrayList<? extends Number> arrayNumber1 = new ArrayList<Float>();
2、无界通配符 <?>
<?> 无限定的通配符,可以看成是任何类型都支持,我们一般使用<?> 一般是因为作为使用时,所用到的方法不依赖于T。
例如打印日志,如下无论是 List< Integer>, List< String> 都可以正常输出。
public static void printList(List<?> list) {
for (Object elem: list)
System.out.print(elem + " ");
System.out.println();
}
那么<?> 和 < Object> 一样吗?
printList() 的目标是用来打印任何类型的列表,但上述代码中它只能打印 Object 实例的列表
List<Integer>, List<String> 等都是 List<?> 的子类
但是List<Integer>, List<String> 和 List<Object> 并无关系
并且<?> 可以存放 null。
public static void main(String[] args) {
List<?> list1 = Arrays.asList("test1", "haha", "hello");
List<?> list2 = Arrays.asList(111,222,333);
System.out.println("按照List<?> 模式打印列表:");
printList(list1);
printList(list2);
List<Object> list3 = Arrays.asList("test1", "haha", "hello");
List<Object> list4 = Arrays.asList(111,222,333);
System.out.println("按照List<Object> 模式打印列表:");
printListObject(list3);
printListObject(list4);
}
public static void printListObject(List<Object> list) {
for (Object elem : list)
System.out.println(elem + " ");
System.out.println();
}
3、下界通配符 <? super T>
类型参数的范围为泛型T及其父类
的所有类型
例如一个 List 容器想存放 Integer 类型数据,为了更大限度的提高灵活性,该方法需要适用于 List< Integer>、List< Number> 和 List< Object> 等任何可以保存 Integer 值的对象。此时就可以用到下届通配符。
List<? super Integer> list1 = new ArrayList();