1. 程式人生 > 實用技巧 >【Java學習筆記六】——一文教你入門泛型

【Java學習筆記六】——一文教你入門泛型

宣告:本文章內容主要摘選自尚矽谷宋紅康Java教程、《Java核心卷一》、廖雪峰Java教程,示例程式碼部分出自本人,更多詳細內容推薦直接觀看以上教程及書籍,若有錯誤之處請指出,歡迎交流。

一、簡單定義泛型類

1.必要性

  • 在Java中增加泛型類之前,泛型程式設計是用繼承實現的。ArrayList類只維護一個Object引用的陣列:
public class ArragList
{
      public Object[] elementData;
      ...
      public object get(int i){};
      public void add(Object o){};
}
  • 這種方法有兩個問題。當獲取一個值時必須進行強制型別轉換。

    Arraylist files=new Arraylist o;
    String filenane=(String)files.get(o);
    
  • 此外,這裡沒有錯誤檢查。可以向陣列列表中新增任何類的物件。

  • files.add(new File("..."));對於這個呼叫,編譯和執行都不會出錯。然而在其他地方,如果將get的結果強制型別轉換為String型別,就會產生一個錯誤。

  • 泛型提供了一個更好的解決方案:型別引數(type parameters)。ArrayList類有一個型別引數用來指示元素的型別:

    Arraylist<String> files = new Arraylist<String>;
    
  • 這使得程式碼具有更好的可讀性和安全性。人們一看就知道這個陣列列表中包含的是String物件。

2.簡單定義泛型類

一個泛型類(generic class)就是具有一個或多個型別變數的類。我們使用一個簡單的Order類作為例子。對於這個類來說,我們只關注泛型,而不會為資料儲存的細節煩惱。

class Order<T> {

    String orderName;
    int orderId;

    T orderT;

    public Order(){};

    public Order(String orderName, int orderId, T orderT) {
        this.orderName = orderName;
        this.orderId = orderId;
        this.orderT = orderT;
    }

    public T getOrderT() {
        return orderT;
    }
    public void setOrderT(T orderT){
        this.orderT = orderT;
    }
}

//使用泛型後,我們可以任意定義Order類的型別
Order<String> o1 = new Order<>();
Order<Integer> o2 = new Order<>();

//如果定義了泛型類,例項化沒有指明類的泛型,則認為此泛型型別為0bject型別
Order o3 = new Order();

二、使用泛型

1.使用泛型

使用ArrayList時,如果不定義泛型型別時,泛型型別實際上就是Object:

// 編譯器警告:
List list = new ArrayList();
list.add("Hello");
list.add("World");
String first = (String) list.get(0);
String second = (String) list.get(1);

此時,只能把<T>當作Object使用,沒有發揮泛型的優勢。
當我們定義泛型型別<String>後,List<T>的泛型介面變為強型別List<String>

// 無編譯器警告:
List<String> list = new ArrayList<String>();
list.add("Hello");
list.add("World");
// 無強制轉型:
String first = list.get(0);
String second = list.get(1);

當我們定義泛型型別<Number>後,List<T>的泛型介面變為強型別List<Number>

List<Number> list = new ArrayList<Number>();
list.add(new Integer(123));
list.add(new Double(12.34));
Number first = list.get(0);
Number second = list.get(1);

編譯器看到泛型型別List<Number>就可以自動推斷出後面的ArrayList<T>的泛型型別必須是ArrayList<Number>,因此,可以把程式碼簡寫為:

  List<Number> list = new ArrayList<>();

2.泛型介面

除了ArrayList使用了泛型,還可以在介面中使用泛型。例如,Arrays.sort(Object[])可以對任意陣列進行排序,但待排序的元素必須實現Comparable<T>這個泛型介面:

我們可以直接對String陣列進行排序:

String[] ss=new String[]{"orange","Apple","Pear"}; 
Arrays.sort(ss);
System.out.println(Arrays.toString(ss));

這是因為String本身已經實現了Comparable<String>介面。如果換成我們自定義的Person型別則需要讓Person實現Comparable<T>介面:

class Person implements Comparable<Person>{
    private String name;
    private int age;
    private double salary;

    public Person(String name, int age, double salary) {
        this.name = name;
        this.age = age;
        this.salary = salary;
    }

    public int compareTo(Person other)
    {
        return this.name.compareTo(other.name);//按照姓名從小到大排列,若加負號則為從大到小;若將name改變成其他屬性如age即可改變排序依據
     }
    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", salary=" + salary +
                '}' + '\n';
    }
}
@Test      //單元測試方法
    public void test1(){
        Person[] ps=new Person[]{
                new Person("Bob",61,1000),
                new Person("Alice",88,3000),
                new Person("Lily",75,2000),
        };
        Arrays. sort(ps);
        System. out. println(Arrays. toString(ps));
    }

拓展:除了Comparable<T>介面,還有Comparator介面

注:Comparator介面的使用:定製排序
1.背景:
當元素的型別沒有實現Java.Lang.Comparable介面而又不方便修改程式碼,或者實現了java.Lang.Comparable介面的排序規定不適合當前的操作,麼可以考感使用Comparator 的物件來排序.
2.重寫compare(Object o1,Object o2)方法,比較o1和o2的大小:
如果方法返回正整數,則表示o1大於o2;如果返回0,表示相等;返回負整數,表示o1小於o2。

public void test() {
        String[] str = {"AA", "II", "GG", "CC", "EE"};
        Arrays.sort(str, new Comparator() {
            @Override
            public int compare(Object o1, Object o2){
                if(o1 instanceof String && o2 instanceof String){
                    String s1 = (String)o1;
                    String s2 = (String)o2;
                    return -s1.compareTo(s2);//如果不加負號就是從小到大
                }
                throw new RuntimeException("輸入的資料型別不一致");
            }
        });
        System.out.println(Arrays.toString(str));
    }

三、泛型繼承

一個類可以繼承自一個泛型類。例如:父類的型別是Pair,子類的型別是IntPair,可以這麼繼承:

  public class IntPair extends Pair<Integer> {
  }

使用的時候,因為子類IntPair並沒有泛型型別,所以,正常使用即可:

  IntPair ip = new IntPair(1, 2);

四、萬用字元

1.extends萬用字元

我們前面已經講到了泛型的繼承關係:Pair不是Pair的子類。
假設我們定義了Pair

  public class Pair<T> { ... }

然後,我們又針對Pair型別寫了一個靜態方法,它接收的引數型別是Pair

public class PairHelper {
    static int add(Pair<Number> p) {
        Number first = p.getFirst();
        Number last = p.getLast();
        return first.intValue() + last.intValue();
    }
}

上述程式碼是可以正常編譯的。使用的時候,我們傳入:

  int sum = PairHelper.add(new Pair<Number>(1, 2));

注意:傳入的型別是Pair,實際引數型別是(Integer, Integer)。

既然實際引數是Integer型別,試試傳入Pair

public static void main(String[] args){
      Pair<Integer>p=new Pair<>(123,456); 
      int n=add(p); 
      System.out.println(n); 
}
static int add(Pair<Number>p){
      Number first=p.getFirst(); 
      Number last=p.getLast(); 
      return first.intValue()+last.intValue();
}
/*
直接執行,會得到一個編譯錯誤:
incompatible types: Pair<Integer> cannot be converted to Pair<Number>
原因很明顯,因為Pair<Integer>不是Pair<Number>的子類,因此,add(Pair<Number>)不接受引數型別Pair<Integer>。
問題在於方法引數型別定死了只能傳入Pair<Number>。

此時使用Pair<? extends Number>使得方法接收所有泛型型別為Number或Number子類的Pair型別。我們把程式碼改寫如下:*/
static int add(Pair<? extends Number>p){
      Number first=p.getFirst(); 
      Number last=p.getLast(); 
      return first.intValue()+last.intValue();
}

這樣一來,給方法傳入Pair型別時,它符合引數Pair<? extends Number>型別。這種使用<? extends Number>的泛型定義稱之為上界萬用字元(Upper Bounds Wildcards),即把泛型型別T的上界限定在Number了。

除了可以傳入Pair<Integer>型別,我們還可以傳入Pair<Double>型別,Pair<BigDecimal>型別等等,因為Double和BigDecimal都是Number的子類。

如果我們考察對Pair<? extends Number>型別呼叫getFirst()方法,實際的方法簽名變成了:

  <? extends Number> getFirst();

即返回值是Number或Number的子類,因此,可以安全賦值給Number型別的變數:

  Number x = p.getFirst();

然後,我們不可預測實際型別就是Integer,例如,下面的程式碼是無法通過編譯的:

  Integer x = p.getFirst();

這是因為實際的返回型別可能是Integer,也可能是Double或者其他型別,編譯器只能確定型別一定是Number的子類(包括Number型別本身),但具體型別無法確定。

2.super萬用字元

我們前面已經講到了泛型的繼承關係:Pair不是Pair的子類。

考察下面的set方法:

void set(Pair<Integer> p, Integer first, Integer last) {
    p.setFirst(first);
    p.setLast(last);
}

傳入Pair<Integer>是允許的,但是傳入Pair<Number>是不允許的。

和extends萬用字元相反,這次,我們希望接受Pair<Integer>型別,以及Pair<Number>、Pair<Object>,因為Number和Object是Integer的父類,setFirst(Number)和setFirst(Object)實際上允許接受Integer型別。

我們使用super萬用字元來改寫這個方法:

void set(Pair<? super Integer> p, Integer first, Integer last) {
    p.setFirst(first);
    p.setLast(last);
}

注意到Pair<? super Integer>表示,方法引數接受所有泛型型別為Integer或Integer父類的Pair型別。

下面的程式碼可以被正常編譯:

public static void main(String[] args){
      Pair<Number>p1=new Pair<>(12.3,4.56);
      Pair<Integer>p2=new Pair<>(123,456); 
      setSame(p1,100); 
      setSame(p2,200); 
      System. out. println(p1. getFirst()+","+pl. getLast()); 
      System. out. println(p2. getFirst()+","+p2. getLast());
}
static void setsame(Pair<? super Integer>p, Integer n){
      p.setFirst(n); 
      p.setLast(n);
}

考察Pair<? super Integer>的setFirst()方法,它的方法簽名實際上是:

  void setFirst(? super Integer);

因此,可以安全地傳入Integer型別。

再考察Pair<? super Integer>的getFirst()方法,它的方法簽名實際上是:

  ? super Integer getFirst();

這裡注意到我們無法使用Integer型別來接收getFirst()的返回值,即下面的語句將無法通過編譯:

Integer x = p.getFirst();
因為如果傳入的實際型別是Pair<Number>,編譯器無法將Number型別轉型為Integer。

注意:雖然Number是一個抽象類,我們無法直接例項化它。但是,即便Number不是抽象類,這裡仍然無法通過編譯。此外,傳入Pair<Object>型別時,編譯器也無法將Object型別轉型為Integer。

唯一可以接收getFirst()方法返回值的是Object型別:

  Object obj = p.getFirst();

因此,使用<? super Integer>萬用字元表示:

允許呼叫set(? super Integer)方法傳入Integer的引用;

不允許呼叫get()方法獲得Integer的引用。

唯一例外是可以獲取Object的引用:Object o = p.getFirst()。

換句話說,使用<? super Integer>萬用字元作為方法引數,表示方法內部程式碼對於引數只能寫,不能讀

3.無限制萬用字元

我們已經討論了<? extends T>和<? super T>作為方法引數的作用。實際上,Java的泛型還允許使用無限定萬用字元(Unbounded Wildcard Type),即只定義一個?:

  void sample(Pair<?> p) {
  }

因為<?>萬用字元既沒有extends,也沒有super,因此:

  • 不允許呼叫set(T)方法並傳入引用(null除外);
  • 不允許呼叫T get()方法並獲取T引用(只能獲取Object引用)。
  • 萬用字元有一個獨特的特點,就是:Pair是所有Pair的超類

為什麼要使用這樣脆弱的型別?它對於許多簡單的操作非常有用。例如,下面這個方法將用來測試一個pair是否包含一個null引用,它不需要實際的型別。

public static boolean hasNu1ls(Pair<?>p)
{
      return p.getFirstO==null || p.getSecond() == null;
}
//通過將hasNulls轉換成泛型方法,可以避免使用萬用字元型別:
public static<T> boolean hasNulls(Pair<T> p)
//但是,帶有萬用字元的版本可讀性更強

拓展內容

1.對比extends和super萬用字元

我們再回顧一下extends萬用字元。作為方法引數,<? extends T>型別和<? super T>型別的區別在於:

<? extends T>允許呼叫讀方法T get()獲取T的引用,但不允許呼叫寫方法set(T)傳入T的引用(傳入null除外);

<? super T>允許呼叫寫方法set(T)傳入T的引用,但不允許呼叫讀方法T get()獲取T的引用(獲取Object除外)。

一個是允許讀不允許寫,另一個是允許寫不允許讀。

先記住上面的結論,我們來看Java標準庫的Collections類定義的copy()方法:

public class Collections {
    // 把src的每個元素複製到dest中:
    public static <T> void copy(List<? super T> dest, List<? extends T> src) {
        for (int i=0; i<src.size(); i++) {
            T t = src.get(i);
            dest.add(t);
        }
    }
}

它的作用是把一個List的每個元素依次新增到另一個List中。它的第一個引數是List<? super T>,表示目標List,第二個引數List<? extends T>,表示要複製的List。我們可以簡單地用for迴圈實現複製。在for迴圈中,我們可以看到,對於型別<? extends T>的變數src,我們可以安全地獲取型別T的引用,而對於型別<? super T>的變數dest,我們可以安全地傳入T的引用。

這個copy()方法的定義就完美地展示了extends和super的意圖:

copy()方法內部不會讀取dest,因為不能呼叫dest.get()來獲取T的引用;

copy()方法內部也不會修改src,因為不能呼叫src.add(T)。

這是由編譯器檢查來實現的。如果在方法程式碼中意外修改了src,或者意外讀取了dest,就會導致一個編譯錯誤:

public class Collections {
    // 把src的每個元素複製到dest中:
    public static <T> void copy(List<? super T> dest, List<? extends T> src) {
        ...
        T t = dest.get(0); // compile error!
        src.add(t); // compile error!
    }
}
//這個copy()方法的另一個好處是可以安全地把一個List<Integer>新增到List<Number>,但是無法反過來新增:

// copy List<Integer> to List<Number> ok:
List<Number> numList = ...;
List<Integer> intList = ...;
Collections.copy(numList, intList);
// ERROR: cannot copy List<Number> to List<Integer>:
Collections.copy(intList, numList);
//而這些都是通過super和extends萬用字元,並由編譯器強制檢查來實現的。

2.PECS原則

何時使用extends,何時使用super?為了便於記憶,我們可以用PECS原則:Producer Extends Consumer Super

即:如果需要返回T,它是生產者(Producer),要使用extends萬用字元;如果需要寫入T,它是消費者(Consumer),要使用super萬用字元。

還是以Collections的copy()方法為例:

public class Collections {
    public static <T> void copy(List<? super T> dest, List<? extends T> src) {
        for (int i=0; i<src.size(); i++) {
            T t = src.get(i); // src是producer
            dest.add(t); // dest是consumer
        }
    }
}

需要返回T的src是生產者,因此宣告為List<? extends T>,需要寫入T的dest是消費者,因此宣告為List<? super T>。

此筆記僅針對有一定程式設計基礎的同學,且本人只記錄比較重要的知識點,若想要入門Java可以先行觀看相關教程或書籍後再閱讀此筆記。

最後附一下相關連結:
Java線上API中文手冊
Java platform se8下載
尚矽谷Java教學視訊
《Java核心卷一》