一、泛型的作用
1.对外部输入的数据进行类型控制和检查,减少低级错误的发生
2.将数据类型抽象出来,可以由外部调用时传入。-->本质就是将数据类型参数化,使用过程中将数据类型作为参数传入。
二、常用泛型表示
T :代表一般的任何类。
E :代表 Element 元素的意思,或者 Exception 异常的意思。
K :代表 Key 的意思。
V :代表 Value 的意思,通常与 K 一起配合使用。
S :代表 Subtype 的意思,文章后面部分会讲解示意。
三、ClassCastException异常
@ Test
public void test() {
ArrayList list = new ArrayList();
list.add("aaa");
list.add("bbb");
list.add("ccc");
list.add(111);
for (int i = 0; i < list.size(); i++) {
System.out.println((String)list.get(i));
}
}
在编译时没有报错,但在运行时却抛出了一个 ClassCastException 异常
,其原因是 Integer 对象不能强转为 String 类型。
解决办法:
将代码加上泛型,利用泛型的安全检查机制。
@ Test
public void test() {
ArrayList<String> list = new ArrayList<>();
list.add("aaa");
list.add("bbb");
list.add("ccc");
list.add(111);// 在编译阶段,编译器会报错
for (int i = 0; i < list.size(); i++) {
System.out.println((String)list.get(i));
}
}
ArrayList加上泛型写法< String >,对集合中元素的数据类型进行了限定【只能存放String】,加入非String类型时编译器报错,这就达到了在编码时就发现问题并及时得到了解决。
四、静态成员中不允许使用泛型类声明的泛型参数
public class Test<T> {
public static T one; // 编译错误
public static T show(T one){ // 编译错误
return null;
}
}
错误原因:
静态成员在类加载时已经初始化完毕了,可以直接通过类名调用;在泛型类的类型参数未确定时,静态成员有可能被调用,因此泛型类的类型参数是不能在静态成员中使用的。
注:泛型类不只接受一个类型参数,它还可以接受多个类型参数。
五、泛型接口中的常见错误用法
interface IUsb<U, R> {
int n = 10;
U name;// 报错! 接口中的属性默认是静态的,因此不能使用类型参数声明
R get(U u);// 普通方法中,可以使用类型参数
void hi(R r);// 抽象方法中,可以使用类型参数
// 在jdk8 中,可以在接口中使用默认方法, 默认方法可以使用泛型接口的类型参数
default R method(U u) {
return null;
}
}
错误原因:
在接口中声明的成员默认是静态的。
public static -->常常被省略,所以此处使用接口泛型定义的类型进行变量声明是不正确的。
五、泛型类声明的和泛型方法中声明的相互独立
public class Test<T> {
public void testMethod(T t) {
System.out.println(t);
}
public <T> T testMethod1(T t) {
return t;
}
}
上面代码中,Test< T > 是泛型类,testMethod() 是泛型类中的普通方法,其使用的类型参数是与泛型类中定义的类型参数。
而 testMethod1() 是一个泛型方法,他使用的类型参数是与方法签名中声明的类型参数。
虽然泛型类中定义的类型参数标识和泛型方法中定义的类型参数标识都为< T >,但它们彼此之间是相互独立的。也就是说,泛型方法始终以自己声明的类型参数为准。
六、类型擦除
public class GenericType {
public static void main(String[] args) {
ArrayList<String> arrayString = new ArrayList<String>();
ArrayList<Integer> arrayInteger = new ArrayList<Integer>();
System.out.println(arrayString.getClass() == arrayInteger.getClass());// true
}
}
比较两个类型不同类型的ArrayList集合,惊喜的发现他们getClass竟然是同一个。为什么呢?
原因:
在编译期间,所有的泛型信息
都会被擦除, ArrayList< Integer > 和 ArrayList< String >类型,在编译后都会变成ArrayList< Objec t>
类型。
编写一个抽象类使用javac编译之后,再对其进行反编译查看泛型被替换为了什么。
源代码:
public class Caculate<T> {
private T num;
}
反编译代码
public class Caculate {
public Caculate() {}// 默认构造器,不用管
private Object num;// T 被替换为 Object 类型
}
是不是所有的类型参数被擦除后都以 Object 类进行替换呢?
- 答案是否定的,大部分情况下,类型参数 T 被擦除后都会以 Object 类进行替换;而有一种情况则不是,那就是使用到了 extends 和 super 语法的
有界类型参数
(即泛型通配符
)。
现象解释
extends 和 super 是一个限定类型参数边界的语法,extends 限定 T 只能是 Number 或者是 Number 的子类。 也就是说,在创建 Caculate 类对象的时候,尖括号 <> 中只能传入 Number 类或者 Number 的子类的数据类型,所以在创建 Caculate 类对象时无论传入什么数据类型,Number 都是其父类,于是可以使用 Number 类作为 T 的原始数据类型,进行类型擦除并替换。
泛型擦除疑问
1、不是说泛型信息在编译的时候就会被擦除掉吗?
2.那既然泛型信息被擦除了,如何保证我们在集合中只添加指定的数据类型的对象呢?
换而言之,我们虽然定义了 ArrayList< Integer > 泛型集合,但其泛型信息最终被擦除后就变成了 ArrayList< Object > 集合,那为什么不允许向其中插入 String 对象呢?
解答
- 在创建一个泛型类的对象时, Java 编译器是先检查代码中传入 < T > 的数据类型,并记录下来,然后再对代码进行编译,
编译的同时进行类型擦除
;如果需要对被擦除了泛型信息的对象进行操作,编译器会自动将对象进行类型转换。
七、泛型通配符
List<Integer> list = new ArrayList<Integer>(); //编译通过
已知 Integer 类是 Number 类的子类,那如果 ArrayList<> 泛型集合中,在 <> 之间使用向上转型
,也就是将 ArrayList< Integer > 对象赋值给 List< Number > 的引用,是否被允许呢?
List<Number> list01 = new ArrayList<Integer>();// 编译错误
ArrayList<Number> list02 = new ArrayList<Integer>();// 编译错误
编译器为了避免发生这种错误,根本就不允许把 ArrayList< Integer >对象向上转型为 ArrayList< Number >;换而言之, ArrayList< Integer > 和 ArrayList< Number > 两者之间没有继承关系。
八、 上界通配符<? extends T>
public class Test {
public static void main(String[] args) {
ArrayList<? extends Number> list = new ArrayList();
list.add(null);// 编译正确
list.add(new Integer(1));// 编译错误
list.add(new Float(1.0));// 编译错误
}
public static void fillNumList(ArrayList<? extends Number> list) {
list.add(new Integer(0));//编译错误
list.add(new Float(1.0));//编译错误
list.set(0, new Integer(2));// 编译错误
list.set(0, null);// 编译成功,但不建议这样使用
}
}
在 ArrayList<? extends Number> 集合中,不能添加任何数据类型的对象,只能添加空值 null,因为 null 可以表示任何数据类型
结论
使用 extends 通配符表示可以读,不能写。
九、下界通配符 <? super T>
public class GenericType {
public static void main(String[] args) {
ArrayList<Integer> list01 = new ArrayList<Number>();// 编译错误
ArrayList<? super Integer> list02 = new ArrayList<Number>();// 编译正确
}
}
- 逻辑上可以将 ArrayList<? super Integer> 看做是 ArrayList< Number > 的父类,因此,在使用了下界通配符 <? super Integer> 后,便可以将 ArrayList< Number > 对象
向上转型
了。
总结:
使用 super 通配符表示可以写,不能读。
本人认为,上面这篇文章将泛型总结得非常全面仔细,推荐各位小伙伴去阅读原文,你一定会对泛型有一个全新的理解。Java 中的泛型(两万字超全详解)_java 泛型-CSDN博客