泛型
泛型,即“参数化类型”。一提到参数,最熟悉的就是定义方法时有形参,然后调用此方法时传递实参。那么参数化类型怎么理解呢?顾名思义,就是将类型由原来的具体的类型参数化,类似于方法中的变量参数,此时类型也定义成参数形式(可以称之为类型形参),然后在使用/调用时传入具体的类型(类型实参)。
泛型的本质是为了参数化类型(在不创建新的类型的情况下,通过泛型指定的不同类型来控制形参具体限制的类型)。也就是说在泛型使用过程中,操作的数据类型被指定为一个参数,这种参数类型可以用在类、接口和方法中,分别被称为泛型类、泛型接口、泛型方法。
泛型的表示
泛型可以定义在类、接口、方法中,分别表示为泛型类、泛型接口、泛型方法。泛型的使用需要先声明,声明通过<符号>的方式,符号可以任意,编译器通过识别尖括号和尖括号内的字母来解析泛型。
泛型的类型只能为类,不能为基本数据类型。尖括号的位置也是固定的,只能在类名之后或方法返回值之前。
一般泛型有约定的符号:E 代表 Element,<E> 通常在集合中使用;T 代表 Type,<T >通常用于表示类;K 代表 Key,V 代表 Value,<K, V> 通常用于键值对的表示;? 代表泛型通配符。
泛型的表达式有如下几种:
- 普通符号 <T>
- 无边界通配符 <?>
- 上界通配符 <? extends E> 父类是 E
- 下界通配符 <? super E> 是 E 的父类
泛型类
格式:修饰符 class 类名<代表泛型的变量> { }
/*
1:把泛型定义在类上
2:类型变量定义在类上,方法中也可以使用
*/
public class ObjectTool<T> {
private T obj;
public T getObj() {
return obj;
}
public void setObj(T obj) {
this.obj = obj;
}
}
泛型接口
在接口上定义的泛型,当一个类型未确定的类实现接口时,需要声明该类型
public interface CalcGeneric<T> {
T add(T num1, T num2);
}
public class CalculatorGeneric<T> implements CalcGeneric<T> {
@Override
public T add(T num1, T num2) {
return null;
}
}
泛型方法
格式:修饰符 <代表泛型的变量> 返回值类型 方法名(参数){ }
//定义泛型方法..
public <T> T show(T t) {
System.out.println(t);
}
泛型通配符
使用 ? 表示未知通配符
<? extends Person>:表示可以传递Person及其子类
<? super Person>:表示可以传递Person及其父类
泛型数组
数组是支持协变的,什么是数组的协变呢?
举个例子:这段代码中:
① 数组支持以 1 的方式定义数组,因为 Integer 是 Number 的子类,一个 Integer 对象也是一个 Number 对象,所以一个 Integer 的数组也是一个 Number 的数组,这就是数组的协变。虽然这种写法编译时能通过,但是数组实际上存储的是 Integer 对象。
② 如果加入 Double 对象,那么在运行时就会抛出 ArrayStoreException 异常,该种设计存在缺陷。
③ 3 方式所示的定义数组方式编译错误,
④ 4 所指示的代码才是正确的。
⑤ 泛型是不变的,没有内建的协变类型,使用泛型的时候,类型信息在编译期会被类型擦除,所以泛型将这种错误检测移到了编译器。泛型的设计目的之一就是保证了类型安全,让这种运行时期的错误在编译期就能发现,所以泛型是不支持协变的,如 5 所示的该行代码会有编译错误。
public class Test {
public static void main(String[] args) {
Number[] numbers = new Integer[10]; // 1 正确
// java.lang.ArrayStoreException: java.lang.Double
numbers[0] = new Double(1); // 2 错误
List<String>[] list = new ArrayList<String>[10]; // 3 错误
List<String>[] list2 = new ArrayList[10]; // 4 正确
List<Number> list3 = new ArrayList<Integer>(); // 5
}
}
泛型擦除
在泛型内部,无法获得任何有关泛型参数类型的信息,泛型只在编译阶段有效,泛型类型在逻辑上可看成是多个不同的类型,但是其实质都是同一个类型。因为泛型是在JDK5之后才出现的,需要处理 JDK5之前的非泛型类库。擦除的核心动机是它使得泛化的客户端可以用非泛化的类库实现,反之亦然,这经常被称为"迁移兼容性"。
代价:泛型不能用于显式地引用运行时类型地操作之中,例如转型、instanceof 操作和 new 表达式,因为所有关于参数地类型信息都丢失了。无论何时,当你在编写这个类的代码的时候,提醒自己,他只是个Object。catch 语句不能捕获泛型类型的异常。
public static void method1() {
List<Integer> integerArrayList = new ArrayList();
List<String> stringArrayList = new ArrayList();
System.out.println(integerArrayList.getClass());
System.out.println(stringArrayList.getClass());
System.out.println(integerArrayList.getClass() == stringArrayList.getClass());
}
结果:
class java.util.ArrayList
class java.util.ArrayList
true
将上面的 Java 代码编译成字节码后查看也可看见两个集合都是 java/util/ArrayList
public static method1()V
L0
LINENUMBER 14 L0
NEW java/util/ArrayList
DUP
INVOKESPECIAL java/util/ArrayList.<init> ()V
ASTORE 0
L1
LINENUMBER 15 L1
NEW java/util/ArrayList
DUP
INVOKESPECIAL java/util/ArrayList.<init> ()V
ASTORE 1
因为在运行期间类型擦除的关系,可以通过反射在运行期间修改集合能添加的类,不过添加后查询该集合会抛出 ClassCastException 异常,代码如下。
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
ArrayList<String> stringArrayList = new ArrayList<>();
stringArrayList.add("hnc");
stringArrayList.add("boy");
System.out.println("之前长度:" + stringArrayList.size());
// 通过反射增加元素
Class<?> clazz = stringArrayList.getClass();
Method method = clazz.getDeclaredMethod("add", Object.class);
method.invoke(stringArrayList, 60);
System.out.println("之后长度:" + stringArrayList.size());
// 存的还是 Integer 类型
// java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
for (int i = 0; i < stringArrayList.size(); i++) {
System.out.println(stringArrayList.get(i).getClass());
}
}
运行结果:
之前长度:2
之后长度:3
class java.lang.String
class java.lang.String
Exception in thread "main" java.lang.ClassCastException:
java.lang.Integer cannot be cast to java.lang.String at XXX
总结
数组不支持泛型
泛型的类型不能为基础数据类型
泛型只在编译阶段有效