day11 11、泛型與枚舉(聯繫C++)

11、泛型與枚舉

11.1 泛型

Java 集合有個缺點,就是把一個對象“丟進”集合裏之後,集合就會“忘記”這個對象的數據類型,當再次取出該對象時,該對象的編譯類型就變成了 Object 類型(其運行時類型沒變)。

但這樣做帶來如下兩個問題:

  1. 集合對元素類型沒有任何限制,這樣可能引發一些問題。例如,想創建一個只能保存 Dog 對象的集合,但程序也可以輕易地將 Cat 對象“丟”進去,所以可能引發異常。
  2. 由於把對象“丟進”集合時,集合丟失了對象的狀態信息,集合只知道它盛裝的是 Object,因此取出集合元素後通常還需要進行強制類型轉換。這種強制類型轉換既增加了編程的複雜度,也可能引發 ClassCastException 異常。

所以爲了解決上述問題,從 Java 1.5 開始提供了泛型。泛型可以在編譯的時候檢查類型安全,並且所有的強制轉換都是自動和隱式的,提高了代碼的重用率

11.1.1 泛型集合

泛型本質上是提供類型的“類型參數”,也就是參數化類型。我們可以爲類、接口或方法指定一個類型參數,通過這個參數限制操作的數據類型,從而保證類型轉換的絕對安全。不能爲int等基本數據類型
例如,假設有一下java 代碼:

Vector<String> vector = new Vector<String>();//利用泛型創建存儲string元素類型的vector
	vector.add(new String("hello"));//添加一個元素
	String str = vector.get(0);

編譯時,上面的代碼會被改寫爲:

Vector vector = new Vector();
vector.add(new String("hello"));
String str = (String)vector.get(0);

11.1.2 泛型類

除了可以定義泛型集合之外,還可以直接限定泛型類的類型參數。語法格式如下:

public class class_name<data_type1,data_type2,>{}//類似於C++但是沒有了template,也不用加typename,直接是類型名

其中,class_name 表示類的名稱,data_ type1 等表示類型參數。Java 泛型支持聲明一個以上的類型參數,只需要將類型用逗號隔開即可。

泛型類一般用於類中的屬性類型不確定的情況下。在聲明屬性時,使用下面的語句:

private data_type1 property_name1;
private data_type2 property_name2;

該語句中的 data_type1 與類聲明中的 data_type1 表示的是同一種數據類型。
例 子
在實例化泛型類時,需要指明泛型類中的類型參數,並賦予泛型類屬性相應類型的值。例如,下面的示例代碼創建了一個表示學生的泛型類,該類中包括 3 個屬性,分別是姓名、年齡和性別。

public class Stu<N, A, S> {
    private N name; // 姓名
    private A age; // 年齡
    private S sex; // 性別
    // 創建類的構造函數
    public Stu(N name, A age, S sex) {
        this.name = name;
        this.age = age;
        this.sex = sex;
    }
    // 下面是上面3個屬性的setter/getter方法
    public N getName() {
        return name;
    }
    public void setName(N name) {
        this.name = name;
    }
    public A getAge() {
        return age;
    }
    public void setAge(A age) {
        this.age = age;
    }
    public S getSex() {
        return sex;
    }
    public void setSex(S sex) {
        this.sex = sex;
    }
}

接着創建測試類。在測試類中調用 Stu 類的構造方法實例化 Stu 對象,並給該類中的 3 個屬性賦予初始值,最終需要輸出學生信息。測試類的代碼實現如下:

public class Test14 {
    public static void main(String[] args) {
        Stu<String, Integer, Character> stu = new Stu<String, Integer, Character>("張曉玲", 28, '女');//不能爲基本數據類型,int要變爲包裝類Integer等;
        String name = stu.getName();
        Integer age = stu.getAge();
        Character sex = stu.getSex();
        System.out.println("學生信息如下:");
        System.out.println("學生姓名:" + name + ",年齡:" + age + ",性別:" + sex);
    }
}

11.1.3 泛型方法

到目前爲止,我們所使用的泛型都是應用於整個類上。泛型同樣可以在類中包含參數化的方法,而方法所在的類可以是泛型類,也可以不是泛型類。也就是說,是否擁有泛型方法,與其所在的類是不是泛型沒有關係。

泛型方法使得該方法能夠獨立於類而產生變化。如果使用泛型方法可以取代類泛型化,那麼就應該只使用泛型方法。另外,對一個 static 的方法而言,無法訪問泛型類的類型參數。因此,如果 static 方法需要使用泛型能力,就必須使其成爲泛型方法。

定義泛型方法的語法格式如下:

[訪問權限修飾符][static][final]<類型參數列表>返回值類型方法名([形式參數列表])

例如:

public static List<T> find(Class<T>class,int userId){}

一般來說編寫 Java 泛型方法,其返回值類型至少有一個參數類型應該是泛型,而且類型應該是一致的,如果只有返回值類型或參數類型之一使用了泛型,那麼這個泛型方法的使用就被限制了。下面就來定義一個泛型方法,具體介紹泛型方法的創建和使用。
例 3
使用泛型方法打印圖書信息。定義泛型方法,參數類型使用“T”來代替。在方法的主體中打印出圖書信息。代碼的實現如下:

public class Test16 {
    public static <T> void List(T book) { // 定義泛型方法,book的類型爲T
        if (book != null) {
            System.out.println(book);
        }
    }
    public static void main(String[] args) {
        Book stu = new Book(1, "細學 Java 編程", 28);
        List(stu); // 調用泛型方法,stu的類型爲Book,所有T book = Book,中T 爲Book類型
    }
}

11.1.4 泛型的高級用法

泛型的用法非常靈活,除在集合、類和方法中使用外,還包括限制泛型可用類型、使用類型通配符、繼承泛型類和實現泛型接口。
1)限制泛型可用類型
在 Java 中默認可以使用任何類型來實例化一個泛型類對象。當然也可以對泛型類實例的類型進行限制,語法格式如下:

class 類名稱<T extends anyClass>//T必須繼承、或者實現該類,或者是接口,都是使用extends關鍵字

其中,anyClass 指某個接口或類。使用泛型限制後,泛型類的類型必須實現或繼承 anyClass 這個接口或類。無論 anyClass 是接口還是類,在進行泛型限制時都必須使用 extends 關鍵字。

例如,在下面的示例代碼中創建了一個 ListClass 類,並對該類的類型限制爲只能是實現 List 接口的類。

// 限制ListClass的泛型類型必須實現List接口
public class ListClass<T extends List> {
    public static void main(String[] args) {
        // 實例化使用ArrayList的泛型類ListClass,正確
        ListClass<ArrayList> lc1 = new ListClass<ArrayList>();
        // 實例化使用LinkedList的泛型類LlstClass,正確
        ListClass<LinkedList> lc2 = new ListClass<LinkedList>();
        // 實例化使用HashMap的泛型類ListClass,錯誤,因爲HasMap沒有實現List接口
        // ListClass<HashMap> lc3=new ListClass<HashMap>();
    }
}

當沒有使用 extends 關鍵字限制泛型類型時,其實是默認使用 Object 類作爲泛型類型。因此,Object 類下的所有子類都可以實例化泛型類對象

在這裏插入圖片描述
2)使用類型通配符
Java 中的泛型還支持使用類型通配符,它的作用是在創建一個泛型類對象時限制這個泛型類的類型必須實現或繼承某個接口或類。

使用泛型類型通配符的語法格式如下:

泛型類名稱<? extends List>a = null;

其中,“<? extends List>”作爲一個整體表示類型未知,當需要使用泛型對象時,可以單獨實例化。

例如,下面的示例代碼演示了類型通配符的使用。

A<? extends List>a = null;//這個時候只要<>裏的類實現或者繼承了List接口就像
a = new A<ArrayList> ();    // 正確
b = new A<LinkedList> ();    // 正確
c = new A<HashMap> ();    // 錯誤

在上述代碼中,同樣由於 HashMap 類沒有實現 List 接口,所以在編譯時會報錯。

3) 繼承泛型類和實現泛型接口
定義爲泛型的類和接口也可以被繼承和實現。例如下面的示例代碼演示瞭如何繼承泛型類。

public class FatherClass<T1>{}//定義一個泛型類
public class SonClass<T1,T2,T3> extents FatherClass<T1>{}

如果要在 SonClass 類繼承 FatherClass 類時保留父類的泛型類型,需要在繼承時指定,否則直接使用 extends FatherClass 語句進行繼承操作,此時 T1、T2 和 T3 都會自動變爲 Object,所以一般情況下都將父類的泛型類型保留。

下面的示例代碼演示瞭如何在泛型中實現接口。

interface interface1<T1>{}
interface SubClass<T1,T2,T3> implements Interface1<T2>{}

11.1.5 泛型與C++模板的區別與聯繫

聲明
泛型本質上是提供類型的"類型參數",它們也被稱爲參數化類型(parameterized type)或參量多態(parametric polymorphism)。其實泛型思想並不是 Java 最先引入的,C++ 中的模板就是一個運用泛型的例子。

區別

  1. Java的泛型的實現根植於“類型消除”這一概念。當源代碼被轉換成Java虛擬機字節碼時,這種技術會消除參數化類型。有了Java泛型,我們可以做的事情也並沒有真正改變多少;它只是讓代碼變得漂亮些。鑑於此,Java泛型有時也被成爲”語法糖“。而在在C++中,模板本質上就是一套宏指令集,只是換了個名頭,編譯器會針對每種類型創建一份模板代碼的副本。有個證據可以證明這一點:MyClass<Foo>不會與MyClass<Bar>共享靜態變量。然而,兩個MyClass<Foo>實例則會共享靜態變量。
  2. java中沒有template的關鍵字,c++中有
  3. C++裏定義類型參數使用typename關鍵字,java只需要寫T,而且不能是基本數據類型,它只能接受引用類型。C++可以,它是寬泛的。List<typename t1,typename t2>——java List<t1,t2>
  4. C++中,類型參數可以實例化,Java不可以實例化
  5. 在java中,尖括號通常放在方法名前,而c++則是放在方法名後,c++的方式容易產生歧義,例如g(f<a,b>©),這個則有兩種解釋,一種是f的泛型調用,c爲參數,a,b爲泛型參數。另一種解釋,則是,g調用,兩個bool類型的參數。
  6. 在 C++ 模板中,編譯器使用提供的類型參數來擴充模板,因此,爲 List<A> 生成的 C++ 代碼不同於爲 List<B> 生成的代碼,List<A> 和 List<B> 實際上是兩個不同的類。而 Java 中的泛型則以不同的方式實現,編譯器僅僅對這些類型參數進行擦除和替換。類型 ArrayList<Integer> 和 ArrayList<String> 的對象共享相同的類,並且只存在一個 ArrayList 類。因此在c++中存在爲每個模板的實例化產生不同的類型,這一現象被稱爲“模板代碼膨脹”,而java則不存在這個問題的困擾。java中虛擬機中沒有泛型,只有基本類型和類類型,泛型會被擦除,一般會修改爲Object如果有限制,例如 T extends Comparable,則會被修改爲Comparable。而在C++中不能對模板參數的類型加以限制,如果程序員用一個不適當的類型實例化一個模板,將會在模板代碼中報告一個錯誤信息。
  7. Java中,可以將模板的類型參數限定爲某種特定類型。例如,你可能會使用泛型實現CardDeck,並規定參數必須擴展自CardGame。
  8. Java中,類型參數不能用於靜態方法和變量,因爲ArrayList<Integer> 和 ArrayList<String> 的對象共享相同的類,並且只存在一個 ArrayList 類。但在C++中,這些類是不同的,類型參數可以用於靜態方法和靜態變量。

總結

Java的泛型和C++模板,雖然在很多方面看起來都一樣,實則大不相同。
參考博客1
參考博客2

11.2 枚舉類型

枚舉是一個被命名的整型常數的集合,用於聲明一組帶標識符的常數。枚舉在日常生活中很常見,例如一個人的性別只能是“男”或者“女”,一週的星期只能是 7 天中的一個等。類似這種當一個變量有幾種固定可能的取值時,就可以將它定義爲枚舉類型。

在 JDK 1.5 之前沒有枚舉類型,那時候一般用接口常量來替代。而使用 Java 枚舉類型 enum 可以更貼近地表示這種常量。

11.2.1 聲明枚舉

聲明枚舉時必須使用 enum 關鍵字,然後定義枚舉的名稱、可訪問性、基礎類型和成員等。枚舉聲明的語法如下:

enum-modifiers enum enumname:enum-base {
    enum-body,
}

其中,enum-modifiers 表示枚舉的修飾符主要包括 public、private 和 internalenumname 表示聲明的枚舉名稱;enum-base 表示基礎類型;enum-body 表示枚舉的成員,它是枚舉類型的命名常數。

任意兩個枚舉成員不能具有相同的名稱,且它的常數值必須在該枚舉的基礎類型的範圍之內,多個枚舉成員之間使用逗號分隔。如果沒有顯式地聲明基礎類型的枚舉,那麼意味着它所對應的基礎類型是 int。

例 1
下面代碼定義了一個表示性別的枚舉類型 SexEnum 和一個表示顏色的枚舉類型 Color。

public enum SexEnum {
    male,female;
}
public enum Color {
    RED,BLUE,GREEN,BLACK;
}

之後便可以通過枚舉類型名直接引用常量,如 SexEnum.male、Color.RED。

使用枚舉還可以使 switch 語句的可讀性更強,例如以下示例代碼:

enum Signal {
    // 定義一個枚舉類型
    GREEN,YELLOW,RED
}
public class TrafficLight {
    Signal color = Signal.RED;
    public void change() {
        switch(color) {
            case RED:
                color = Signal.GREEN;
                break;
            case YELLOW:
                color = Signal.RED;
                break;
            case GREEN:
                color = Signal.YELLOW;
                break;
        }
    }
}

11.2.2 枚舉類

Java 中的每一個枚舉都繼承自 java.lang.Enum 類。當定義一個枚舉類型時,每一個枚舉類型成員都可以看作是 Enum 類的實例,這些枚舉成員默認都被 final、public, static 修飾,當使用枚舉類型成員時,直接使用枚舉名稱調用成員即可。

所有枚舉實例都可以調用 Enum 類的方法,常用方法如表 所示。

方法名稱 描述
values() 以數組形式返回枚舉類型的所有成員
valueOf() 將普通字符串轉換爲枚舉實例
compareTo() 比較兩個枚舉成員在定義時的順序
ordinal() 獲取枚舉成員的索引位置

例 2
通過調用枚舉類型實例的 values( ) 方法可以將枚舉的所有成員以數組形式返回,也可以通過該方法獲取枚舉類型的成員。

下面的示例創建一個包含 3 個成員的枚舉類型 Signal,然後調用 values() 方法輸出這些成員。

enum Signal {
    // 定義一個枚舉類型
    GREEN,YELLOW,RED;
}
public static void main(String[] args) {
    for(int i = 0;i < Signal.values().length;i++) {
        System.out.println("枚舉成員:"+Signal.values()[i]);
    }
}

輸出結果如下:
枚舉成員:GREEN
枚舉成員:YELLOW
枚舉成員:RED

例 3
創建一個示例,調用valueOf() 方法獲取枚舉的一個成員,再調用 compareTo() 方法進行比較,並輸出結果。具體實現代碼如下:

public class TestEnum {
    public enum Sex {
        // 定義一個枚舉
        male,female;
    }
    public static void main(String[] args) {
        compare(Sex.valueOf("male"));    // 比較
    }
    public static void compare(Sex s) {//枚舉量
        for(int i = 0;i < Sex.values().length;i++) {
            System.out.println(s + "與" + Sex.values()[i] + "的比較結果是:" + s.compareTo(Sex.values()[i]));//compreTo()返回差值
        }
    }
}

上述代碼中使用 Sex.valueOf(“male”) 取出枚舉成員 male 對應的值,再將該值與其他枚舉成員進行比較。最終輸出結果如下:
male與male的比較結果是:0
male與female的比較結果是:-1

11.2.3 爲枚舉添加方法

Java 爲枚舉類型提供了一些內置的方法,同時枚舉常量也可以有自己的方法。此時要注意必須在枚舉實例的最後一個成員後添加分號,而且必須先定義枚舉實例。
例 5
下面的代碼創建了一個枚舉類型 WeekDay,而且在該類型中添加了自定義的方法。

enum WeekDay {
    Mon("Monday"),Tue("Tuesday"),Wed("Wednesday"),
    		Thu("Thursday"),Fri("Friday"),Sat("Saturday"),Sun("Sunday");
    // 以上是枚舉的成員,必須先定義,而且使用分號結束
    private final String day;//成員變量
    private WeekDay(String day) {//方法
        this.day = day;
    }
    public static void printDay(int i) {
        switch(i) {
            case 1:
                System.out.println(WeekDay.Mon);
                break;
            case 2:
                System.out.println(WeekDay.Tue);
                break;
            case 3:
                System.out.println(WeekDay.Wed);
                break;
            case 4:
                System.out.println(WeekDay.Thu);
                break;
            case 5:
                System.out.println(WeekDay.Fri);
                break;
            case 6:
                System.out.println(WeekDay.Sat);
                break;
            case 7:
                System.out.println(WeekDay.Sun);
                break;
            default:
                System.out.println("wrong number!");
        }
    }
    public String getDay() {
        return day;
    }
}

上面代碼創建了 WeekDay 枚舉類型,下面遍歷該枚舉中的所有成員,並調用 printDay() 方法。示例代碼如下:

public static void main(String[] args) {
    for(WeekDay day : WeekDay.values()) {
        System.out.println(day+"====>" + day.getDay());
    }
    WeekDay.printDay(5);
}

輸出結果如下:

Mon====>Monday
Tue====>Tuesday
Wed====>Wednesday
Thu====>Thursday
Fri====>Friday
Sat====>Saturday
Sun====>Sunday

Java 中的 enum 還可以跟 Class 類一樣覆蓋基類的方法。下面示例代碼創建的 Color 枚舉類型覆蓋了 toString() 方法。

public class Test {
    public enum Color {
        RED("紅色",1),GREEN("綠色",2),WHITE("白色",3),YELLOW("黃色",4);
        // 成員變量
        private String name;
        private int index;
        // 構造方法
        private Color(String name,int index) {
            this.name = name;
            this.index = index;
        }
        // 覆蓋方法
        @Override
        public String toString() {
            return this.index + "-" + this.name;
        }
    }
    public static void main(String[] args) {
        System.out.println(Color.RED.toString());    // 輸出:1-紅色
    }
}

11.3 EnumMap 與 EnumSet(java枚舉新類)

爲了更好地支持枚舉類型,java.util 中添加了兩個新類:**EnumMap 和 EnumSet。**使用它們可以更高效地操作枚舉類型。

11.3.1 EnumMap 類

EnumMap 是專門爲枚舉類型量身定做的 Map 實現。雖然使用其他的 Map(如 HashMap)實現也能完成枚舉類型實例到值的映射,但是使用 EnumMap 會更加高效。

HashMap 只能接收同一枚舉類型的實例作爲鍵值,並且由於枚舉類型實例的數量相對固定並且有限,所以 EnumMap 使用數組來存放與枚舉類型對應的值,使得 EnumMap 的效率非常高。
例 6
下面是使用 EnumMap 的一個代碼示例。枚舉類型 DataBaseType 裏存放了現在支持的所有數據庫類型。針對不同的數據庫,一些數據庫相關的方法需要返回不一樣的值,例如示例中 getURL() 方法。

// 定義數據庫類型枚舉
public enum DataBaseType {
    MYSQUORACLE,DB2,SQLSERVER
}
// 某類中定義的獲取數據庫URL的方法以及EnumMap的聲明
private EnumMap<DataBaseType,String>urls = new EnumMap<DataBaseType,String>(DataBaseType.class);
public DataBaseInfo() {
    urls.put(DataBaseType.DB2,"jdbc:db2://localhost:5000/sample");
    urls.put(DataBaseType.MYSQL,"jdbc:mysql://localhost/mydb");
    urls.put(DataBaseType.ORACLE,"jdbc:oracle:thin:@localhost:1521:sample");
    urls.put(DataBaseType.SQLSERVER,"jdbc:microsoft:sqlserver://sql:1433;Database=mydb");
}
//根據不同的數據庫類型,返回對應的URL
// @param type DataBaseType 枚舉類新實例
// @return
public String getURL(DataBaseType type) {
    return this.urls.get(type);
}

在實際使用中,EnumMap 對象 urls 往往是由外部負責整個應用初始化的代碼來填充的。這裏爲了演示方便,類自己做了內容填充。

從本例中可以看出,使用 EnumMap 可以很方便地爲枚舉類型在不同的環境中綁定到不同的值上。本例子中 getURL 綁定到 URL 上,在其他的代碼中可能又被綁定到數據庫驅動上去。

11.3.2 EnumSet 類

EnumSet 是枚舉類型的高性能 Set 實現,它要求放入它的枚舉常量必須屬於同一枚舉類型。EnumSet 提供了許多工廠方法以便於初始化,如表所示。

方法名稱 描述
allOf(Class element type) 創建一個包含指定枚舉類型中所有枚舉成員的 EnumSet 對象
complementOf(EnumSet s) 創建一個與指定 EnumSet 對象 s 相同的枚舉類型 EnumSet 對象,幷包含所有 s 中未包含的枚舉成員
copyOf(EnumSet s) 創建一個與指定 EnumSet 對象 s 相同的枚舉類型 EnumSet 對象,並與 s 包含相同的枚舉成員
noneOf(<Class elementType) 創建指定枚舉類型的空 EnumSet 對象
of(E first,e…rest) 創建包含指定枚舉成員的 EnumSet 對象
range(E from ,E to) 創建一個 EnumSet 對象,該對象包含了 from 到 to 之間的所有枚舉成員

EnumSet 作爲 Set 接口實現,它支持對包含的枚舉常量的遍歷。

for(Operation op:EnumSet.range(Operation.PLUS,Operation.MULTIPLY)) {
    doSomeThing(op);
}

11.4 泛型限制裏的菱形語法

在 Java 7 版本以前,如果使用帶泛型的接口、類定義變量,那麼調用構造器創建對象時構造器的後面也必須帶泛型,這顯得有些多餘了。例如如下兩條語句:

List<String> strList = new ArrayList<String>();
Map<String, Integer> scores = new HashMap<String, Integer>();

上面兩條語句中等號右邊的尖括號部分完全是多餘的,Java 7 版本以前是必需的,不能省略。從 Java 7 開始,Java 允許在構造器後不帶完整的泛型信息,只要給出一對尖括號<>即可。Java 可以推斷出尖括號裏應該是什麼泛型信息。

即上面兩條語句可以改寫爲如下形式:

List<String> strList = new ArrayList<>();
Map<String, Integer> scores = new HashMap<>();

把兩個尖括號並排放在一起非常像一個菱形,這種語法也就被稱爲“菱形”語法。“菱形”語法對原有的泛型並沒有改變,只是更好地簡化了泛型編程。

Java 9 再次增強了“菱形”語法,它甚至允許在創建匿名內部類時使用菱形語法,Java 可根據上下文來推斷匿名內部類中泛型的類型。下面程序示範了在匿名內部類中使用菱形語法。

interface Foo<T> {
    void test(T t);
}
public class AnnoymousTest {
    public static void main(String[] args) {
        // 指定Foo類中泛型爲String
        Foo<String> f = new Foo<>() {//內部使用匿名類時可以使用菱形語法
            // test()方法的參數類型爲String
            public void test(String t) {
                System.out.println("test 方法的 t 參數爲:" + t);
            }
        };
        // 使用泛型通配符,此時相當於通配符的上限爲Object
        Foo<?> fo = new Foo<>() {
            // test()方法的參數類型爲Object
            public void test(Object t) {
                System.out.println("test 方法的 Object 參數爲:" + t);
            }
        };
        // 使用泛型通配符,通配符的上限爲Number
        Foo<? extends Number> fn = new Foo<>() {
            // 此時test ()方法的參數類型爲Number
            public void test(Number t) {
                System.out.println("test 方法的 Number 參數爲:" + t);
            }
        };
    }
}

上面程序先定義了一個帶泛型聲明的接口,接下來第 8、15 和 22 行代碼分別示範了在匿名內部類中使用菱形語法。第 8 行代碼聲明變量時明確地將泛型指定爲 String 類型,因此在該匿名內部類中 T 類型就代表了 String 類型;第 15 行代碼聲明變量時使用通配符來代表泛型(相當於通配符的上限爲 Object),因此係統只能推斷出 T 代表 Object,所以在該匿名內部類中 T 類型就代表了 Object 類型;第 22 行代碼聲明變量時使用了帶上限(上限是 Number)的通配符,因此係統可以推斷出 T 代表 Number 類。

無論以上哪種方式,Java 9 都允許在使用匿名內部類時使用菱形語法。

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