Java面向对象系列[v1.0.0][泛型通配符]

当使用一个泛型类时,都应该为这个泛型类传入一个类型实参,如果没有传入类型实际参数,编译器会提出泛型警告,假设现在需要定义一个方法,该方法 里有一个集合形参,集合形参的元素类型是不确定的

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

List是一个有泛型声明的接口,此处使用List接口时候没有传入实际类型参数,这将引起泛型警告,为List接口传入实际的类型参数,因为不确定List集合里的元素类型会是什么,因此此处给Object

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<>();
// 将strList作为参数来调用上面的test方法
test(strList);

编译这个程序会报错:

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

这说明List对象不能被当成List对象使用,也就是说List不是List的子类,也就是说虽然String是Object的子类,但并不代表List和List具有相同的父子关系

数组与泛型不同,假设Apple是Fruit的一个子类型(子类或者子接口),那么Apple[]依然是Fruit[]的子类型,但是List和List不具备同样的父子关系。Apple[]自动向上转型为Fruit[]的方式被称为型变,也就是说java的数组支持型变,但集合并不支持型变

import java.util.*;

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; 


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

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

使用类型通配符

为了表示各种泛型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<?, ?>等
但是这种带通配符的泛型仅仅表示它是各种泛型的父类,并不能把元素加入其中,因为并不知道集合的元素类型,唯一例外的元素是null,它是所有引用类型的实例
程序可以调用get()方法来获取List<?>集合指定索引处的的元素,返回的虽然是个未知类型,但是可以肯定的是它一定是个Object类型,因此可以直接赋值给一个Object类型的变量

设定类型通配符上限

当直接使用List<?>这种形式时,即表明它是任何泛型List的父类,但是有一种特殊情况,就是不希望它是所有泛型List的父类,只希望它是某一类泛型List的父类

//定义一个抽象类Shape
public abstract class Shape
{
	public abstract void draw(Canvas c);
}
// 定义Shape的子类Circle
public class Circle extends Shape
{
	//实现画图方法,以打印字符串来模拟画图方法实现
	public void draw(Canvas c)
	{
		System.out.println("在画布" + c + "上画个圆");
	}
}
// 定义Shape的子类Rectangle
public class Rectangle extends Shape
{
	// 实现画图方法,以打印字符串来模拟画图方法实现
	public void draw(Canvas c)
	{
		System.out.println("把一个矩形画在画布" + c "上");
	}
}
// 定义Canvas实现类
public class Canvas
{
	// 同时在画布上绘制多个图形
	public void drawAll(List<Shape> shapes)
	{
		for (Shape s : shapes)
		{
			s.draw(this);
		}
	}
}

drawAll()方法的行参类型是List, 而List 并不是List的子类型,因此如下代码会报错

List<Circle> circleList = new ArrayList<>();
Canvas c = new Canvas();
// 不能把List<Circle>当成List<Shape>来使用
c.drawAll(circleList);

因此可以使用List<?>表示List集合的父类,但从List<?>集合中取出来的元素只能被编译器当作Object来处理,为了表示List集合的所有元素是Shape的子类,java泛型提供了被限制的泛型通配符

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

修改Canvas类代码如下

// 定义Canvas实现类
public class Canvas
{
	// 同时在画布上绘制多个图形
	public void drawAll(List<? extends Shape> shapes)
	{
		for (Shape s : shapes)
		{
			s.draw(this);
		}
	}
}

这样就可以把List 对象当成List<? extends Shape>使用,换句话说List<? extends Shape>可以表示List /List的父类,只要尖括号里的类型是Shape的子类即可
因此可以将Shape看成是这个通配符的上限
同样的,由于程序无法确定这个受限制的通配符的具体类型,所以不能把Shape对象或者其子类的对象加入这个泛型集合中,错误的示范

public void addRectangle(List<? extends Shape> shapes)
{
	shapes.add(0, new Rectangles()); // 编译时会报错
}

虽然指定了通配符的上限,但是只能从集合中取元素,取出来的一定在设置的上限范围内,不能向集合中添加元素,因为添加元素编译器无法确定集合元素实际是哪种子类型
这也就引出了协变的概念,就是说比如Apple是Fruit的子类,List就是List<? extends Fruit>的子类,可以将List赋值给List<? extends Fruit>类型的变量,这种型变方式就是协变
对于未指定通配符上限的泛型类,相当于通配符上限是Object

设定类型通配符的下限

指定通配符的下限是为了支持类型型变,比如Apple是Fruit的子类,当程序需要一个List<? supper Fruit>变量时,程序可以将List、List的值赋给List<? super Fruit>类型的变量,这种型变方式被称为逆变
对于逆变的泛型集合来说,编译器只知道集合元素是下限的父类型,但具体是哪种父类型不确定,因此这种逆变的泛型结合可以向其中添加元素,因为实际赋值的集合元素总是逆变声明的父类,从集合中取元素时只能被当成Object类型处理,无法确定是哪个父类的对象

import java.util.*;
import static java.lang.System.*;

public class MyUtils
{
	// 下面dest集合元素类型必须与src集合元素类型相同,或是其父类
	public static <T> T copy(List<? super T> dest, List<T> src)
	{
		T last = null;
		for (var ele : src)
		{
			last = ele;
			// 逆变的泛型集合添加元素是安全的
			dest.add(ele);
		}
		return last;
	}
	public static void main(String[] args)
	{
		// var声明的变量,后面必须具体指定泛型的类型
		var ln = new ArrayList<Number>();
		var li = new ArrayList<Integer>();
		li.add(5);
		// 此处可准确的知道最后一个被复制的元素是Integer类型而不是笼统的Number类型
		// 与src集合元素的类型相同
		Integer last = copy(ln, li);  
		out.println(ln);
	}
}

再看一个例子:
创建一个TreeSet集合,并传入一个可以比较String大小的Comparator,这个Comparator既可以是Comparator,也可以是Comparator只要尖括号里传入的类型是String的父类型或者它本身即可

import java.util.*;
import static java.lang.System.*;
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 hashCode() > snd.hashCode() ? 1
					: 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");
		out.println(ts1);
		out.println(ts2);
	}
}

通过使用这种通配符下限的方式来定义TreeSet构造器的参数,就可以将所有可用的Comparator作为参数传入,增加了程序的灵活性

设定泛型行参的上限

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类时传入的实际类型参数只能是Number或者Number的子类

某些时候程序需要为泛型行参设定多个上限,但最多又一个父类上限的同时可以要求实现一个或多个接口

// 表明T类型必须是Number类或者子类,并必须实现java.io.Serializable接口
public class Apple<T extends Number & java.io.Serializable>
{
	...
}
发布了207 篇原创文章 · 获赞 124 · 访问量 5万+

猜你喜欢

转载自blog.csdn.net/dawei_yang000000/article/details/105315220