翻譯 | Java 中的變型(Variance)

原文自國外Java社區javacodegeeks,做者爲 George Aristy,傳送門html

前幾天,我在偶然的狀況下看到一篇文章,講述了文章做者在使用了 GO 8個多月後對其的利弊見解。在使用 GO 工做了至關長的一段時間的我來講,基本上贊成做者說的點。java

儘管是這種序言,但這篇文章時關於 Java 的變型的,目標是從新理解什麼是變型,以及在 Java 實現中的一些細微差異。編程

什麼是變型?

維基上是這樣描述變型的:數組

所謂的變型是指如何根據組成類型之間的子類型關係,來肯定更復雜的類型之間的子類型關係。安全

「更復雜的類型」在這裏是指諸如容器、函數等高級別的結構。所以,變型是關於由經過類型層次結構(Type Hierarchy)鏈接的參數組成的容器及函數之間的賦值兼容。它容許參數和子類型多態性的安全集成。例如,是否能夠將一個方法中返回的cat 列表賦值到類型爲 「list of animals」 的變量中?我可否一將奧迪汽車的對象列表傳遞給一個接受 Cars 列表的方法當中?函數

在 Java,是定義在使用點變型(use-site)當中。spa

變型的4種類型

在維基中的闡述中,類型構造器指:code

  • 協變(Covariant):接受子類型不接受超類型
  • 逆變(Contravariant):接受超類型不接受子類型
  • 雙變(Bivariant):同時接受子類型和超類型
  • 不可變(Invariant):不接受子類型和超類型

(顯然,聲明的類型參數在全部狀況下都是能夠接受的)cdn

Java 中的不可變性(Invariance)

使用點變型在類型參數中必須不設定邊界。server

若是 AB 的其中一個超類型,那麼 GenericType<A> 並非 GenericType<B> 的超類型,反之亦然。

這表示兩種類型彼此沒有聯繫,而且在任何狀況下都沒法轉換成對方。

不變容器

在 Java 中,不變量多是你遇到過的第一個,而且是最直觀的泛型示例。正如所指望的,類型參數的方法是可以使用的。參數類型的全部方法都是可訪問的。

但它們沒法互換:

// 類型層級:Person :> Joe :> JoeJr
List<Person> p = new ArrayList<Joe>(); // 編譯錯誤
List<Joe> j = new ArrayList<Person>(); // 編譯錯誤
複製代碼

但可以添加對象:

// 類型層級:Person :> Joe :> JoeJr
List<Person> p = new ArrayList<>();
p.add(new Person()); // ok
p.add(new Joe()); // ok
p.add(new JoeJr()); // ok
複製代碼

也可以讀取到:

// 類型層級:Person :> Joe :> JoeJr
List<Joe> joes = new ArrayList<>();
Joe j = joes.get(0); // ok
Person p = joes.get(0); // ok
複製代碼

Java 中的協變

使用點變型必須對類型參數有一個公開的下界。

若是 BA 的子類型,那麼 GenericType<B>GenericType<? extends A> 的子類型。

Java 中的數組一直是協變的

在 Java 1.5 引入泛型以前,數組是惟一可用的泛型容器。它們一直具備協變性,例如,Integer[]Object[] 的子類型。編譯器容許你將 Integer[] 傳遞給接收 Object[] 的方法中。若是方法插入一個 Integer 的超類型, ArrayStoreException 異常會在運行時拋出。協變泛型類型規則在編譯時實現了此類檢查,在第一時間防止錯誤的發生。

public static void main(String... args) {
  Number[] numbers = new Number[]{1, 2, 3, 4, 5};
  trick(numbers);
}
 
private static void trick(Object[] objects) {
  objects[0] = new Float(123);  // ok
  objects[1] = new Object();  // ArrayStoreException 在運行時拋出
}
複製代碼

協變容器

Java 容許子類型(協變)泛型類型,可是它根據最小驚訝原則(POLA)限制了這些泛型類型怎樣作到「流入和流出」。換而言之,返回類型參數值的方法是可訪問的,而具備類型參數輸入參數的方法是不可訪問的。

你能夠將超類型替換爲子類型:

// 類型層級:Person :> Joe :> JoeJr
List<? extends Joe> = new ArrayList<Joe>(); // ok
List<? extends Joe> = new ArrayList<JoeJr>(); // ok
List<? extends Joe> = new ArrayList<Person>(); // 編譯錯誤
複製代碼

從容器中讀取也很直觀:

// 類型層級: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); //
複製代碼

但不容許跨層寫入(違反直覺),以預防數組陷阱。例如,在下面的例子中,List<Joe> 的調用者/擁有者會感到驚訝若是其餘帶有協變參數 List<? extends Person> 的方法添加一個 Jill 對象。

// 類型層級:Person :> Joe :> JoeJr
List<? extends Joe> joes = new ArrayList<>();
joes.add(new Joe());  // 編譯錯誤 (你不清楚哪一種 Joe 的超類型在列表中)
joes.add(new JoeJr()); // 編譯錯誤 (同上)
joes.add(new Person()); // 編譯錯誤
joes.add(new Object()); // 編譯錯誤
複製代碼

Java 中的逆變

使用點變型必須對類型參數有一個公開的界。

若是 AB 的超類型,那麼 GenericType<A>GenericType<? extends B> 的超類型。

逆變容器

逆變容器的行爲和常識相反:與協變容器相反,訪問具備類型參數返回值的方法是不可行的,而訪問具備類型參數入參的方法是可行的:

你能夠將子類型替換爲超類型:

// 類型層級:Person :> Joe :> JoeJr
List<? super Joe> joes = new ArrayList<Joe>();  // ok
List<? super Joe> joes = new ArrayList<Person>(); // ok
List<? super Joe> joes = new ArrayList<JoeJr>(); // 編譯錯誤
複製代碼

沒法在讀取時捕獲特定類型:

// 類型層級:Person :> Joe :> JoeJr
List<? super Joe> joes = new ArrayList<>();
Joe j = joes.get(0); // 編譯錯誤 (可以爲 Object 或者 Person)
Person p = joes.get(0); // 編譯錯誤 (同上)
Object o = joes.get(0); // 容許,由於在 Java everything IS-A Object
複製代碼

能夠添加「下界」的子類型:

// 類型層級:Person :> Joe :> JoeJr
List<? super Joe> joes = new ArrayList<>();
joes.add(new JoeJr()); // 容許
複製代碼

但你不能添加超類型:

// 類型層級:Person :> Joe :> JoeJr
List<? super Joe> joes = new ArrayList<>();
joes.add(new Person()); // 編譯錯誤
joes.add(new Object()); // 編譯錯誤
複製代碼

雙變類型

使用點變型必須在類型參數中聲明無界通配符

具備無界通配符的泛型類型是同一泛型類型的全部有界變體的超類型。例如,GenericType<?>GenericType<String> 的超類型。因爲無界類型是 hierarchy 類型的根,所以,對於它的參數類型,它只能訪問繼承自 java.lang.Object 的方法。

GenericType<?> 視爲 GenericType<Object>

N型參數結構的變型

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() {} // 重寫
  public void fail() throws IOException {} // 重寫
}
複製代碼

可是試圖用協變參數覆蓋方法只會致使重載:

interface Person {
  void add(Person p);
}
 
interface Joe extends Person {
  void add(Joe j);
}
 
class JoeImpl implements Joe {
  public void add(Person p) {}  // 重載
  public void add(Joe j) {} // 重載
 }
複製代碼

結語

變型爲 Java 帶來了額外的複雜性。雖然圍繞變型的類型規則很容易理解,可是關於類型參數方法的可訪問性規則是違反常識的。理解它們不只僅要達到「顯而易見」 – 須要停下來來思考當中的邏輯。

然而,個人平常經驗是告訴我,這些細微的差異一般都不礙事:

  • 我一直沒試過必須聲明一個逆變參數的實例,並且我也不多遇到它們(儘管它們確實存在)
  • 協變參數視彷佛更常見一些,但幸運的是他們也更容易推理出來

考慮到子類型是面向對象編程中其中一種基本的技術,而變型就是其最大的一個優勢。

結論:變型在我平常編程中提供適當的收益,特別是當須要與子類型兼容的時候(這在面向對象編程中很常見)。


小喇叭

廣州蘆葦科技Java開發團隊

蘆葦科技-廣州專業互聯網軟件服務公司

抓住每一處細節 ,創造每個美好

關注咱們的公衆號,瞭解更多

想和咱們一塊兒奮鬥嗎?lagou搜索「 蘆葦科技 」或者投放簡歷到 server@talkmoney.cn 加入咱們吧

關注咱們,你的評論和點贊對咱們最大的支持

相關文章
相關標籤/搜索