kotlin 協變、逆變 - 貓和魚的故事

網上找的一段協變、逆變比較正式的定義:java

逆變與協變用來描述類型轉換後的繼承關係,其定義:若是 A、B 表示類型,f(⋅) 表示類型轉換, 表示繼承關係(好比,A≦B 表示 A 是由 B 派生出來的子類): 當 A ≦ B 時,若是有 f(A) ≦ f(B) ,那麼 f 是協變的; 當 A ≦ B 時,若是有 f(B) ≦ f(A) ,那麼 f 是逆變的; 若是上面兩種關係都不成立,即 (A)f(B) 相互之間沒有繼承關係,則叫作不變的。安全

java 中能夠經過以下泛型通配符以支持協變和逆變:markdown

  • ? extends 來使泛型支持協變。修飾的泛型集合只能讀取不能修改,這裏的修改僅指對泛型集合添加元素,若是是 remove(int index) 以及 clear 固然是能夠的。
  • ? super 來使泛型支持逆變。修飾的泛型集合只能修改不能讀取,這裏說的不能讀取是指不能按照泛型類型讀取,你若是按照 Object 讀出來再強轉固然也是能夠的。

以動物舉例,看代碼。ide

abstract class Animal {
    void eat() {
        System.out.println("我是" + myName() + ", 我最喜歡吃" + myFavoriteFood());
    }

    abstract String myName();

    abstract String myFavoriteFood();
}

class Fish extends Animal {

    @Override
    String myName() {
        return "魚";
    }

    @Override
    String myFavoriteFood() {
        return "蝦米";
    }
}

class Cat extends Animal {

    @Override
    String myName() {
        return "貓";
    }

    @Override
    String myFavoriteFood() {
        return "小魚乾";
    }
}

public static void extendsFun() {
    List<Fish> fishList = new ArrayList<>();
    fishList.add(new Fish());
    List<Cat> catList = new ArrayList<>();
    catList.add(new Cat());
    List<? extends Animal> animals1 = fishList;
    List<? extends Animal> animals2 = catList;

    animals2.add(new Fish()); // 報錯
    Animal animal1 = animals1.get(0);
    Animal animal2 = animals2.get(0);
    animal1.eat();
    animal2.eat();
}

//輸出結果:
我是魚, 我最喜歡吃蝦米
我是貓, 我最喜歡吃小魚乾
複製代碼

協變就比如有多個集合,每一個集合存儲的是某中特定動物(extends Animal),可是不告訴你那個集合裏存儲的是魚,哪一個是貓。因此你雖然能夠從任意一個集合中讀取一個動物信息,沒有問題,可是你沒辦法將一條魚的信息存儲到魚的集合裏,由於僅從變量 animals一、animals2 的類型聲明上來看你不知道哪一個集合裏存儲的是魚,哪一個集合裏是貓。 假如報錯的代碼不報錯了,那不就說明把一條魚塞進了一堆貓裏,這屬於給貓加菜啊,因此確定是不行的。? extends 類型通配符所表達的協變就是這個意思。學習

那逆變是什麼意思呢?仍是以上面的動物舉例:spa

public static void superFun() {
    List<Fish> fishList = new ArrayList<>();
    fishList.add(new Fish());
    List<Animal> animalList = new ArrayList<>();
    animalList.add(new Cat());
    animalList.add(new Fish());
    List<? super Fish> fish1 = fishList;
    List<? super Fish> fish2 = animalList;

    fish1.add(new Fish());
    Fish fish = fish2.get(0); //報錯
}
複製代碼

從變量 fish一、fish2 的類型聲明上只能知道里面存儲的都是魚的父類,若是這裏也不報錯的話可就從 fish2 的集合裏拿出一隻貓賦值給一條魚了,這屬於謀殺親魚。因此確定也是不行。? super 類型通配符所表達的逆變就是這個意思。code

kotlin 中對於協變和逆變也提供了兩個修飾符:orm

  • out:聲明協變;
  • in:聲明逆變。

它們有兩種使用方式:對象

  • 第一種:和 java 同樣在使用處聲明;
  • 第二種:在類或接口的定義處聲明。

當和 java 同樣在使用處聲明時,將上面 java 示例轉換爲 kotlin繼承

fun extendsFun() {
    val fishList: MutableList<Fish> = ArrayList()
    fishList.add(Fish())
    val catList: MutableList<Cat> = ArrayList()
    catList.add(Cat())
    val animals1: MutableList<out Animal> = fishList
    val animals2: MutableList<out Animal> = catList
    animals2.add(Fish()) // 報錯
    val animal1 = animals1[0]
    val animal2 = animals2[0]
    animal1.eat()
    animal2.eat()
}

fun superFun() {
    val fishList: MutableList<Fish> = ArrayList()
    fishList.add(Fish())
    val animalList: MutableList<Animal> = ArrayList()
    animalList.add(Cat())
    animalList.add(Fish())
    val fish1: MutableList<in Fish> = fishList
    val fish2: MutableList<in Fish> = animalList
    fish1.add(Fish())
    val fish: Fish = fish2[0] //報錯
}
複製代碼

能夠看到在 kotlin 代碼中除了將 ? extends 替換爲了 out,將 ? super 替換爲了 in,其餘地方並無發生變化,而產生的結果是同樣的。那在類或接口的定義處聲明 in、out 的做用是什麼呢。

假設有一個泛型接口 Source<T>,該接口中不存在任何以 T 做爲參數的方法,只是方法返回 T 類型值:

// Java
interface Source<T> {
  T nextT();
}
複製代碼

那麼,在 Source <Object> 類型的變量中存儲 Source <String> 實例的引用是極爲安全的——沒有消費者-方法能夠調用。可是 Java 並不知道這一點,而且仍然禁止這樣操做:

// Java
void demo(Source<String> strs) {
  Source<Object> objects = strs; // !!!在 Java 中不容許
  // ……
}
複製代碼

爲了修正這一點,咱們必須聲明對象的類型爲 Source<? extends Object>,但這樣的方式很複雜。而在 kotlin 中有一種簡單的方式向編譯器解釋這種狀況。咱們能夠標註 Source 的類型參數 T 來確保它僅從 Source<T> 成員中返回(生產),並從不被消費。爲此咱們使用 out 修飾符修飾泛型 T

interface Source<out T> {
    fun nextT(): T
}

fun demo(strs: Source<String>) {
    val objects: Source<Any> = strs // 這個沒問題,由於 T 是一個 out-參數
    // ……
}
複製代碼

還記得開篇協變的定義嗎?

A ≦ B 時,若是有 f(A) ≦ f(B) ,那麼 f 是協變的; 當 A ≦ B 時,若是有 f(B) ≦ f(A) ,那麼 f 是逆變的;

也就是說:

當一個類 C 的類型參數 T 被聲明爲 out 時,那麼就意味着類 C 在參數 T 上是協變的;參數 T 只能出如今類 C 的輸出位置,不能出如今類 C 的輸入位置。

一樣的,對於 in 修飾符來講

當一個類 C 的類型參數 T 被聲明爲 in 時,那麼就意味着類 C 在參數 T 上是逆變的;參數 T 只能出如今類 C 的輸如位置,不能出如今類 C 的輸出位置。

interface Comparable<in T> {
    operator fun compareTo(other: T): Int
}

fun demo(x: Comparable<Number>) {
    x.compareTo(1.0) // 1.0 擁有類型 Double,它是 Number 的子類型
    // 所以,咱們能夠將 x 賦給類型爲 Comparable <Double> 的變量
    val y: Comparable<Double> = x // OK!
}
複製代碼

總結以下表:

image

你們有其餘見解的能夠留言一塊兒交流學習!點個讚唄!

相關文章
相關標籤/搜索