作者:武艺最新单曲问月09 | 来源:互联网 | 2023-10-11 10:14
泛型通配符的作用 为了让大家更好地理解泛型通配符的作用及使用场景,我们先来看一个例子: 我们假定有一个抽象类Shape
,它有一个抽象方法draw
,我们规定draw
方法将会打印一串描述性的字符串。Triangle
、Rectangle
、Circle
是Shape
的子类,均重写了draw
方法()。
abstract class Shape { abstract protected void draw ( ) ; } class Triangle extends Shape { @Override protected void draw ( ) { System . out. println ( "draw a Triangle" ) ; } } class Rectangle extends Shape { @Override protected void draw ( ) { System . out. println ( "draw a Rectangle" ) ; } } class Circle extends Shape { @Override protected void draw ( ) { System . out. println ( "draw a Circle" ) ; } }
我们现在有这样一个需求:我们需要提供一个方法,它能够接受任意一个Shape
,然后在方法内部调用它的draw
方法。 我们可以利用方法可以重载 这一特性来实现:
public static void shapeToDraw ( Triangle triangle) { triangle. draw ( ) ; } public static void shapeToDraw ( Rectangle rectangle) { rectangle. draw ( ) ; } public static void shapeToDraw ( Circle circle) { circle. draw ( ) ; } shapeToDraw ( new Triangle ( ) ) ; shapeToDraw ( new Circle ( ) ) ;
我们可以为每一种具体的子类实现一个特定的方法,所有的方法除了入参以外完全一样。编译器会根据实际传入的参数类型帮我们判断哪一个方法是我们真正需要的。 虽然方法的重载非常有用,但是用在此处却不太合适。想象一下,如果Shape
有100个子类,为了兼顾所有的情况,我们需要提供100个对应的方法!
稍微有一点经验的开发者很容易想到下面的实现:
public static void shapeToDraw ( Shape shape) { shape. draw ( ) ; } shapeToDraw ( new Triangle ( ) ) ; shapeToDraw ( new Circle ( ) ) ;
shapeToDraw(Shap shap)
方法接收一个抽象的Shape
,而不是具体的子类。得益于Java的多态性
,该程序可以正常地运行,且具备一定的扩展性。例如,如果后续我们为Shape
再添加一个子类Square
,该方法不需要做任何修改。
现在,我们的需求发生了变化: 我们需要提供一个方法,它能够接受任意一个Shape集合
,然后在方法内部依次调用集合元素的draw
方法。 (为了简化问题,我们假定集合特指List
) 有了上面需求的经验,我们可以很容易地得到如下实现:
public static void shapesToDraw ( List < Shape > shapes) { for ( Shape shape : shapes) { shape. draw ( ) ; } }
shapesToDraw
方法接受一个List
&#xff0c;在方法内部迭代调用draw
方法。由于List
里面可以装载Shape
的所有子类&#xff0c;因此这个实现看起来似乎是完美的。但是如果使用者传入的是一个List
呢&#xff1f;该方法还能正常运行吗&#xff1f;答案是不能。该方法不能接收List
作为入参&#xff0c;原因是&#xff1a;一个List
并不是一个List
&#xff0c;这确实让人感到非常费解&#xff0c;因为Circle
是Shape
的子类&#xff0c;那么一个Circle
就是一个Shape
&#xff0c;为什么一个List
不能是一个List
呢&#xff1f; 要想理解这个悖论&#xff0c;我们可以看一下下面这个例子&#xff1a;
List < Shape > shapeList; List < Circle > circleList &#61; new ArrayList < > ( ) ; shapeList &#61; circleList; shapeList. add ( new Triangle ( ) ) ; Circle circle &#61; circleList. get ( 0 ) ;
由于将一个List
当成一个 List
会出现上述问题&#xff0c;因此&#xff0c;Java 语言规定, 即使集合之间的元素存在继承关系&#xff0c;也不能认为泛型的集合之间存在继承关系&#xff0c;
显然这不是我们希望的&#xff0c;因为使用者当然希望程序将一个 List
看做是一个 List
。那么该如何实现呢&#xff1f;答案是&#xff1a;泛型通配符。 我们只需要将上述程序稍作修改&#xff0c;就能满足现在的需求&#xff1a;
public static void shapesToDraw ( List < ? extends Shape > shapes) { for ( Shape shape : shapes) { shape. draw ( ) ; } }
List extends Shape>
描述了这样一类List
&#xff1a;List
的泛型类型是Shape
的子类(当然也可以是Shape
本身)。 这一点是非常重要的&#xff0c;在使用泛型通配符之前&#xff0c;我们只能描述List
内部元素的类型&#xff0c;如List
描述了一个内部元素是Shape
子类(因为Shape
是一个抽象类)的List
。而有了泛型通配符以后&#xff0c;我们拥有了描述泛型类型 的能力&#xff01;
泛型通配符上界 基本语法形式&#xff1a;
List < ? extends Shape >
它规定了泛型类型 的上界。也就是说&#xff0c;如果使用上述表达式作为某个方法的入参&#xff0c;那么在实际调用方法时&#xff0c;可以传入的泛型类型必须为 Shape
的子类。另外还有一点需要特别注意&#xff1a;
因为我们只知道泛型类型是Shape
的子类而不能确定其具体的泛型类型&#xff0c;因此往该List
中添加任何元素都是非法的(会引起编译错误)。
我们可以这样理解这句话&#xff1a; 我们只知道 List extends Shape>
接受的 List
泛型是 Shape
的子类&#xff0c;因此它既可以是List
也可以是List
&#xff0c;还可以是List
。当我们想要往这个List
中添加数据的时候会发生什么&#xff1f;我们根本不知道应该向它传入什么样的数据&#xff01;&#xff01;&#xff01; 我们应该向它传入一个Circle
吗&#xff1f;如果调用方法的时候实际传入的参数是一个List
怎么办&#xff1f;List
并不能装载一个Circle
&#xff01;那我们向它传入一个Triangle
&#xff1f;一个Rectangle
&#xff1f;都不行&#xff01;不论我们向它传入什么类型的对象&#xff0c;我们都会遇到前面的问题。最根本的原因是&#xff1a;我们无法确定泛型的类型&#xff0c;因此我们无法确定应该向该List
添加什么类型的元素 。
当我需要从List extends Shape>
中取出元素的时候&#xff0c;会发生什么呢&#xff1f; 因为我们知道List extends Shape>
实际的泛型类型一定是Shape
的子类&#xff0c;那么List
中的元素类型也一定是Shape
的子类&#xff0c;因此我们使用Shape
或者Shape
的父类(本例中Shape
没有父类)去接收取出的元素一定是安全的。
泛型通配符下界 基本语法形式&#xff1a;
List < ? super Triangle >
它规定了泛型类型 的下界。也就是说&#xff0c;如果使用上述表达式作为某个方法的入参&#xff0c;那么在实际调用方法时&#xff0c;可以传入的泛型类型必须为 Triangle
的父类(当然也包括Triangle
本身)。在本例中&#xff0c;传入的参数类型可以是List
、List
&#xff0c;当然也可以是List
。另外还有一点需要特别注意&#xff1a;
因为我们只知道泛型类型是Triangle
的父类而不能确定其具体的泛型类型&#xff0c;因此从该List
取出元素的时候&#xff0c;我们不能知道该元素的确切类型。但是我们知道&#xff0c;任何一个类都是Object
的子类&#xff0c;因此可以使用Object
来接收该元素&#xff0c;除此之外的其他任何类型都不行。(但是得到一个Object
类型的元素对我们来说似乎没有太大的作用&#xff0c;因为我们使用泛型就是希望能够得到更加确切的数据类型&#xff0c;而不是一个笼统的Object
)
当我们需要往List super Triangle>
中添加元素的时候&#xff0c;会发生什么呢&#xff1f; 因为我们知道List super Triangle>
实际的泛型类型一定是Triangle
的父类&#xff0c;那么List
中的元素类型也一定是Triangle
的父类&#xff0c;因此我们往List
中添加Triangle
或者Triangle
的子类元素一定是安全的。
基于上述原因&#xff0c;阿里巴巴Java开发手册中规定&#xff1a;
List> 通过上面的分析我们已经知道了List>
表示List
的泛型类型(一定要注意是泛型类型&#xff0c;而不是元素类型)可以为任意类型。也就是说如果一个方法签名的参数是List>
&#xff0c;那么实际传入的类型可以是List
、List
、List
或者任意一个带泛型的List
(甚至可以是不带泛型的List
)。 当我们试图往这个List
中插入元素的时候&#xff0c;我们会得到一个编译错误&#xff0c;原因是我们无法得知泛型的确切类型(与上文上界通配符中的解释一致)。 当我们试图从这个List
中取出元素的时候&#xff0c;我们只能得到一个Object
(与上文下界通配符中的解释一致)。
结论 泛型通配符定义的是泛型的类型&#xff0c;而不是集合元素的类型&#xff1b; 上界通配符&#xff0c;形如 extends T>
适合频繁往外读取数据的场景&#xff1b; 下界通配符&#xff0c;形如 super T>
适合频繁插入数据的场景&#xff1b; 无法往List>
插入任何数据&#xff0c;只能从List>
中得到Object
类型的元素。