JAVA基礎知識|泛型
一、什麼是泛型?
泛型,即“引數化型別”。
比如定義一個變數A,我們可以通過如下方式將這個變數定義為字串型別或者整形。
String A;
Integer A;
當然這是在變數型別已知的情況下,如果有一種情況,我們在定義變數的時候不知道以後會需要什麼型別,或者說我們需要相容各種型別的時候,又該如何定義呢?
鑑於以上這種情況,我們可以引入“泛型”,將String和Integer型別進行引數化。在使用的時候,再傳入具體的引數型別。
泛型的本質是為了引數化型別(通過傳入的不同型別來決定形參的具體型別)。也就是說在泛型使用過程中,資料的型別被指定為一個引數,這種引數型別可以用在類、介面和方法中,分別被稱為泛型類、泛型介面、泛型方法。
二、泛型類
Computer類,這個類中包含一個屬性t,型別為String。
public class Computer {
private String t;
public void set(String t) {
this.t = t;
}
public String get() {
return this.t;
}
}
泛型類Computer
//這裡的"T"並不是固定寫法,也可以用V或M等字元 public class Computer<T> { private T t; public void set(T t) { this.t = t; } public T get() { return this.t; } } //可以傳入多種泛型引數 public class Computer<T, V> { private T t; private V v; public void set(T t, V v) { this.t = t; this.v = v; } public T getT() { return this.t; } public V getV() { return this.v; } }
定義泛型類的好處就是,我們可以在需要的時候,再去指定屬性t的型別,增強了通用性。
Computer<String> computer1= new Computer<String>();
Computer<Integer> computer2= new Computer<Integer>();
Computer<Double> computer3= new Computer<Double>();
三、泛型介面
//定義介面Calculatable public interface Calculatable<T> { T execute(); } //傳入String型別的實參 public class Computer implements Calculatable<String> { @Override public String execute() { return "ok"; } } //傳入Integer型別的實參 public class Calculator implements Calculatable<Integer> { @Override public Integer execute() { return 100; } } //未傳入具體實參,繼續丟擲,由下層傳入 public class Phone<V> implements Calculatable<V> { private V v; @Override public V execute() { return this.v; } }
四、泛型方法
public class Computer<T> {
private T t;
//不是泛型方法
public void set(T t) {
this.t = t;
}
//不是泛型方法
public T get() {
return this.t;
}
//泛型方法
//首先public與返回值型別之間的<V>必不可少,只有聲明瞭<V>的方法才是泛型方法
//可以宣告多個泛型,如 public <V,M> genericMethod(V v,M m)
public <V> V genericMethod(V v){
return v;
}
}
Computer<String> computer = new Computer<String>();
Integer v= 100;
System.out.println(computer.genericMethod(v).getClass().toString());
Double v2= 100d;
System.out.println(computer.genericMethod(v2).getClass().toString());
輸出結果:
class java.lang.Integer
class java.lang.Double
泛型類,是在例項化類的時候指明泛型的具體型別;泛型方法,是在呼叫方法的時候指明泛型的具體型別。
泛型方法,可以通過傳入的實參,判斷V的具體型別。
五、靜態方法與泛型
如果靜態方法不可以使用類中定義的泛型,如果靜態方法想要使用泛型,需要將自身宣告為泛型方法。
public class Computer<T> {
public static <V> void get(V v){
//T t;報錯,不能使用類中定義的泛型
}
}
六、萬用字元及上下邊界
舉一個簡單的例子
//水果類
public class Fruit {
private String name;
public Fruit(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
fsadfdsfd
//蘋果類
public class Apple extends Fruit {
public Apple(String name) {
super(name);
}
}
//裝水果的袋子
public class GenericHolder<T> {
private T obj;
public GenericHolder() {
}
public GenericHolder(T obj) {
this.obj = obj;
}
public T getObj() {
return obj;
}
public void setObj(T obj) {
this.obj = obj;
}
}
測試類:
public class AppTest extends TestCase {
@Test
public void test() {
//這是一個貼了水果標籤的袋子貼了
GenericHolder<Fruit> fruitHolder = new GenericHolder<Fruit>();
//這是一個貼了蘋果標籤的袋子
GenericHolder<Apple> appHolder = new GenericHolder<Apple>();
//這是一個水果
Fruit fruit = new Fruit("水果");
//這是一個蘋果
Apple apple = new Apple("蘋果");
//現在我們把水果放進去
fruitHolder.setObj(fruit);
//呼叫一下吃水果的方法
eatFruit(fruitHolder);
//貼了水果標籤的袋子放水果當然沒有問題
//現在我們把水果的子類——蘋果放到這個袋子裡看看
fruitHolder.setObj(apple);
//同樣是可以的,其實這時候會發生自動向上轉型,apple向上轉型為Fruit型別後再傳入fruitHolder中
//但不能再將取出來的物件賦值給redApple了,因為袋子的標籤是水果,所以取出來的物件只能賦值給水果類的變數
//無法通過編譯檢測 redApple = fruitHolder.getObj();
//呼叫一下吃水果的方法
eatFruit(fruitHolder);
//放蘋果的標籤,自然只能放蘋果
appHolder.setObj(apple);
//報錯,這時候無法把appHolder傳入eatFruit,因為GenericHolder<Fruit> 和 GenericHolder<Apple>是兩種不同的型別,GenericHolder<Fruit>只允許傳入水果類的袋子
// eatFruit(appHolder);
}
public static void eatFruit(GenericHolder<Fruit> fruitHolder){
System.out.println("我正在吃 " + fruitHolder.getObj().getName());
}
}
執行結果:
我正在吃 水果 我正在吃 蘋果
GenericHolder<Fruit> 和 GenericHolder<Apple>是兩種不同的型別,所以無法通過編譯。
從Java繼承的角度上可以分析:
蘋果 IS-A 水果
裝蘋果的袋子 NOT-IS-A 裝水果的袋子
那麼問題來了,如果我想讓eatFruit方法能同時處理GenericHolder<Fruit> 和 GenericHolder<Apple>兩種型別怎麼辦?而且這也是很合理的需求,畢竟Apple是Fruit的子類,能吃水果,為啥不能吃蘋果???如果要把這個方法過載一次,未免也有些小題大做了。
這個時候,泛型的邊界符就有它的用武之地了。我們先來看效果:
public class AppTest extends TestCase {
@Test
public void test() {
//這是一個貼了水果標籤的袋子
GenericHolder<Fruit> fruitHolder = new GenericHolder<Fruit>();
//這是一個貼了蘋果標籤的袋子
GenericHolder<Apple> appHolder = new GenericHolder<Apple>();
//這是一個水果
Fruit fruit = new Fruit("水果");
//這是一個蘋果
Apple apple = new Apple("蘋果");
//現在我們把水果放進去
fruitHolder.setObj(fruit);
//呼叫一下吃水果的方法
eatFruit(fruitHolder);
//放蘋果的標籤,自然只能放蘋果
appHolder.setObj(apple);
// 這時候可以順利把appHolder 傳入eatFruit
eatFruit(appHolder);
//這種泛型 ? extends Fruit不能存放Fruit,但是可以使用 ? extends Fruit的變數指向GenericHolder<Fruit>的物件
GenericHolder<? extends Fruit> fruitHolder22 = fruitHolder;
// fruitHolder22.setObj(fruit);//報錯,不能存放fruit
System.out.println(fruitHolder22.getObj().getName()+"----");
//這種泛型 ? extends Fruit不能存放apple,但是可以使用 ? extends Fruit的變數指向GenericHolder<apple>的物件
fruitHolder22 = appHolder;
// fruitHolder22.setObj(apple);//報錯不能存放apple
System.out.println(fruitHolder22.getObj().getName()+"^^^^");
}
public static void eatFruit(GenericHolder<? extends Fruit> fruitHolder){
System.out.println("我正在吃 " + fruitHolder.getObj().getName());
Fruit fruit1 = new Fruit("水果1");
//報錯,不能存入fruit1 。因為引入傳入的引數fruitHolder是Fruit的子類。
//Error:(35, 28) java: 不相容的型別: Fruit無法轉換為capture#1, 共 ? extends Fruit
//fruitHolder.setObj(fruit1);
}
}
執行結果:
我正在吃 水果
我正在吃 蘋果
水果----
蘋果^^^^
這就是泛型的邊界符,用<? extends Fruit>的形式表示。邊界符的意思,自然就是定義一個邊界,這裡用?表示傳入的泛型型別不是固定型別,而是符合規則範圍的所有型別,用extends關鍵字定義了一個上邊界。
有上邊界,自然有下邊界,泛型裡使用形如<? super Fruit>的方式使用下邊界。
這兩種方式基本上解決了我們之前的問題,但是同時,也有一定的限制(PECS原則)。
1.上界<? extends T>不能往裡存,只能往外取
不要太疑惑,其實很好理解,因為編譯器只知道容器裡的是Fruit或者Fruit的子類,但不知道它具體是什麼型別,所以存的時候,無法判斷是否要存入的資料的型別與容器種的型別一致,所以會拒絕set操作。注意:取出來的是實際的型別,例如上面指向的是GenericHolder<Fruit>,那麼取出的就是Fruit,如果指向的是GenericHolder<Apple>,那麼取出的就是Apple。
2.下界<? super T>往外取只能賦值給Object變數,不影響往裡存
因為編譯器只知道它是Fruit或者它的父類,這樣實際上是放鬆了型別限制,Fruit的父類一直到Object型別的物件都可以往裡存,但是取的時候,就只能當成Object物件使用了。
3.如果既要存又要取,那麼就不要使用任何萬用字元
所以如果需要經常往外讀,則使用<? extends T>,如果需要經常往裡存,則使用<? super T>。
如何閱讀過一些Java集合類的原始碼,可以發現通常我們會將兩者結合起來一起用,比如像下面這樣:
public class Collections {
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
for (int i=0; i<src.size(); i++)
dest.set(i, src.get(i));
}
}
這種就我看來,應該是相當於:
public class Collections {
public static <T> void copy(List<T> dest, List<T> src) {
for (int i=0; i<src.size(); i++)
dest.set(i, src.get(i));
}
}