泛型中通配符存在的意义

泛型中易犯的错误

当使用一个泛型类时(包括声明变量和创建对象两种情况),都应该为这个泛型类传入一个类型实参。如果没有传入类型实际参数,编译器就会提出泛型警告。

假设现在需要定义一个方法,该方法里有一个集合形参,集合形参的元素类型是不确定的。那该怎么样定义呢?

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));
    }
}

表面上看起来方法声明没有问题,这个方法声明确实没有任何问题。问题是调用该方法传入的实际参数值可能不是我们所预期的。试图调用方法看看:

在这里插入图片描述

无法将测试中的test (java.util.List<java.lang.Object>)应用于(java.util.List<java.lang.String>)

上面程序出现了编译错误,这表明List对象不能被当成List对象使用,也就是说,List类不是List类的子类。

注意!!如果Foo是Bar的一个子类型(子类或者子接口),而G是具有泛型声明的类或接口,G并不是G的子类型!这一点非常值得注意,因为它与大部分人的习惯认为是不同的。

与数组进行对比

数组中,程序可以直接把一个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 // 报错
     }
 }

在Java的早期设计中,允许Integer[ ]数组赋值给Number[ ]变量存在缺陷,因此Java在泛型设计时进行了改进, 它不再允许把List对象赋值给List变量。

List<Integer> iList = new ArrayList<>();
// 下面代码导致编译错误
List<Number> nList = iList;

Java泛型的设计原则是,只要代码在编译时没有出现警告,就不会遇到运行时ClassCaseException异常。

数组和泛型有所不同,假设Foo是Bar的子类型(子类或者子接口),那么Foo[ ]依然是Bar[ ]的子类型;但是G不是G的子类型。Foo[ ]自动向上转型为Bar[ ]的方式被称为型变。也就是说,Java的数组支持型变,但是Java集合并不支持型变。

这就是为什么使用类通配符了

设定类型通配符的上限

当直接使用List<?>这种形式时,即表明这个List集合可以是任何泛型List的父类,但还有一种情况是,程序员不希望这个List<?>是任何泛型List的父类,只希望它代表某一类泛型List的父类。

举例:

Shape.java

public abstract class Shape {
	public abstract void draw(Canvas c);
}

Rectangle.java

public class Rectangle extends Shape{
	@Override
	public void draw(Canvas c) {
		System.out.println("把一个矩形画在画布"+c+"上");
	}

}

Circle.java

public class Circle extends Shape{
	@Override
	public void draw(Canvas c) {
		System.out.println("在画布上"+c+"上画了一个圆");		
	}
	
}

Canvas.java

import java.util.List;

public class Canvas {
	public void drawall(List<Shape> shapes) {
		for (Shape shape : shapes){
			shape.draw(this);
		}
	}
}

Test.java

import java.util.ArrayList;
import java.util.List;

public class Test {
	public static void main(String[] args) {
		List<Circle> circles = new ArrayList<Circle>();
		Canvas canvas = new Canvas();
		canvas.drawall(circles); // 报错The method drawall(List<Shape>) in the type Canvas is not applicable for the arguments (List<Circle>)
	}
}

关键在于List 并不是List的子类型,所以不能把List 对象当成List使用。为了表示List 的父类,可以考虑使用List<?>,但此时从List<?>集合中取出的元素只能被编译器当成Object处理。为了表示List集合的所有元素是Shape的子类,Java泛型提供了被限制的泛型通配符。

// 它表示泛型形参必须是Shape子类的List
List<? extends Shape>

有了这种被限制的泛型通配符,所以就将Canvas改为如下形式

import java.util.List;

public class Canvas {
	public void drawall(List<? extends Shape> shapes) {
		for (Shape shape : shapes){
			shape.draw(this);
		}
	}
}

这样就可以把List 对象当成List<? extends Shape>使用。即List<? extends Shape>可以表示List 、List的父类-----只要List后尖括号里的类型是Shape的子类型即可。

总结

简而言之,这种指定通配符上限的集合,只能从集合中取出元素(取出的元素总是上限的类型),不能向集合中添加元素(因为编译器没法确定集合元素实际是哪种子类型)。

对于更广泛的泛型类来说。指定通配符上限就是为了支持类型型变。比如Foo是Bar的子类,这样A就相当于A<? extends Bar>的子类,可以将A赋值给A<? extends Bar>类型的变量,这种型变方式被称为协变。

对于协变的泛型类来说,它只能调用泛型类型作为返回值类型的方法(编译器会将该方法返回值当成通配符上限的类型);而不能调用泛型类型作为参数的方法。口诀是:协变只出不进!

List <? extends Shape> 可以传入其子类

List <? super Shape> 可以传入其父类

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Micek

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值