日日操夜夜添-日日操影院-日日草夜夜操-日日干干-精品一区二区三区波多野结衣-精品一区二区三区高清免费不卡

公告:魔扣目錄網為廣大站長提供免費收錄網站服務,提交前請做好本站友鏈:【 網站目錄:http://www.ylptlb.cn 】, 免友鏈快審服務(50元/站),

點擊這里在線咨詢客服
新站提交
  • 網站:51998
  • 待審:31
  • 小程序:12
  • 文章:1030137
  • 會員:747


從面向對象說起

JAVA作為一門面相對象的語言,當然是支持面相對象的三大基本特性的,反手就蹦出三個詞:封裝、繼承、多態。

我們假設有三個類,動物、貓、狗。父類是動物Animal,有兩個子類貓Cat和狗Dog。

那在Java中或其它任何支持面相對象的語言中,子類可以把引用賦值給父類。下面這段代碼沒有任何問題:

Animal animalOne = new Cat();
Animal animalTwo = new Dog();
復制代碼

理論上來說,一只貓是一只動物,一只狗也是一只動物,所以這完全是可以理解的。其實,這也是SOLID原則中的“里氏替換原則”的一種體現。

數組的協變

如果一只貓是一只動物,那一群貓是一群動物嗎?一群狗是一群動物嗎?Java數組認為是的。于是你可以這樣寫:

Animal[] animals = new Cat[2];
復制代碼

這看起來也沒有什么問題。但既然都是一群動物了,我往這一群動物中添加一只貓、一只狗,它還是一群動物,這應該是合理的對吧?來看看這段代碼:

Animal[] animals = new Cat[2];
animals[0] = new Cat();
// 下面這行代碼會拋運行時異常
animals[1] = new Dog();
Animal animal = animal[0];
復制代碼

很好,編譯沒有任何問題。但是一運行,會拋出一個運行時異常:ArrayStoreException。這個異常頭頂的注釋已經寫得很明顯了,如果你往數組中添加一個類型不對的對象,就會拋這個異常。它是從JDK 1.0就存在的一個異常。

這么一想,對啊,animals雖然門面上是一個Animal數組,但是它運行時的本質還是一個Cat數組啊,一個Cat數組怎么能添加一個Dog呢?但Java編譯器并沒有這么智能,而且上述代碼在編

譯器看來也是合理合法的,所以也就讓它編譯過了。

所以這種情況,編譯器100%過,而運行時100%拋異常,這不是大寫的BUG是啥?

如果Cat是Animal的子類型,那么Cat[]也是Animal[]的子類型,我們稱這種性質為協變(covariance)。Java中,數組是協變的

泛型的不變性

在Java 1.5之前,是沒有泛型的。那個時候從集合中存取對象都是Object類型,所以每次取出對象后必須進行強轉:

List list = new LinkedList();
list.add(123);
list.add("123");

int a = (int)list.get(0);
// 下面這段代碼會在運行時拋異常
int b = (int)list.get(1);
復制代碼

如果不小心存入集合中對象類型是錯的,會在運行時報強轉異常。而1.5提供泛型以后,可以讓編譯器自動幫助轉換,并對代碼進行檢查,使程序更加安全。

在Java8又加入了泛型的類型推導功能,使用泛型以后,我們的代碼看起來變得簡潔又安全了:

List<Integer> list = new LinkedList<>();
list.add(123);
// 下面這局代碼編譯節點會報錯
list.add("123");

int a = list.get(0);
復制代碼

《Effective Java》中,第28條(第三版)說,列表優先于數組。Java在使用列表+泛型時,吸取了上面數組的教訓。前面提到,Java中數組是協變的,所以會有些問題。而Java中的泛型是不變(invariance)的,也就是說,List<Cat>并不是List<Animal>的子類型。所以像下面這樣寫,編譯器會直接報錯。

List<Cat> cats = new LinkedList<>();
// 編譯器報錯
List<Animal> animals = cats;
復制代碼

這樣就可以在編譯期對代碼進行檢查,防止它在運行期才發現錯誤拋異常。

不變不能解決所有問題

泛型是不變的,所以我們使用泛型的時候,能夠更加安全。

但是在使用一門面向對象的語言中,我們難免會有需要集合也支持一些面向對象的特性的場景。我們可以簡單地把它們分成生產場景和消費場景

消費場景的協變

比如,我希望有一個Animal的集合,我不用去管它里面存的具體類型是什么,但我每次從這個集合取出來的,一定是一個Animal或其子類。這是一種典型的消費場景,從集合中取出元素來消費。

在消費場景,Java提供了通配符和extends關鍵字來支持泛型的協變。來看看這段代碼:

List<? extends Animal> animals = new LinkedList<Cat>();
// 以下四行代碼都不能編譯通過
// animals.add(new Dog());
// animals.add(new Cat());
// animals.add(new Animal());
// animals.add(new Object());
// 可以添加null,但沒意義
animals.add(null);
// 可以安全地取出來
Animal animal = animals.get(0);
復制代碼

也就是說,雖然因為泛型的不變性,List<Cat>并不是List<Animal>的子類型,但Java通過其它方式來支持了泛型的協變,List<Cat>是List<? extends Animal>的子類型。與此同時,Java在編譯器層面通過禁止寫入的方式,保證了協變下的安全性

為什么協變下不能寫入呢?因為協變下寫入是不安全的,想想文章最開頭那個數組的協變的例子。

生產場景的逆變

我們希望有一個集合,可以往里面寫入Animal及其子類。那可以通過super關鍵字來定義泛型集合:

// 下面這行代碼編譯不通過
// List<? super Animal> animals = new LinkedList<Cat>();
// 下面都是OK的寫法
// List<? super Animal> animals = new LinkedList<Object>();
// List<? super Animal> animals = new LinkedList<Animal>();
// 等價于上面一行的寫法
List<? super Animal> animals = new LinkedList<>();
animals.add(new Cat());
animals.add(new Dog());
// 取出來一定是Object
Object object = animals.get(0);

// 這樣寫是OK的
List<? super Cat> cats = new LinkedList<Animal>();
復制代碼

逆變(contravariance),也稱逆協變,從名字可以看出來,它與協變的性質是相反的。也就是說,List<Animal>是List<? super Cat>的子類型。

上界和下界

我們會在很多資料里看到對Java中泛型extends和super關鍵字的解讀,說extends決定了上界,super決定了下界。

為什么這么說呢?其實看完上面兩個小節,你會明白,這里的上界和下界,其實本質上指的是,在定義泛型的時候,子類型的邊界。換句話說,在運行時真正的類型

我們用X來指代類型,看看下面兩行代碼:

// X可以是Animal及其子類,Animal是X的上界
List<? extends Animal> animals = new LinkedList<X>();
// X可以是Cat及其父類,Cat是X的下界
List<? super Cat> cats = new LinkedList<X>();
復制代碼

任意類型通配符

在Java代碼中,你可能還看到這種寫法:<?>,它代表任意類型通配符。老規矩,直接上代碼:

List<?> anyOne = new LinkedList<Animal>();
List<?> anyTwo = new LinkedList<Cat>();
List<?> anyThree = new LinkedList<Object>();
// anyFour等價于anyThree的寫法
List<?> anyFour = new LinkedList<>();
// 這種寫法編譯不通過
// List<?> anyFive = new LinkedList<?>();

// 具有extends和super的性質
// 這種寫法編譯不通過
// anyOne.add(new Cat());
// anyOne.add(new Object());
// 能取出來Object類型
Object o = anyOne.get(0);
復制代碼

也就是說,它是“無界”的,對于任意類型X,List<X>都是List<?>的子類型。但List<?>不能add,get出來也是Object類型。它同時具有協變和逆變的兩種性質,上界是Object,但不能調用add方法。

那它與List<Object>有什么區別呢?根據前面的推斷,有兩個比較明顯的區別:

  • List<Object>可以調用add方法,但List<?>不能。
  • List<?>可以協變,上界是Object,但List<Object>不能協變。

Collection源碼解讀

看到這里你可能還有一些疑惑,什么時候應該用泛型的協變、逆變呢?我們來看看Collection接口的幾個方法簽名(JDK 1.8版本)。

boolean add(E e);
boolean addAll(Collection<? extends E> c);
boolean contains(Object o);
boolean containsAll(Collection<?> c);

default boolean removeIf(Predicate<? super E> filter) {
    Objects.requireNonNull(filter);
    boolean removed = false;
    final Iterator<E> each = iterator();
    while (each.hasNext()) {
        if (filter.test(each.next())) {
            each.remove();
            removed = true;
        }
    }
    return removed;
}
復制代碼

add和addAll

首先我們來看add和addAll方法。下面這段代碼:

Collection<Animal> animals = new LinkedList<>();
animals.add(new Cat());
animals.add(new Animal());

Collection<Cat> cats = new LinkedList<>();
Collection<Object> objects = new LinkedList<>();
animals.addAll(cats);
// 以下代碼編譯不通過,因為不安全
animals.addAll(objects);
復制代碼

為什么這段代碼可以編譯通過且運行時安全?對于animals,它的泛型是<Animal>,根據里氏替換原則,add方法可以添加Animal及其子類對象。

而對于addAll方法來說,因為方法參數聲明的是<? extends E>,而這里的E是我們聲明Collection用的泛型Animal,所以其實addAll的方法參數類型是Collection<? extends Animal>。

結合前文我們知道,這里應用了協變的特性,Collection<Cat>在參數傳遞的時候被轉換成了Collection<? extends Animal>。

而我們看源碼可以發現,這里的參數傳進來之后,是只讀的,也就是只有消費場景,所以可以使用協變。而如果是allAll(Collection<E> c)這種方法參數的話,就不能支持上述代碼,往其中添加一個cats了。

contains和containsAll

contains方法沒有使用泛型,而是直接使用了一個Object對象,它可以在任何時候調用。那為什么contains方法不像add方法一樣,使用泛型,是contains(T t)呢?

因為如果這樣定義了的話,contains方法也會像add方法一樣,受到協變的限制,聲明為Collection<? extends Animal>的對象就不能使用contains方法了。盡管我們確信在contains方法內部并不會修改List中的對象(因此不會有類型安全的問題)。在Java中我們沒有辦法解決這個問題,因此,只能寫成contains(Object o)。

對于containsAll方法,先看看這段代碼:

Collection<Animal> animals = new LinkedList<>();
Collection<Cat> cats = new LinkedList<>();
Collection<Object> objects = new LinkedList<>();

animals.containsAll(cats);
animals.containsAll(objects);
復制代碼

為什么containsAll的方法參數是Collection<?> c呢?

首先,不能用Collection<Object> c,因為這樣的話,就不能協變了,上述代碼animals.containsAll(cats)就會編譯不通過,盡管我們知道這段代碼是安全的。

然后,為什么不能像allAll方法那樣,用協變Collection<? extends E> c呢?因為我們知道,containsAll方法對Collection沒有副作用,而addAll有。所以我們不能animals.addAll(objects),但可以animals.containsAll(objects)。

最后,為什么又不能用逆變Collection<? super E> c呢?因為這樣的話,就不能讓animals.containsAll(cats)編譯通過了。

所以只能選擇Collection<?> c。它是無界的,且具有協變性質,且取出來是Object對象,剛好內部實現也是循環去調用contains方法,與contains方法的參數類型Object一致。

同理,remove和removeAll和這兩個方法是類似的寫法,這里就不過多描述了。

removeIf

這個方法的參數是一個Predicate。用過Java 8的都知道,這是一個函數式接口。在這里使用了逆變,Predicate<? super E> filter定義了filter的下界。對于Predicate來說,這里是一個生產場景,所以應該使用逆變。

這里為什么要用逆變其實也很簡單,因為在調用removeIf的時候,我們只能保證animals里面的元素是Animal,但我們并不知道具體的子類型。所以下面這種代碼是不安全的,

Collection<Animal> animals = new LinkedList<>();
Predicate<Cat> catPredicate = cat -> true;
// 因為removeIf逆變的限制,所以下面這行代碼編譯不通過
animals.removeIf(catPredicate);
復制代碼

對我們日常工作有什么用?

看到這里,可能有的朋友已經開始吐槽了,我有必要了解這些嗎?面試造火箭,工作擰螺絲?

其實不然,泛型是Java乃至很多面向對象語言的一種最基本的語言特性,所以知道它為什么這么設計是非常重要的。平時我們看源碼的時候,看到這樣的代碼才會心中有數。

另一方面,隨著編程水平的提高,難免有一些比較復雜的代碼設計,或多或少會使用到泛型。合理地使用泛型、結合泛型的協變和逆變的特性能夠讓我們的代碼變得更安全,比如上面Collection中用到的Predicate,就用了逆變的性質。

簡單總結一下,Java的數組是協變的,泛型是不變的。但泛型可以通過extends關鍵字實現協變,通過super關鍵字實現逆變,分別應用于不同的場景。協變應用于消費場景,定義了上界。逆變應用于生產場景,定義了下界。

當然了,不同語言有不同的解決方案。后面會有一篇文章為大家分享Kotlin是如何設計泛型和協變/逆變的,敬請期待~

分享到:
標簽:Java 泛型
用戶無頭像

網友整理

注冊時間:

網站:5 個   小程序:0 個  文章:12 篇

  • 51998

    網站

  • 12

    小程序

  • 1030137

    文章

  • 747

    會員

趕快注冊賬號,推廣您的網站吧!
最新入駐小程序

數獨大挑戰2018-06-03

數獨一種數學游戲,玩家需要根據9

答題星2018-06-03

您可以通過答題星輕松地創建試卷

全階人生考試2018-06-03

各種考試題,題庫,初中,高中,大學四六

運動步數有氧達人2018-06-03

記錄運動步數,積累氧氣值。還可偷

每日養生app2018-06-03

每日養生,天天健康

體育訓練成績評定2018-06-03

通用課目體育訓練成績評定