泛型类是任何编程语言中的强大工具,但它们也可能带来很多混乱。例如,为什么List不是一个子类List,即使Double是一个亚型Number?在本文中,我们将探讨围绕子类化泛型类型的各种规则,并构建Java提供的泛型继承机制的内聚视图。但是,在深入研究这个重要主题之前,我们将定义各种不同的技术来定义泛型类型和泛型类参数。
了解泛型
面向对象语言中泛型的目的是允许将任意聚合类型提供给类,而不必为每种提供的类型编写新的类。例如,如果我们想写一个列表类来存储对象,不使用泛型我们将被迫要么创建为每种类型的通过了新的类别(例如IntegerList,DoubleList等)或有列表类店的内部结构ObjectS作为如下清单所示:
public class MyList {
private Object[] elements;
public void addElement(Object element) {
// … add to the elements array …
}
public Object getElementAtIndex(int index) {
// … retrieve the element at the given index …
}
}
通用参数
尽管使用Object确实解决了存储Object源自的任何类型的问题Object,但它仍然具有重要的缺陷。其中最重要的是编译时类型安全性的损失。例如,如果我们addElement 使用参数 调用 Integer,则提供 Integer 的不再被视为其实际类型,而是被视为Object。这意味着我们必须Integer在检索时强制转换,并且使用此类的控件之外的代码MyList可能没有足够的信息来知道将检索的元素强制转换为哪种类型。
如果我们添加另一个元素,那么这个问题就复杂了,但这一次是type Double。如果我们的MyList类的使用者需要Integer对象列表,Integer则将ClassCastException在运行时对检索到的元素执行强制转换。另一方面,如果我们期望MyList该类包含类型的值Number(其中的Integer和Double都是子类),则我们尚未传输此信息以确保在编译时类型安全。从本质上讲,从编译器的角度来看,我们的列表同时包含Integer和Double对象这一事实是任意的。由于它不了解我们的意图,因此它无法执行检查以确保我们确实遵守我们声明的意图。
为了解决这个问题,Java开发工具包(JDK)5向Java引入了通用类的概念,该类允许在类名之后的方括号内指定类型参数。例如,我们现在可以List 如下重写我们的 类:
public class MyList {
private T[] elements;
public void addElement(T element) {
// … add to the elements array …
}
public T getElementAtIndex(int index) {
// … retrieve the element at the given index …
}
}
现在,我们可以创建MyList的 Integer 对象:
MyList listOfIntegers = new MyList();
请注意,如果我们想创建另一个MyList存储Double对象,则不必创建另一个类:我们可以简单地实例化一个MyList。我们还可以Number 通过类似的方式创建对象列表 :
MyList listOfNumbers = new MyList<>();
listOfNumbers.addElement(new Integer(1));
listOfNumbers.addElement(new Double(3.41));
上界通用参数
在设计泛型类时,我们可能还希望限制可以作为泛型参数提供给类的值的类型(实例化泛型类时映射到泛型参数的类型)。例如,如果我们创建一个ListOfNumbers类,我们可能要附带的通用参数限制是数种或延长Number(注意,钻石经营者,<>,是在JDK 7中引入并允许类型推断,在一般的参数上假定右侧恰好是赋值左侧的泛型参数):
public class ListOfNumber {
public Number sum() {
// … sum all values and return computed value …
}
在这种情况下,sum方法假定所有存储的元素都是 Number 对象或从派生的对象Number,从而允许计算数值。如果我们不包括该泛型类型的上限,则客户端可以实例化一个Object或其他非数字类型的列表,并且我们将被期望计算总和(从域或问题的角度来看这是没有意义的)。请注意,上限的扩展部分可用于指定通用参数必须实现的接口或指定多个接口(或一个类和多个接口)。有关更多信息,请参见Oracle 的“ 绑定类型参数”文章。
通配符
当我们实例化泛型类型时,可能在某些情况下我们并不关心列表的实际泛型参数。例如,如果我们想要从中求和ListOfNumbers,但又不希望从列表中添加或检索任何元素,则可以使用通配符(以问号表示为通用)来忽略列表的实际通用参数类型。参数):
public class ListOfNumberFactory {
public static ListOfNumber getList() {
return new ListOfNumber();
}
}
ListOfNumber<?> list = ListOfNumberFactory.getList();
System.out.println(list.sum());
在继续之前,必须对命名的通用参数(例如T和通配符)进行重要区分:
定义通用类或方法 时,将使用命名通用参数 来表示实例化该类或使用该方法时的实际通用参数。当采用通用类或方法表示实际的通用参数(或对通用参数不关心)时,使用通配符 。
这意味着我们不能创建具有公共类类型的泛型类,MyIncorrectList<?> {}也不能实例化该形式的泛型类,new MyList();除非它包含在另一个泛型类的定义中,例如以下情况:
public class OuterGeneric {
private MyList list;
// … other fields and methods …
}
泛型参数和通配符之间的区别是一个重要的区别,当我们处理泛型子类型时,泛型参数和通配符将合并到同一类型层次结构中,这将变得更加重要。
上界通配符
就像上限泛型参数一样,在某些情况下,我们不关心泛型参数的类型,只是它是指定类型的子类或实现指定的接口。例如,假设我们要Number在循环中处理对象列表。在这种情况下,我们需要指定期望列表的上限为Number,如下所示:
public class ListOfNumber implements Iterable {
public Number sum() {
// … compute the sum …
}
@Override
public Iterator iterator() {
// … return an iterator …
}
}
ListOfNumber<? extends Number> list = ListOfNumberFactory.getList();
for (Number number: list) {
// … do something with the Number …
}
仅将list的类型设置为ListOfNumber,这很诱人,但是这会将我们的用法限制为完全ListOfNumber对象。例如,我们将无法返回ListOfNumber从ListOfNumberFactory.getList()和执行相同的操作。在稍后讨论泛型类层次结构时,我们将更清楚地看到这种区别的重要性。
请注意,ListOfNumber由于在使用上限通配符时我们不知道列表的实际泛型参数,因此从实用上限制了将任何对象添加到类中:我们仅知道其实际实现类型是的子类型Number。例如,可能很容易想到我们可以将Integer对象插入ListOfNumber<? extends Number>,但是如果我们这样做,编译器将抛出错误,因为它不能保证此类插入的类型安全。通用参数可能是 Double,在这种情况下,我们无法将Integer 对象添加 到的列表中Double。有关更多信息,请参见StackOverflow说明。
下界通配符
与命名的通用参数不同,通配符还可以指定下限。例如,假设我们要向Integer列表添加一个。自然的倾向是指定列表的类型为MyList,但这会任意限制我们可以操作的列表的类型。难道我们不能添加Integer对象列表Number或列表Object中呢?在这种情况下,我们可以指定通配符的下限,从而允许使用相同或下限类型的超类的任何泛型参数类型:
public class IntegerListFactory {
public static MyList<? super Integer> getList() {
// … return MyList, MyList, MyList, etc…
}
}
MyList<? super Integer> integerList = IntegerListFactory.getList();
integerList.addElement(new Integer(42));
尽管下限通配符不如无界或上限通配符流行,但它们仍在泛型类型的子类化中发挥重要作用,正如我们将在下一节中看到的那样。
子类化通用类
通常,泛型子类可分为两类:(1)泛型参数子类型和(2)通配符泛型子类型。在与之前我们分别使用泛型参数和泛型通配符看到的泛型的定义和用法之间的划分相似的意义上,这两个类别中的每一个都有自己的细微差别和重要的继承规则。
通用参数子类型
了解了泛型及其用途之后,我们现在可以开始研究可以使用泛型建立的继承层次结构。从泛型开始时最常见的错误名词之一是多态泛型参数隐含了多态泛型类。实际上,泛型参数的多态性与泛型类的多态性之间不存在任何关系:
多态通用参数 并不意味着多态通用类
例如,如果我们有一个List,List (其中Double的子类型Number)不是的子类型List。实际上,List和之间的唯一关系List是它们都继承自Object(并且我们将很快看到List<?>)。为了说明这种情况,我们可以定义以下类集:
public class MyList {}
public class MySpecializedList extends MyList {}
public class My2ParamList<T, S> extends MySpecializedList {}
这组类导致以下继承层次结构:
从顶部开始,我们可以看到所有泛型类仍从Object该类继承。当我们进入到一个新的水平,我们可以看到,虽然Double是一个亚型Number,MyList 是不是一个亚型MyList。为了理解这种区别,我们必须看一个具体的例子。如果要实例化a MyList,则可以按以下方式插入a Number或subtype的任何对象Number:
public class MyList {
public void insert(T value) {
// … insert the value …
}
}
MyList numberList = new MyList<>();
numberList.insert(new Integer(7));
numberList.insert(new Double(5.72));
为了MyList确实成为的子类型MyList,MyList根据Liskov替换原理,它必须可以替换为的任何实例(即,MyList可以在使用a的任何地方,MyList都必须提供相同的行为,例如 在列表中添加 Integer 或。 Double)。如果实例化a MyList,我们很快就会发现该原理不成立,因为我们不能将一个Integer对象添加到我们的对象中MyList,因为Integer它不是的子类型Double。因此,MyList并非在所有情况下都可以替代,因此不是的子类型MyList。
当我们继续向下的层次结构时,我们可以自然地看到,通用类(例如)MySpecializedList是的子类型MyList,只要Tmatch 的通用参数即可。例如,MySpecializedList是的子类型MyList,但MySpecializedList不是的子类型MyList。同样地,MySpecializedList不是的子类型MyList为同样的原因MyList不是的子类型MyList。
最后,只要前一个类扩展了后一个类并且共享的通用参数匹配,则包含其他通用参数的通用类就是另一个通用类的子类型。例如,My2ParamList<T, S>子类型是MySpecializedList,只要T是同一类型(因为它是共享的通用参数)。如果未共享通用参数(例如)S,则它可以独立变化而不会影响通用层次结构。例如,My2ParamList<Number, Integer>和和My2ParamList<Number, Double>都是MySpecializedList共享的通用参数匹配的子类型。
通配符子类型
尽管通用参数层次结构相对简单明了,但通用通配符引入的层次结构却细微得多。在通配符方案中,我们必须考虑三种不同的情况:(1)无界通配符,(2)上界通配符,和(3)下界通配符。我们可以在下图中看到这些各种情况之间的关系:
为了理解此层次结构,我们必须关注呈现的每个通配符中的约束(请注意,与处理有关的类Double和退出处理的箭头Double是绿色,与处理有关的类和与处理有关的Number箭头退出的类Number是蓝色)。从顶部开始,MyList<?>继承自Object,因为该MyList对象可以包含任何引用类型的泛型参数(此列表中可以包含任何对象)。实际上,从概念上讲,此列表可以认为仅包含type的对象Object。
建立层次结构的顶部之后,我们将移至底部并关注MyList(左下角的绿色类)。有两个类别是该类别的直接父母:(1)MyList<? extends Double>和MyList<? super Double>。前一种情况简单地指出MyList是的子类,MyList其中包含任何Double对象或的子类型的对象Double。从另一种角度来看,我们说的MyList是仅包含的Double是的特例MyList,其中包含的Double对象或任何其他子类Double。如果我们要替换MyList其中MyList<? extends Double>预期,我们知道,我们MyList会含有Double或亚型Double(实际上,它将包含只Double,但仍足以满足的要求Double或子类型Double)。
后一种情况(MyList<? super Double>作为父项)只是在相反的方向上陈述了同一件事:如果我们期望MyList包含Double或父类的Double,则提供a MyList就足够了。与前一种情况类似,MyList仅包含Double对象可以被视为是的特例MyList,其中包含Double或是的子类型的对象Double。实际上,MyList是的更受限版本MyList<? super Double>。因此,MyList<? super Double>只要提供a MyList,就可以在逻辑上满足任何期望的a ,根据定义,该成为MyLista的子类型MyList<? super Double>。
完成Double层次结构的一部分后,我们看到它MyList是的子类MyList<? extends Number>。要了解这种血统,我们必须考虑这个上限的含义。简而言之,我们要求MyList包含type的对象Number或type的任何子类Number。因此,MyList仅包含类型的对象Double(是类型的子类Number)的,是MyList包含的Number对象或子类型的约束更严格的版本Number。根据定义,这是MyList的子类型MyList<? extends Number>。
Number 层次结构的 一部分只是Double已经讨论过的部分的反映。从底部取出,任何地方MyList的Number或超类型Number预期(MyList<? super Number>),一个MyList可以是足够的。同样, Number 是的超型Double,因此,在任何预期的Double或超型Double(MyList<? super Double>)处,a MyList就足够了。最后,在需要MyList包含Number或任何子类型Number(MyList<? extends Number>)的任何地方,MyList包含 Number 就足够了,因为它只是此要求的特例。
推论主题
尽管我们涵盖了该层次结构的大部分内容,但仍然存在三个必然的主题:(1)MyList<? super Number>是的子类型MyList<? super Double>,(2)MyList<? extends Double>是的子类型MyList<? extends Number>,以及(3)MyList和之间的公共超类型MyList。
1.在第一种情况下,MyList<? super Double>简单地指出,我们预计MyList含有Double或任何超类型Double,其中Number之一。因此,由于Number它将作为的超型就足够了Double,提供MyList<? super Number>是的更受限制的版本MyList<? super Double>,从而使前者成为后者的子类型。
2.在第二种情况下,情况恰恰相反。如果我们预期MyList含有Number或任何亚型Number,被认为Double是一个亚型Number,MyList含有Double或亚型Double可以被看作是一种特殊的情况下MyList含Number或亚型Number。
3.在后一种情况下,只MyList<?>充当之间的共同超MyList和MyList。如上所述,Double和之间的多态关系Number并不构成MyList和之间的多态关系MyList。因此,两种类型之间唯一的共同祖先是a MyList,其中包含任何引用类型(对象)。
结论
泛型为面向对象的语言添加了一些非常强大的功能,但是它们也可能给新手和有经验的开发人员在语言的概念模型中带来深深的困惑。在这些混乱中,最重要的是各种通用案例之间的继承关系。在本文中,我们探讨了泛型背后的目的和思考过程,并介绍了命名泛型参数和通配符泛型的正确继承方案。
最后,开发这么多年我也总结了一套学习Java的资料与面试题,如果你在技术上面想提升自己的话,可以关注我,私信发送领取资料或者在评论区留下自己的联系方式,有时间记得帮我点下转发让跟多的人看到哦。