1 泛型
为什么需要泛型?举例,Java集合中有个 ArrayList
类可以看做是一个“可变长度”的数组,在 ArrayList
类中其实是在用一个 Object
数组来存储真正的元素,如果直接使用 ArrayList
存储 String
类型,在读取数据的时候,就需要强制转型了,这样一来就会很不方便而且容易出错,如果为存储 String
类型单独编写一个 ArrayList
(内部使用一个 String
数组),这样的话编译器就能帮上忙了(其实都是想依靠编译器的强制检查),问题会暂时解决,但如果还需要存储 Integer
类型呢?
所以需要把 ArrayList
变成一种模板(类是实例的模板,泛型是类的模板),这样一来就实现了编写一次模板可以创建任意类型的 ArrayList
。
而泛型就是定义这样一个模板,这样的 类的模板 填充具体类型之后构建出来的类,就拥有具体类型了,此时编译器也能针对具体类型作检查。
类是实例的模板,泛型是类的模板
ArrayList<T>
是泛型ArrayList<String>
是通过泛型模板出来的类new ArrayList<String>()
是类的实例
注意泛型的继承关系,ArrayList<Integer>
可以向上转型为 List<Integer>
,而 ArrayList<Integer>
和 ArrayList<Number>
之间没有继承关系。
试想一下,假如 ArrayList<Integer>
可以向上转型为 ArrayList<Number>
,然后这个转型后的 ArrayList<Number>
是可以接受 Float
类型的,问题就来了,这个转型后的 ArrayList<Number>
实际上还是一个 ArrayList<Integer>
对象,它不可能接受一个 Float
类型,所以在获取数据的时候,会产生 ClassCastException
,例子:
// 假如 ArrayList<Integer> 可以向上转型为 ArrayList<Number>
ArrayList<Integer> integerList = new ArrayList<Integer>();
integerList.add(123);
ArrayList<Number> numberList = integerList;
numberList.add(123.123);
integerList.get(1); // 抛出 ClassCastException,编译器为了避免这样的错误,根本不允许这样转换
因此 编译器为了避免这样的错误,根本不允许这样转换,即使二者比较了 Class
的 isAssignableFrom
也返回了 true
。
个人又能否这样认为泛型的继承关系?
因为 ArrayList<T>
与 List<T>
是 类的模板,T
是通配符,本来 ArrayList
就是 List
的子类,ArrayList<T>
可以向上转型为 List<T>
,所以 ArrayList<Integer>
也就可以向上转型为 List<Integer>
。
而 ArrayList<Integer>
和 ArrayList<Number>
二者只是通过 类的模板 创建的 具体的类型 罢了,是互不相干的类型,它们并没有继承关系,也就无法向上转型了。
1.1 使用泛型
如使用 List
不指定泛型参数类型的时候,即 T
作为 Object
使用,List
的API会变为 Object
类型,当定义了泛型如 List<String>
时,List
的API会变为强类型。
在使用泛型时,可以省略编译器能自动推断出的类型,如:
List<Number> list = new ArrayList<Number>();
// 省略编译器能自动推断出的类型
List<Number> list2 = new ArrayList<>();
1.2 编写泛型类
记得之前有人跟我说过你接触的java泛型类,十之有十都与集合类有关,很少会需要编写泛型类。
其实编写泛型只需根据正常地方式编写一个类,然后把特定的类型扣掉,转换成 <T>
即可,而在类名之后申明 <T>
,如:
public class One<T> {
public T first;
}
定义泛型的时候可以同时定义多个类型,在编写泛型类的时候,泛型类型 <T>
无法用于静态方法或静态字段,它会导致编译错误,而必须另外定义其他参数类型如 <K>
来实现,如:
public static <K> One<K> create(K first) {....}
1.3 泛型的实现方式
Java的泛型实现方式是 擦拭。
在Java泛型代码编译的时候,编译器实际是将泛型参数类型 <T>
统一视为 Object
类型,然后在需要的时候,会根据 实现了安全的强制转型,如:
public class One<T> {
public T first;
}
转换为:
public class One {
public Object first;
}
换句话说JVM虚拟机其实对泛型一无所知,所有的工作都是编译器做的,例如下面两段代码,第一段是我们书写的实际代码,第二段是虚拟机实际上处理的代码:
第一段
public static void main(String[] args) {
One<String> one = new One<>("i am first");
String first = one.first;
}
第二段
public static void main(String[] args) {
One one = new One("i am first"); // 编译器将 <T> 视为 Object 类型
String first = (String) one.first; // 根据 <T> 实现了安全的强制转型
}
所以Java的泛型实际上是由编译器在编译的时候进行的,编译器内部永远把参数类型 <T>
视为 Object
类型处理,因此这就有局限了,如:
- 参数类型
<T>
不能是基本类型 - 无法获取带泛型的
Class
,如One<String>.class
,因为它们都是返回同一个Class
值。比如获取One<String>
实例的Class
和获取One<Integer>
实例的Class
一样,都是返回同一个One.class
,因此也无法判断比较泛型的Class
,比如使用x instanceof One<String>
或使用isAssignableFrom
- 不能直接实例化
T
类型,因为擦拭后实际上是new Object()
,要实例化T
类型可以使用到Class<T>
- 还要注意由擦拭带来的方法重名问题,如使用
<T>
作为参数写了一个public boolean equals(T obj)
方法,由于擦拭参数类型T
会被是为Object
,这就与源生的equals
方法发生重复定义了。
1.4 泛型类的继承
平时定义的类可以继承泛型类,如:
public class IntOne extends One<Integer> {
}
这时候编译器就需要将父类的参数类型 T
即 Integer
保存到子类的 Class
中,因此就可以使用子类的 Class
获取到父类的泛型类型,流程如下:
如果一个子类可以通过 getGenericSuperclass
方法得到 ParameterizedType
实例,就可以通过得到的 ParameterizedType
实例使用 getActualTypeArguments
方法获取 Type
数组,通过强制转换为 Class
,就可以得知该父类的泛型,代码如下:
public static void main(String[] args) {
// 获取子类的 class
Class<IntOne> cls = IntOne.class;
// 这个 Type 的实际类型是 ParameterizedType (参数类型)
Type t = cls.getGenericSuperclass();
// 测试该子类通过 getGenericSuperclass 是否能得到 ParameterizedType 数值,有则强制转换
if (t instanceof ParameterizedType) {
ParameterizedType pType = (ParameterizedType) t;
// 通过使用它的 getActualTypeArguments,便可以返回 Type 类型的数组,其中包含的就是父类中的泛型数组
Type[] types = pType.getActualTypeArguments();
// 这里实例只定义一个 Integer 参数类型,所以数组长度只为 1
Type firstType = types[0];
// 转化为 Class,?号是通配符
Class<?> typeClass = (Class<?>) firstType;
// 输出 class java.lang.Integer
System.out.println(typeClass);
}
}
JDK中定义的
Type
接口,它的实现类有Class
,ParameterizedType
(泛型类),GenericArrayType
(泛型数组) 和WildCardType
(通配符)
1.5 extends
通配符
One<Integer>
不是 One<Number>
的子类,所以在一个方法中想要兼容传入二者,就需要使用到 <? extends Number>
了,它可以让方法接受所有 泛型类型 为 Number
或 Number
子类的 One
类,如:
// 泛型类
class One<T> {
private T first;
public T getFirst() {
return first;
}
public void setFirst(T first) {
this.first = first;
}
}
// 使用 <? extends Number> 通配符,此时只能传入泛型参数类型为Number或子类的One类型了
class OneHelper {
static void test(One<? extends Number> one) {
// 此时编译器不可预测实际类型究竟是Integer类型还是其他,但可以安全地将数值赋予给Number类型的变量
Number float = new Float(1.2f);
Number number = one.getFirst();
// one.setFirst(float);
}
}
查看上面代码,在 test
方法中 one
的 getFirst
方法签名可以看作为:
? extends Number getFirst();
此时编译器不可预测实际类型究竟是 Integer
还是其他,但可以肯定的是它一定是 Number
的子类,所以能够安全地将数值赋予给 Number
类型变量
而在 test
方法中使用 setFirst
方法编译器会报错,其中 setFirst
签名可以看作为:
void setFirst(? extends Number)
假设该方法运行不会报错,那么当传入一个实际类型为 One<Integer>
,而在方法内部创建了一个 Number
实例但实际类型是 Float
,很显然 One<Integer>
没办法接受一个实际类型是 Float
的 Number
。
理解以上的概念要结合什么时候能向上转型和什么时候能向下转型的知识。
因此在使用
<? extends xxx>
统配符作为方法参数的时候,方法中只能允许调用get
方法获取xxx
的引用,而不能调用set
方法传入xxx
的引用,唯一例外就是可以调用set
时传入的是null
值
1.6 super
通配符
针对 extends
通配符作为作为方法参数的时候不能传入引用,这里可以使用 super
通配符。比如上面情况使用 <? super Integer>
通配符可以使方法接受所有 泛型类型 为 Integer
类型或其父类的 One
类。
此时的 setFirst
方法的签名可以看作:
void setFirst(? super Integer)
此时就可以安全的传入 Integer
变量,因为编译器已知 Integer
已经是最底层的子类,即使传入的是不同泛型类型的 One
类,Integer
类也是它们的子类,也就可以安全的转换了。
然而此时也就无法使用 getFirst
方法了,此时它的方法签名可以看作:
? super Integer getFirst();
因为编译器此时无法得知将要获取的数值的类型会是什么,这个类型除 Integer
之外都是 Integer
的父类,也就无法安全的转换为合适的类型(除了 Object
)。
因此在使用
<? super xxx>
统配符作为方法参数的时候,方法中不允许调用get
方法获取xxx
的引用,唯一例外就是将该值转换为Object
类型,允许调用set
方法传入xxx
的引用
1.7 super
通配符与 extends
通配符
在JDK中集合的复制API就是传入了 super
通配符与 extends
通配符的方法参数,这个API为什么要使用这两个通配符定义,而不是简单地使用 <T>
呢?
这是为了实现 List<Integer>
可以复制为 List<Number>
,而 List<Number>
不可以复制为 List<Integer>
1.8 泛型与反射
部分反射API是泛型:
Class<String> cls = String.class;
Class<? super String> supCls = cls.getSuperclass();