Java編程思想__泛型(五)

邊界

  • 邊界使得你可以在用於泛型的參數類型上設置限制條件。
  • 儘管這使得你可以強制規定泛型可以應用的類型,但是其潛在的一個重要的效果是你可以按照自己的邊界類型來調用方法。
  • 因爲擦除移除了類型信息,所以,可以用無邊界泛型參數調用的方法只是那些可以用 Object調用的方法。但是如果能夠將這個參數限制爲某個類型子集,那麼你就可以用這些類型子集來調用方法。
  • 爲了執行這種限制,Java 泛型重用了 extends 關鍵字。對你來說有一點很重要,既要理解 extends 關鍵字在泛型邊界上下文環境中和在普通情況下所具有的意義是完全不同的。
public interface HasColor {

   public void getColor();
}

class Colored<T extends HasColor>{
    T t;
    Colored(T t){
        this.t=t;
    }
    T getT(){
        return t;
    }
    //界限允許您調用方法
    void color(){
        t.getColor();
    }
}
  1. 這只是單一界限,如果多重限制的話,這樣寫是行不通的, extends 後面第一個必須是類,然後在是接口。如下
class Dimension{
    public int x,y,z;
}

class ColoredDimension<T extends Dimension & HasColor>{
    T t;

    public ColoredDimension(T t) {
        this.t = t;
    }

    public T getT() {
        return t;
    }
    void color(){
        t.getColor();
    }

    int getX(){
        return t.x;
    }
    int getY(){
        return t.y;
    }

    int getZ(){
        return t.z;
    }
}
  1. 界限的 extends 後面是否可以跟多個 類(class),這樣做是不可以的。與繼承一樣,您只能擁有一個具體的類,但是可以有多個接口。
interface Weight{
    int weight();
}

class Solid<T extends Dimension & HasColor & Weight>{
    T t;

    public Solid(T t) {
        this.t = t;
    }
    public T getT() {
        return t;
    }
    void color(){
        t.getColor();
    }
    int getX(){
        return t.x;
    }
    int getY(){
        return t.y;
    }
    int getZ(){
        return t.z;
    }
    int weight(){
        return t.weight();
    }
}
  1. 接下來我們創建測試一下創建一個 即繼承某個類,又實現多個接口的類。
class Bounded extends Dimension implements HasColor,Weight{
    @Override
    public void getColor() {
        System.out.println("Bounded getColor method");
    }
    @Override
    public int weight() {
        return 0;
    }
}
class BasicBounds{
    public static void main(String[] args) {
        Solid<Bounded> solid=new Solid<>(new Bounded());
        solid.color();
        solid.getX();
        solid.weight();
    }
}
  1. 你可能已經觀察到了, BasicBounds.java 看上去包含可以通過繼承可以消除冗餘。下面,可以看到如何在繼承的每個層次上添加邊界限制。
public class HoldItem <T>{
    T t;

    public HoldItem(T t) {
        this.t = t;
    }
    T getT(){
        return t;
    }
}

class Colored2 <T extends HasColor> extends HoldItem<T>{

    public Colored2(T t) {
        super(t);
    }
    void color(){
        t.getColor();
    }
}


class ColoredDimesion2 <T extends  Dimension & HasColor> extends Colored2<T>{

    public ColoredDimesion2(T t) {
        super(t);
    }
    int getX(){
        return t.x;
    }
    int getY(){
        return t.y;
    }
    int getZ(){
        return t.z;
    }
}

class Solid2 <T extends Dimension & HasColor & Weight> extends ColoredDimesion2<T>{
    public Solid2(T t) {
        super(t);
    }
    int weight(){
        return t.weight();
    }
}

class InheritBounds{
    public static void main(String[] args) {
        Solid2<Bounded> solid2=new Solid2<>(new Bounded());
        solid2.color();
        solid2.getX();
        solid2.weight();
    }
}
  1. HoldItem 直接持有一個對象,因此這種行爲被繼承到了 Colored2 中,它也要求其參數與 HoldColor 一致。ColoredDimension2 和 Solid2 進一步擴展了這個層次結構,並且每個層次上都添加了邊界。現在這些方法被繼承,因而不必在每個類中重複。
public interface SuperPower {}

interface XRayVision extends SuperPower{
    void seeThroughWalls();
}

interface SuperHearing extends SuperPower{
    void hearThroughNoises();
}

interface SuperSmell extends SuperPower{
    void trackBySmell();
}

class SuperHerp<T extends SuperPower>{
    T t;

    public SuperHerp(T t) {
        this.t = t;
    }

    public T getT() {
        return t;
    }
}

class SuperSleuth<T extends XRayVision> extends SuperHerp<T> {

    public SuperSleuth(T t) {
        super(t);
    }
    void see(){
        t.seeThroughWalls();
    }
}

class CanineHero<T extends SuperHearing & SuperSmell> extends SuperHerp<T>{

    public CanineHero(T t) {
        super(t);
    }
    void hear(){
        t.hearThroughNoises();
    }
    void smell(){
        t.trackBySmell();
    }
}

class SuperHearSmell implements SuperHearing,SuperSmell{

    @Override
    public void hearThroughNoises() {}
    @Override
    public void trackBySmell() {}
}

class DogBoy extends CanineHero<SuperHearSmell>{

    public DogBoy() {
        super(new SuperHearSmell());
    }
}

class EpicBattle{
    //bounds in generic methods 泛型方法的界限
    static <T extends SuperHearing> void useSuperHearing(SuperHerp<T> tSuperHerp){
        tSuperHerp.getT().hearThroughNoises();
    }

    static  <T extends SuperHearing & SuperSmell> void superFind(SuperHerp<T> superHerp){
        superHerp.getT().hearThroughNoises();
        superHerp.getT().trackBySmell();
    }

    public static void main(String[] args) {
        DogBoy dogBoy=new DogBoy();
        useSuperHearing(dogBoy);
        superFind(dogBoy);

        //you can do this  你可以這樣做
        //List<? extends SuperHearing> audioBoys
        //But you can't do this 但是你做不到
        //List<? extends SuperHearing & SuperSmell> dogBoys

    }
}
  1. 注意,通配符被限制爲單一邊界。

通配符

  • 我們開始入手的示例要展示數組的一種特殊行爲: 可以嚮導出類型的數組賦予基類型的數組引用。
public class Fruit {
}

class Apple extends Fruit {
}

class Jonathan extends Apple {
}

class Orange extends Fruit {
}

class CovariantArrays {
    public static void main(String[] args) {
        Fruit[] fruits = new Apple[10];
        fruits[0] = new Apple();
        fruits[1] = new Jonathan();
        //運行時類型是 apple[] 而不是 Fruit[] 或 Orange
        try {
            //編譯器允許您添加 Fruit
            //ArrayStoreException 異常
            fruits[2] = new Fruit();
        } catch (Exception e) {
            System.err.println(e);
        }
        try {
            //編譯器不允許你添加 Orange
            //ArrayStoreException 異常
            fruits[3] = new Orange();
        }catch (Exception e){
            System.err.println(e);
        }
    }
}

//運行結果爲
java.lang.ArrayStoreException: generic.Fruit
java.lang.ArrayStoreException: generic.Orange
  1. main() 中的第一行創建了一個Apple 數組,並將其賦值給了一個 Fruit 數組的引用。這是有意義的,因爲 Apple 也是一種 Fruit ,因此Apple 數組應該也是一個 Fruit 數組。
  2. 但是,如果實際的數組類型是 Apple[] ,你應該只能在其中放置 Apple 和 Apple 的子類型,這在編譯期和運行時都可以工作。但是請注意,編譯器允許你將 Fruit 放置到這個數組中,這對於編譯器來說是有意義的,因爲它有一個 Fruit[] 引用___它有什麼理由不允許將 Fruit 對象或者任何從 Fruit 集成出來的對象(如 Orange),放置到這個數組中呢?
  3. 因此,在編譯期, 這是允許的,但是,運行時的數組機制知道它處理的是 Apple[] ,因此會在向數組中放置異構類型時拋出異常(ArrayStoreException)
  4. 實際上,向上轉型不適合在這裏。你真正做的是將一個數組賦值給另一個數組。數組的行爲應該是它可以持有其他對象,這裏只是因爲我們能夠向上轉型而已,所以很明顯,數組對象可以保留有關它們包含的對象類型的規則。就好像數組對它們持有的對象時有意識的,因此在編譯期檢查和運行時檢查之間,你不能濫用它們。
  5. 對數組的這種賦值並不是那麼可怕,因爲在運行時可以發現你已經插入了不正確的類型。但是泛型的主要目標之一是將這種錯誤監測移入到編譯期。因此當我們試圖使用泛型容器來代替數組時,會發生什麼呢?
class NonCovariantGenerics{
    //編譯異常 不兼容的類型
    List<Fruit> fruitList=new ArrayList<Apple>();
}
  1. 儘管你在第一次閱讀這段代碼時會認爲 不能將一個Apple 容器賦值給一個 Fruit容器。 別忘了,泛型不僅和容器相關正確的說法是 不能把一個涉及 Apple 泛型賦值給一個涉及 Fruit 的泛型。
  2. 如果就像在數組的情況中一樣,編譯器對代碼的瞭解足夠多,可以確定所涉及到的容器,那麼它可能會留下一些餘地。但是它不知道任何有關這方面的信息,因此它拒絕向上轉型。
  3. 然而實際上這根本不是向上轉型___Apple 的List 不是 Fruit 的List 。Apple 的List 將持有Apple 和 Apple 的子類型,而Fruit 的List 將持有任何類型的 Fruit ,誠然這包括 Apple 在內,但是它不是一個 Apple 的List,它人就是 Fruit 的List 。Apple 的List 在類型上不等價 Fruit 的List , 即使 Apple 是一種Fruit類型。
  • 真正的問題是我們再談論容器的類型,而不是容器持有的類型。與數組不同,泛型沒有內建的協變類型。這是因爲數組在語言中是完全定義的,因此可以內建了編譯期和運行時的檢查,但是在使用泛型時,編譯期和運行時系統都不知道你想用類型做些什麼,以及應該採用什麼樣的規則。
class GenericAndGCovariange{
    public static void main(String[] args) {
        //wildcards allow covariance 通配符允許協方差
        List<? extends Fruit> list=new ArrayList<Apple>();
        //編譯錯誤:無法添加任何類型的對象
        
        //list.add(new Apple());
        //list.add(new Orange());
        //list.add(new Fruit());
        
        //合法但無趣
        list.add(null);
        //我們知道它至少返回 fruit
        Fruit fruit = list.get(0);
    }
}
  1. list 類型現在是 List<? extends Fruit> , 你可以將其讀作 : 具有任何從 Fruit繼承的類型的列表。但是,這實際上並不意味着這個List 將持有任何類型的 Fruit。
  2. 通配符引用的是明確的類型,因此它意味着 某種list引用沒有指定的具體類型。因此這個被複制的List 必須持有諸如 Fruit 或 Apple 這樣的某種執行類型,但是爲了向上轉型爲 list ,這個類型是什麼並沒有人關心。
  3. 如果唯一的限制是這個List 要持有某種具體的 Fruit 或 Fruit 子類型,但是你實際上並不關心它是什麼,那麼你能用這樣的List 做什麼呢?如果不知道List持有什麼類型,那麼你怎麼才能安全地向其中添加對象呢? 就像在 CovariantArrays.java中向上轉型數組一樣,你不能,除非編譯器而不是運行時系統可以阻止這種操作的發生。你很快就會發現這一問題。
  4. 你可能會認爲,事情變得有點走極端了,因爲現在你甚至不能向剛剛聲明過將持有 Apple 對對象的List 中放置一個 Apple 對象了。是的,但是編譯器並不知道這一點。 List<? extends Fruit> 可以合法地指向一個 List<Orange> 。一旦執行這種類型的向上轉型,你就將丟失掉向其中傳遞任何對象的能力,甚至是傳遞Object 也不行。
  5. 另一方面,如果你調用一個返回Fruit 的方法,則是安全的,因爲你知道在這個List中的任何對象至少具有Fruit 類型,因此編譯器將允許這麼做。

 

編譯器有多聰明

  • 現在,你可能會猜想自己被阻止去調用任何接受參數的方法,請考慮如下程序。
class CompilerIntelligence{
    public static void main(String[] args) {
        List<? extends Fruit> list= Arrays.asList(new Apple());
        //沒有警告
        Apple apple= (Apple) list.get(0);
        //Argument is Object
        list.contains(new Apple());
        list.indexOf(new Apple());

    }
}
  1. 你可以看到,對contains() 和 indexOf() 調用,這倆個方法都接受 Apple 對象作爲參數,而這些調用都可以正常執行。這意味着編譯器實際上將檢查代碼,以查看是否有某個特定的方法修改了它的對象?
  2. 通過查看ArrayList文檔,我們可以發現,編譯器並沒有這麼聰明。儘管 add() 將接受一個具有泛型參數類型的參數,但是 contains() 和 indexOf() 將接受Object 類型的參數。因此當你指定一個 ArrayList<? extends Fruit> 時, add() 的參數就變成了 ? extends Fruit 。
  3. 從這個描述中,編譯器並不瞭解這裏需要Fruit 的那個具體子類型,因此它不會接受任何類型的 Fruit。如果先將 Apple 向上轉型爲 Fruit ,也無關緊要___編譯器將直接拒絕對參數列表中涉及通配符的方法(如 add()) 的調用。
  • 在使用 contains() 和 indexOf() 時,參數類型是Object , 因此不涉及任何通配符,而編譯器也將允許這個調用。這意味着將由泛型類的涉及者來決定哪些調用是安全的,並使用Object類型作爲其參數類型。爲了在類型中使用了通配符的情況下禁止這類調用,我們需要在參數列表中使用類型參數。
public class Holder<T> {
    private T t;
    public Holder(T t) {
        this.t = t;
    }
    public Holder() {
    }
    public T getT() {
        return t;
    }
    public void setT(T t) {
        this.t = t;
    }
    @Override
    public boolean equals(Object object) {
        return t.equals(object);
    }

    public static void main(String[] args) {
        Holder<Apple> holder = new Holder<>(new Apple());
        Apple apple = holder.getT();
        holder.setT(apple);

        //Holder<Fruit> fruitHolder=holder;  無法向上轉型
        Holder<? extends Fruit> fruit = holder;
        Fruit fruit1 = fruit.getT();
        //返回的結果是 object
        apple= (Apple) fruit.getT();
        try {
            Orange orange = (Orange) fruit.getT();
        }catch (Exception e){
            System.out.println(e);
        }
        //fruit.setT(new Apple());
        //fruit.setT(new Fruit());
        System.out.println(fruit.equals(apple));
    }
}

//運行結果爲
java.lang.ClassCastException: generic.Apple cannot be cast to generic.Orange
true
  1. Holder 有一個接受 T類型對象的set() 方法,一個 get() 方法,以及一個接受 Object 對象的 equals() 方法。 
  2. 正如你已經看到的,如果創建了一個 Holder<Apple> ,不能將其向上轉型爲 Holder<Fruit> ,但是可以將其向上轉型爲 Holder<? extends Fruit> 。
  3. 如果調用 getT() ,它只會返回一個 Fruit ___這就是在給定 任何擴展自 Fruit 的對象 這一邊界之後,它所能知道的一切了。
  4. 如果能夠了解更多的信息,那麼你可以轉型到某種具體的Fruit 類型,而這不是導致任何警告,但是你存在着得到 ClassCastException 的風險。
  5. setT() 方法不能工作於 Apple 或 Fruit ,因爲 setT() 的參數也是 ? extends Fruit 這意味着它可以是任何事物,而編譯器無法驗證任何事物的類型安全性。
  6. 但是 , equals() 方法工作良好,因爲它將Object 類型而並非T 類型的參數。因此,編譯器只關注傳遞進來和要返回的對象類型,它並不會分析代碼,以查看是否執行了任何實際的寫入和讀取操作。

 

逆變

  • 還可以走另外一條路,即使用 超類型通配符。這裏聲明通配符是由某個特定類的任何基類來界定的,方法是指定 <? super MyClass>  甚至或者使用類型參數: <? super T> (儘管你不能對泛型參數給出一個超類型邊界,即不能聲明 <T super MyClass>)。 這使得你可以安全地傳遞一個類型對象到泛型類型中。
  • 因此,有了超類型通配符,就可以向 Collection寫入瞭如下。
public class SuperTypeWildcards {
    public static void main(String[] args) {
        List<? super Apple> apples=new ArrayList<>();
        apples.add(new Apple());
        apples.add(new Jonathan());
        
        //apples.add(new Fruit()); 編譯失敗
    }
}
  1. 參數 Apple 是 Apple 的某種基類型的List , 這樣你就知道向其中添加 Apple 或Apple 子類型是安全的。
  2. 但是,既然Apple 是下界,那麼你可以知道向這樣的 List 中添加 Fruit是不安全的,因爲這將使這個 List 敞開口子,從而可以向其中添加非Apple類型的對象,而這時違反靜態類型安全的。
  3. 因此你可能會根據如何能夠向一個泛型類型 寫入(傳遞給一個方法), 以及如何能夠從一個泛型類型中 讀取(從一個方法中返回) , 來着手思考子類型和超類型邊界。
  4. 超類型邊界放鬆了在可以向方法傳遞的參數上所作的限制。
public class GenericWriting {
    static List<Apple> apples=new ArrayList<>();
    static List<Fruit> fruits=new ArrayList<>();


    static void f1(){
        writeExact(apples,new Apple());
        writeExact(fruits,new Apple());
    }
    static <T> void writeWithWildcard(List<? super T> list,T item){
        list.add(item);
    }


    static <T> void writeExact(List<T> list,T item){
        list.add(item);
    }

    static void f2(){
        writeWithWildcard(apples,new Apple());
        writeWithWildcard(fruits,new Apple());
    }

    public static void main(String[] args) {
        f1();
        f2();
    }
}
  1. writeExact() 方法使用了一個確切參數類型(無通配符) 。 
  2. 在 writeWithWildcard() 中,其參數現在是 List<? super T> ,因此這個List 將持有從T 導出的某種具體;類型,這樣就可以安全地將一個T 類型的對象或者從T 導出的任何對象作爲參數傳遞給List 的方法。
  3. 在 f2() 中可以看到這一點,在這個方法中我們仍舊可以像前面那樣,將Apple 放置到List<Apple> 中,但是現在我們可以如你所期望的那樣,將Apple放置到List<Fruit>。

 

 

 

 

 

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章