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的資料與面試題,如果你在技術上面想提升自己的話,可以關注我,私信發送領取資料或者在評論區留下自己的聯繫方式,有時間記得幫我點下轉發讓跟多的人看到哦。在這裏插入圖片描述

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