Java集合框架入門之泛型和包裝類

前言: 本章主要是為瞭後面學習集合框架所做的知識補充。補充瞭泛型以及包裝類兩個知識,但是該章泛型的講解不夠全面,主要是為瞭集合框架學習做鋪墊。

1. 預備知識-泛型(Generic)

1.1 泛型的引入

我們之前實現過的順序表,實現的是保存某一類型的元素(如 int 型)

示例代碼:

public class MyArrayList{
    private int[] array;	  // 保存順序表的元素,元素都為 int 類型
    private int size;		  // 保存順序表內存數據個數
	public MyArrayList(){
        this.array=new int[10];
    }
    public void add(int val){
        // 尾插
        this.array[size]=val;
        this.size++;
    }
    public int get(int index){
        // 獲取 index 位置的元素
        return this.array[index];
    }
    ...
}

但是這樣寫的話,這個順序表就隻能存儲 int 類型的元素瞭

如果現在需要保存指向 Person 類型對象的引用的順序表,該如何解決呢?如果又需要保存指向 Book 類型對象的引用呢?

  • 首先,我們在學習多態的時瞭解到:基類的引用可以指向子類的對象
  • 其次,我們也知道 Object 類是 Java 中所有所有類的祖先類

因此,要解決上述問題,我們可以這樣做

將我們的順序表的元素類型定義成 Object 類型,這樣我們的 Object 類型的引用可以指向 Person 類型的對象或者指向 Book 類型的對象

示例代碼:

public class MyArrayList{
    private Object[] array;	  // 保存順序表的元素,即 Object 類型的引用
    private int size;		  // 保存順序表內存數據個數
	public MyArrayList(){
        this.array=new Object[10];
    }
    public void add(Object val){
        // 尾插
        this.array[size]=val;
        this.size++;
    }
    public Object get(int index){
        // 獲取 index 位置的元素
        return this.array[index];
    }
    ...
}

這樣,我們就可以很自由的存儲指向任意類型的對象的引用到我們的順序表瞭

示例代碼:

MyArrayList books = new MyArrayList();
for(int i=0; i<10;i++){
    books.add(new Book());	// 插入10本書到順序表
}

MyArrayList people = new MyArrayList();
for(int i=0; i<10; i++){
    people.add(new Person());	// 插入10個人到順序表
}

遺留問題: 現在的 MyArrayList 雖然可以做到添加任意類型的引用到其中,但會遇到下面的問題

當我們使用這樣的代碼時,明知道存儲的是哪種類型的元素,但還是要進行強制轉換。如

MyArrayList books = new MyArrayList();
books.add(1);

// 將 Object 類型轉換為 int 類型 (需要類型轉換才能成功)
int val=(int)books.get(0);
System.out.println(val);
// 結果為:1

雖然知道返回的元素是 int 類型,但還是要進行強制類型轉換

創建的一個 MyArrayList 中可以存放各種類型,形成瞭一個大雜燴。並且將 Object 類型(具體是 A 類型)轉換為 B 類型時,即使強制轉換,也會產生異常 ClassCastException

MyArrayList books = new MyArrayList();
books.add(new Book());
    
// 將 Object 類型轉換為 Person (需要類型轉換才能成功)
Person person = (Person)books.get(0);
// 但是雖然編譯正確瞭,運行時還是會拋出異常 ClassCastException

因此 Java 針對這一問題就出現瞭泛型

Java 泛型(generics)是 JDK 5 中引入的一個新特性,泛型提供瞭編譯時類型安全檢測機制,該機制允許程序員在編譯時檢測到非法的類型。

泛型的本質是參數化類型,也就是說所操作的數據類型被指定為一個參數。

1.2 泛型的分類

泛型可以分為兩類

  • 泛型類
  • 泛型方法

預備知識主要是為瞭學習、理解集合框架,所以這裡隻簡單介紹泛型類,後面將會專門為泛型寫一個章節。

1.3 泛型類的定義

規則:

  • 在類名後面添加瞭類型參數聲明
  • 泛型類的類型參數聲明部分包含一個或多個類型參數,參數間用逗號隔開。一個泛型參數,也被稱為一個類型變量,是用於指定一個泛型類型名稱的標識符
  • 泛型的泛型參數一定是類類型,如果是簡單類型,那麼必須是對應的包裝類

這裡直接將上面定義的 MyArrayList 類改寫成泛型類

示例代碼:

public class MyArrayList<T>{
    private T[] array;
    private int size;
	public MyArrayList(){
        this.array=(T[])new Object[10];
    }
    public void add(T val){
        this.array[size]=val;
        this.size++;
    }
    public T get(int index){
        return this.array[index];
    }
    ...
}

此時我們就將這個順序表改寫成瞭一個泛型類,接下來我們來使用它

示例代碼:

MyArrayList<String> myArrayList = new MyArrayList<>();
myArrayList.add("Hello");
myArrayList.add("Goodbye");
String s = myArrayList.get(0);
System.out.println(s);
// 結果為:Hello

上述的 myArrayList 隻能存放 String 類型的元素,並且不需要再添加強制類型轉換

泛型的意義:

  • 自動進行類型的檢查
  • 自動進行類型的轉換

Java 中泛型標記符: 類型形參一般使用一個大寫字母表示,如:

  • E — Element(在集合中使用,因為集合中存放的是元素)
  • T — Type(Java 類)
  • K — Key(鍵)
  • V — Value(值)
  • N — Number(數值類型)
  • ? —表示不確定的 Java 類型

1.4 泛型編譯的機制

如果不重寫 toString 方法,輸出某個類的實例化對象,如

代碼示例:

// 假設創建瞭一個 Person 類
Person person = new Person();
System.out.println(person);

結果為:

在這裡插入圖片描述

如果用上述的泛型類,輸出其實例化對象,如

代碼示例:

MyArrayList<String> myArrayList1 = new MyArrayList<>();
System.out.println(myArrayList1);
MyArrayList<Integer> myArrayList2 = new MyArrayList<>();
System.out.println(myArrayList2);
MyArrayList<Boolean> myArrayList3 = new MyArrayList<>();
System.out.println(myArrayList3);

結果為:

在這裡插入圖片描述

我們發現:

泛型類和非泛型類輸出的樣例格式都是一樣的:類名@地址

為什麼泛型類的實例化對象結果不是輸出泛型類後面的泛型參數 < T > 呢?

這裡就要瞭解泛型是怎麼編譯的

泛型的編譯使用瞭一種機制:擦除機制

擦除機制隻作用於編譯期間,換句話說,泛型就是編譯時期的一種機制,運行期間沒有泛型的概念

解釋:

  • 當我們存放元素的時候,泛型就會根據 <T> 自動進行類型的檢查。
  • 但編譯的時候,這些 <T> 就被擦除成瞭 Object

2. 預備知識-包裝類(Wrapper Class)

Object 引用可以指向任意類型的對象,但有例外出現瞭,8 種基本數據類型不是對象,那豈不是剛才的泛型機制要失效瞭?

實際上也確實如此,為瞭解決這個問題,Java 中引入瞭一類特殊的類,即這 8 種基本數據類型的包裝類。在使用過程中,會將類似 int 這樣的值包裝到一個對象中去。

2.1 基本數據類型和包裝類的對應關系

基本數據類型 包裝類
byte Byte
short Short
int Integer
long Long
float Float
double Double
char Character
boolean Boolean

2.2 包裝類介紹

Java 是一個面向對象的語言,基本類型並不具有對象的性質,為瞭與其他對象“接軌”就出現瞭包裝類型

既然包裝類是一個類,那麼就有它對應的成員變量和成員方法。打孔大傢可以具體的去查看文檔瞭解各個包裝類

2.3 裝箱(boxing)和拆箱(unboxing)

包裝類中有兩個重要的知識點,裝箱和拆箱

  • 裝箱: 把基本數據類型轉為對應的包裝類型
  • 拆箱: 把包裝類型轉換為基本數據類型

裝箱示例代碼:

// 方式一
Integer i1 = 10;
// 方式二
Integer i2 = Integer.valueOf(10);
// 方式三
Integer i3 = new Integer(10);

拆箱示例代碼:

// 方式一
int i = i1;
// 方式二
int i = i1.intValue();

2.4 自動裝箱(autoboxing)和自動拆箱(autounboxing)

那自動裝箱又是什麼呢?我們可以對下面這份代碼進行反編譯(反編譯指令為 javap -c 類名

代碼示例:

public class TestDemo {
    public static void main(String[] args) {
        Integer i = 10;
        int j = i;
    }
}

通過反編譯指令,得到瞭如下結果:

在這裡插入圖片描述

  • 我們發現在底層中 10 是通過 Integer.valueOf 這個靜態方法賦值給瞭 i,進行裝箱操作
  • 再將 i 通過 Integer.intValue 這個方法復制給瞭 j,進行拆箱操作

那麼什麼是手動裝箱和手動拆箱呢?

就是和底層原理一樣,通過 Integer.valueOfInteger.intValue 方法進行的裝箱和拆箱就是手動的

而不是通過這些方法進行的裝箱和拆箱就是自動的

2.5 包裝類面試題

思考下列代碼結果:

Integer a = 120;
Integer b = 120;
System.out.println(a == b);

結果為:true

再看一個代碼:

Integer a = 130;
Integer b = 130;
System.out.println(a == b);

結果為:false

在這裡插入圖片描述

這是為什麼呢?

  • 首先我們看到 a 和 b 都進行瞭裝包操作,因此我們就要去瞭解裝包的時候發生瞭什麼
  • 通過轉到 Integer.valueOf 的定義我們看到

在這裡插入圖片描述

  • 該定義意思就是:如果 i 大於等於 IntegerCache 的最小值,小於它的最大值,就返回 IntegerCache.cache[i + (-IntegerCache.low)] ,否則就返回 new Integer(i)
  • 而 new 一個對象的話,相當於比較的就是地址的值瞭,所以是 false
  • 因此我們要知道 IntegerCache 的最大值以及最小值是多少,此時我們轉到它的定義

在這裡插入圖片描述

  • 上圖中我們瞭解到 low 為 -128、high為 127,而 cache 其實就是一個數組。我們知道數組的下標是從 0 開始的,而 i + (-IntegerCache.low) 表示的最小值正好就是 0,也就是說明數組下標為 0 時存儲的值就為 -128,並且依次往後遞推。
  • 因此數值在 -128 到 127 之間時返回的就是和這個數相同的值,所以結果為 true

那為什麼要專門創建一個數組呢?所有數字返回 new 的對象不就行瞭嗎?

這是因為,這樣做可以提高效率。實例化對象是需要消耗資源的。而數組其實就是一個對象,可以減少資源的消耗。

到此這篇關於Java集合框架入門之泛型和包裝類的文章就介紹到這瞭,更多相關Java 泛型詳解內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: