泛型(张孝祥Java视频笔记)
为什么使用泛型
之所以使用泛型,很大程度上是为了让集合记住其中元素的数据类型类型,在泛型出现之前,一旦把一个对象丢进集合中,集合就会忘记对象的类型,把所有的对象当成object类型来进行处理。当程序从集合中取出对象后,就需要进行强制类型转换,这会让代码变得臃肿,同时类型转换的时候也有可能会出现ClassCastException异常,代码不安全。使用了泛型之后,编译器在编译代码的时候,会进行类型检查,如果试图向集合中添加不满足类型要求的对象,编译器会提示类型错误,这提高了代码的安全性,而且取出元素的时候,会自动进行强制类型转换,代码更加简洁,使代码不那么臃肿;其次使用泛型,可以提高代码的通用性。
37节视频
泛型是给编译器看的,让编译器挡住非法类型的输入,编译器编译带类型说明的集合,生成字节码前会去除掉“类型”信息,使程序的运行效率不受影响,对于参数化的泛型类型,getClass()方法的返回值跟原始类型相同。由于编译生成的字节码会去除掉泛型的类型信息,只要能跳过编译器,就可以往某个泛型的集合中加入另一个类型的数据。
1 | ArrayList<String> collection2 = new ArrayList<String>(); |
泛型术语
整个称为ArrayList<E>
泛型类型ArrayList<E>
中E称为类型变量或类型参数
整个ArrayList<Integer>
称为参数化的类型ArrayList<Integer>
中的Integer称为类型参数的实例或者实际类型参数ArrayList<E>
中的,<>称为type ofArrayList
称为原始类型
参数化类型跟原始类型兼容 即:为了与JDK1.4兼容,泛型可以一边有约束,一边无约束
(1)参数化的类型可以引用一个原始类型的对象1
ArrayList<String> list = new ArrayList<>();
(2)原始类型可以引用一个参数化类型的对象1
Collection c = new ArrayList<String>();
注:将参数化的类型转换成原始类型后,可能会发生类型错误,例如:1
2
3
4ArrayList<String> strList = new ArrayList<>();
List list =strList;//ok
list.add(1);// only a compile-time warning 不会提示错误,只会提示一个警告,但将list的数据取出进行强制类型转换的时候会出错
String str = (String)list.get(0);//Error 报错
为了避免上述错误,可以用检查视图来检测这类问题1
2
3
4
5ArrayList<String> strLista = new ArrayList<>();
List<String> strListb = Collections.checkedList(strLista,String.class);
List list =strListb;//ok
list.add(1);//Error 报错
String str = (String)list.get(0);
参数化类型不考虑类型继承关系,即:泛型二边要么都不使用约束,要么二边约束一致类型,或者一边有约束一边没有约束,同时二边必须使用引用类型(不能用基本类型实例化类型参数)1
2ArrayList<Integer>并不是ArrayList<Object>的子类,前者的不能赋值给后者
ArrayList<Object> obj = new ArrayList<Integer>;//这是错误的
与数组对比,数组是可以这样赋值的:1
2
3Integer[] array = new Integer[100];
Number[] a = array;
a[0]=0.5;
编译的时候不会出错,但是运行的时候会出错,这是因为0.5并不是Integer类型。在Java设计的早期允许将Integer[]变量赋值Number[]是存在缺陷的,因此Java在设计泛型的时候进行了改进,不再允许把ArrayList
在创建数组实例时,数组的元素不能使用参数化的类型,可以声明这样的数组,但是实例化数组的时候会出错1
ArrayList<String>[] array = new ArrayList<String>[10];//这是错误的
38节视频
泛型中的通配符
使用?
可以引用其他各种参数化的类型,?
通配符定义的变量主要用作引用,可以调用参数无关的方法,不能调用参数化有关的方法1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16public static void printCollection(Collection<Object> c){
for(Object obj :c){
System.out.println(obj);
}
c.add("abc");//正确
c = new ArrayList<String>();//错误
}
public static void printCollection(Collection<?> c){
for(Object obj :c){
System.out.println(obj);
}
c.size();//正确
c.add("abc");//错误
c = new ArrayList<String>();//正确
}
类型通配符的扩展1
2ArrayList<? extends Number>
ArrayList<? super Integer>
39节视频
1 | HashMap<String,Integer> maps = new HashMap<String, Integer>(); //泛型参数化,不能是基本类型 |
40,41节视频
自定义泛型方法1
2
3
4
5
6
7private static <T> T add(T x,T y){
return null;
}
add(3,4); T ---Integer
add(3,3.5); T ---Number
add(3,"abc"); T ---Object
T只能是引用类型,不能是基本类型,上面的基本类型被自动装箱成相应的引用类型。
1 | private static <T> void swap(T[] a,int i,int j){ |
T只能是引用类型,不能是基本类型,此处无法进行装箱操作
T 可以添加限定,限定: 上限,下限,实现的接口
类型限定符的扩展1
2ArrayList<T extends Number> //即使是接口 ,也用extends而不是implements
ArrayList<T super Integer>
一个类型变量或者是通配符可以有很多个限定,例如1
T extends Comparable&Serializable
限定类型用&分隔,而逗号用来分隔类型变量
在Java的继承中,可以根据需要拥有多个接口超类型,但限定中至多只有一个类。如果用一个类作为限定,他必须是限定列表的第一个。
泛型代码和虚拟机
虚拟机没有泛型类型对象—所有对象都属于普通类。
无论何时定义一个泛型类型,都自动提供了一个相应的原始类型(raw type)。原始类型的 名字就是删去类型参数后的泛型类型名。 擦除( erased)类型变量,并替换为限定类型(无限 定的变量用Object)。
原始类型用第一个限定的类型变量来替换,如果没有给定限定就用Object替换。类Pair<T>
中的类型变量没有显式的限定,因此,原始类型用Object替换T,Pair<T extends Number>
原始类型用Number替换T。
翻译泛型表达式
当程序调用泛型方法时,如果擦除返回类型,编译器插入强制类型转换。例如,下面这个1
2Pair<Employee> pair = new ...
Emplotee employee = pair.getFirst();
语句序列
擦除getFirst的返回类型后将返回Object类型。编译器自动插入Employee的强制类型转换。也就 是说,编译器把这个方法调用翻译为两条虚拟机指令:
- 对原始方法Pair.getFirst的调用。
- 将返回的Object类型强制转换为Employee类型。
当存取一个泛型域时也要插入强制类型转换。假设Pair类的first域和second域都是公有的 (也许这不是一种好的编程风格,但在Java中是合法的)。表达式:1
Emplotee employee = pair.first;
也会在结果字节码中插入强制类型转换。
翻译泛型方法
类型擦除也会出现在泛型方法中。程序员通常认为下述的泛型方法1
public static <T extends Number> T get()
是一个完整的方法族,而擦除类型之后,只剩下一个方法:1
public static Number get()
注意,类型参数T已经被擦除了,只留下了限定类型Number。
练习
《一》1
2
3
4
5private static <T> T autoConvert(Object obj){
return (T)obj;
}
Object obj = "abc";
String x3 = autoConvert(obj);
《二》1
2
3
4
5 private static <T> void fillArray(T[] a,T obj){
for(int i=0;i<a.length;i++){
a[i] = obj;
}
}
《三》1
2
3
4
5
6
7
8
9
10
11
12
13
14public static <T> void printCollection(Collection<T> collection,T obj){
System.out.println(collection.size());
for(Object obj : collection){
System.out.println(obj);
}
collection.add(obj);
}
public static void printCollection(Collection<?> collection){
System.out.println(collection.size());
for(Object obj : collection){
System.out.println(obj);
}
}
通配符方法比泛型方法更有效,此处利用泛型方法可以对集合添加元素
《四》类型推断
1 | public static <T> void copy1(Collection<T> dest,T[] src){ |
42节视频
定义泛型类型
如果类中有多个方法使用泛型,使用类级别的泛型1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21public class GenericDao<E> {
public void add(E x){
}
public E findById(int id){
return null;
}
public void delete(E obj){
}
public void delete(int id){
}
public void update(E obj){
}
}
泛型类,可以看做普通类的工厂类
静态方法,静态代码块,静态成员变量不能使用泛型,这是因为类型参数的类型只有在实例化的时候才能确定,而静态成员变量,静态方法,可以通过通过类名直接进行访问,不用实例化对象,所以不能在静态方法,静态代码块,静态成员变量中使用泛型。
1 | //此处错误 |
43节视频
通过反射获得泛型的实际类型参数
泛型编译后会去参数化(擦拭法),因此无法直接用反射获取泛型的参数类型可以把泛型用做一个方法的参数类型,方法可以保留参数的相关信息,这样就可以用反射先获取方法的信息 然后再进一步获取泛型参数的相关信息,这样就得到了泛型的实际参数类型。
<一>通过反射获得方法的实际类型参数
1 | //声明一个空的方法,并将泛型用做为方法的参数类型 |
打印结果:1
2
3
4
5
6
7
8
9
10方法参数:interface java.util.Collection
泛型参数的实际类型:class java.lang.Number
方法参数:interface java.util.Map
泛型参数的实际类型:class java.lang.String
泛型参数的实际类型:class java.lang.Integer
getGenericParameterTypes方法获得的参数:class java.lang.String
getGenericParameterTypes方法获得的参数:class java.lang.Integer
getParameterTypes方法获得的参数:class java.lang.String
getParameterTypes方法获得的参数:class java.lang.Integer
<二>通过反射获得成员变量的数据类型1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25private Map<String , Integer> score;
Class<GenericTest> clazz = GenericTest.class;
Field f = clazz.getDeclaredField("score");
// 直接使用getType()取出Field类型只对普通类型的Field有效
Class<?> a = f.getType();
// 下面将看到仅输出java.util.Map
System.out.println("score的类型是:" + a);
// 获得Field实例f的泛型类型
Type gType = f.getGenericType();
// 如果gType类型是ParameterizedType对象
if(gType instanceof ParameterizedType)
{
// 强制类型转换
ParameterizedType pType = (ParameterizedType)gType;
// 获取原始类型
Type rType = pType.getRawType();
System.out.println("原始类型是:" + rType);
// 取得泛型类型的泛型参数
Type[] tArgs = pType.getActualTypeArguments();
System.out.println("泛型类型是:");
for (int i = 0; i < tArgs.length; i++)
{
System.out.println("第" + i + "个泛型类型是:" + tArgs[i]);
}
}
getType方法只能获得普通类型的Field的数据类型;对于增加了泛型参数的的Field,如果想要获取其中增加的泛型参数的类型,应该使用getGenericType()方法获得其类型
<三>通过反射获得父类的类的泛型参数的数据类型
1 | public class Person<T> { |
打印结果:1
2
3class com.test.Person
com.test.Person<com.test.Student>
class com.test.Student
运行时类型查询只适用于原始类型
虚拟机中的对象总有一个特定的非泛型类型。因此,所有的类型查询只产生原始类型。 例如,1
2
3List<String> list = new ArrayList<>();
if(list instanceof ArrayList<String>){//ERROR
}
实际上仅仅测试list是否是任意类型的一个ArrayList。下面的测试同样如此1
2if(list instanceof ArrayList<T>){//ERROR
}
这样是正确的1
list instanceof ArrayList<?>
或强制类型转换1
List<Number> listb = (ArrayList<Number>)list //WARNING--can only test that list is a ArrayList
要记住这一风险,无论何时使用instanceof或涉及泛型类型的强制类型转换表达式都会看到 一个编译器警告。
同样的道理, getClass方法总是返回原始类型。例如1
2
3List<String> lista = new ArrayList<>();
List<Number> listb = new ArrayList<>();
if(lista.getClass()==listb.getClass())// they are equal
其比较的结果是true,这是因为两次调用getClass都将返回ArrayList.class。
不能抛出也不能捕获泛型类实例
不能抛出也不能捕获泛型类的对象。事实上,泛型类扩展Throwable都不合法。例如,下 面的定义将不会通过编译:1
public class Problem<T> extends Exception{}//Error--can not extend Throwable
不能在catch子句中使用类型变量。例如,下面的方法将不能通过编译:1
2
3
4
5
6
7
8public static <T extends Throwable> void doWork(T t)
{
try{
}
catch(T e){ //Erroe--can not catch type variable
}
}
但是,在异常声明中可以使用类型变量。下面这个方法是合法的: ??????1
2
3
4
5
6
7
8
9public static <T extends Throwable> void doWork(T t) throws T//ok
{
try{
}
catch(Throwable realCause){
t.initCause(realCause);
throw t;
}
}
不能实例化类型变量
不能使用像new T(…), new T[…]或T.class这样的表达式中的类型变量。但是,可以通过反射调 用Class.newInstance方法来构造泛型对象。遗憾的是,细节有点复杂。具体参考java核心技术讲义。
泛型类的静态上下文中类型变量无效
不能在静态域或方法中引用类型变量。例如,下列高招将无法施展:1
2
3
4
5
6
7
8
9public static Singleton<T>{
private static T singleInstance;//Error
pubclic static T getSingleInstance(){////Error
if(singleInstance==null){
//construct new instance of T
return singleInstance;
}
}
}
如果这个程序能够运行,就可以声明一个Singleton