前言:在学习泛型时,很多人对泛型的各种声明搞昏了,泛型可以这样声明List<String> list=new ArrayList<String>();
可以这样声明:List<String> list=new ArrayList<>();
还可以这样声明List list=new ArrayList();
甚至还可以这样声明List<?> list=new ArrayList<>();
那声明的这些泛型对象到底有什么区别,可不可以这样声明:List<> list=new ArrayList();
呢?现在就给大家讲解一下
一、回答这个问题之前,我们先看下各种可能的方式?
从前面可知,泛型声明的语法格式为:类名[a] 变量名=new 类名b;
其中[a]可以为:<String>、<>、<?>、省略
四种类型
其中[b]可以为:<String>、<>、<?>、省略
四种类型
计算一下可知泛型的声明共有4×4=16种可能的声明方式,如下:
1.List<String> list=new ArrayList<String>();
2.List<String> list=new ArrayList<>();
3.List<String> list=new ArrayList<?>();
4.List<String> list=new ArrayList();
5.List<> list=new ArrayList<String>();
6.List<> list=new ArrayList<>();
7.List<> list=new ArrayList<?>();
8.List<> list=new ArrayList();
9.List<?> list=new ArrayList<String>();
10.List<?> list=new ArrayList<>();
11.List<?> list=new ArrayList<?>();
12.List<?> list=new ArrayList();
13.List list=new ArrayList<String>();
14.List list=new ArrayList<>();
15.List list=new ArrayList<?>();
16.List list=new ArrayList();
没错,就是这么多种可能性,共16种,这些声明肯定有些是不合法(不能编译通过),有些是合法的(能编译通过);而泛型对编译通过的代码设计原则是:只要代码在编译时未提出"[unchecked]未经检查的转换",那么在程序运行时不会引发ClassCastException警告,所以可以将上面16种声明分为:不合法的声明、有警告的声明和没有警告的声明共三种情况,下面我们可以将上面代码放到开发工具中查看以上声明到底属于哪种情况
二、16种泛型声明放到开发工具中得出的分析结果:
List<String> list1=new ArrayList<String>();//合法,没有警告
List<String> list2=new ArrayList<>();//合法,菱形写法,没有警告
List<String> list3=new ArrayList<?>();//不合法,需要强制转换为(List<String>),转换后也不能实例化ArryList<?>,因此此句不合法
List<String> list4=new ArrayList();//合法,unchecked警告且ArrayList是原始类型
List<> list5=new ArrayList<String>();//不合法,类型列表的参数数目不正确
List<> list6=new ArrayList<>();//不合法,类型列表的参数数目不正确
List<> list7=new ArrayList<?>();//不合法,不能实例化ArryList<?>且类型列表的参数数目不正确
List<> list8=new ArrayList();//不合法,类型列表的参数数目不正确
List<?> list9=new ArrayList<String>();//合法,没有警告
List<?> list10=new ArrayList<>();//合法,没有警告
List<?> list11=new ArrayList<?>();//不合法,不能实例化ArryList<?>
List<?> list12=new ArrayList();//合法,警告ArrayList是原始类型
List list13=new ArrayList<String>();//合法,警告List是原始类型
List list14=new ArrayList<>();//合法,警告List是原始类型
List list15=new ArrayList<?>();//不合法,不能实例化ArryList<?>
List list16=new ArrayList();//合法,警告ArrayList是原始类型,List是原始类型
2.1.分析不合法的结果:
List<String> list3=new ArrayList<?>();//不合法,需要强制转换为(List<String>),转换后也不能实例化ArryList<?>,因此此句不合法
List<> list5=new ArrayList<String>();//不合法,类型列表的参数数目不正确
List<> list6=new ArrayList<>();//不合法,类型列表的参数数目不正确
List<> list7=new ArrayList<?>();//不合法,不能实例化ArryList<?>且类型列表的参数数目不正确
List<> list8=new ArrayList();//不合法,类型列表的参数数目不正确
List<?> list11=new ArrayList<?>();//不合法,不能实例化ArryList<?>
List list15=new ArrayList<?>();//不合法,不能实例化ArryList<?>
可看出不合法(编译不通过)产生的原因有两种:一种是new ArrayList<?>();
导致的,因为?代表通配符,代表不确定的类型,如果该实例化语句能通过编译器,JVM在实例化构造器的时候,也会因为不知道是什么类型,根本不知道分配什么样的内存空间给该实例,所以在编译阶段new ArrayList<?>();
这样的语句是不能通过编译的。另一种是List<> list这样的声明语句导致的,因为List类定义为public class List<E>{...}
,类型参数数目为1,而List<>
表示List的类型参数数目为0,所以导致编译不通过
2.2分析合法结果:
从上面的结果可以看出声明如下的泛型类引用变量是合法的:
List<String> list
List<?> list
List list
实例化如下泛型类也是合法的:
new ArrayList<String>()
new ArrayList<>()
new ArrayList()
再算一下,得出合法的创建泛型类实例并赋值给泛型类引用的方式共有3×3=9种,如下:
List<String> list1=new ArrayList<String>();//合法,没有警告
List<String> list2=new ArrayList<>();//合法,菱形写法,没有警告
List<String> list4=new ArrayList();//合法,unchecked警告且ArrayList是原始类型
List<?> list9=new ArrayList<String>();//合法,没有警告
List<?> list10=new ArrayList<>();//合法,没有警告
List<?> list12=new ArrayList();//合法,警告ArrayList是原始类型
List list13=new ArrayList<String>();//合法,警告List是原始类型
List list14=new ArrayList<>();//合法,警告List是原始类型
List list16=new ArrayList();//合法,警告ArrayList是原始类型,List是原始类型
从上面结果可以看出,在创建和声明泛型类的时候,如果没有尖括号(<>),都会将该泛型对象或类型变量看做原始类型,因此new ArrayList()
和List list
会发出"ArrayList是原始类型,List是原始类型
"这样的警告。而在编译时编译器不会对原始类型进行类型安全检查,却会对带参数的类型进行检查,因此将原始类型对象或引用赋给带参数的引用警告会发出unchecked警告,比如:List<String> list4=new ArrayList();//合法,unchecked警告且ArrayList是原始类型
。那这些合法的语法有什么区别呢?下面将列出各种可能的疑问,解答各位的问题。
三、合法泛型声明之间的区别疑问?
1.泛型是什么?
答:泛型又叫参数化形参,将类型当做参数化处理,比如我们定义一个泛型类class Person<T>{}
,在这里T就是类型形参,和以往方法中的数据形参(我们也叫形参)不同,这里的T是类型,只能传入类名。
2.既然T是一种类型形参,按照调用含有数据形参的方法时需要传入参数的思路,为什么在使用List时声明List list=new ArrayList();
不使用尖括号<>传入实际的类型呢?
答:这是为了解决兼容性的问题,老版本泛型类的声明和创建实例都是使用了List list=new ArrayList();
这种形式的,为了和老版本兼容,在后面的设计中,就允许了这种写法,在此顺便介绍下各种版本泛型集合的声明和书写形式:
老版本:
List list=new ArrayList();
List list=new ArrayList<String>();
List<String> list=new ArrayList();
JDK1.5推荐写法
List<String> list1=new ArrayList<String>();
JDK1.7推荐的菱形写法
List<String> list2=new ArrayList<>();
3.List list=new ArrayList();
既然没有传入类型形参,那为什么可以创建实例呢?比如我定义了一个泛型类class Person<T>{ T a;}
,然后创建Person泛型类实例:new Person();由于没有传入T的类型,a的类型应该是不知道的,JVM根本不知道分配什么样的内存空间给a,那这样的语句new Person();new ArrayList();都是违法的啊,为什么可以没错呢?
答:因为在源程序送交给JVM执行之前,都会先经过编译成.class
文件再交给JVM执行,泛型类在编译成.class
文件时,如果没有指定类型参数T,默认会将T擦除,替换为T的上限类型,比如上面的Person类中的T,会擦除并替换为Object,我们将没有指定类型参数,即没有使用尖括号(<>)指定类型参数的类称为原始类型,这就是为什么List list16=new ArrayList();
会出现警告:ArrayList是原始类型,List是原始类型
4.List list14=new ArrayList<>();//合法,警告List是原始类型
中new ArrayList<>()的尖括号中也没有指定类型参数,为什么就不会出现ArrayList是原始类型的警告呢?
答:这是一种规定,只要在泛型类后加上尖括号(<>),则编译器就不会报泛型类是原始类型的警告。如果是List<String> list2=new ArrayList<>();
这种写法,编译器会推断ArrayList的类型变量T是String;如果是创建匿名泛型类对象,`new ArrayList<>();中的类型变量T是其上限类型Object,因此使用匿名泛型类对象的add方法可以添加任何元素。
5.List list,List<String> list,List<?>
三种声明的区别?
答:1.List list
表明该list引用的List是原始类型,在编译时不会进行类型检查,类型参数T被擦除为T的上限Object,因此在调用list的方法,比如add(T e)时,由于T被擦除替换为Object,相当于调用了add(Object e),因此可以往list中添加任何类型的元素。2.List<String>
list则表明类型参数T为String,在编译时会进行类型检查,因此在调用list的方法,比如add(T e)时,会先检查传入的参数是否为String类型,如果不是,则不通过编译。如果是,则再将T擦除替换为Object,编译成.class
文件。3.LIst<?> list
是比较误导人的,因为?代表类型通配符,可以匹配所有的类型,那是不是代表在调用list方法时可以传入任何类型呢?答案为不是,比如调用list的add(T e)方法,实际是调用了add(? e)方法,因为?代表不确定类型,不是代表具体的Object类型,所以在调用该方法分配栈内存的时候,不知道分配什么样的内存给参数e,所以一般不能调用add(? e)这样的方法,但因为null是所有类型变量的实例,所以调用这样的方法只能是list.add(null)这样的形式。
6.new ArrayList<>();new ArrayList<String>();new ArrayList();
的区别?
答:要回答这个问题,我们得先知道new的作用,new就是调用类的构造器,分配一块堆内存,初始化实例变量,而在调用new分配实例内存之前,会触发类的加载,加载.class
文件,在类加载机制完成后,生成对应的.class
文件的对象,再根据该对象对实例变量分配内存空间,在此,我们编写如下代码进行分析:
class Person<T>{
T myTest;
public static void main(String[] args){
Person p1=new Person<String>();//代码1
Person p2=new Person<Integer>();//代码2
Person p3=new Person();//代码3
}
}
由于Person类含有类型参数不确定的T,如果直接将源代码编译后不改变为Person.class,myTest的类型参数仍为T,显然由于JVM不知道myTest的类型,不知道如何分配内存,这种做法是行不通的,那怎么办呢?只有改变编译后的Person.class文件才可以,必须确定编译后的Person.class文件中myTest变量的类型,能不能为每个传入Person类型参数的实例分别建立一个Person.class文件呢?比如new Person<String>();
就建立一个Person<String>.class
文件,new Person<Integer>();
就建立一个Person<Integer>.class
文件,这种方法看起来可行,但是T是可以为任意类型的,假设有有1万种不同的类型传给T并创建实例,就要建立1万个Person<类名>.class
这样的文件,很显然是行不通的。编译上面源文件,发现就只生成了一个Person.class文件,也就是说在Person.class文件中T的类型参数是确定,这就是Java中编译器对泛型类的擦除机制,在检验正确完成后,编译器会擦除所有的尖括号<T>
并将实例源代码中的T替换为T的上限类型,比如上面的Person.class文件中mytest的类型就被擦除替换为确定的类型Object,所以new Person<String>();new Person<Integer>();new Person();
都是根据同一份Person.class文件创建对象实例的,实例变量myTest的类型都是Object。
7.List<Objetct>
为什么不能是List<Integer>
的父类?
答:这个问题可以从多方面考虑,比较两个引用类型是否是父子关系,其实是从它们加载了.class
文件后生成的对应对象比较的,由于List<Objetct>
和List<Integer>
都是加载同一份.class
文件,当然就没有继承一说;另一方面,如果List<Objetct>
是List<Integer>
的父类,将List<Integer>
中的元素放到List<Object>
中自然是没问什么的,但是当List<Object>
向下转型给List<Integer>
中就会出现问题,因为List<Object>
可以放各种类型的元素,当向下转型给List<Integer>
就得将List<Object>
中的元素取出后比较类型是否为Integer才能转型给List<Integer>
,这样将导致性能的下降等问题;再一方面由于Java编译器的擦除机制,其实List<Integer>
中的类型参数擦除后也是Object,自然就不存在List<Integer>
和List<Object>
存在父子关系。无论如何,只要记住Java中规定这两者之间不存在父子关系即可。
8.?类型通配符的一点讲解?
答:?类型通配符的作用主要用于方法形参定义中限定类型参数的上限和下限以及不限制三种情况,以下面代码说明:
public Test<T>{
public void test1(List<? extends Number>){}//表明test1只接收类型为List<Number>、List<Integer>、List<Float>这样的引用变量
public void test2(List<? super Number>){}//表明test2只接收类型为List<Number>、List<Oubject>这样的对象引用变量
public void test3(List<?>){}//表名test3可接收任何形式的List引用变量,相当于List<? extends Object>
public static void main(String[] args){
Test<String> test=new Test();//必须声明为Test<String> test,才会在调用test的方法时触发类型检查,否则下面代码不会报编译错误
List<Object> list1=null;
List<Number> list2=null;
List<Integer> list3=null;
List<?> list4=null;
List list5=null;//警告List是原始类型
//测试test1
test.test1(list1);//编译不通过,因为Object不是Number的子类或Number本身
test.test1(list2);
test.test1(list3);
test.test1(list4);//编译不通过,因为?代表未知类型,不能和Number比较
test.test1(list5);//编译通过,前面说过将原始类型赋给指定类型的类型变量会出现unchecked警告
//测试test2
test.test2(list1);
test.test2(list2);
test.test2(list3);//编译不通过,因为Integer不是Number的父类或Number本身
test.test2(list4);//编译不通过,因为?代表未知类型,不能和Number比较
test.test2(list5);//编译通过,前面说过将原始类型赋给指定类型类型变量会出现unchecked警告
//测试test3
test.test3(list1);
test.test3(list2);
test.test3(list3);
test.test3(list4);
test.test3(list5);
}
}
9.总结:
总之,Java中的泛型是伪泛型,设计过程中有着诸多的缺陷,靠一本书上的知识是不够弄清泛型的缺陷,只有多提疑问,多编程,多比较,多总结才能发现Java泛型中的缺陷,上面介绍的缺陷知识Java泛型中的一部分,希望读者多给自己提问,多编程以熟悉Java泛型。