Java泛型类型通配符

1.* 类型通配符

正如前面讲的,当使用一个泛型类时(包括声明变量和创建对象两种情况),都应该为这个泛型类传入一个类型实参。如果没有传入类型实际参数,编译器就会提出泛型警告。假设现在需要定义一个方法,该方法里有一个集合形参,集合形参的元素类型是不确定的,那应该怎样定义呢?

考虑如下代码:

点击查看代码
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<>();
test(strList);

编译上面程序,将在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异常。例如如下程序:

查看代码

上面程序在na[0] = 0.5代码处会引发ArrayStoreException运行时异常,这就是一种潜在的风险。

提示:一门设计优秀的语言,不仅需要提供强大的功能,而且能提供强大的“错误提示”和“出错警告”,这样才能尽量避免开发者犯错。而Java允许Integer[]数组赋值给Number[]变量显然不是一种安全的设计。

在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.*.& 使用类型通配符

为了表示各种泛型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的父类,并不能把元素加入到其中。

例如,如下代码将会引起编译错误:

点击查看代码
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.*.& 设定类型通配符的上限

当直接使用List<?>这种形式时,即表明这个List集合可以是任何泛型List的父类。但还有一种特殊的情形,我们不想使这个List<?>是任何泛型List的父类,只想表示它是某一类泛型List的父类。考虑一个简单的绘图程序,下面先定义三个形状类:

查看代码

然后定义Circle类:

查看代码

接着定义Rectangle类:

查看代码

上面定义了三个形状类,其中BaseShape是一个抽象父类,该抽象父类有两个子类:Circle和Rectangle。接下来定义一个Canvas类,该画布类可以画数量不等的形状(BaseShape子类的对象),我们该如何定义这个Canvas类呢?考虑如下的Canvas实现类:

查看代码

注意上面的drawAll()方法的形参类型是List<BaseShape>List<Circle>并不是List<BaseShape>的子类型,因此,下面代码将引起编译错误:

点击查看代码
List<Circle> circleList = new ArrayList<>();
Canvas c = new Canvas();
//不能把List<Circle>当成List<BaseShape>使用,所以下面代码引起编译错误
c.drawAll(circleList);

关键在于List<Circle>并不是List<BaseShape>的子类型,所以不能把List<Circle>对象当成List<BaseShape>使用。为了表示List<Circle>的父类,可以考虑使用List<?>,把Canvas改为如下形式:

点击查看代码
public class Canvas {
    public void drawAll(List<?> shapes) {
        for (Object obj : shapes) {
            BaseShape s = (BaseShape) obj;
            s.draw(this);
        }
    }
}

上面程序使用了通配符来表示所有的类型。上面的drawAll()方法可以接受List<Circle>对象作为参数,问题是上面的方法实现体显得极为臃肿而烦琐:使用了泛型还需要进行强制类型转换。

实际上,我们需要一种泛型表示方法,它可以表示所有BaseShape泛型List的父类。为了满足这种需求,Java泛型提供了被限制的泛型通配符。被限制的泛型通配符表示如下:

点击查看代码
//它表示所有BaseShape泛型List的父类
List<? extends BaseShape>

有了这种被限制的泛型通配符,我们就可以把上面的Canvas程序改为如下形式:

点击查看代码
public class Canvas {
    //同时在画布上绘制多个形状,使用被限制的泛型通配符
    public void drawAll(List<? extends BaseShape> shapes){
        for (BaseShape s : shapes){
            s.draw(this);
        }
    }
    ...
}

将Canvas改为如上形式,就可以把List<Circle>对象当成List<? extends BaseShape>使用。即List<? extends BaseShape>可以表示List<Circle>List<Rectangle>的父类,只要List后尖括号里的类型是BaseShape的子类型即可。

List<? extends BaseShape>是受限制通配符的例子,此处的问号“?”代表一个未知的类型,就像前面看到的通配符一样。但是此处的这个未知类型一定是BaseShape的子类型(也可以是BaseShape本身),因此我们把BaseShape称为这个通配符的上限(upper bound)。

因为我们不知道这个受限制的通配符的具体类型,所以不能把BaseShape对象或其子类的对象加入这个泛型集合中。例如,下面代码就是错误的:

点击查看代码
public void addRectangle(List<? extends BaseShape> shapes){
    //下面代码引起编译错误
    shapes.add(0, new Rectangle());
}

与使用普通通配符相似的是,shapes.add()的第二个参数类型是“? extends BaseShape”,它表示BaseShape未知的子类,我们无法准确知道这个类型是什么,所以无法将任何对象添加到这种集合中。

1.*.& 设定类型形参的上限

Java泛型不仅允许在使用通配符形参时设定上限,而且可以在定义类型形参时设定上限,用于表示传给该类型形参的实际类型要么是该上限类型,要么是该上限类型的子类。下面程序示范了这种用法:

查看代码

上面程序定义了一个Apple泛型类,该Apple类的类型形参的上限是Number类,这表明使用Apple类时为T形参传入的实际类型参数只能是Number或Number类的子类。上面程序在Apple<String> as = new Apple<>()处将引起编译错误:类型形参T的上限是Number类型,而此处传入的实际类型是String类型,既不是Number类型,也不是Number类型的子类型,所以将会导致编译错误。

在一种更极端的情况下,程序需要为类型形参设定多个上限(至多有一个父类上限,可以有多个接口上限),表明该类型形参必须是其父类的子类(是父类本身也行),并且实现多个上限接口。如下代码所示:

点击查看代码
//表明T类型必须是Number类或其子类,并必须实现java.io.Serializable接口
public class Apple<T extends Number & java.io.Serializable> {
    //...
}

与类同时继承父类、实现接口类似的是,为类型形参指定多个上限时,所有的接口上限必须位于类上限之后。也就是说,如果需要为类型形参指定类上限,类上限必须位于第一位。

原文地址:http://www.cnblogs.com/hzhiping/p/16906200.html

1. 本站所有资源来源于用户上传和网络,如有侵权请邮件联系站长! 2. 分享目的仅供大家学习和交流,请务用于商业用途! 3. 如果你也有好源码或者教程,可以到用户中心发布,分享有积分奖励和额外收入! 4. 本站提供的源码、模板、插件等等其他资源,都不包含技术服务请大家谅解! 5. 如有链接无法下载、失效或广告,请联系管理员处理! 6. 本站资源售价只是赞助,收取费用仅维持本站的日常运营所需! 7. 如遇到加密压缩包,默认解压密码为"gltf",如遇到无法解压的请联系管理员! 8. 因为资源和程序源码均为可复制品,所以不支持任何理由的退款兑现,请斟酌后支付下载 声明:如果标题没有注明"已测试"或者"测试可用"等字样的资源源码均未经过站长测试.特别注意没有标注的源码不保证任何可用性