【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
我們可以直接對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
public class IntPair extends Pair<Integer> {
}
使用的時候,因為子類IntPair並沒有泛型型別,所以,正常使用即可:
IntPair ip = new IntPair(1, 2);
四、萬用字元
1.extends萬用字元
我們前面已經講到了泛型的繼承關係:Pair
假設我們定義了Pair
public class Pair<T> { ... }
然後,我們又針對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型別,試試傳入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<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
考察下面的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核心卷一》