当使用一个泛型时(包括声明变量和创建对象两种情况),都应该为这个泛型类传入一个类型实参。如果没有传入类型实际参数,编译器会提出泛型警告。假设现在需要定义一个方法,该方法里面有一个集合形参,集合形参的元素类型是不确定的,那么应该如何定义呢?考虑下面的代码:
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>对象
List<String> strList = new ArrayList<String>();
//将strList作为参数来调用签名的test方法
test(strList); //①
编译上面的程序,将在①
处发生如下编译错误:
无法将Test中的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[]
变量。如果试图把一个Double
对象保存到该Number[]
数组中,编译可以通过,但在运行时抛出ArrayStoreException
异常。例如下面的程序:
public class ArrayErr
{
public static void main(String[] args)
{
// 定义一个Integer数组
Integer[] ia = new Integer[5];
// 可以把一个Integer[]数组赋给Number[]变量
Number[] na = ia;
// 下面代码编译正常,但运行时会引发ArrayStoreException异常
// 因为0.5并不是Integer
na[0] = 0.5; //①
}
}
上面的程序会在①
处引发ArrayStoreException
运行时异常,这就是一种潜在的风险。
在java
的早期设计中,允许Integer[]
数组赋值给Number[]
变量存在缺陷,因此java
在泛型设计时进行了改进,它不再允许把List<Integer>
对象赋值给List<Number>
变量。例如,下面的代码将导致编译错误(程序使用上面的代码)。
List<Integer> iList = new ArrayList<>();
// 下面代码导致编译错误
List<Number> nList = iList;
Java
泛型的设计原则是,只要代码在编译时没有出现警告,就不会遇到运行时ClassCastException
异常。
注意:
数组和泛型有所不同,假设Foo
是Bar
的一个子类型(子类或者子接口),那么Foo[]
依然是Bar[]
的字类型;但是G<Foo>
不是G<Bar>
的子类型。
1.1 使用类型通配符
为了表示各种泛型List
的父类,我们需要使用泛型通配符,类型通配符是以一个问号(?
),将一个问号作为类型实参传给List
集合,写作:List<?>(意思是未知类型元素的List
)。这个问号(?
)被称为通配符,它的元素类型可以匹配任何类型。可以将上面方法改写成如下形式:
public void test(List<?> c)
{
for(int i=0;i<c.size();i++)
{
System.out.println(c.get(i));
}
}
现在使用任何类型的List
来调用它,程序依然可以访问集合c
中的元素,其类型是Object
,这永远是安全的,因为不管List
的真实类型是什么,它包含的都是Object
。
注意:
上面程序中使用的List<?>
,其实这种写法可以适应于任何支持泛型声明的接口和类,比如写成Set<?>
,Collection<?>
,Map<?,?>
等。
但这种带通配符的List
仅表示它是各种泛型的父类,并不能把元素加入到其中,例如,如下代码将会引起编译错误。
List<?> c = new ArrayList<String>();
//下面程序引起编译错误
c.add(new Object());
因为我们不知道上面程序中c
集合里元素的类型,所以不能向其中添加对象。根据前面的List<E>
接口定义的代码可以发现:add
方法有类型参数E
作为集合的元素类型,所以传给add
的参数必须是E
类的对象或者子类的对象。但因为在该例中不知道E
是什么类型,所以程序无法将任何对象放入该集合。唯一例外的是null
,它是所有引用类型的实例。
另一方面,程序可以调用get()
方法来返回List<?>
集合指定索引处的元素,其返回值是一个未知类型,但可以肯定的是,它总是一个Object
。因此,把get()
的返回值赋值给一个Object
的类型的变量,或者放在任何希望是Object
类型的地方都可以。
1.2 设定类型通配符的上限
当直接使用List<?>
这种形式时,即表明这个List
集合可以是任何泛型的父类。但是还有一种特殊的情形,我们不想使这个List<?>
是任何泛型的父类,只能表示它是某一类泛型的父类。下面考虑一个简单的绘图程序,先定义三个形状类:
Shape.java:
//定义一个抽象类Shape
public abstract class Shape
{
public abstract void draw(Canvas c);
}
Circle.java:
// 定义Shape的子类Circle
public class Circle extends Shape
{
// 实现画图方法,以打印字符串来模拟画图方法实现
public void draw(Canvas c)
{
System.out.println("在画布" + c +"上画一个圆");
}
}
Rectangle.java:
// 定义Shape的子类Rectangle
public class Rectangle extends Shape
{
// 实现画图方法,以打印字符串来模拟画图方法实现
public void draw(Canvas c)
{
System.out.println("把一个矩形画在画布" + c + "上");
}
}
上面定义了三个形状类,其中Shape
是一个抽象父类,该抽象父类有两个子类:Circle
和Rectangle
,接下来定义一个Canvas
类,该画布类可以画数量不等的形状(Shape
子类的对象),我们该如何定义着个Canvas
类呢?考虑如下的Canvas
实现类。
public class Canvas
{
// 同时在画布上绘制多个形状
public void drawAll(List<Shape> shapes)
{
for (Shape s : shapes)
{
s.draw(this);
}
}
}
注意上面的drawAll()
方法的形参类型是Lis<Shape>,List<Circle>
并不是List<Shape>
的子类型,因此,下面代码将引起编译错误。
List<Circle> circleList = new ArrayList<>();
Canvas c = new Canvas();
//不能把List<Circle>当成List<Shape>使用,所以下面代码引起编译错误
c.drawAll(circleList );
关键在于List<Circle>
并不是List<Shape>
的子类型,所以不能把List<Circle>
对象当成List<Shape>
使用。为了表示List<Circle>
的父类,可以考虑使用List<?>
,把Canvas
改为如下形式(程序代码同上):
public class Canvas
{
// 同时在画布上绘制多个形状
public void drawAll(List<?> shapes)
{
for (Object obj : shapes)
{
Shape s = (Shape)obj;
s.draw(this);
}
}
}
上面程序使用了通配符来表示所有的类型。上面的drawAll()
方法可以接受List<Circle>
对象作为参数,问题是上面的方法实现体显得极臃肿而繁琐;使用了泛型还需要进行强制类型转换。
实际上,我们需要一种泛型表示方法,它可以表示所有Shape
泛型List
的父类。为了满足这种需求,java
泛型提供了被限制的泛型通配符。被限制的泛型通配符表示如下:
//它表示所有Shape泛型List的父类
List<? extends Shape>
有了这种被限制的泛型通配符,我们就可以把上面的Canvas
程序改为如下形式(程序代码同上):
public class Canvas
{
// 同时在画布上绘制多个形状,使用被限制的泛型通配符
public void drawAll(List<? extends Shape> shapes)
{
for (Shape s : shapes)
{
s.draw(this);
}
}
}
将Canvas
改为如上形式,就可以把List<Circle>
对象当成List<? extends Shape>
使用。即List<? extends Shape>
可以表示List<Circle>
,List<Rectangle>
的父类,只要List
尖括号里的类型是Shape
的子类型即可。
List<? extends Shape>
是受限制通配符的例子,此处的问号(?
)代表一个未知的类型,就像前面看到的通配符一样。但是此处的这个未知类型一定是Shape
的子类型(也可以是Shape
本身),因此我们把Shape
称为这个通配符的上限(upper bound
)。
因为我们不知道这个受限制的通配符的具体类型,所以不能把Shape
对象或其子类的对象加入这个泛型集合中。例如,下面代码就是错误的。
public void addRectangle(List<? extends Shape> shapes){
//下面代码引发编译错误
shapes.add(0, new Rectangle());
}
与使用普通通配符相似的是,shapes.add()
的第二个参数类型是? extends Shape
,它表示Shape
未知的子类,我们无法准确知道这个类型是什么,所以无法将任何对象添加到这种集合中。
1.3 设定通配符的上限
Java
泛型不仅允许在使用通配符形参时设定上限,而且可以在定义类型形参时设定上限,用于表示传给该类型形参的实际类型要么是该上限类型,要么是该上限类型的子类。下面演示这种用法:
public class Apple<T extends Number>
{
T col;
public static void main(String[] args)
{
Apple<Integer> ai = new Apple<>();
Apple<Double> ad = new Apple<>();
// 下面代码将引起编译异常,下面代码试图把String类型传给T形参
// 但String不是Number的子类型,所以引发编译错误
Apple<String> as = new Apple<>(); //①
}
}
上面程序定义了一个Apple
泛型类,该Apple
类的类型形参的上限是Number
类,这表明使用Apple
类时为T形参传入的实际类型参数只能是Number
或者Number
的子类。上面程序在①
处将引起编译错误:类型形参T的上限是Number
类型,而此处传入的实际类型是String
类型,既不是Number
类型,也不是Number
类型的子类,所以将会导致编译错误。在一种更加极端的情况下,程序需要为类型形参设定多个上限(至多有一个父类上限,可以有多个接口上限),表明该类型形参必须是其父类的子类(是父类本身也行),并且实现多个上限接口,如下代码所示:
//表明T类型必须是Number类或其子类,并必须实现java.io.Serialiable接口
public class Apple<T extends Number & java.io.Serialiable>
{
...
}
与类同时继承父类,实现接口类似的是,为类型形参指定多个上限时,所有的接口上限必须位于类上限之后。也就是说,如果需要为类型形参指定类上限,类上限必须位于第一位。
附:源代码示例
公众号ID:longjiazuoA

未经允许不得转载:人生设计师 » 泛型系列(三):类型通配符