Java中的方差详解

什么是方差?
维基百科有关方差的文章说:
差异是指更复杂类型之间的子类型如何与其组件之间的子类型相关。
这里的“更复杂的类型”指的是更高层次的结构,例如容器和函数。因此,方差是关于容器与通过类型层次结构连接的参数组成的函数之间的分配兼容性。它允许参数多态性和子类型多态性1的安全集成。例如,我可以将返回猫列表的函数的结果分配给“动物列表”类型的变量吗?我可以将奥迪汽车列表传递给接受汽车列表的方法吗?我可以在这一系列动物中插入狼吗?
在Java中,在使用站点 2定义了方差 。
四种方差
解释维基文章,类型构造函数是:
如果协变量接受子类型但不接受超类型
逆变如果它接受父类型而不是子类型
如果双变量同时接受超类型和子类型
不变式 既不接受超类型也不接受子类型
(显然,在所有情况下都接受声明的类型参数。)
Java中的不变性
使用站点的type参数必须没有界限。
如果 A是的超类型 B,那么 GenericType是 不是的超类型 GenericType,反之亦然。
这意味着这两种类型彼此无关,并且在任何情况下都不能互换。
不变容器
在Java中,不变式可能是您将遇到的泛型的第一个示例,并且是最直观的。类型参数的方法是可以预期的。类型参数的所有方法都是可访问的。
它们不能交换:
// Type hierarchy: Person :> Joe :> JoeJr
List p = new ArrayList(); // COMPILE ERROR (a bit counterintuitive, but remember List is invariant)
List j = new ArrayList(); // COMPILE ERROR

您可以向它们添加对象:
// Type hierarchy: Person :> Joe :> JoeJr
List p = new ArrayList<>();
p.add(new Person()); // ok
p.add(new Joe()); // ok
p.add(new JoeJr()); // ok

您可以从中读取对象:
// Type hierarchy: Person :> Joe :> JoeJr
List joes = new ArrayList<>();
Joe j = joes.get(0); // ok
Person p = joes.get(0); // ok

Java中的协方差
使用地点的type参数必须具有开放的下限。
如果 B是的子类型 A,则 GenericType是的子类型 GenericType<? extends A>。
Java中的数组始终是协变的
在Java中引入泛型之前1.5,数组是唯一可用的泛型容器。它们一直是协变的,例如。Integer[]是的子类型Object[]。始终存在危险,就是如果您将您Integer[]的方法传递给accepts的方法Object[],那么该方法实际上会在其中放置任何内容。使用第三方代码时,无论多么小,这都是您要冒的风险。
协变容器
Java允许子类型化(协变)泛型类型,但是根据最小惊讶原则3,它对可以“流入和流出”这些泛型类型的内容进行了限制。换句话说,具有类型参数返回值的方法是可访问的,而具有类型参数的输入参数的方法是不可访问的。
您可以将超类型交换为子类型:
// Type hierarchy: Person :> Joe :> JoeJr
List<? extends Joe> = new ArrayList(); // ok
List<? extends Joe> = new ArrayList(); // ok
List<? extends Joe> = new ArrayList(); // COMPILE ERROR

从他们那里读是很直观的:
// Type hierarchy: Person :> Joe :> JoeJr
List<? extends Joe> joes = new ArrayList<>();
Joe j = joes.get(0); // ok
Person p = joes.get(0); // ok
JoeJr jr = joes.get(0); // compile error (you don’t know what subtype of Joe is in the list)

禁止向他们写信(违反直觉),以防止上述数组引起的陷阱。在下面的示例代码中,如果其他人的带有协变arg的方法添加了a ,则a的调用者/所有者List将感到惊讶。List<? extends Person>Jill
// Type hierarchy: Person > Joe > JoeJr
List<? extends Joe> joes = new ArrayList<>();
joes.add(new Joe()); // compile error (you don’t know what subtype of Joe is in the list)
joes.add(new JoeJr()); // compile error (ditto)
joes.add(new Person()); // compile error (intuitive)
joes.add(new Object()); // compile error (intuitive)

Java的协变性
使用站点的type参数必须具有开放的上限。
如果 A是的超型 B,则 GenericType是的超型 GenericType<? super B>。
逆向容器
逆变容器的行为违反直觉:违背协变容器,使用带参数是类型的返回值的方法无法访问,而与参数类型的输入参数的方法是访问:
您可以将子类型交换为超类型:
// Type hierarchy: Person > Joe > JoeJr
List<? super Joe> joes = new ArrayList(); // ok
List<? super Joe> joes = new ArrayList(); // ok
List<? super Joe> joes = new ArrayList(); // COMPILE ERROR

但是当您从中读取特定类型时,您无法捕获它们:
// Type hierarchy: Person > Joe > JoeJr
List<? super Joe> joes = new ArrayList<>();
Joe j = joes.get(0); // compile error (could be Object or Person)
Person p = joes.get(0); // compile error (ditto)
Object o = joes.get(0); // allowed because everything IS-A Object in Java

您可以添加“下界”的子类型:
// Type hierarchy: Person > Joe > JoeJr
List<? super Joe> joes = new ArrayList<>();
joes.add(new JoeJr()); // allowed

但是您不能添加超类型:
// Type hierarchy: Person > Joe > JoeJr
List<? super Joe> joes = new ArrayList<>();
joes.add(new Person()); // compile error (again, could be a list of Object or Person or Joe)
joes.add(new Object()); // compile error (ditto)

Java中的双方差
使用站点必须在type参数上声明一个无界通配符。
具有无界通配符的泛型类型是同一泛型类型的所有有界变体的超类型。例如, GenericType<?>是的超类型GenericType。由于无界类型是类型层次结构的根,因此它遵循其参数类型的根,并且只能访问从继承的方法java.lang.Object。
认为 GenericType<?>是 GenericType。
具有N型参数的结构的方差
那么诸如函数之类的更复杂的类型呢?适用相同的原则;您只需要考虑更多类型参数:
// Type hierarchy: Person > Joe > JoeJr
// Invariance
Function<Person, Joe> personToJoe = null;
Function<Joe, JoeJr> joeToJoeJr = null;
personToJoe = joeToJoeJr; // COMPILE ERROR (personToJoe is invariant)
// Covariance
Function<? extends Person, ? extends Joe> personToJoe = null; // covariant
Function<Joe, JoeJr> joeToJoeJr = null;
personToJoe = joeToJoeJr; // ok
// Contravariance
Function<? super Joe, ? super JoeJr> joeToJoeJr = null; // contravariant
Function<? super Person, ? super Joe> personToJoe = null;
joeToJoeJr = personToJoe; // ok

方差与继承
Java允许使用协变返回类型和异常类型的重写方法:
interface Person {
Person get();
void fail() throws Exception;
}
interface Joe extends Person {
JoeJr get();
void fail() throws IOException;
}
class JoeImpl implements Joe {
public JoeJr get() {} // overridden
public void fail() throws IOException {} // overridden
}

但是,尝试使用协变参数覆盖方法只会导致重载:
interface Person {
void add(Person p);
}
interface Joe extends Person {
void add(Joe j);
}
class JoeImpl implements Joe {
public void add(Person p) {} // overloaded
public void add(Joe j) {} // overloaded
}

最后的想法
差异为Java引入了额外的复杂性。尽管围绕方差的键入规则很容易理解,但有关类型参数的方法可访问性的规则却违反直觉。了解它们不仅是“显而易见的”,而且还需要暂停思考逻辑上的后果。
但是,我的日常经验是,细微差别通常不会出现:
我无法回忆起必须声明一个反变量的实例,而且我很少遇到它们(尽管它们确实 存在)。
协变量参数似乎更常见(示例4),但是(很幸运)它们更易于推理。
考虑到子类型化是面向对象编程的基本技术,协方差是它的最强优点(例如:参见注释4)。
结论:在我的日常编程中,方差可提供中等的净收益,尤其是在需要与子类型兼容的情况下(在OOP中经常发生)。
最后,开发这么多年我也总结了一套学习Java的资料与面试题,如果你在技术上面想提升自己的话,可以关注我,私信发送领取资料或者在评论区留下自己的联系方式,有时间记得帮我点下转发让跟多的人看到哦。在这里插入图片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章