前言:
泛型的知識其實在前面 Java 的泛型和包裝類 這章介紹過了一些,但那些知識是為后面介紹 Java 集合框架做的鋪墊,而今天這章再配合之前那章,將會完整的介紹 Java 中的泛型!
文章目錄
- 1. 前章回顧
- 1.1 泛型類的代碼示例
- 1.2 泛型類的意義
- 1.3 泛型是如何編譯的
- 2. 泛型類的定義
- 2.1 語法
- 2.2 示例
- 3. 內部類
- 3.1 概念
- 3.2 實體內部類
- 3.3 靜態內部類
- 3.4 匿名內部類
- 4. 泛型類的使用
- 4.1 語法
- 4.2 示例
- 4.3 型別推導(Type Inference)
- 5. 裸型別(Raw Type)
- 6. 泛型類的型別邊界
- 6.1 概念
- 6.2 語法
- 6.3 示例
- 7. 型別擦除
- 7.1 概念
- 7.2 示例
- 8. 通配符的使用(Wildcards)
- 8.1 引入
- 8.2 通配符——上界
- 8.3 通配符——下界
- 9. 泛型中的父子型別
- 10. 泛型方法
- 10.1 語法
- 10.2 示例
- 10.3 型別型推導(Type Inference)
- 11. 泛型的限制
1. 前章回顧
1.1 泛型類的代碼示例
在之前那章我們介紹了泛型類的基本定義,這里我們直接來創建并使用一個使用了泛型的堆疊來回顧泛型的定義
// 出現的 <T> 就表示當前的類是一個泛型類,T 是一個占位符
class Stack<T>{
private T[] elem;
private int usedSize;
public Stack(){
this.elem=(T[])new Object[10];
}
// 入堆疊(不考慮堆疊滿)
public void push(T val){
this.elem[this.usedSize++]=val;
}
// 出堆疊(不考慮堆疊空)
public T pop(){
this.usedSize--;
return this.elem[this.usedSize];
}
}
public class TestDemo{
public static void main(String[] args){
Stack<Integer> stack=new Stack<Integer>();
stack.push(1);
stack.push(2);
int val=stack.pop();
System.out.println(val);
System.out.println(stack);
}
}
// 結果為:2 和 Stack@1b6d3586
注意: 上述代碼的構造方法為什么代碼塊是這樣的:this.elem=(T[])new Object[10];
- 如果寫成
this.elem=new T[10];,那么我們在編譯時根本不知道具體的型別是什么,因此不能直接使用泛型去實體化物件- 使用上述方式可以的原因是:此時發生了泛型的擦除機制,即將泛型 T 擦除為 Object,從而此時的泛型具有了 Object 的特質,所以如果寫成這樣
this.elem=new T[10];就等價于代碼是這樣的this.elem=new Object[10];- 但是我們想要的是一個非 Object 型別的不通用的陣列,即后期不需要進行強制型別轉換,故在擦除機制的前提下我們就可以寫成
this.elem=(T[])new Object[10];
1.2 泛型類的意義
- 自動進行型別的檢查,如:在編譯期間會根據指定泛型的資訊來檢查你插入的值是否匹配,檢查完后泛型的資訊就被擦除了
- 自動進行型別的轉換,如:只要我們使用了泛型,就可以在創建某個具體型別的實體的時候不必要進行強制型別轉換
1.3 泛型是如何編譯的
- 泛型是編譯期間的一種機制,即擦除機制
- 擦除機制指的是:在編譯的時候將泛型 T,擦除為了 Object(此時所有的泛型資訊都被擦除了,在生成的 Java 位元組碼中是不包含泛型重點型別資訊的)
證明方式:
- 如果不重寫
toString方法,輸出某個類的實體化物件,結果為:型別@物件地址- 而上述代碼的列印結果為:
Stack@1b6d3586,而不是Stack<Integer>@1b6d3586,即泛型的的資訊在編譯期間就被擦除了
2. 泛型類的定義
2.1 語法
-
一個型別形參
class 泛型類名稱<型別形參>{ // 該代碼塊中可以直接使用型別引數 } -
多個型別形參
class 泛型類名稱<型別形參1, 型別形參2, ..., 型別形參n>{ // 該代碼塊中可以直接使用所有型別引數 } -
泛型類可以繼承類(包括泛型類)
class 泛型類名稱<型別形參> extends 父類名稱<型別形參>{ // 該代碼塊中可以直接使用所有型別引數 } -
泛型類可以是一個介面
interface 泛型類名稱<型別形參>{ // 該代碼塊中可以直接使用型別引數 }
常用型別形參: 型別形參一般使用一個大寫字母表示,常有名稱如下
E:表示 Element,即元素,運用在集合中K:表示 Key,即鍵V:表示 Value,即值N:表示 Number,即數值型別T:表示 Type,即 Java 型別?:表示不確定的 Java 型別
2.2 示例
class Stack<T>{
private T[] elem;
private int usedSize;
public Stack(){
this.elem=(T[])new Object[10];
}
// 入堆疊(不考慮堆疊滿)
public void push(T val){
this.elem[this.usedSize++]=val;
}
// 出堆疊(不考慮堆疊空)
public T pop(){
this.usedSize--;
return this.elem[this.usedSize];
}
}
3. 內部類
3.1 概念
定義在類內部的類叫做內部類
分類:
- 本地內部類:定義在方法里面的類,很少見
- 實體內部類:指沒有用 static 修飾的內部類,有的地方也稱為非靜態內部類
- 靜態內部類:指使用 static 修飾的內部類
- 匿名內部類:是沒有名字的內部類
3.2 實體內部類
示例代碼:
class OuterClass{
// 在外部類中成員變數都是可以正常定義的
public int data1=1;
public static int data2=2;
private int data3=3;
// 定義實體內部類
class InnerClass{
public int data4=4;
// 實體內部類中靜態變數無法定義
// public static int data5=5; 該變數無法定義
// 但是增加一個 final 就可以定義了
public static final int data5=5;
private int data6=6;
public void func(){
System.out.println("這是一個實力內部類的 func 方法,也可以正常定義");
System.out.println(data1);
System.out.println(data2);
System.out.println(data3);
System.out.println(data4);
System.out.println(data5);
System.out.println(data6);
}
}
}
結論1: 在實體內部類當中,是不可以定義一個靜態的成員變數
因為實體內部類的呼叫是需要依賴物件的,而 static 修飾的成員是靜態的,是不依賴物件的,就如普通的方法中定義靜態的變數也是不行的
結論2: 如果加一個 final,那么就可以在實體內部類中使用 static
因為此時表示的是常量了,而常量在編譯期間就已經確定了
結論3: 實體化實體內部類的方式是:先實體化外部類,再通過下面第二行代碼的形式去實體化
OuterClass outerClass=new OuterClass();
OuterClass.InnerClass innerClass=outerClass.new InnerClass();
結論4: 實體內部類中的方法也可以呼叫外部類的一些成員變數
innerClass.func();
// 結果為:
// 這是一個實力內部類的 func 方法,也可以正常定義
// 1 2 3 4 5 6
結論5: 如果實體內部類中定義的變數名和外部類中的某個變數名相同,那么實體內部類默認呼叫的是內部類的變數,即使用 this,也表示的是此時內部類的物件,如果要使用外部類的同名變數,則可以通過:外部類名.this.外部類變數名 來呼叫
結論6: 當我們去我們看我們定義的靜態內部類的位元組碼檔案時,它其實是這樣的
應用:
比如我們自己創建鏈表時,Node 節點是定義在 LinkedList 類外部的,但是可以將 Node 類寫成它的一個實體內部類
3.3 靜態內部類
示例代碼:
class OuterClass{
// 在外部類中成員變數都是可以正常定義的
public int data1=1;
public static int data2=2;
private int data3=3;
// 定義靜態內部類
static class InnerClass{
public int data4=4;
public static final int data5=5;
private int data6=6;
public void func(){
System.out.println("這是一個實力內部類的 func 方法,也可以正常定義");
System.out.println(data1);
System.out.println(data2);
System.out.println(data3);
System.out.println(data4);
System.out.println(data5);
System.out.println(data6);
}
}
}
結論1: 以下是實體化靜態內部類的方法,相比實體內部類,它不需要外部類去創建物件
OuterClass.InnerClass innerClass=new OuterClass.InnerClass();
結論2: 在靜態內部類當中,不能呼叫外部類的普通成員變數
因為普通成員變數需要靠外部類的物件來呼叫
結論3: 如果要想在靜態內部類中呼叫外部類的普通成員變數,則可以在靜態內部類當中實體化一個外部類的物件,通過這個參考就可以訪問外部類的普通成員變數
static class InnerClass{
public OuterClass out=new OuterClass();
System.out.println(out.data1);
}
結論4: 當內部類和外部類有同名的靜態變數時,默認呼叫的是內部類本身的,要想呼叫外部類的,則可以通過:外部類名.變數名 來使用
3.4 匿名內部類
實體代碼:
不使用匿名內部類來實作抽象方法
abstract class Person {
public abstract void eat();
}
class Child extends Person {
public void eat() {
System.out.println("eat something");
}
}
public class TestDemo {
public static void main(String[] args) {
Person p = new Child();
p.eat();
}
}
// 結果為:eat something
如果上述 Child 類只使用一次,那么單獨寫一個類出來就比較麻煩,所以可以使用匿名內部類
abstract class Person {
public abstract void eat();
}
public class TestDemo {
public static void main(String[] args) {
Person p = new Person() {
public void eat() {
System.out.println("eat something");
}
};
p.eat();
}
}
// 結果為:eat something
結論1: 由于沒有名字,所以匿名內部類只能使用一次
結論2: 使用匿名內部類的前提是:必須繼承一個父類或實作一個介面
結論3: 匿名內部類的形式就是直接在宣告的物件后面接一個大括號,里面就寫該類需要使用的內容
應用:
最常用的情況就是在多執行緒的實作上,因為要實作多執行緒必須繼承 Thread 類或是繼承 Runnable 介面
4. 泛型類的使用
4.1 語法
泛型類<型別實參> 變數名 = new 泛型類<型別實參>(構造方法實參);
4.2 示例
Stack<Integer> stack=new Stack<Integer>();
4.3 型別推導(Type Inference)
當編譯器可以根據背景關系推匯出型別實參時,可以省略類型實參的填寫
上述示例就可以省略后面一個型別實參
Stack<Integer> stack=new Stack<>();
5. 裸型別(Raw Type)
概念:
裸型別是一個泛型類但沒有帶著型別引數
示例: 上述代碼創建的泛型類 Stack<T> ,如果將 Stack 單拿出來不加 <T> 去使用的話,那么它就是一個裸型別,我們可以直接使用它去實體化物件
Stack list = new Stack();
注意:
我們不要自己去使用裸型別,裸型別是為了兼容老版本的 API 保留的機制,如果使用他的話,就跟不用泛型沒兩樣了,泛型的作用和意義也就沒了
6. 泛型類的型別邊界
6.1 概念
在定義泛型類時,有時需要對傳入的型別引數做一定的約束,可以通過型別邊界來約束
注意:
泛型只有上界,沒有下界
6.2 語法
class 泛型類名稱<型別引數 extends 型別邊界>{
}
上述泛型類可以傳入的型別引數必須是型別邊界的類或者子類
6.3 示例
示例一: 讓泛型引數只接受數值類 Number 的子型別
class Stack<T extends Number>{
}
故此時泛型引數傳 Integer 是可以的,但傳 String 是不行的
Stack<Integer> l1; // 正確,因為 Integer 是 Number 的子型別
Stack<String> l2; // 編譯錯誤,因為 String 不是 Number 的子型別
示例二: 寫一個泛型類 Algorithm,我們要這個類中有一個方法可以實作找到陣列的最大值
-
其實我自己的第一想法,就是寫成這樣
class Algorithm<T>{ public T findMax(T[] array){ T max=array[0]; for(int i=0;i<array.length;i++){ if(array[i]>max){ max=array[i]; } } return max; } }但是報錯了,自己一想估摸是泛型引數其實是型別別,即大小比較的是參考值,那么估摸要使用 Comparable 介面或者 Comparator 介面

-
那么我就直接用 compareTo 方法,但是發現使用不了,原因如下
這是由于型別擦除,使得這個 T 被擦除成了 Object,而我們知道 Object 是所有類的祖先類,他是不繼承任何類或者介面的,故 compareTo 方法就使用不了
-
為此,我們就有了這樣的寫法
class Algorithm<T extends Comparable<T>>{ public T findMax(T[] array){ T max=array[0]; for(int i=0;i<array.length;i++){ if(array[i].compareTo(max)>0){ max=array[i]; } } return max; } }這里使用了型別邊界來進行了一個約束,代表在進行擦除時,擦除到了 Comparable 介面的地方,通俗點講,就是這樣寫,那么這個 T 就一定要實作 Comparable 介面,并且擦除時不會擦除成 Object,而是擦除成了 Comparable
問題: 示例二繼承了 Comparable 介面為什么沒有重寫 compareTo 方法?
因為我們要傳入的引數型別是本身一定要實作 Comparable 這個介面的,既然本身已經實作了,那么 compareTo 這個方法在這個引數型別中就得到了重寫
7. 型別擦除
7.1 概念
- 泛型是作用在編譯期間的一種機制,實際上運行上是沒有這么多類的,那么運行期間是什么型別呢?這就是型別擦除所作的事情
- 型別擦除主要以其型別邊界而定
補充: 編譯器在型別擦除階段所做什么?
- 將型別變數用擦除后的型別替換
- 加入必要的型別轉換陳述句
- 加入必要的
bridge method保證多型的正確性
7.2 示例
示例一: 擦除后為 Object
class Stack<T>{
}
示例二: 擦除后為型別邊界(這里是 Comparable)
class Stack<T extends Comparable<T>{
}
8. 通配符的使用(Wildcards)
8.1 引入
以下這個代碼的目的是遍歷順序表
class Generic{
public static<T> void print(ArrayList<T> list){
for(T t: list){
System.out.print(t+" ");
}
System.out.println();
}
}
上述代碼中我們使用了泛型,并且指定了它的型別引數是 T,故我們使用時這個方法已經知道它的型別是 T 了,而這個 T 是我們指定的,有時這個方法本身也不知道傳入的這個順序表的引數型別是什么?那該怎么寫呢?
這里就要使用到通配符 ?
class Generic{
// 既然不知道具體型別,那么 static 后面也不需要加 <T> 了
public static void print(ArrayList<?> list){
// 由于不知道具體型別是什么,就使用 Object
for(Object obj: list){
System.out.println(obj+" ");
}
System.out.println();
}
}
8.2 通配符——上界
語法:
<? extends 上界>
表示可以傳入的型別實參是上界型別的子類的任意型別
示例:
// Stack 物件中可以傳入的型別實參是 Number 子類的任意型別的 Stack
public static void printAll(Stack<? extends Number> stack){
}
// 以下呼叫都是正確的
printAll(new Stack<Integer>());
printAll(new Stack<Double>());
printAll(new Stack<Number>());
// 以下呼叫是編譯錯誤的
printAll(new Stack<String>());
printAll(new Stack<Object>());
8.3 通配符——下界
語法:
<? super 下界>
表示可以傳入的型別實參是下界型別的父類的任意型別
示例:
// Stack 物件中可以傳入的型別實參是 Integer 父類的任意型別的 Stack
public static void printAll(Stack<? Super Integer> stack){
}
// 以下呼叫都是正確的
printAll(new Stack<Integer>());
printAll(new Stack<Object>());
printAll(new Stack<Number>());
// 以下呼叫是編譯錯誤的
printAll(new Stack<String>());
printAll(new Stack<Double>());
9. 泛型中的父子型別
我們知道 Object 是 Number 的父型別,Number 是 Integer 的父型別
但是類如 Stack<Object> 就不是 Stack<Number> 的父型別, Stack<Number> 也不是 Stack<Integer> 的父型別,
因為泛型的引數型別不參與型別的組成
如果要確定泛型的父子型別,則需要使用通配符,如
Stack<?> 是 Stack<? extends Number> 的父型別, Stack<? extends Number> 也是 Stack<Integer> 的父型別
10. 泛型方法
10.1 語法
方法限定符 <型別形參串列> 回傳值型別 方法名稱(形參串列){
}
10.2 示例
示例一: 寫一個泛型類 Algorithm,我們要這個類中有一個方法可以實作陣列中兩個值的交換,要求使用這個方法不需要實體化物件
class Algorithm{
public static<T> swap(T[] array,T i, T j){
T tmp=array[i];
array[i]=array[j];
array[j]=tmp;
}
}
示例二: 寫一個泛型類 Algorithm,我們要這個類中有一個方法可以實作找到陣列的最大值,要求使用這個方法不需要實體化物件
class Algorithm{
public static<T extends Comparable<T>> T findMax(T[] array){
T max=array[0];
for(int i=1;i<array.length;i++){
if(array[i].compareTo(max)>0){
max=array[i];
}
}
return max;
}
}
10.3 型別型推導(Type Inference)
當編譯器可以根據背景關系推匯出型別實參時,可以省略型別實參的填寫
示例:通過示例中的示例二的 Algorithm 類,去找到陣列的最大值
Integer[] array={1,4,2,9,10};
// 使用 <Integer> 表示我們要傳入的值都是 Integer 型別的
Integer ret=Algorithm.<Integer>findMax(array);
但是由于我們通過勺ò干以判斷這個值是 Integer 型別的,所以上述代碼可以省略 <Integer>
Integer[] array={1,4,2,9,10};
Integer ret=Algorithm.findMax(array);
11. 泛型的限制
- 泛型型別引數不支持基本資料型別
- 無法實體化泛型型別的物件
- 無法使用泛型型別宣告靜態的屬性
- 無法使用
instanceof判斷帶型別引數的泛型型別- 無法創建泛型型別陣列
- 無法
create、catch、throw一個泛型類例外,即例外不支持泛型- 泛型型別不是形參一部分,無法多載
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/356112.html
標籤:java
