首頁 > 軟體

帶大家認識Java語法之泛型與萬用字元

2022-03-04 16:00:10

⭐️前面的話⭐️

本篇文章帶大家認識Java語法——泛型與萬用字元,泛型和萬用字元是一個非常抽象的概念,簡單來說,兩者都可以將型別作為“引數”進行傳遞,不過泛型是在你知道傳入什麼型別的時候使用的,而萬用字元是你不確定傳入什麼型別的時候使用,本文將介紹泛型與萬用字元的使用及兩者間的區別。

題外話: 泛型與萬用字元是Java語法中比較難懂的兩個語法,學習泛型和萬用字元的主要目的是能夠看懂原始碼,實際使用的不多

1.泛型

1.1泛型的用法

1.1.1泛型的概念

《Java程式設計思想》上有這麼一句話:一般的類和方法,只能使用具體的型別: 要麼是基本型別,要麼是自定義的類。如果要編寫可以應用於多種型別的程式碼,這種刻板的限制對程式碼的束縛就會很大。

所以從Java5開始引入了泛型機制,這個泛型是什麼意思呢?由於一般的類和方法只能使用一種具體得型別,這就使程式碼受到了很大的束縛,比如一個求三個數中最大值的方法,假設一開始方法中的參數列的型別是Integer,你從三個整型資料中找出一個最大值沒有任何問題,這個程式能夠完美地執行,但是你要找三個浮點數中的最大值時,這個程式編譯都通不過,這時你可以選擇另寫一個過載方法將參數列和實現功能都基於Double再實現一遍,這樣也可以解決問題,但是,你想過一個問題沒有,萬一有一萬甚至一百萬種型別需要求三個物件中最大那一個,那應該怎麼辦,寫一百萬個過載方法?這是不可能的,為了解決這種型別的問題,引入了泛型,泛型實現了引數化型別的概念,使程式碼可以應用多種型別,通俗說泛型就是“適用於許多許多型別”的意思。 使用泛型可以將型別作為“引數”傳遞至類,介面,方法中,這樣類和方法就可以具有最廣泛的表達能力,不需要因為一個引數不同而去另建型別。

注意:任意的基本型別都不能作為型別引數。

1.1.2泛型類

我們通過一段程式碼來認識泛型,首先看下面一段不使用程式碼的泛型的程式碼:

/**
 * 不使用泛型
 */
class A {
}
class Print {
    private A a;

    public Print(A a) {
        setA(a);
        System.out.println(this.a);
    }
    public void setA(A a) {
        this.a = a;
    }
    public A getA() {
        return this.a;
    }
}

public class Generic {
   public static void main(String[] args) {
        Print print = new Print(new A());
    }
}
//output:A@1b6d3586

不使用泛型取建立一個類沒有任何問題,但是這個類的重用性就不怎麼樣了,它只能持有類A的物件,不能持有其他任何類的物件,我們不希望為碰到的每種型別都編寫成一個新的類,這是不現實的。我們學習類的時候,知道Object類是所有類的父類別,所以Object類可以接受所有的型別參照,我們可以讓Print類持有Object型別的物件。

/**
 * 使用Object類
 */
class B{ }
class Print1 {
    private Object b;

    public Print1(Object b) {
        setB(b);
        System.out.println(this.b);
    }

    public void print(Object b) {
        setB(b);
        System.out.println(this.b);
    }
    
    public void setB(Object b) {
        this.b = b;
    }
}
public class Generic1 {
    public static void main(String[] args) {
        Print1 print1 = new Print1(new B());//列印B型別
        int i = 2022;
        print1.print(i);//列印整型型別
        print1.print("這是一個字串物件!");//列印字串型別
    }
}
//output:
//B@1b6d3586
//2022
//這是一個字串物件!

Print1可以接收並列印任何型別,但是這並不是我們想要的結果,你想想如果實現的是一個順序表類,裡面是通過一個陣列來實現,如果這個陣列什麼型別都可以接收,那就非常混亂了,取出資料的時候不能確定取出的到底是什麼型別的資料,而且取出的資料是Object類,需要進行強制型別轉換,那能不能實現指定類持有什麼型別的物件並且編譯器能夠檢查型別的正確性。

泛型就完美實現了這個目的,下面我們將上述程式碼改寫成泛型類,那麼首先得知道泛型的語法,泛型類建立語法如下:

class 類名<泛型參數列> {
    許可權修飾 泛型引數 變數名;//泛型成員變數
    許可權修飾 返回值型別 方法名 (參數列){}//參數列和返回值型別可以是泛型
}

例如:

class Print2<T> {
    private T c;

    public void print(T c) {
        setC(c);
        System.out.println(this.c);
    }

    public void setC(T c) {
        this.c = c;
    }
}

泛型類的使用語法如下:

泛型類<型別實參> 變數名; // 定義一個泛型類參照 
new 泛型類<型別實參>(構造方法實參); // 範例化一個泛型類物件

例如:

Print2<Integer> print3 = new Print2<Integer>();

使用泛型實現一個類,並使用它:

/**
 * 使用泛型
 */
class C{ }
class Print2<T> {
    private T c;

    public void print(T c) {
        setC(c);
        System.out.println(this.c);
    }

    public void setC(T c) {
        this.c = c;
    }
}
public class Generic2{
    public static void main(String[] args) {
        Print2<C> print2 = new Print2<>();//列印C型別
        print2.print(new C());
        Print2<Integer> print3 = new Print2<>();//列印整型型別
        print3.print(2022);
        Print2<String> print4 = new Print2<>();//列印字串型別
        print4.print("這是一個字串物件!");
    }
}
/**
 * output:
 *C@1b6d3586
 * 2022
 * 這是一個字串物件!
 */

類名後的 <T>代表預留位置,表示當前類是一個泛型類。

【規範】型別形參一般使用一個大寫字母表示,常用的名稱有:

E 表示 Element

K 表示 Key

V 表示 Value

N 表示 Number

T 表示 Type

S, U, V 等等 - 第二、第三、第四個型別

//一個泛型類
class ClassName<T1, T2, ..., Tn> { }

使用泛型類時,指定了這個類的物件持有的型別,則該物件只能接收該型別的物件,傳入其他型別物件,編譯器會報錯,並且接收泛型類中泛型方法的返回值時,不需要進行強制型別轉換(向下轉型),而使用Object類需要強制型別轉換。

1.1.3型別推導

使用泛型類時,可以通過泛型型別中傳入的型別來推導範例化該泛型類時所需的型別引數,換個說法,定義泛型物件時,前面的尖括號內必須指定型別,後面範例化時可以不指定。

如:

Print2<Integer> print3 = new Print2<>();//後面尖括號內可省略

1.2裸型別

裸型別其實很好理解,就是一個泛型類,你不去指定泛型物件持有的型別,這樣的一個型別就是裸型別。

比如:

    public static void main(String[] args) {
        Print2 print2 = new Print2();
        print2.print(2022);
        print2.print("字串");
    }
    //output:
    //2022
	//字串

我們不要自己去使用裸型別,裸型別是為了相容老版本的 API 保留的機制。

1.3擦除機制

1.3.1關於泛型陣列

介紹泛型的擦除機制之前,我們先來了解泛型陣列·,先說結論,在Java中不允許範例化泛型陣列,如果一定要建立一個泛型陣列,正確的做法只能通過反射來實現,當然有一個“捷徑”可以不使用反射來建立泛型陣列。建立的程式碼如下:

1.通過捷徑建立,大部分情況下不會出錯。

public class MyArrayList<T> {
    public T[] elem ;
    private int usedSize;

    public MyArrayList(int capacity) {
        this.elem = (T[])new Object[capacity];
    }
}

2.通過反射建立,現在只給程式碼,具體為什麼要這麼做後續介紹反射再說。

public class MyArrayList<T> {
    public T[] elem ;
    private int usedSize;
    
    public MyArrayList(Class<T> clazz, int capacity) { 
        this.elem = (T[]) Array.newInstance(clazz, capacity); 
    }
}

1.3.2泛型的編譯與擦除

我們先來實現一個簡單的泛型順序表,不考慮擴容問題,只實現簡單的增刪操作,來看看構造方法部分編譯後的反組合。

import java.lang.reflect.Array;

public class MyArrayList<T> {
    public T[] elem ;
    private int usedSize;

    public MyArrayList(int capacity) {
        this.elem = (T[])new Object[capacity];
    }
    public MyArrayList(Class<T> clazz, int capacity) {
        this.elem = (T[]) Array.newInstance(clazz, capacity);
    }
}

我們發現所有的泛型預留位置T都被擦除替換成Object了,這就說明Java的泛型機制是在編譯期實現的,而泛型機制實現就是通過像這樣的擦除機制實現的,並在編譯期間完成型別的檢查。

我們通過列印持有不同型別的MyArrayList類來看看,泛型機制到底是不是不會出現在執行期間,如果是的話,列印出的型別都應該是MyArrayList。

    public static void main(String[] args) {
        MyArrayList<Integer> list1 = new MyArrayList<>(10);
        MyArrayList<String> list2 = new MyArrayList<>(10);

        System.out.println(list1);
        System.out.println(list2);
    }
    /**
     * output:
     * MyArrayList@1b6d3586
     * MyArrayList@4554617c
     */

我們發現列印的型別是一樣的,都是MyArrayList,所以可以得出一個結論,泛型是發生在編譯期,泛型的型別檢查是在編譯期完成的,泛型的實現是通過擦除機制實現的,類後面的預留位置都會被擦除,其他的預留位置都會被替換成Object。當然,這是在泛型引數沒有指定上界的情況下,如果存在上界,那預留位置會擦除成上界的型別或介面,其實沒有指定上界,上界預設為Object,什麼是泛型上界,噓,等一下再說。

根據擦除機制,也能解釋為什麼Java當中不能範例化泛型陣列了,因為泛型陣列前面的預留位置會被擦除成Object,實際上是建立一個Object陣列,而Object陣列中什麼型別都能放,這就導致取資料時不安全,因為你不能確定陣列裡面存放的元素全部都是你預期的型別,所以為了安全,Java不允許範例化泛型陣列。

1.4泛型的上界

1.4.1泛型的上界

在定義泛型類時,有時需要對傳入的型別變數做一定的約束,可以通過型別邊界來約束。

class 泛型類名稱<型別形參 extends 型別邊界> {
    ...
}

例如:Number是Integer,Float,Double等相關數位型別的父類別。

public class MyArrayList<T extends Number> {
	
}

那麼這個MyArrayList泛型類只能指定持有Number類以及Number的子類,像這樣就給泛型的型別傳參做了約束,這個約束就是泛型的上界,泛型類被型別邊界約束時,只能指定泛型類持有型別邊界這個類及其子類。

        MyArrayList<Integer> list1 = new MyArrayList<>(10);//正確
        MyArrayList<Double> list2 = new MyArrayList<>(10);//正確
        MyArrayList<String> list3 = new MyArrayList<>(10);//錯誤,因為String不是Number的子類

1.4.2特殊的泛型上界

假設需要設計一個泛型類,能夠找出陣列中最大的元素。

class MaxVal<T extends Comparable<T>> {
    public T max(T[] data) {
        T max = data[0];
        for (int i = 0; i < data.length; i++) {
            if (max.compareTo(data[i]) < 0) max = data[i];
        }
        return max;
    }
}

由於參照型別的比較需要使用Comparable介面來判斷大小,所以所傳入的類需要實現Comparable介面,上面這個泛型的型別引數的上界是一個特殊的上界,表示所傳入的型別必須實現Comparable介面,不過實現了Comparable介面的類,那也就是Comparable的子類了,綜上,像這樣類似需要通過實現某一個介面來達到預期功能的型別,使用泛型時需指定泛型的上界,並且該傳入的型別必須實現該上界介面。

1.4.3泛型方法

有泛型類,那麼就一定有泛型介面,泛型方法,其中泛型介面與泛型類的建立和使用是一樣的,所以我們重點介紹泛型方法的建立與使用。
建立泛型方法的基本語法:

方法限定符 <型別形參列表> 返回值型別 方法名稱(形參列表) { ... }

例如上面實現求陣列中最大元素泛型版的方法如下:

class MaxVal<T extends Comparable<T>> {
    public <T extends Comparable<T>> T max(T[] data) {
        T max = data[0];
        for (int i = 0; i < data.length; i++) {
            if (max.compareTo(data[i]) < 0) max = data[i];
        }
        return max;
    }
}

對於非static修飾的靜態方法, <型別形參列表>可以省略,上述程式碼可以變成:

class MaxVal<T extends Comparable<T>> {
    public T max(T[] data) {
        T max = data[0];
        for (int i = 0; i < data.length; i++) {
            if (max.compareTo(data[i]) < 0) max = data[i];
        }
        return max;
    }
}

但是,如果是一個static修飾的靜態方法,<型別形參列表>不可以省略,因為靜態方法不依賴與物件,它的使用不用範例化物件,所以必須有單獨的型別參數列來指定持有的物件型別。

class MaxVal<T extends Comparable<T>> {
    public static <T extends Comparable<T>> T max(T[] data) {
        T max = data[0];
        for (int i = 0; i < data.length; i++) {
            if (max.compareTo(data[i]) < 0) max = data[i];
        }
        return max;
    }
}

1.4.4型別推導

和泛型類一樣,泛型方法也有型別推導的機制,如果不使用型別推導,那麼泛型方法是這麼使用的:

使用型別推導圖中畫圓圈部分可以省略。

在泛型類中沒有如下的父子類關係:

public class MyArrayList<E> { ... }
 // MyArrayList<Object> 不是 MyArrayList<Number> 的父類別型 
 // MyArrayList<Number> 也不是 MyArrayList<Integer> 的父類別型

但是使用萬用字元這兩種類是有符子類關係的。

2.萬用字元

2.1萬用字元的概念

?就是一個萬用字元,用與泛型的使用,與泛型不同的是,泛型T是確定的型別,傳入型別實參後,它就確定下來了,而萬用字元更像是一種規定,規定一個範圍,表示你能夠傳哪些引數。

一個泛型類名尖括號之內僅含有一個?,就會限制這個泛型類傳入的型別為Object,相當於沒有限制,但是獲取元素時由於不能確定具體型別,只能使用Object參照接收,所以<?>也被稱為無界萬用字元。

    //使用泛型列印順序表
    public static<T> void printList1(ArrayList<T> list) {
        for (T x:list) {
            System.out.println(x);
        }
    }
    //使用萬用字元列印順序表
    public static void printList2(ArrayList<?> list) { 
        for (Object x:list) { 
            System.out.println(x); 
        }
    }

使用泛型T能夠確定傳入的型別就是T型別,所以使用T型別的變數接收,而萬用字元?沒有設定邊界的情況下,預設上界是Object沒有下界,為了保證安全,只能使用Object型別的變數接收。

萬用字元是用來解決泛型無法協變的問題的,協變指的就是如果Student是Person的子類,那麼List<Student>也應該是List<Person>的子類。但是泛型是不支援這樣的父子類關係的。

2.2萬用字元的上界

萬用字元也有上界,可以限制傳入的型別必須是上界這個類或者是這個類的子類。

基本語法:

<? extends 上界> 
<? extends Number>//可以傳入的實參型別是Number或者Number的子類

例如:

    public static void printAll(ArrayList<? extends Number> list) {
        for (Number n: list) {
            System.out.println(n);
        }
    }

我們對printAll方法的一個形參限制了型別的上界Number,所以在遍歷這個順序表的時候,需要使用Number來接收順序表中的物件,並且使用該方法時,只能遍歷輸出Number及其子類的物件。

    public static void main(String[] args) {
        printAll(new ArrayList<Integer>());//ok
        printAll(new ArrayList<Double>());//ok
        printAll(new ArrayList<Float>());//ok

        printAll(new ArrayList<String>());//error
    }

假設有如下幾個類:

class Animal{}
class Cat extends Animal{}
class Dog extends Animal{}
class Bird extends Animal{}

Animal是Cat,Dog,Bird類的父類別,我們來看一看使用泛型和使用萬用字元在列印物件結果上會有什麼區別?我們對這兩者都設定了上界,當列印不同的物件時,到底會呼叫誰的toString方法。

	//泛型
    public static <T extends Animal> void printAnimal1(ArrayList<T> list) {
        for (T animal: list) {
            System.out.println(animal);
        }
    }
    //萬用字元
        public static void printAnimal2(ArrayList<? extends Animal> list) {
        for (Animal animal: list) {
            System.out.println(animal);
        }
    }

我們先來看泛型,使用泛型指定型別後,那麼指定什麼型別,那它就會輸出什麼型別的物件,比如你指定順序表中放的型別是Cat,那麼它呼叫的就是Cat物件的toString方法。

    public static void main(String[] args) {
        Cat cat = new Cat();
        Dog dog = new Dog();
        Bird bird = new Bird();

        //泛型
        ArrayList<Cat> list1 = new ArrayList<>();
        ArrayList<Dog> list2 = new ArrayList<>();
        ArrayList<Bird> list3 = new ArrayList<>();
        list1.add(cat);
        list2.add(dog);
        list3.add(bird);
        printAnimal1(list1);//Cat
        printAnimal1(list2);//Dog
        printAnimal1(list3);//Bird
    }

再來看一看萬用字元,使用萬用字元是規定能夠使用Animal及其子類,不倫你傳入哪一個子類物件,都是父類別的參照接收,但是具體哪一個子類,並不清楚。

    public static void main(String[] args) {
        Cat cat = new Cat();
        Dog dog = new Dog();
        Bird bird = new Bird();

        //萬用字元
        ArrayList<Cat> list1 = new ArrayList<>();
        ArrayList<Dog> list2 = new ArrayList<>();
        ArrayList<Bird> list3 = new ArrayList<>();
        list1.add(cat);
        list2.add(dog);
        list3.add(bird);
        printAnimal2(list1);//Cat
        printAnimal2(list2);//Dog
        printAnimal2(list3);//Bird
    }

父類別參照接收子類物件發生了向上轉型,當列印父類別參照的子類物件時,會優先使用子類的toString方法,在介紹多型的時候也講過這個問題,所以輸出結果與使用泛型是一樣的,但是泛型和萬用字元的效果是不一樣的,泛型是你傳入什麼型別,那這個類就會持有什麼型別的物件,而萬用字元是規定一個範圍,規定你能夠傳哪一些型別。

萬用字元的上界是支援如下的父子類關係的,而泛型的上界不支援:

MyArrayList<? extends Number> 是 MyArrayList <Integer>或者 MyArrayList<Double>的父類別型別 
MyArrayList<?> 是 MyArrayList<? extends Number> 的父類別型

對於萬用字元的上界有個特點,先說結論,使用萬用字元上界可以讀取資料,但是並不適合寫入資料,因為不能確定類所持有的物件具體是什麼。

    public static void main(String[] args) {
        ArrayList<Integer> arrayList1 = new ArrayList<>();
        ArrayList<Double> arrayList2 = new ArrayList<>();
        arrayList1.add(10);
        List<? extends Number> list = arrayList1;
        System.out.println(list.get(0));//ok
        Integer = list.get(0);//error因為不能確定list所持有的物件具體是什麼
        list.add(2);//error因為不能確定list所持有的物件具體是什麼,為了安全,這種情況Java不允許插入元素
    }

因為從list獲取的物件型別一定Number或者Number的子類,所以可以使用Number參照來獲取元素,但是插入元素時你並不能確定它到底是哪一種型別,為了安全,使用萬用字元上界的list不允許插入元素。

2.3萬用字元的下界

與泛型不同,萬用字元可以擁有下界,語法層面上與萬用字元的上界的區別是講關鍵字extends改為super。

<? super 下界> 
<? super Integer>//代表 可以傳入的實參的型別是Integer或者Integer的父類別型別

既然是下界那麼萬用字元下界與上界對傳入類的規定是相反的,即規定一個泛型類只能傳入下界的這個類型別或者這個類的父類別型別。比如<? super Integer>代表 可以傳入的實參的型別是Integer或者Integer的父類別型別(如Number,Object)

    public static void printAll(ArrayList<? super Number> list) {
        for (Object n: list) {			//此處只能使用Object接收,因為傳入的類是Number或者是Number的父類別
            System.out.println(n);
        }
    }
    public static void main(String[] args) {
        printAll(new ArrayList<Number>());//ok
        printAll(new ArrayList<Object>());//ok

        printAll(new ArrayList<Double>());//error
        printAll(new ArrayList<String>());//error
        printAll(new ArrayList<Integer>());//error
    }

同理萬用字元的下界也是滿足像下面這種父子類關係的。

MyArrayList<? super Integer> 是 MyArrayList<Integer>的父類別型別 
MyArrayList<?> 是 MyArrayList<? super Integer>的父類別型別

總結:

?是? extends ....和? super ....的父類別,看萬用字元之間的父子類關係,最關鍵的是看萬用字元所“規定的”範圍,判斷父子類是根據這個範圍來判斷的。

萬用字元的下界也有一個特點,那就是它能夠允許寫入資料,當然能夠寫入的資料物件是下界以及下界的子類,但是並不擅長讀資料,與萬用字元的上界相反。

    public static void main(String[] args) {
        ArrayList<? super Animal> list = new ArrayList<Animal>(); 
        ArrayList<? super Animal> list2 = new ArrayList<Cat>();//編譯報錯,list2只能參照Animal或者Animal父類別型別的list
        list.add(new Animal());//新增元素時,只要新增的元素的型別是Animal或者Animal的子類就可以
        list.add(new Cat());
        Object s2 = list.get(0);//可以
        
        ArrayList<? super Animal> list3 = new ArrayList<Object>();
        Cat s1 = list3.get(0);//error因為構造物件時可以構造Animal父類別型別的ArrayList,取出的物件不一定是Animal或者Animal的子類
    }

對於這個栗子新增元素時,只要新增的元素的型別是Animal或者Animal的子類就可以,獲取元素時,只能使用Object參照接收,不能使用其他的參照接收,因為因為構造物件時可以構造Animal父類別型別的ArrayList,雖然可以插入Animal以及其子類物件,但取出的物件不能保證是Animal或者Animal的子類。

關於泛型和萬用字元就介紹到這裡了,這兩個概念是非常抽象而且難懂的,而且用的也不多,因為基本上以後你也沒機會用了,因為泛型萬用字元在編寫類似Java或者其他語言原始碼的時候才用得到,學習泛型萬用字元的主要目的是能夠讀懂原始碼,看得懂其他人寫的程式碼。

哎!寫關於java語法的用法的時候真的是一頭霧水啊!

總結

到此這篇關於Java泛型與萬用字元的文章就介紹到這了,更多相關Java泛型與萬用字元內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com