Java中的差异
差异是指更复杂类型之间的子类型如何与其组件之间的子类型相关。这里的“更复杂的类型”是指更高级别的结构,如容器和函数。因此,方差是关于容器和由通过类型层次结构连接的参数组成的函数之间的分配兼容性。它允许参数和子类型多态性的安全集成。
In java, variance is defined at the use-site.
方差的种类:方差有以下4种。 类型构造函数具有以下类型:Covariant If it accepts subtypes but not supertypes Contravariant If it accepts supertypes but not subtypes Bivariant If it accepts both supertypes and subtypes Invariant If it accepts neither supertypes nor subtypes.
Java中的不变性:使用站点在类型参数上必须没有开放边界。如果 A 是 B 的超类型,则 GenericType 不是 GenericType 的超类型,反之亦然。这意味着这两种类型彼此没有关系,在任何情况下都不能互换。
类型 1:不变容器
在Java中,不变量可能是您将遇到的第一个泛型示例,并且最直观。类型参数的方法可以按预期使用。类型参数的所有方法都可以访问。它们不能交换,您可以从它们中读取两者,如下图所示。
图 1:不可更换
// Type hierarchy Person :> Joe :> JoeJr
List p = new ArrayList<>();
// Ok
p.add(new Person());
// Ok
p.add(new Joe());
// Ok
p.add(new JoeJr());
插图 2:从它们读取对象
// Type hierarchy : Person :>Joe :> JoeJr
List joes = new ArrayList<>();
// Ok
Joe j = joes.get(0);
// Ok
Person p = joes.get(0);
类型 2: Java中的协方差
使用站点必须在类型参数上有一个开放的下限。如果 B 是 A的子类型,则 GenericType 是 GenericType。
注意: Java中的数组一直是协变的
在Java 1.5中引入泛型之前,数组是唯一可用的泛型容器。它们一直是协变的,例如。 Integer[]是Object[]的子类型。编译器允许您将Integer[]传递给接受Object[]的方法。如果该方法插入Integer的超类型,则在运行时抛出 ArrayStoreException。协变泛型类型规则在编译时实现这种检查,首先不允许错误发生。
例子
Java
class GFG {
public static void main(String args[])
{
Number[] numbers = new Number[] { 1, 2, 3, 4, 5 };
trick(numbers);
}
private static void trick(zobject[] objects)
{
objects[0] = new Float(123); // ok
objects[1] = new Objects(); // ArrayStoreException
// thrown at runtime
}
}
输出:
现在让我们讨论协变容器。 Java允许子类型化(协变)泛型类型,但它根据最小惊讶原则对可以“流入和流出”这些泛型类型的内容进行了限制。换句话说,具有类型参数返回值的方法是可访问的,而具有类型参数输入参数的方法是不可访问的。
示例 1:可以将超类型替换为子类型:
// Type hierarchy : Person :> Joe :> JoeJr
List extends Joe> = new ArrayList(); //ok
List extends Joe> = new ArrayList(); //ok
List extends Joe> = new ArrayList(); // Compile error
插图 2:从它们中读取是直观的:
//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
禁止写入它们(违反直觉)以防止上述数组的陷阱。例如。在下面的示例代码中,如果其他人的方法具有协变参数List< ? extends Person>添加了一个Jill。
// Type hierarchy : Person > Joe > JoeJr
List extends Joe> joes = new ArrayList<>();
joes.add(new Joe()); // compile error (you don't what subtype of Joe is in the list)
joes.add(new JoeJr()); // compile error
joes.add(new Person()); //compile error
joes.add(new Object()); // compile error
类型 3:逆变容器
逆变容器的行为违反直觉:与协变容器相反,无法访问具有类型参数的返回值的方法,而具有类型参数的输入参数的方法是可访问的:
图 1:您可以将子类型替换为超类型:
List<> super Joe> joes = new ArrayList(); // ok
List super Joe> joes = new ArrayList(); // ok
List super Joe> joes = new ArrayList(); //Compile Error
图 2:从它们读取时无法捕获特定类型:
List super Joe> joes = new ArrayList<>();
Joe j = joes.get(0); // compile error
Person p = joes.get(0); // compile error
Object o = joes.get(0); // because everything is a object in java
图 3:您可以添加“下限”的子类型:
List super Joe> Joes = new ArrayList<>();
joes.add(new JoeJr()); allowed
图 4:但是您不能添加超类型:
List super Joe> joes = new ArrayList<>();
joes.add(new Person()); // compile error
joes.add(new Object()); // compile error
类型 4: Java中的二元性
使用站点必须在类型参数上声明一个无界通配符。
具有无界通配符的泛型类型是同一泛型类型的所有有界变体的超类型。例如GenericType>是GenericType
Think of GenericType> as GenericType
让我们讨论具有 N 型参数的结构的方差。更复杂的类型(例如函数)呢?相同的原则适用,您只是需要考虑更多类型参数。下图说明了回答此类困境的方法。
插图:
Function personToJoe = null;
Function joeToJoeJr = null;
personToJoe = joeToJoeJr; // compile error
// Covariance
Function extends Person, ? extends Joe> personToJoe
= null;
Function jorToJorJr = null;
personToJoe = joeToJoeJr; // ok
// Contravariance
Function super Joe, ? super JoeJr> joeToJoeJr = null;
Function super Person, ? super Joe> personToJoe = null;
joeToJoeJr = personToJoe; // ok
变异和继承
插图 1: 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() {} // overriden
public void fail throws IOException {} // Overriden
}
说明 2:尝试使用协变参数覆盖方法只会导致重载:
interface Person {
void add(Person p);
}
interface Joe extends Person {
void add(Joe j);
}
class JoeImpl implements Joe {
public void add(Person p) {} // overload
public void add(Joe j) {} // overload
}
结论:差异给Java带来了额外的复杂性。虽然围绕方差的类型规则很容易理解,但有关类型参数方法的可访问性的规则是违反直觉的。理解它们不仅仅是“显而易见的”。它需要停下来思考逻辑后果。
方差在我的日常编程中提供了适度的净收益,特别是当需要与子类型兼容时(这在 OOP 中很常见)。