1. 程式人生 > >JAVA基礎知識|泛型

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));
    }
}