1. 程式人生 > >再談對協變和逆變的理解

再談對協變和逆變的理解

去年寫過一篇部落格談了下我自己對協變和逆變的理解,現在回頭看發現當時還是太過“膚淺”,根本沒理解。不久前還寫過一篇“黑”Java泛型的部落格,猛一回頭又是“膚淺”,今天學習Java泛型的時候又看到了協變和逆變,感覺又理解了點,記錄一下,但不免還是“膚淺”,看了這篇部落格的同學,歡迎留言交流下。

什麼是協變和逆變?

到底什麼是協變和逆變?先看例子:

  1. //Java
  2. Object[] objects =newString[2];
  3. //C#
  4. object[] objects =newstring[2];

這就是協變,C#和Java都是支援陣列協變的語言,好像說了等於沒說,別急,慢慢來。

我們都知道C#和Java中String型別都是繼承自Object的,姑且記做String ≦ Object

,表示String是Object的子型別,String的物件可以賦給Object的物件。

而Object的陣列型別Object[],我們可以理解成是由Object構造出來的一種新的型別,可以認為是一種構造型別,記f(Object)(可以類比下初中數學中函式的定義),那麼我們可以這麼來描述協變和逆變:

  • 當A ≦ B時,如果有f(A) ≦ f(B),那麼f叫做協變
  • 當A ≦ B時,如果有f(B) ≦ f(A),那麼f叫做逆變
  • 如果上面兩種關係都不成立則叫做不可變

其實顧名思義,協變和逆變表示的一種型別轉變的關係:“構造型別”之間相對“子型別”之間的一種關係。只不過平時我(可能也包括大家)被網上的一些文章搞糊塗了。“協”表示一種自然而然的轉換關係,比如上面的String[] ≦ Object[]

,這就是大家學習面向物件程式語言中經常說的:

子類變數能賦給父類變數,父類變數不能賦值給子類變數。

而“逆”則不那麼直觀,平時用的也很少,後面講Java泛型中的協變和逆變會看到例子。

不可變的例子就很多了,比如Java中List< Object >List< String >之間就是不可變的。

  1. List<String> list1 =newArrayList<String>();
  2. List<Object> list2 = list1;

這兩行程式碼在Java中肯定是編譯不過的,反過來更不可能,C#中也是一樣。

那麼協變

逆變作用到底是什麼呢?我個人膚淺的理解:主要是語言設計的一種考量,目的是為了增加語言的靈活性和能力。

里氏替換原則

再說下面內容之前,提下這個大家都知道的原則:

有使用父型別物件的地方都可以換成子型別物件。

假設有類Fruit和Apple,Apple ≦ Fruit,Fruit類有一個方法fun1,返回一個Object物件:

  1. publicObject fun1(){
  2. returnnull;
  3. }
  4. Fruit f =newFruit();
  5. //...
  6. //某地方用到了f物件
  7. Object obj = f.fun1();

那麼現在Aplle物件覆蓋fun1,假設可以返回一個String物件:

  1. @Override
  2. publicString fun1(){
  3. return"";
  4. }
  5. Fruit f =newApple();
  6. //...
  7. //某地方用到了f物件
  8. Object obj = f.fun1();

那麼任何使用Fruit物件的地方都能替換成Apple物件嗎?顯然是可以的。

舉得例子是返回值,如果是方法引數呢?呼叫父類方法fun2(String)的地方肯定可以被一個能夠接受更寬型別的方法替代:fun2(Object)......

返回值協變和引數逆變

上面提到的Java和C#語言都沒有把函式作為一等公民,那麼那些支援一等函式的語言,即把函式也看做一種型別是如何支援協變和逆變的以及里氏原則的呢?

也就是什麼時候用一個函式g能夠替代其他使用函式f的地方。答案是:

函式f可以安全替換函式g,如果與函式g相比,函式f接受更一般的引數型別,返回更特化的結果型別。《維基百科》

這就是是所謂的對輸入型別是逆變的而對輸出型別是協變的

雖然Java是面向物件的語言,但某種程度上它仍然遵守這個規則,見上一節的例子,這叫做返回值協變,Java子類覆蓋父類方法的時候能夠返回一個“更窄”的子型別,所以說Java是一門可以支援返回值協變的語言。

類似引數逆變是指子類覆蓋父類方法時接受一個“更寬”的父型別。在Java和C#中這都被當作了方法過載

可能到這又繞糊塗了,返回值協變引數逆變又是什麼東東?回頭看看協變和逆變的理解。把方法當成一等公民: 
構造型別:Apple ≦ Fruit 
返回值:String ≦ Object 
引數:Object ≧ String

以上都是我個人對協變和逆變這兩個概念的理解(歡迎拍磚)。說個題外話:“概念”是個很抽象的東西,之前聽到一個不錯說法,說概念這個單詞英文叫做conceptcon表示“共同的”,cept表示“大腦”。

Java泛型中的協變和逆變

一般我們看Java泛型好像是不支援協變或逆變的,比如前面提到的List<Object>List<String>之間是不可變的。但當我們在Java泛型中引入萬用字元這個概念的時候,Java 其實是支援協變和逆變的。

看下面幾行程式碼:

  1. // 不可變
  2. List<Fruit> fruits =newArrayList<Apple>();// 編譯不通過
  3. // 協變
  4. List<?extendsFruit> wildcardFruits =newArrayList<Apple>();
  5. // 協變->方法的返回值,對返回型別是協變的:Fruit->Apple
  6. Fruit fruit = wildcardFruits.get(0);
  7. // 不可變
  8. List<Apple> apples =newArrayList<Fruit>();// 編譯不通過
  9. // 逆變
  10. List<?superApple> wildcardApples =newArrayList<Fruit>();
  11. // 逆變->方法的引數,對輸入型別是逆變的:Apple->Fruit
  12. wildcardApples.add(newApple());

可見在Java泛型中通過extends關鍵字可以提供協變的泛型型別轉換,通過supper可以提供逆變的泛型型別轉換。

關於Java泛型中supperextends關鍵字的作用網上有很多文章,這裡不再贅述。只舉一個《Java Core》裡面supper使用的例子:下面的程式碼能夠對實現Comparable介面的物件陣列求最小值。

  1. publicstatic<T extendsComparable<T>> T min(T[] a){
  2. if(a ==null|| a.length ==0){
  3. returnnull;
  4. }
  5. T t = a[0];
  6. for(int i =1; i < a.length; i++){
  7. if(t.compareTo(a[i])>0){
  8. t = a[i];
  9. }
  10. }
  11. return t;
  12. }

這段程式碼對Calendar類是執行正常的,但對GregorianCalendar類則無法編譯通過:

  1. Calendar[] calendars =newCalendar[2];
  2. Calendar ret3 =CovariantAndContravariant.<Calendar> min(calendars);
  3. GregorianCalendar[] calendars2 =newGregorianCalendar[2];
  4. GregorianCalendar ret2 =CovariantAndContravariant.<GregorianCalendar> min(calendars2);//編譯不通過

如果想工作正常需要將方法簽名修改為: 
public static <T extends Comparable<? super T>> T min(T[] a)

至於原因,大家看下原始碼和網上大量關於supper的作用應該就明白了,我這裡希望能夠給看了上面內容的同學提供另外一個思路......

結束語

C#雖然不支援泛型型別的協變和逆變(介面和委託是支援的,我之前的那篇部落格也提到了),至於為什麼C#不支援,《深入解析C#》中說是主要歸結於兩種語言泛型的實現不同:C#是執行時的,Java只是一個“編譯時”特性。但究竟是為什麼還是沒說明白,希望有時間再研究下。