1. 程式人生 > >【Java入門提高篇】Day15 Java泛型再探——泛型通配符及上下邊界

【Java入門提高篇】Day15 Java泛型再探——泛型通配符及上下邊界

編譯器 pan 會有 認識 方法重載 上界 圖片 解決 int

  上篇文章中介紹了泛型是什麽,為什麽要使用泛型以及如何使用泛型,相信大家對泛型有了一個基本的了解,本篇將繼續講解泛型的使用,讓你對泛型有一個更好的掌握和更深入的認識。

  上篇中介紹完泛型之後,是不是覺得泛型挺好用的?既消除了Object的不安全類型轉化,又可以很方便的進行類型對象的存取,但是,等一下,有沒有考慮到這樣的情況。

  我們先定義一個水果類:

public class Fruit {
    private String name;

    public Fruit(String name){
        this.name = name;
    }

    public
String getName() { return name; } public void setName(String name) { this.name = name; } }

  然後再定義一個蘋果類:

public class Apple extends Fruit{
    public Apple(String name) {
        super(name);
    }
}

  接下來定義一個泛型容器:

public class GenericHolder<T> {
    private T obj;

    
public GenericHolder(){} public GenericHolder(T obj){ this.obj = obj; } public T getObj() { return obj; } public void setObj(T obj) { this.obj = obj; } }

  接下來開始我們的測試:

public class Test {

    /**
     * 吃水果
     * @param fruitHolder
     */
    public
static void eatFruit(GenericHolder<Fruit> fruitHolder){ System.out.println("我正在吃 " + fruitHolder.getObj().getName()); } public static void main(String args[]){ //這是一個貼了水果標簽的袋子 GenericHolder<Fruit> fruitHolder = new GenericHolder<Fruit>(); //這是一個貼了蘋果標簽的袋子 GenericHolder<Apple> appHolder = new GenericHolder<Apple>(); //這是一個水果 Fruit fruit = new Fruit("水果"); //這是一個蘋果 Apple apple = new Apple("蘋果"); //現在我們把水果放進去 fruitHolder.setObj(fruit); //調用一下吃水果的方法 eatFruit(fruitHolder); //貼了水果標簽的袋子放水果當然沒有問題 //現在我們把水果的子類——蘋果放到這個袋子裏看看 fruitHolder.setObj(apple); //同樣是可以的,其實這時候會發生自動向上轉型,apple向上轉型為Fruit類型後再傳入fruitHolder中 //但不能再將取出來的對象賦值給redApple了 //因為袋子的標簽是水果,所以取出來的對象只能賦值給水果類的變量 //無法通過編譯檢測 redApple = fruitHolder.getObj(); eatFruit(fruitHolder); //放蘋果的標簽,自然只能放蘋果 appHolder.setObj(apple); // 這時候無法把appHolder 傳入eatFruit // 因為GenericHolder<Fruit> 和 GenericHolder<Apple>是兩種不同的類型 // eatFruit(appHolder); } }

  運行結果:

我正在吃 水果
我正在吃 蘋果

  在這裏,我們往eatFruit方法裏傳入fuitHolder的時候,是可以正常編譯的,但是如果將appHolder傳入,就無法通過編譯了,因為作為參數時,GenericHolder<Fruit> 和 GenericHolder<Apple>是兩種不同的類型,所以無法通過編譯,那麽問題來了,如果我想讓eatFruit方法能同時處理GenericHolder<Fruit> 和 GenericHolder<Apple>兩種類型怎麽辦?而且這也是很合理的需求,畢竟Apple是Fruit的子類,能吃水果,為啥不能吃蘋果???如果要把這個方法重載一次,未免也有些小題大做了(而且事實上也無法通過編譯,具體原因之後會有說明)。

  在代碼的邏輯裏:

  • 蘋果 IS-A 水果
  • 裝蘋果的盤子 NOT-IS-A 裝水果的盤子

  這個時候,泛型的邊界符就有它的用武之地了。我們先來看效果:

public class Test {

    /**
     * 吃水果
     * @param fruitHolder
     */
    public static void eatFruit(GenericHolder<? extends Fruit> fruitHolder){
        System.out.println("我正在吃 " + fruitHolder.getObj().getName());
    }

    public static void main(String args[]){
        //這是一個貼了水果標簽的袋子
        GenericHolder<Fruit> fruitHolder = new GenericHolder<Fruit>();
        //這是一個貼了蘋果標簽的袋子
        GenericHolder<Apple> appHolder = new GenericHolder<Apple>();
        //這是一個水果
        Fruit fruit = new Fruit("水果");
        //這是一個蘋果
        Apple apple = new Apple("蘋果");

        //現在我們把水果放進去
        fruitHolder.setObj(fruit);
        //調用一下吃水果的方法
        eatFruit(fruitHolder);

        //放蘋果的標簽,自然只能放蘋果
        appHolder.setObj(apple);
        // 這時候可以順利把appHolder 傳入eatFruit
        eatFruit(appHolder);
    }
}

  運行結果:

我正在吃 水果
我正在吃 蘋果

  這裏我們只是使用了一點小小的魔法,把參數類型改成了GenericHolder<? extends Fruit>,這樣就能將 GenericHolder<Apple>類型的參數順利傳入了,怎麽樣?很好用吧,這就是泛型的邊界符,用<? extends Fruit>的形式表示。邊界符的意思,自然就是定義一個邊界,這裏用?表示傳入的泛型類型不是固定類型,而是符合規則範圍的所有類型,用extends關鍵字定義了一個上邊界,也就是說這裏的?可以代表任何繼承於Fruit的類型,你也許會問,為什麽是上邊界,好問題,一圖勝千言:

技術分享圖片

  從這個圖可以很好的看出這個“上邊界”的概念了吧。有上邊界,自然有下邊界,泛型裏使用形如<? super Fruit>的方式使用下邊界,此時,?只能代表Fruit及其父類。

技術分享圖片

  (這兩個圖是摳過來的,不要罵我懶。)

  這兩種方式基本上解決了我們之前的問題,但是同時,也有一定的限制。

  1.上界<? extends T>不能往裏存,只能往外取

  不要太疑惑,其實很好理解,因為編譯器只知道容器裏的是Fruit或者Fruit的子類,但不知道它具體是什麽類型,所以存的時候,無法判斷是否要存入的數據的類型與容器種的類型一致,所以會拒絕set操作。

  2.下界<? super T>往外取只能賦值給Object變量,不影響往裏存

  因為編譯器只知道它是Fruit或者它的父類,這樣實際上是放松了類型限制,Fruit的父類一直到Object類型的對象都可以往裏存,但是取的時候,就只能當成Object對象使用了。

  所以如果需要經常往外讀,則使用<? extends T>,如果需要經常往外取,則使用<? super T>。

  至此,本篇講解完畢,歡迎大家繼續關註!

【Java入門提高篇】Day15 Java泛型再探——泛型通配符及上下邊界