泛型
1、泛型
把明确类型的工作推迟到创建对象或调用方法的时候,可以把运行时的问题提前到编译时期。提供了编译期的类型安全,确保把正确类型的对象放入集合中,避免了在运行时出现 ClassCastException 异常
- 可以明确集合中的数据类型,提高安全性、可读性,可以使用增强 for 循环遍历
- 代码更加简洁,不需要强制转换
- 程序更加健壮,只要编译时期没有警告,那么运行时期就不会出现 ClassCastException 异常
1.1 通配符
本质上这些都是通配符,没有区别,只有字面意思,只是一个约定的规范,Test<T> 与 Test<A> 在执行效果上并没有什么区别
- T:代表一个具体的 Java 类型
- E:代表 Element 的意思,或者 Exception 异常的意思
- K:代表 Key 的意思,通常与 V 一起配合使用
- V:代表 Value 的意思,通常与 K 一起配合使用
- S:代表 Subtype 的意思
- ?:无限定的通配符,代表不确定的 Java 类型
- 主要针对泛型类的限制, 无法像 T 类型参数一样单独存在
? extends E:设定通配符上限,接收 E 类型或者 E 的子类型? super E:设定通配符下限,接收 E 类型或者 E 的父类型
1.2 泛型的使用
1.2.1 泛型类
用于类的定义中,用户使用该类的时候,才把类型明确下来。通过泛型可以完成对一组类的操作对外开放相同的接口,如 List、Set、Map 等容器类。泛型类的子类,如果有明确的类型参数,需将用泛型的地方全部替换成传入的实参类型,否则需将泛型的声明也一起加到类中。类上声明的泛型只对非静态成员有效,如果静态方法要使用泛型的话,必须将静态方法也定义成泛型方法
public class Test<T> {
private T t;
public T getT() {
return t;
}
public void setT(T t) {
this.t = t;
}
public static void main(String[] args) {
Test<Integer> test = new Test<>();
test.setT(1);
System.out.println(test.getT());
}
}
- 继承泛型类的子类,未传入泛型实参时,在声明类的时候,需将泛型的声明也一起加到类中。如果不声明泛型,编译器会报错
class One<T> extends Test<T> {}
class Two<String> extends Test<T> {} // 报错
class Three extends Test<T> {} // 报错
class Four<T> extends Test<String> {}
class Five<String> extends Test<String> {}
class Six extends Test<String> {}
class Seven extends Test {}
1.2.2 泛型接口
与泛型类相似
public interface Test<T> {
T doAnything();
}
public class One<T> implements Test<T> {
T doAnything();
}
// 当实现泛型接口的类,传入泛型实参时,需将用泛型的地方全部替换成传入的实参类型
public class One<String> implements Test<String> {
String doAnything();
}
1.2.3 泛型方法
在调用方法的时候指明泛型的具体类型,类型参数写在返回值前面,声明的类型参数,还可以当作返回值的类型
public class Test {
// 只有声明了<T>的方法才是泛型方法,泛型类中的使用了泛型的成员方法并不是泛型方法
public <T> T method(T t){
return t;
}
// 静态方法使用泛型,必须将静态方法也定义成泛型方法
public static <T> void staticMethod(T t) {
System.out.println("static方法");
}
public static void main(String[] args) {
Test test = new Test();
System.out.println(test.method("fuck"));
System.out.println(test.method(12));
staticMethod("fuck");
staticMethod(12);
}
}
1.3 泛型擦除
泛型是通过类型擦除来实现的,泛型信息只存在于代码编译阶段,在进入 JVM 之前,与泛型相关的信息会被擦除
List<String> l1 = new ArrayList<String>();
List<Integer> l2 = new ArrayList<Integer>();
System.out.println(l1.getClass() == l2.getClass()); // true
无论何时定义一个泛型类型,都自动提供了一个相应的原始类型(raw type)。原始类型就是擦除了泛型信息,最后在字节码中的类型变量的真正类型。擦除(erased)类型变量,并替换为限定类型(无限定的变量用 Object)
1.3.1 局限性
泛型擦除,是泛型能够与之前的 Java 版本代码兼容共存的原因。但也因为类型擦除,它会抹掉很多继承相关的特性,这是它带来的局限性
List<Integer> list = new ArrayList<>();
list.add(1);
// 因为泛型的限制,以下代码会报错,基于对类型擦除的了解,利用反射,就可以绕过这个限制
// list.add("String");list.add(27.12);会报错
Method method = list.getClass().getDeclaredMethod("add", Object.class);
method.invoke(list, "String");
method.invoke(list, 27.12);
for (Object o : list) {
System.out.println(o); // 成功打印
}
1.4 可以把 List<String> 传递给一个接受 List<Object> 参数的方法吗
会导致编译错误,因为 List<Object> 可以存储任何类型的对象包括 String、Integer 等,而 List<String> 却只能用来存储 String
public class Test {
public static void main(String[] args) {
Test test = new Test();
List<String> list = new ArrayList<>();
// 编译错误
test.demo(list);
}
public void demo(List<Object> list) {
System.out.println("success");
}
}
- 有两种解决办法
- 将 Test 设为泛型类
- 将 demo 方法中的泛型
<Object>改为<?>
1.5 Class<T> 和 Class<?> 的区别
Class<T> 在实例化的时候,T 要替换成具体类,Class<?> 是个通配泛型,? 可以代表任何类型,主要用于声明时的限制情况
1.6 PECS 原则
- 如果要从集合中读取类型 T 的数据,并且不能写入,可以使用
? extends 通配符(Producer Extends) - 如果要从集合中写入类型 T 的数据,并且不需要读取,可以使用
? super 通配符(Consumer Super) - 如果既要存又要取,那么就不要使用任何通配符
1.7 泛型数组
Java 不能创建具体类型的泛型数组
// 这两行代码是无法在编译器中编译通过的。原因是类型擦除带来的影响
List<Integer>[] list = new ArrayList<Integer>[];
List<Boolean> list = new ArrayList<Boolean>[];
List<Integer> 和 List<Boolean> 在 JVM 中等同于 List<Object>,所有的类型信息都被擦除,程序也无法分辨一个数组中的元素类型具体是 List<Integer> 类型还是 List<Boolean> 类型
List<?>[] list = new ArrayList<?>[10];
list[1] = new ArrayList<String>();
List<?> v = list[1];
借助于无限定通配符却可以,?代表未知类型,所以它涉及的操作都基本上与类型无关,因此 JVM 不需要针对它对类型作判断,因此它能编译通过
更多:泛型就这么简单、java 泛型详解、Java 泛型,你了解类型擦除吗、JAVA泛型通配符T,E,K,V区别、面试常问的PECS原则,到底是什么鬼?、Java泛型解惑之上下通配符

浙公网安备 33010602011771号