Java程序设计9——泛型
泛型是对集合的补充,JDK1.5增加泛型支持很大程度上都是为了让集合能记住其元素的数据类型。在没有泛型之前,一旦把一个对象丢进Java集合中,集合就会忘记对象的类型,把所有的对象都当成Object类型处理。当程序从集合中取出对象后,就需要进行强制类型转换,这种强制类型转换不仅让代码臃肿,而且容易引起ClassCastException异常。
增加了泛型支持后的集合,完全可以记住集合中元素的类型,并可以在编译时检查集合中元素的类型,如果试图向集合中添加不满足类型要求的对象,编译器就会提示错误。增加泛型后的集合,可以让代码更简洁、健壮(Java泛型可以保证如果程序在编译时没有警告,运行时就不会产生ClassCastException异常)
本章不仅会介绍如何通过泛型来实现编译时检测集合元素的类型,更会深入介绍Java泛型的详细用法:包括定义泛型类、泛型接口,以及类型通配符、泛型方法等知识
1 泛型入门
Java的集合有个缺点:当我们把一个对象"丢进"集合里后,集合就会"忘记"这个对象的数据类型,当再次取出该对象时 ,该对象的编译类型就变成了Object类型(其运行时类型没变)。
Java集合之所以被设计成这样,是因为设计集合的程序员不会知道我们需要用它来保存什么类型的对象,所以他们把集合设计成能保存任何类型的对象,只要求具有很好的通用性。但这样的问题是:
1.集合对元素类型没有任何限制,这样可能引发一些问题:例如想创建一个只能保存Dog对象的集合,但程序也可以轻易地将Cat对象丢进去,所以可能引发异常。
2.由于把对象丢进集合时,集合丢失了对象的状态信息,集合只知道它盛装的是Object,因此取出集合元素后通常还需要强制类型转换,这种强制类型转换会增加编程的复杂度,也可能引发ClassCastException。
1.1 编译时不检查类型的异常
1 package chapter8; 2 3 import java.util.*; 4 5 public class ListErr { 6 public static void main(String[] args){ 7 List strList = new ArrayList(); 8 //添加字符串类型对象到List中 9 strList.add("English"); 10 strList.add("Deutsch"); 11 //不小心添加了一个Integer类型 12 strList.add(5); 13 for(int i = 0;i < strList.size();i++){ 14 String str = (String)strList.get(i); 15 System.out.println(str); 16 } 17 18 } 19 } 20 输出结果: 21 English 22 Deutsch 23 Exception in thread "main" java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String 24 at chapter8.ListErr.main(ListErr.java:14)
上面创建了一个List集合,而且该List集合对象只希望保存字符串对象——但我们没有办法进行任何限制,所以把Integer对象丢进去时候,系统也没有发型,这将导致运行时候引起ClassCastException异常:因为城乡试图把一个Integer对象转换为String类型。
1.2 使用泛型
上面程序成功创建了一个特殊的List:strList,这个List集合只能保存字符串对象,不能保存其他类型的对象。创建这种特殊集合的方法是:在集合接口、类后增加尖括号,尖括号里面放一种数据类型,即表明这个集合接口、集合类只能保存特定类型的对象。我们制定的List并不是任意的List,而是一个String的List,写作List<String>。我们说List是带一个类型参数的接口——也就是type parameters接口,不知道谁翻译成泛型(type parameters)接口。本例中类型参数是String。List的参数类型是String或者类型参数是String都说的通,一个意思。集合可以记住元素的类型,所以不需要转换。
2 深入泛型
所谓泛型:就是允许在定义类、接口时指定类型形参,这个类型形参将在声明变量、创建对象时确定(即传入实际的类型参数,也可称为类型实参)。
2.1 定义泛型接口、类
下面是JDK1.5改写后List接口、Iterator接口和Map的代码片段:
1 //定义接口时指定了一个类型形参,形参类型为E 2 public interface List<E>{ 3 //在该接口里,E可作为类型使用 4 //下面方法可以使用E作为参数类型 5 void add(E x); 6 Iterator<E> iterator(); 7 8 } 9 //定义该接口时指定了两个类型形参,其形参名为K、V 10 public interface Map<K,V>{ 11 //在该接口里K,V完全可以作为类型使用 12 Set<K> keySet() 13 }
在用实际创建集合对象时候,可以设置实参类型,从而将实参传递给形参类型,作为实际的泛型参数。但是并不是只有集合类才可以使用泛型声明,虽然泛型是集合类的重要使用场所,普通的类也可以使用泛型。
注意:泛型一定指的是引用数据类型,因为Map集合里面放的是对象,并不是基本数据类型的。
1 package chapter8; 2 3 public class Apple<T> { 4 //使用T类型作为形参类型 5 private T info; 6 public Apple(){ 7 8 } 9 public Apple(T info){ 10 this.info = info; 11 } 12 public void setInfo(T info){ 13 this.info = info; 14 } 15 public T getInfo(){ 16 return this.info; 17 } 18 public static void main(String[] args){ 19 //实例化时候确定形参的实际类型,因为设定了类型是String,所以T就是String类型 20 Apple<String> a1 = new Apple<String>("苹果"); 21 System.out.println(a1.getInfo()); 22 //实例化时候确定形参的实际类型,因为设定了类型是double,所以T就是double类型 23 Apple<Double> a2 = new Apple<Double>(2.3); 24 System.out.println(a2.getInfo()); 25 } 26 }
上面程序定义了带泛型声明的Apple<T>类,实际使用Apple<T>类时会为T形参传入实际类型,这样就可以生成如Apple<String>、Apple<Double>...形式的多个逻辑子类(物理上并不存在)。JDK在定义List、ArrayList等接口、类时使用了类型形参,所以在使用这些类时为之传入了实际类型参数。
注意:当创建带泛型声明的自定义类时,为该类定义构造器时,构造器名还是原来的类名,不要为构造器增加泛型声明,但调用构造器时候可以增加泛型声明。
2.2 从泛型类派生子类
当创建了带泛型声明的接口、父类之后,可以为该接口创建实现类或从父类派生子类,但必须指出的是,当使用这些接口、父类时不能再包含类型形参。例如下面代码是错误的:
1 //定义类A继承Apple类,Apple类不能跟类型形参 2 public class A extends Apple<T>{}
方法中的形参(这种形参代表变量、常量、表达式等数据,这里称为形参或数据形参),只有当定义方法时可以使用数据形参,当调用方法或者说使用方法时必须为这些数据形参传入实际的数据;类似的是:类、接口中的类型形参,只有在定义类、接口时才可以使用类型形参,当使用类、接口时应为类型形参传入实际的类型。
如果想从Apple类派生一个子类,可以改为如下代码
1 //使用Apple类时为T形参传入String类型 2 public class A extends Apple<String>
使用方法时必须为所有的数据形参传入参数值,与使用使用方法不同的是:使用类、接口时可以不为类型形参传入实际类型,下面的代码是正确的:
1 //使用Apple类时,没有为T形参传入实际的类型参数 2 public class A extends Apple
如果从Apple<String>类派生子类,则在Apple类中所有使用T类型形参的地方都将被替换成String类型,即它的子类将会继承到String getInfo()和void setInfo(String info)两个方法,如果子类需要重写父类的方法,必须注意此:
1 public class A1 extends Apple<String>{ 2 //正确重写了父类的方法,返回值与父类Apple<String>的返回值完全相同 3 public String getInfo(){ 4 return "子类" + super.getInfo(); 5 } 6 /* 7 //下面方法是错误的,重写父类方法时返回值类型不一致 8 public Object getInfo(){ 9 return "子类"; 10 } 11 */ 12 }
如果使用Apple类没有传入实际的类型参数,Java编译器将发出警告,使用了未经检查或不安全的操作——这就睡泛型检查警告。如果希望看到这些警告信息,可以通过javac命令增加-Xlint:unchecked选项来看到详细的提示。此时,系统会把Apple<T>类里的T形参当初Object类型处理
2.3 并不存在泛型类
前面我们提到可以把ArrayList<String>类当成ArrayList的子类,事实上ArrayList<String>类也确实是一种特殊的ArrayList类,这个ArrayList<String>对象只能添加String对象作为集合元素。事实上ArrayList<String>生成新的class文件,而且也不会把ArrayList<String>当成新类来处理。
1 List<String> l1 = new ArrayList<String>(); 2 List<Integer> l2 = new ArrayList<Integer>(); 3 //调用getClass方法来比较l1和l2的类是否相等 4 System.out.println(l1.getClass() == l2.getClass());
运行上面的结果可以发现输出的结果是true,也就是说他们有同样的类。
实际上,泛型对其所有可能的类型参数,都具有同样的行为,从而可以把相同的类当成许多不同的类来处理。与此完全一致的是,类的静态常量和方法也在所有的实例间共享,所以在静态方法、静态初始化或者静态变量的声明和初始化中不允许使用类型形参。
public class R<T>{ //下面代码错误,不能在静态属性声明中使用类型形参 static T info; T age; public void foo(T msg){} //下面代码错误,不能在静态方法声明中使用类型形参 public static void bar(T msg){} 由于系统中并不会正在生产泛型类,所以instanceof运算符后不能使用泛型类,下面代码错误: Collection cs = new ArrayList<String>(); //下面代码编译时引起错误:instanceof运算符后面不能使用泛型类 if(cs instanceof List<String>){...} }
2.4 类型通配符
如前所述,当哦我们使用一个泛型类时,应该为这个泛型类传入一个类型实参,如果没有传入类型实际参数,就会引起泛型警告。假设现在需要定义一个方法,该方法里有一个集合形参,集合形参的元素类型是不确定的。
public void test(List c){ for (int i = 0; i < c.size(); i++){ System.out.println(c.get(i)); } }
上面是一段普通的遍历List集合代码。问题是上面程序List是一个有泛型声明的接口,此处使用List接口时没有传入实际类型参数,将引起泛型警告。为此考虑为List接口传入实际的类型参数————因为List集合里元素类型是不确定的。
public void test(List<Object> c){ for (int i = 0; i < c.size(); i++){ System.out.println(c.get(i)); } } List<String> strList = new ArrayList<String>(); //将strList作为参数来调用前面的test方法 test(strList);
编译上面程序将发生错误,无法将test(java.util.List<java.lang.Object>)应用于(java.util.List<java.lang.String>)。也就是说List<String>对象不能当成List<Object>对象使用,也就是List<String>类并不是List<Object>子类。
也就是说:如果Foo是Bar的一个子类型(子类或者子接口),而G是具有泛型声明的类或接口,那么G<Foo>是G<Bar>的子类型并不成立。这与我们通常的习惯看法不同。
与数组进行一下对比,先看一下数组是如何工作的,在数组中,程序可以直接把一个Integer[]数组赋给一个Number[]变量。但如果试图把一个Float对象保存到该Number[]数组中,那么编译可以通过,但运行时会抛出异常:ArrayStoreException:
1 Integer[] ia = new Integer[5]; 2 //可以把一个Integer[]数组赋给Number[]变量 3 Number[] na = ia; 4 //下面代码编译错误,但运行时会引发ArrayStoreException 5 na[0] = 0.5; 6 如果把上例转换成泛型,那么会在编译时失败,这种赋值是不允许的。 7 List<Integer> iList = new ArrayList<Integer>(); 8 //下面语句引起编译错误 9 List<Number> nList = iList; 10 nList.add(0.5);
也就是说:如果使用泛型,只要代码在编译时没有出现警告,就不会遇到运行时ClassCastException。
数组和泛型有所不同,假设Foo是Bar的一个子类型,那么Foo[]依然是Bar[]的子类型;但G<Foo>不是G<Bar>的子类型。
2.4.1 使用类型通配符
为了表示各种泛型List的父类,我们需要使用类型通配符,类型通配符是一个问号(?),将一个问号作为类型实参传给List集合,写作:Collection<?>意思是未知类型元素的List,这个问号被称为通配符,它的元素类型可以匹配任何类型
1 public void test(List<?> c){ 2 for (int i = 0; i < c.size(); i++){ 3 System.out.println(c.get(i)); 4 } 5 }
现在我们可以使用任何类型的List来调用它,程序依然可以访问集合c中的元素,其类型是Object,这永远是安全的,因为不管List的真实类型是什么,它包含的都是Object。但这种带通配符的List仅表示它是各种泛型List的父类,并不能把元素加入到其中,例如:
1 List<?> c = new ArrayList<String>(); 2 //下面程序引起编译时错误
3 c.add(new Object());
因为我们不知道上面程序c集合里的元素类型,所以不能向其中添加对象。所以不能向其中添加对象。根据前面的List<E>接口定义的代码可以发现:add方法又类型参数E作为集合的元素类型,所以我们传给add的参数必须是E类的对象或者其子类的对象。但因为在该例中不知道E是什么类型,所以程序无法将任何对象丢进该集合。唯一例外是null,它是所有引用类型的实例。
另一方面,程序可以调用get()方法来返回List<?>集合指定索引处的元素,其返回值是一个未知类型,但可以肯定的是:它总是一个Object。因此把get()的返回值赋值给一个Object类型的变量,或者放在任何希望是Object类型的地方都可以。
2.4.2设定类型通配符的上限
当直接使用List<?>这种形式时,即表明这个List集合可以是任何泛型List的父类。但还有一种特殊的情形,我们不想这个List<?>是任何泛型List的父类,只想表示它是某一类泛型List的父类。考虑一个简单的绘图程序:先定义三种形状
1 package chapter8; 2 3 import java.awt.Canvas; 4 //定义一个抽象类 5 public abstract class Shape { 6 public abstract void draw(Canvas c); 7 } 8 package chapter8; 9 10 import java.awt.Canvas; 11 //覆写父类方法 12 public class Circle extends Shape{ 13 public void draw(Canvas c){ 14 System.out.println("在画布" + c + "上话一个圆"); 15 } 16 } 17 18 19 package chapter8; 20 21 import java.awt.Canvas; 22 23 public class Rectangle extends Shape{ 24 public void draw(Canvas c){ 25 System.out.println("在画布" + c + "画一个矩形"); 26 } 27 } 28 29 30 考虑Canvas实现类 31 public class Canvas{ 32 //同时在画布上绘制多个形状 33 public void drawAll(List<Shape> shapes){ 34 for (Shape s:shapes){ 35 s.draw(this) 36 } 37 } 38 }
注意上面的drawAll方法的形参类型是List<Shape>,List<Shape>不是List<Circle>的父类,因此将引起编译错误。
1 List<Circle> circleList = new ArrayList<Circle>(); 2 Canvas c = new Canvas(); 3 //不能把List<Circle>当成List<Shape>使用,所以将一起编译错误 4 c.drawAll(shapeList); 5 关键在于List<Shape>不是List<Circle>的父类,所以不能把List<Circle>当成List<Shape>使用,为了表示List<Circle>的父类,可以使用List<?>,把Canvas改写如下: 6 public void drawAll(List<?> shapes){ 7 for (Object obj:shapes){ 8 Shape s = (Shape)obj; 9 s.draw(this) 10 } 11 } 12 }
上面的方法是很丑陋的,我们需要一种泛型表示方法,它可以表示所有Shape泛型List的父类,为了满足这种需求,Java泛型提供了被限制的泛型通配符
1 //它表示所有Shape泛型List的父类的子类 2 List<? extends Shape> 3 4 再次改写上面的程序 5 package chapter8; 6 7 import java.util.ArrayList; 8 import java.util.List; 9 10 public class Canvas { 11 //同时在多个画布上画多个形状 12 public void drawAll(List<? extends Shape> shapes){ 13 for(Shape s:shapes){ 14 s.draw(this); 15 } 16 } 17 public static void main(String[] args){ 18 List<Circle> circleList = new ArrayList<Circle>(); 19 circleList.add(new Circle()); 20 Canvas c = new Canvas(); 21 c.drawAll(circleList); 22 } 23 }
上面这个是受限通配符例子,这个未知类型总是Shape的子类或者Shape本身。也就是说把Shape当成这个通配符的上限。
与前面完全相同,因为我们不知道这个受限制的通配符的具体类型,所以不能把Shape对象或其子类的对象加入这个泛型集合中
1 public void addRectangle(List<? extends Shape> shapes){ 2 //下面代码引起编译错误 3 shapes.add(0,new Rectangle()); 4 } 5 6 设定类型形参的上限 7 Java泛型不仅允许在使用通配符形参时设定类型上限,也可以在定义类型形参时设定上限,用于表示传给该类型形参的实际类型必须是该上限类型或上限类型的子类 8 package chapter8; 9 10 public class Apple<T extends Number> { 11 T col; 12 public static void main(String[] args){ 13 Apple<Integer> ai = new Apple<Integer>(); 14 Apple<Double> ad = new Apple<Double>(); 15 //下面的代码将引起异常 16 //因为String类并非Number的子类 17 //Apple<String> ai = new Apple<String>(); 18 } 19 }
如果需要设定多个上限,可以使用&运算符
1 package chapter8; 2 3 public class Apple<T extends Number & java.io.Serializable> { 4 T col; 5 public static void main(String[] args){ 6 Apple<Integer> ai = new Apple<Integer>(); 7 Apple<Double> ad = new Apple<Double>(); 8 //下面的代码将引起异常 9 //因为String类并非Number的子类 10 //Apple<String> ai = new Apple<String>(); 11 } 12 }
3 泛型方法
在接口和类中可以定义泛型形参,这样在创建对象时候,为类型形参提供类型实参即可,在方法中也可以使用泛型。
3.1 定义泛型方法
假设需要实现这样一个方法,该方法负责将一个Object数组的所有元素添加到一个Collection集合中,考虑采用如下代码实现该方法:
1 static void fromArrayToCollection(Object[] a,Collection<Object> c){ 2 for (Object o : a){ 3 c.add(o) 4 } 5 }
上面定义的方法没有任何问题,关键在于上面方法中的c形参,它的数据类型是Collection<Object>。正如前面所介绍的,Collection<Object>不是Collection<String>类的父类————所以这个方法的功能非常有限,它只能将Object数组的元素复制到Object(Object的子类不行)Collection集合,即下面代码引起编译错误:
1 String[] str = {"a","b"}; 2 List<String> strList = new ArrayList<String>(); 3 //Collection<String>对象不能当成Collection<Object>使用,下面代码将出现编译错误 4 fromArrayToCollection(str,strList);
可见上面方法的参数类型不可以使用Collection<Object>,那使用通配符Collection<?>是否可行?显然也不行,我们不能把对象放进一个未知类型的集合中去。
泛型方法就是在声明方法时定义一个或多个类型形参。
泛型方法的用法格式是: 修饰符 <T , S> 返回值类型 方法名(形参列表){ //方法体... }
把上面方法格式和普通方法的格式对比,可以发现泛型方法签名比普通方法的方法签名多了类型形参声明,类型形参声明用尖括号括起来,多个类型形参之间以逗号隔开,所有类型形参声明放在方法修饰符和方法返回值之间。
采用支持泛型的方法,可以将上面的fromArrayToCollection方法改为如下形式:
1 package chapter8; 2 3 import java.util.ArrayList; 4 import java.util.Collection; 5 6 public class TestGenericMethod { 7 //声明一个泛型方法,该泛型方法带一个T形参 8 static <T> void fromArrayToCollection(T[] a, Collection<T> c){ 9 for(T o : a){ 10 c.add(o); 11 } 12 } 13 public static void main(String[] args){ 14 Object[] oa = new Object[100]; 15 Collection<Object> co = new ArrayList<Object>(); 16 //下面代码中T代表Object类型 17 fromArrayToCollection(oa, co); 18 String[] sa = new String[100]; 19 Collection<String> cs = new ArrayList<String>(); 20 //下面代码中T代表String类型 21 fromArrayToCollection(sa, cs); 22 //下面代码中T代表Object类型 23 fromArrayToCollection(sa, co); 24 Integer[] ia = new Integer[100]; 25 Float[] fa = new Float[100]; 26 Number[] na = new Number[100]; 27 Collection<Number> cn = new ArrayList<Number>(); 28 //下面代码中T代表Number类型 29 fromArrayToCollection(ia, cn); 30 //下面代码中T代表Number类型 31 fromArrayToCollection(fa, cn); 32 //下面代码中T代表Number类型 33 fromArrayToCollection(na, cn); 34 //下面代码中T代表String类型 35 fromArrayToCollection(na, co); 36 //下面代码中T代表String类型,但是na是一个Number数组 37 //因为Number既不是String类型,也不是它的子类,所以编译错误 38 //fromArrayToCollection(na, cs); 39 } 40 }
上面程序定义了一个泛型方法,该泛型方法中定义了一个T类型形参,这个T类型形参就可以在该方法内当成普通类型使用。与接口、类声明中定义的类型形参不同的是,方法声明中定义的形参只能在该方法里使用,而接口、类声明中定义的类型形参则可以在整个接口、类中使用。
与类、接口中使用泛型参数不同的是,方法中的泛型参数无须显式传入实际类型参数,如上面程序所示,当程序调用fromArrayToCollection时,无须在调用该方法前传入String、Object等类型,但系统依然可以知道类型形参的数据类型,因为编译器根据实参推断类型形参的值。它通常推断出最直接的类型参数。
例如fromArrayToCollection(na, cn);cn是Collection<Number>类型,与此方法的方法签名进行比较————只比较泛型参数,容易知道T代表的是Number类型。
为了让编译器能准确地判断出泛型方法中类型形参的类型,不要让编译器找不到准确的类型。
1 import java.util.*; 2 3 public class Test{ 4 //声明一个泛型方法,该泛型方法中带一个T形参 5 // static <T> void test(Collection<T> a, Collection<T> c) 6 //正确的方式 7 static <T> void test(Collection<? extends T> a, Collection<T> c){ 8 for (T o : a){ 9 c.add(o); 10 } 11 } 12 public static void main(String[] args){ 13 List<Object> ao = new ArrayList<Object>(); 14 List<String> as = new ArrayList<String>(); 15 //下面代码将产生编译错误 16 test(as , ao); 17 } 18 }
产生错误的原因是无法辨认出T所代表的类型(不知道是String还是Object),而采用了类型通配符的方式后,只要test方法的前一个Collection集合里元素类型是后一个Collection集合里元素类型的子类即可。
3.2 泛型方法和类型通配符的区别
大多数时候都可以使用泛型方法来代替类型通配符,例如对于JDK的Collection接口中两个方法定义:
1 public interface Collection<E>{ 2 boolean containAll(Collection<?> c); 3 boolean addAll(Collection<? extends E> c); 4 } 5 上面集合中两个方法的形参都采用了类型通配符的形式,也可以采用泛型方法的形式来代替它。 6 public interface Collection<E>{ 7 boolean <T> containAll(Collection<T> c); 8 boolean <T extends E> addAll(Collection<T> c); 9 }
上面方法使用了<T extends E>泛型形式,这是定义类型形参时设定上限(其中E是Collection接口里定义的类型形参,在该接口里E可当成普通类型使用)。
上面两个方法中类型形参T只使用了一次,类型形参T的唯一效果是可以在不同的调用点传入不同的实际类型。对于这种情况,应该使用通配符:通配符就是被设计用来支持灵活的子类化的。
泛型方法运行类型形参被用料表示方法的一个或多个参数之间的类型依赖关系,或者返回值与参数之间的类型依赖关系。如果没有这样的类型依赖关系,不应该使用泛型方法。
如果某个方法一个形参a的类型或返回值类型依赖于另一个形参b的类型,则形参b的类型声明不应该使用通配符————因为形参a或返回值类型依赖于该形参b的类型,如果形参b的类型无法确定,程序无法定义形参a的类型,在这种情况下,只能考虑使用在方法签名中声明类型形参。
3.3 设定通配符的下限
假设自己实现一个工具方法:实现将src集合里元素复制到dest集合里的功能,因为dest集合可以保持所有src集合里所有元素,所以dest集合元素的类型是src集合元素类型的父类。为了表示两个参数之间的类型依赖,考虑使用通配符、泛型参数来实现该方法。
1 public static <T> void copy(Collection<T> dest, Collection<? extends T> src){ 2 for(T ele : src){ 3 dest.add(ele); 4 } 5 } 6 Java使用<? super Type>来实现通配符下限 7 8 package chapter8; 9 10 import java.util.*; 11 12 public class MyUtils{ 13 // 下面dest集合元素类型必须与src集合元素类型相同,或是其父类 14 public static <T> T copy( Collection<T> src,Collection<? super T> dest){ 15 T last = null; 16 for (T ele : src){ 17 last = ele; 18 dest.add(ele); 19 } 20 return last; 21 } 22 public static void main(String[] args){ 23 List<Number> ln = new ArrayList<Number>(); 24 List<Integer> li = new ArrayList<Integer>(); 25 li.add(5); 26 // 此处可准确的知道最后一个被复制的元素是Integer类型 27 // 与src集合元素的类型相同 28 Integer last = copy(li , ln); // ① 29 System.out.println(ln); 30 } 31 }
3.4 泛型方法与方法重载
因为泛型既运行设定通配符的上限,也允许设定通配符的下限,从而允许在一个类里包含这样两个方法定义。总之:源一定是目的地址的子类或者相同的类。
1 public class TestUtils{ 2 public static <T> T copy( Collection<? extends T> src,Collection<T> dest) 3 public static <T> T copy( Collection<T> src,Collection<? super T> dest) 4 }
上面包含两个copy方法,这两个方法的参数列表存在一定区别。如果这两个类进包含这两个方法不会有任何错误,但只要调用这个方法就引起编译错误:如
1 List<Number> ln = new ArrayList<Number>(); 2 List<Integer> ln = new ArrayList<Integer>(); 3 copy(ln , li);
编译器无法确定这行代码想调用哪个方法,因为copy方法可以匹配两个,所以是无法通过编译的。
3.5 擦除和转换
在严格的泛型代码里,带泛型声明的类总应该带着类型参数。但为了与老的Java代码保持一致,也允许在使用带泛型声明的类时不指定类型参数。如果没有为这个泛型类指定类型参数,则该类型参数被称作一个raw type(原始类型),默认是声明该参数时指定的第一个上限类型。
当把一个具有泛型信息的对象赋给另一个没有泛型信息的变量时,则所有在尖括号之间的类型信息都被扔掉了。比如说一个List<String>类型被转换为List,则该List对集合元素的类型检查变成了类型变量的上限即Object。下面程序给了这种擦除
1 package chapter8; 2 3 class Apple<T extends Number>{ 4 T info; 5 public Apple(){ 6 7 } 8 public Apple(T info){ 9 this.info = info; 10 } 11 //setter和getter方法 12 public void setInfo(T info){ 13 this.info = info; 14 } 15 public T getInfo(){ 16 return this.info; 17 } 18 } 19 20 public class TestEraSure { 21 public static void main(String[] args){ 22 Apple<Number> a = new Apple<Number>(7); 23 //a的getSize方法返回Integer对象 24 //把a对象赋给一个Apple对象,将丢失Number信息 25 Apple b = a; 26 //b只知道info的类型Number 27 Number info1 = b.getSize(); 28 //下面代码引起编译错误 29 Integer info2 = b.getSize(); 30 } 31 }
Java允许直接把父类对象赋给一个某种类型的父类对象,让编译通过(引发未经检查的转换)的警告。但,如果切换成不兼容的类时,将引发运行时异常。
3.6 泛型与数组
JDK 1.5的泛型有一个很重要的设计原则:如果一段代码在编译时没有产生:[unchecked]未经检查的转换警告,则程序在运行时不会引发ClassCastException异常。正是基于这个原因,所以数组元素的类型不能包含类型变量或类型形参,除非是无上限的类型通配符。但可以声明这样的数组,即声明元素类型包含类型变量或类型形参的数组,也就是说:只能声明List<String>[] 形式的数组,但不能创建ArrayList<String>[10]这样的数组对象。
1 //下面代码实际上是不允许的 2 List<String>[] lsa = new List<String>[10]; 3 //强制类型转换为一个Object数组 4 Object[] oa = (Object[]) o; 5 List<Integer> li = new ArrayList<Integer>(); 6 li.add(new Integer(3)); 7 //将List<Integer>对象作为oa的第一个元素 8 //下面代码没有任何警告 9 oa[1] = li; 10 //下面代码也不不会有任何警告,但将引起ClassCastException异常 11 String s = lsa[1].get(0);
1 将上面的改成下面的形式: 2 //下面代码实际上是不允许的 3 List<String>[] lsa = new ArrayList<String>[10]; 4 //强制类型转换为一个Object数组 5 Object[] oa = (Object[]) o; 6 List<Integer> li = new ArrayList<Integer>(); 7 li.add(new Integer(3)); 8 //将List<Integer>对象作为oa的第一个元素 9 //下面代码没有任何警告 10 oa[1] = li; 11 //下面代码也不不会有任何警告,但将引起ClassCastException异常 12 String s = lsa[1].get(0);
上面程序声明了List<String>[]类型的数组变量,是允许的,但不允许创建List<String>[]类型的对象,所以创建了ArrayList[10]的数组对象,这也是允许。只是把ArrayList[10]对象赋给List<String>[]变量时会有编译警告:[unchecked]未经检查的转换,即编译器不保证这段代码是类型安全的。
Java允许创建无上限的通配符泛型数组,例如new ArrayList<?>[10],因此也可以将第一段代码改为使用无上限的通配符泛型数组。这种情况下,程序不得不进行强制类型转换。
1 List<?>[] lsa = new ArrayList<?>[10]; 2 //强制类型转换为一个Object数组 3 Object[] oa = (Object[]) o; 4 List<Integer> li = new ArrayList<Integer>(); 5 li.add(new Integer(3)); 6 oa[1] = li; 7 //下面代码也不不会有任何警告,但将引起ClassCastException异常 8 String s =(String)lsa[1].get(0); 9 10 允许上面程序将引起ClassCastException异常,程序应该自己通过instanceof运算符保证它的数据类型 11 List<?>[] lsa = new ArrayList<?>[10]; 12 //强制类型转换为一个Object数组 13 Object[] oa = (Object[]) o; 14 List<Integer> li = new ArrayList<Integer>(); 15 li.add(new Integer(3)); 16 oa[1] = li; 17 Object target = lsa[1].get(0); 18 if(target instanceof String){ 19 //下面代码安全了 20 String s = (String)target 21 }
创建元素类型是类型变量的数组对象也引起编译错误。因为类型变量在运行时并不存在,所以编译器无法确定实际类型是什么。
本章概要:
泛型类的定义
1 访问权限 class 类名称 <泛型类型标识1,泛型类型标识2,泛类型标识3> 2 3 访问权限 泛类型标识 变量名称; 4 5 访问权限 泛类型标识 方法名称(){} 6 7 访问权限 泛类型标识 返回值类型 方法名称(泛类型标识 变量名称){ 8 }
泛型对象定义
1 类名称<具体类> 对象名 = new 类名称<具体类>() 2 3 <T>表示泛型,类型T是由外部调用本类时候指定的,可以使用任意字母表示<A>、<B>等等,T是type的首字母大写,这样比较好理解。
加入泛型的最大好处就是避免了类的转换异常
泛型应用中的构造方法
构造方法可以为类中的属性初始化,如果类中的属性类型通过泛型指定,而又需要通过属性设置构造内容时,构造方法和之前并没有异同,不需要像声明类那样指定泛型
指定类的多个泛型
类中涉及到的属性可能不止一个类型,可以在声明类的时候加上多个泛型。
class TangBao<K,V>
在实例化泛型时候最好同时写出实例化对象的泛型类型,否则会有安全警告。比如有个类class TangBao<K,V>
在主类实例化时候:可以这样写Tangbao jiawei = new Tangbao(); 这样的写法是不安全的。要指明类型带上泛型。 TangBao<String,Integer>jiawei = new TanBao<String,Integer>();
也可以不具体指明是哪种,指出是Object统一接收。这样的好处是虽然没有写明是哪个,但是可以消除警告信息。
通配符
在泛型类型中,可以设置通配符来接收任意类型的对象。通配符是:? 泛型传递后面跟上?表示接收任意类型对象
泛型的范围
主要从继承 的角度来限定对象传递的范围。使用两个关键字:一个是super,一个是extends。super是规定对象传递下限的,extends规定上限的
定义上限
1 类的定义:访问权限 类名称 <泛型标识 extends 类名称> 2 3 对象的定义:类名称<? Extends 类名称> 对象名
设置下限
1 类定义:访问权限 类名称<泛型标识 extends 类>{} 2 3 对象声明:类名称<泛型标识 ? super 类> 对象名
泛型与子类继承的限制
一个子类可以通过对象的多态性为其父类实例化,在泛型中,子类的泛型类型,无法使用父类的泛型来接收。如Info<String>不能用Info<Object>