引言
泛型根据字面意思就是,广泛的类型。它可以表示任意一个具体的类型,这就相当于一种表示类型的变量。例如在声明一个一个变量a的时候,如果我们想让这个a根据业务逻辑的不同,可以有多种表现,也就是可以有不同的数据类型的时候,就可以用到泛型。用泛型声明这个变量a。
int a; // 引入泛型前的写法:定死了,只能是int类型
T a; // 引入后 ,类型就是动态的,可以根据业务需求让a声明为对应的类型
泛型是如何实现的?
试想一下,如果让你来实现,你会怎么实现泛型?难点就在于如何能让类型实现这种动态绑定,如何根据传入的类型来决定某个参数的类型。
这时可以想到Object,因为它是所有类的超类,如果我用Object来声明变量,那我可以将任意类型对象赋值给这个变量(向上转型)。取变量值的时候只需要向下转型即可。
class Zoo{
Object animal;
public Zoo(Object animal){
this.animal = animal;
}
}
class Cat{
public void eat(){
System.out.println("猫吃鱼");
}
}
class Dog{
public void eat(){
System.out.println("狗吃骨头");
}
}
public class Test{
public static void main(String[] args){
Zoo zoo1= new Zoo(new Cat()); // 创建一个有小猫的动物园
Cat cat = (Cat)zoo1.animal; // 输出猫对象
cat.eat(); // 输出 猫吃鱼
Zoo zoo2= new Zoo(new Dog()); // 创建一个有小狗的动物园
Dog dog = (Dog)zoo2.animal; // 输出狗对象
dog.eat(); // 输出 狗吃骨头
}
}
上述代码就是在不引用泛型的情况下,设计Zoo动物园类,可以接收任意类型的动物。因为不知道属性animal的具体类型(可以是猫也可以是狗),所以将属性animal设为Object类型,这样无论什么类型都可以通过类型自动向上提升为Object从而被放进animal中。虽然我们想要的效果实现了,但是它的代码却显得很不智能和繁琐,因为在取出的时候还需强转为你放入的类型,而强转的操作都是由程序员自己决定和自己承担风险,这也就意味着程序员如果想正确的运行不同动物的eat方法还需要自己检查实际运行类型是否和强转类型一致。
其实更严谨的设计一点,应该再设计一个Animal类,然后让Cat类和Dog类继承这个类,让Zoo的成员属性animal静态编译类型为Animal,既可以保证接收任意动物,也可以保证接收的一定是动物类型。对于限制类型这一点,泛型的设计者也考虑到了,所以有了泛型边界。如:< ? extends Animal> 限制上界,最高父类为Animal, 限制下界: <? super Animal>最低子类为Animal。
而实际上泛型的设计也确实是通过类似的方式实现的,因为泛型到编译时期就会全部擦除为Object类型,而有上界则会被擦除为上界类型(< ? extends Animal>会被擦除为Animal),有下界的依然被擦除为Object。那既然这样为什么不直接用Object实现这种功能,还要再设计出泛型呢?因为其实泛型还有一个好处就是带来编译时可以进行类型检查,它不允许你把不兼容的数据赋值给你传入的类型修饰的变量。有了这层检查机制之后,也就自然不需要你再强转了,因为编译器认为他已经检查了你数据的类型是一致的。
例如你声明了List<String> list;那么这个list只会允许你加入String类型的数据,你加入其他类型数据,编译器会直接就报红,不会到运行的时候才抛出异常,直接就在编写代码的时候把问题暴露给你,让你进行处理。所以这也是为什么可以把泛型擦除为Object,因为在编译层面它就保证了你加入的只能是你传入的类型(保证了类型安全),这个时候你的编译类型被擦除成什么都不重要了。
为什么不能创建泛型实例或泛型数组?
如果你了解了泛型的本质以及是如何实现泛型的,就可以以此为基础去看待这个问题。前文说了泛型在编译时期就会被全部被擦除为Object(没规定上界的情况下),也就是说泛型这个机制其实只是存在在编写代码时期,也就是只存在你的.java文件中,并且编译器在这个期间会进行类型检查,仅此而已。
假设可以创建泛型实例:
class A<T>{
T instance = new T(); // 相当于 Object instance = new Object();
}
public class Test{
public static void main(String[] args) {
A<Date> a = new A<>();
Date date = a.instance; // 会运行转换异常,因为实际运行类型是Object
date.getTime();
}
}
假设传入的类型是一个Date类,明明是想要创建一个Date的实例,而实际创建的却是一个Object对象。从这里就可以看出Java对泛型的设计并没有这么智能,它只停留在静态层面,并不能实现动态的创建一个我们想要的实例对象。这时候可能就会疑问了,既然泛型并不能创建实例对象,那泛型又是怎么让取出的对象确实是传入类型的实例呢?
还是以List为例:
List<String> list = new ArrayList<>();
list.add("hhh");
String s = list.get(0);
System.out.print(s); // 打印出 hhh
这段代码就可以发现,我们想要一个装String类型的list,而String的实例全都是由程序员自己写入创建的,并不是利用泛型在底层帮我们动态实现的,泛型仅仅是在此时检查了add的类型只能为String。其次就是就算真的能动态创建实例对象,但是像T a = new T(); 代码本身也是有问题的,因为万一传入的类型正好是没有无参构造方法的,运行就会报异常。
至此如果你明白了为什么不能创建泛型实例,相信也会对为什么不能创建泛型数组有一定体会。但是还是有细微区别。因为就算是创建Object[]数组,其实对于数组中存储的每个元素来讲也是没有影响的,只是对数组整体来讲会有影响。因为数组的每个元素依然是程序员自己创建的实例然后放入数组。这样想好像泛型数组没有什么大问题,因为数组中每个元素的实例是程序员自己创建的,而泛型的编译检查机制又会限制程序员只能在数组中放入传入类型的实例。看似保证了安全,其实还是会有隐藏的安全问题。
假设可以创建泛型数组:
class A<T>{
T[] arr = new T[]; // 擦除后: Object[] arr = new Object[];
}
class Test{
public static void main(String[] args) {
A<Integer> a = new A<>();
a.arr[0] = 1; // 成功放入
a.arr[1] = "hhhh"; // 编译报错,编译器检查出了和传入的类型不匹配
Object[] obj = a.arr;
obj[1] = "hhh"; // 成功放入
Intger[] arr = a.arr; // 类型转换异常 ----> Object数组不能转换为Integer数组
}
}
从上面的代码就可以看出,创建泛型数组不仅可能引发像创建泛型实例那样的类型转换异常,还有可能绕过编译检查机制,放入和传入类型不兼容的数据类型。因为数据具有协变性(协变性指:子父类数组的引用可以指向子类数组),所以Object[] obj = a.arr; 这行代码当然是可行的,故obj[1] = "hhh"; 也是可以成功执行的,因为编译器检查发现“hhh”为String类型,obj的静态类型为Object,而String是Object的子类,当然可以允许放入。这样就绕过了检查,并且也不会有运行异常,因为数组的实际运行类型就是Object数组。Object数组的数组元素可以是任意对象。这样就违背了我们的本意,我们传入类型参数Integer就是想让数组只存放Integer类型的数据,但是编译器的检查机制被绕过了,并且运行时也不会报异常,那就会存在一种问题,就是数组里面可能已经含有不是Integer的数据了,但是却无法被发现,这种错误可能会导致后续处理一些代码逻辑时造成安全问题。
并且从数组的层面上,数组的协变性是基于数组的类型安全检查的,也就是数组能够记住元素的类型并进行运行期类型检查。
Number[] numbers=new Integer[10];
numbers[0]=new Double(3.14); // 报错 类型转换异常
上述代码就体现了数组的安全性,因为问题是可以被暴露出来的。泛型数组违背了这种安全机制。
那么就不能使用泛型数组了吗?其实Java只是不允许创建泛型数组,但是可以使用泛型数组。如下:
class A<T>{
T[] arr =(T[]) new Object[];
}
其实好像跟之前的写法没有区别,并且在擦除之后,确实也是跟之前的写法一样的。那为什么这样写就允许呢?因为此时数组是new的一个具体类型,然后再进行强转,这意味着编译器已经提醒你有风险了,但是你依然决定强转,所以你就得自己承担这个风险。
扩展:正确创建泛型数组的方式
可以利用反射来创建泛型数组,这其实相当于真的创建了一个传入类型的实例数组,而不是Object数组。
还是根据上面的测试代码,测试这样创建泛型数组会有什么不一样的结果。
class A<T>{
T[] arr;
public A(Class<T> clazz){
arr = (T[])Array.newinstance(clazz,10);
}
}
class Test{
public static void main(String[] args) {
A<Integer> a = new A<>();
a.arr[0] = 1; // 成功放入
a.arr[1] = "hhhh"; // 编译报错,编译器检查出了和传入的类型不匹配
Object[] obj = a.arr;
obj[1] = "hhh"; // 类型转换异常 ---> String类型不能转换为Integer
Intger[] arr = a.arr; // 成功取出Integer数组
}
}
根据测试结果,可以得出这样创建泛型数组无论在什么层面来看都是安全有保证的。