8.3 类型通配符

一、泛型通配符引入

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

public void test(List c)
{
    for(var i=0;i<c.size();i++)
        {
            System.out.println(c.get(i));
        }
}

上面的程序问题是List是一个有泛型声明的接口,此处的List接口并没有传入实际类型参数,这将引起泛型警告。为此考虑为List接口传入实际的类型参数——因为因为List集合里的元素类型是不确定的,将上面的方法改为如下形式:

public void test(List<Object> c)
{
    for(var i=0;i<c.size();i++)
        {
            System.out.println(c.get(i));
        }
}

上面代码的问题是调用该方法传入的实际参数值可能不是我们所期望的,例如,下面代码试图调用该方法:

import java.util.*;
class  GenericTest
{
	public void test(List<Object> c)
	{
		for(var i=0;i<c.size();i++)
        {
            System.out.println(c.get(i));
        }
	}
	public static void main(String[] args) 
	{
		//创建一个List<String>对象
		List<String> strList=new ArrayList<>();
		strList.add("A");
		strList.add("B");
		//将strList作为参数来调用前面的test方法
		GenericTest g=new GenericTest();
		g.test(strList);
	}
}
---------- 编译Java ----------
GenericTest.java:19: 错误: 不兼容的类型: List<String>无法转换为List<Object>
		g.test(strList);
		       ^
注: 某些消息已经过简化; 请使用 -Xdiags:verbose 重新编译以获得完整输出
1 个错误

输出完成 (耗时 2 秒) - 正常终止

上面程序将报错,这表明List对象不能被当成List对象使用,也就是说List类并不是List类的子类。
与数组进行对比,在数组中,可以直接把一个Integer[]数组赋给一个Number[]变量。如果试图把一个Double对象保存到该NUmber[]数组中,编译可以通过,但在运行时抛出ArrayStoreWxception异常。例如:

import java.util.*;
public class ArrayErr 
{
	public static void main(String[] args) 
	{
		//定义一个Integer数组
		Integer[] ia=new Integer[5];
		//可以把一个Integer[]数组赋给Number[]变量
		Number[] na=ia;
		//遍历na数组
		for(var ele:na)
		{
			System.out.println(ele);
		}
		//下面编译正常,但运行时会引发ArrayStoreException异常
		//因为0.5不是Integer
		na[0]=0.5;//Exception in thread "main" java.lang.ArrayStoreException: java.lang.Double
	}
}

在Java的早期设计中,允许把Integer[]数组赋值给Number[]变量存在缺陷,因此Java泛型设计时进行改进,它不再允许把List对象赋值给List变量。例如下面代码将出现编译错误:

import java.util.*;
public class GenericList 
{
	public static void main(String[] args) 
	{
		List<Integer> iList=new ArrayList<>();
		//下面代码将出现编译错误
		List<Number> nList=iList;
	}
}
---------- 编译Java ----------
GenericList.java:8: 错误: 不兼容的类型: List<Integer>无法转换为List<Number>
		List<Number> nList=iList;
		                   ^
1 个错误

输出完成 (耗时 1 秒) - 正常终止

Java泛型的设计原则是,只要代码在编译时没有出现警告,就不会运行时ClassCastException异常。
Java的数组支持型变,但Java集合并不支持型变。

二、使用泛型通配符

为了表示各种泛型List的父类,可以使用类型通配符,类型通配符是一个问号(?),将一个问号作为类型实参传入List集合,写作:List<?>(意义是元素类型未知的List)。这个问号(?)被称为通配符,它的元素类型可以匹配任何类型。

import java.util.*;
public class  WildcardTest
{
	public void test(List<?> c)
	{
		for(var i=0;i<c.size();i++)
        {
            System.out.println(c.get(i));
        }
	}
	public static void main(String[] args) 
	{
		//创建一个List<String>对象
		List<String> strList=new ArrayList<>();
		strList.add("A");
		strList.add("B");
		System.out.println(strList);
		for(var i=0;i<strList.size();i++)
        {
            System.out.println(strList.get(i));
        }
		//将strList作为参数来调用前面的test方法
		WildcardTest g=new WildcardTest();
		g.test(strList);
	}
}
---------- 运行Java捕获输出窗 ----------
[A, B]
A
B
A
B

输出完成 (耗时 0 秒) - 正常终止

但是这种带通配符的List仅表示它是各种泛型List的父类,并不能把原宿加入其中。例如,下面将引起编译错误:

List<?> c=new ArrayList<String>();
c.add(new Object());

因为程序无法确定集合c中元素类型,所以不能向其中添加对象。

三、设置类型通配符的上限

直接简单使用List这种形式时,即表明这个List集合可以是任何泛型List的父类。但还有一种特殊情形,程序不希望这个List时任何泛型的父类,只希望它代表一类泛型List的父类。

import java.util.*;
//定义一个抽象类Shape
abstract class  Shape
{
	abstract void draw(Canvas c);
}

//定义Shape的子类Circle
class Circle extends Shape
{
	//实现画图方法,以打印字符串来模拟画图方法实现
	@Override
	void draw(Canvas c)
	{
		System.out.println("在画布"+c+"上画一个圆");
	}
}

//定义Shape的子类Rectangle
class Rectangle extends Shape
{
	//实现画图方法,以打印字符串来模拟画图方法实现
	@Override
	void draw(Canvas c)
	{
		System.out.println("把一个矩形画在画布"+c+"上");
	}
}

public  class Canvas
{
	//同时在画布上绘制多个形状
	public void drawAll(List<? extends Shape> shapes)
	{
		for(var s:shapes)
		{
			s.draw(this);
		}
	}
	public static void main(String[] args)
	{
		List<Circle> circleList=new ArrayList<>();
		circleList.add(new Circle());
		circleList.add(new Circle());
		var c=new Canvas();
		c.drawAll(circleList);

		List xxxList=new ArrayList();
		xxxList.add(new Rectangle());
		xxxList.add(new Circle());
		var c1=new Canvas();
		c1.drawAll(xxxList);
	}
}
---------- 运行Java捕获输出窗 ----------
在画布Canvas@e9e54c2上画一个圆
在画布Canvas@e9e54c2上画一个圆
把一个矩形画在画布Canvas@1b28cdfa上
在画布Canvas@1b28cdfa上画一个圆

输出完成 (耗时 0 秒) - 正常终止

从上面程序可以看出,我们可以将List对象当成List<? extends Shape>使用。即List<? extends Shape>可以表示List、List的父类——只要List后面<>里的类型是Shape的子类型即可。
List<? extends Shape>是受限通配符的例子,此处的?代表一个未知类型。但这个未知类型一定是Shape的子类(也可以是Shape类本身),因此把Shape称为通配符的上限(upper bound)
类似的,由于无法知道这个受限的通配符的具体类型,所以不能把Shape对象或其子类的对象加入到这个泛型集合中,例如:

public void addRectangle(List<? extends Shape> shapes)
{
    //下面代码将引起编译错误
    shapes.add(0,new Rectanle());
}

简而言之,这种上限通配符,只能从集合元素中取出元素(取出元素总是上限的类型的或其子类),不能先集合中添加元素(因为编译器无法确定集合元素实际是那种类型)
对于更广泛的泛型来说,指定通配符上限就是为了支持类型型变。比如Foo是bar的子类,这样A就相当于A<? extends Bar>的子类,可以将A赋值给<? extends Bar>类型的变量,这种型变方法称为协变。
对于协变泛型而言,它只能调用泛型类型作为返回值类型的方法(编译器会将该方法返回值当成通配符上限的类型);而不能调用泛型类型作为参数的方法。口诀是:协变只出不进。
注:没有指定通配符上限的泛型类相当于通配符上限是Object

四、设置类型通配符的下限

通配符下限符号:<? super 类型>

指定通配符下限是为了支持类型型变,比如Foo是Bar的子类,当程序需要一个A<? super Foo>变量时,程序可以将A、A赋值给A<? super Foo>类型的变量这种方式称为逆变。

对于逆变的泛型集合来说,编译器只知道元素是下限的父类型,但具体是哪种父类型则不确定。因此这种逆变的泛型集合只能向其中添加元素(因为实际赋值的集合元素总是逆变声明的父类),从集合中取出元素只能被当成Object类型处理(编译器无法确定取出的到底是哪个父类的对象)。
对于逆变的泛型而言,他只能调用泛型类型作为参数的方法;而不能调用泛型类型作为返回值类型的方法。口诀是:逆变只进不出。
假设自己实现一个工具类:将src集合中的元素复制到dest集合的功能,因为dest集合可以保存src所有元素的,所以dest集合元素应该是src集合元素的父类。
dest集合元素的类型与src类型相同或者是src类型的父类。

import java.util.*;
public class MyUtils 
{
	//下面dest集合元素的类型必须与src集合元素的类型相同,或者是父类
	public static <T> T copy(Collection<? super T> dest,Collection<T> src)
	{
		T last=null;
		for(var ele:src)
		{
			last=ele;
			//逆变类型添加元素是安全的
			dest.add(last);
		}
		return last;
	}
	public static void main(String[] args) 
	{
		//Number是Integer的父类
		var ln=new ArrayList<Number>();
		var li=new ArrayList<Integer>();
		li.add(5);
		li.add(8);
		//此处可以准确知道最后一个被复制的元素是Integer类型
		//与src集合元素的类型相同
		copy(ln, li);
		System.out.println(ln);//[5, 8]
	}
}

Java集合框架中TreeSet有一个构造器也用到了这种设定通配符下限的语法,例如:

//下面的E是定义TreeSet类时的泛型形参
TreeSet(Comparator<? super E> c)

如果需要对集合中所有元素进行定制排序,则要求TreeSet对象有一个与之相关联的Comparator对象。上面构造器中的参数c就是定制排序的Comaprable对象。

Comparable接口也是一个带泛型声明的接口:

public interface Comparable<T>
{
    int compare(T fst,T snd);
}

通过这种带下限的通配符,可以在创建TreeSet对象时灵活地选择合适的Comparator.假定需要创建一个TreeSet集合,并传入一个可以比较String大小得Comparator,这个Comparator既可以是Comparator,也可以是Comparator————只要尖括号里传入得类型是String的父类型(或它本身)即可。

import java.util.*;
public class TreeSetTest 
{
	public static void main(String[] args) 
	{
		//Comparator的实际类型是TreeSet的元素的父类,满足要求
		TreeSet<String> ts1=new TreeSet<>(
			new Comparator<Object>()
		{
			public int compare(Object fst,Object snd)
			{
				return fst.hashCode()>snd.hashCode()?1
					:fst.hashCode()<snd.hashCode()?-1:0;
			}
		});
		ts1.add("hello");
		ts1.add("wa");

		// Comparator的实际类型是TreeSet元素的类型,满足要求
		TreeSet<String> ts2 = new TreeSet<>(
			new Comparator<String>()
		{
			public int compare(String first, String second)
			{
				return first.length() > second.length() ? -1
					: first.length() < second.length() ? 1 : 0;
			}
		});
		ts2.add("hello");
		ts2.add("wa");
		System.out.println(ts1);//[wa, hello]
		System.out.println(ts2);//[hello, wa]
	}
}

五、设定泛型形参的上限

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<>();//错误: 类型参数String不在类型变量T的范围内
	}
}

设定类型通配符的上限和设定通配符下限的总结

posted @ 2020-03-30 20:56  小新和风间  阅读(183)  评论(0编辑  收藏  举报