1. 程式人生 > >Java-泛型程式設計-使用萬用字元? extends 和 ? super

Java-泛型程式設計-使用萬用字元? extends 和 ? super

泛型中使用萬用字元有兩種形式:子型別限定<? extends xxx>和超型別限定<? super xxx>。

(1)子型別限定

下面的程式碼定義了一個Pair<T>類,以及Employee,Manager和President類。

public class Pair<T> {
    private T first;
    private T second;

    public Pair(T first, T second) {
        this.first = first;
        this.second = second;
    }

    public T getFirst() {
        return first;
    }

    public T getSecond() {
        return second;
    }

    public void setFirst(T newValue) {
        first = newValue;
    }

    public void setSecond(T newValue) {
        second = newValue;
    }
}

class Employee {
    private String name;
    private double salary;
    
    public Employee(String n, double s) {
        name = n;
        salary = s;
    }
    
    public String getName() {
        return name;
    }

    public double getSalary() {
        return salary;
    }
}

class Manager extends Employee {
    public Manager(String n, double s) {
        super(n, s);
    }
}
<pre name="code" class="java">
class President extends Manager {
    public President(String n, double s) {
        super(n, s);
    }
}
現在要定義一個函式可以列印Pair<Employee>
    public static void printEmployeeBoddies(Pair<Employee> pair) {
        System.out.println(pair.getFirst().getName() + ":" + pair.getSecond().getName());
    }
可是有一個問題是這個函式輸入引數只能傳遞型別Pair<Employee>,而不能傳遞Pair<Manager>和Pair<President>。例如下面的程式碼會產生編譯錯誤
        Manager mgr1 = new Manager("Jack", 10000.99);
        Manager mgr2 = new Manager("Tom", 10001.01);
        Pair<Manager> managerPair = new Pair<Manager>(mgr1, mgr2);
        PairAlg.printEmployeeBoddies(managerPair);

之所以會產生編譯錯誤,是因為Pair<Employee>和Pair<Manager>實際上是兩種型別。

由上圖可以看出,型別Pair<Manager>是型別Pair<? extends Employee>的子型別,所以為了解決這個問題可以把函式定義改成
public static void printEmployeeBoddies(Pair<? extends Employee> pair)

但是使用萬用字元會不會導致通過Pair<? extends Employee>的引用破壞Pair<Manager>物件呢?例如:
Pair<? extends Employee> employeePair = managePair;employeePair.setFirst(new Employee("Tony", 100));
不用擔心,編譯器會產生一個編譯錯誤。Pair<? extends Employee>引數替換後,我們得到如下程式碼
? extends Employee getFirst()
void setFirst(? extends Employee)
對於get方法,沒問題,因為編譯器知道可以把返回物件轉換為一個Employee型別。但是對於set方法,編譯器無法知道具體的型別,所以會拒絕這個呼叫。

(2)超型別限定

超型別限定和子型別限定相反,可以給方法提供引數,但是不能使用返回值。? super Manager這個型別限定為Manager的所有超類。

Pair<? super Manager>引數替換後,得到如下方法

? super Manager getFirst()
void setFirst(? super Manager)

編譯器可以用Manager的超型別,例如Employee,Object來呼叫setFirst方法,但是無法呼叫getFirst,因為不能把Manager的超類引用轉換為Manager引用。

超型別限定的存在是為了解決下面一類的問題。例如要寫一個函式從Manager[]中找出工資最高和最低的兩個,放在一個Pair中返回。

    public static void minMaxSal(Manager[] mgrs, Pair<? super Manager> pair) {
        if (mgrs == null || mgrs.length == 0) {
            return;
        }
        
        pair.setFirst(mgrs[0]);
        pair.setSecond(mgrs[0]);
        //TODO
    }
如此就可以這樣呼叫
        Pair<? super Manager> pair = new Pair<Employee>(null, null);
        minMaxSal(new Manager[] {mgr1, mgr2}, pair);

(3) <T extends Comparable<? super T>>

超型別限定還有一個使用形式: <T extends Comparable<? super T>>

比如要寫一個函式返回陣列中的最小物件。似乎可以如下定義。
public static <T> T min(T[] a)
但是要找到最小值必須進行比較,這樣就需要T實現了Comparable介面。為此可以把方法定義改變成
public static <T extends Comparable> T min(T[] a)
因為Comparable介面本身也是泛型的,所以最終可以改寫成
public static <T extends Comparable<T>> T min(T[] a)

但是考慮到如下的使用場景

        GregorianCalendar[] birthdays = {
                new GregorianCalendar(1906, Calendar.DECEMBER, 9),
                new GregorianCalendar(1815, Calendar.DECEMBER, 10),
                new GregorianCalendar(1903, Calendar.DECEMBER, 3),
                new GregorianCalendar(1910, Calendar.JUNE, 22),
        };
        
        System.out.println("Min Age = " + ArrayAlg.min(birthdays).getTime());
java.util.GregorianCalendar繼承自java.util.Calendar。GregorianCalendar本身沒有實現Comparable介面,其父類Calendar實現了Comparable介面。
但是編譯器會對ArrayAlg.min(birthdays)報錯,因為GregorianCalendar實現了Comparable<Calendar> 而不是Comparable<GregorianCalendar>。
只要把min方法的申明改成<T extends Comparable<? super T>>就可以通過編譯。
完整的方法如下。
注意,由於一個類可能沒有實現Comparable方法,所以還需要定義另外一個方法,傳入Comparator物件進行比較。
import java.util.Comparator;

public class ArrayAlg {
    public static <T extends Comparable<? super T>> T min(T[] a) {
        if (a == null || a.length == 0) {
            return null;
        }
        T smallest = a[0];
        for (int i = 1; i < a.length; i++) {
            if (smallest.compareTo(a[i]) > 0) {
                smallest = a[i];
            }
        }
        return smallest;
    }
            
    public static <T> T min(T[] a, Comparator<? super T> c) {
        if (a == null || a.length == 0) {
            return null;
        }
        T smallest = a[0];
        for (int i = 1; i < a.length; i++) {
            if (c.compare(smallest, a[i]) > 0) {
                smallest = a[i];
            }
        }
        return smallest;
    }
}

傳入Comparator作為引數的min方法的呼叫例子。
        Manager[] managers = new Manager[] {
                new Manager("Jack", 10000.99),
                new Manager("Tom", 10001.01),
        };
        
        Manager minSalMgr = ArrayAlg.min(managers, new Comparator<Employee>() {
            @Override
            public int compare(Employee e1, Employee e2) {
                return Double.compare(e1.getSalary(), e2.getSalary());
            }            
        });
        System.out.println("Min. Salary = " + minSalMgr.getName());

在Java的類庫中,大量使用了這種型別的引數。例如在java.util.Collections類中就定義了下面兩個方法
public static <T extends Comparable<? super T>> void sort(List<T> list)
public static <T> void sort(List<T> list, Comparator<? super T> c) 

(4)無限定萬用字元?

無限定萬用字元表示不需要限定任何型別。例如Pair<?>。
引數替換後的Pair類有如下方法
? getFirst()
void setFirst(?)
所以可以呼叫getFirst方法,因為編譯器可以把返回值轉換為Object。
但是不能呼叫setFirst方法,因為編譯器無法確定引數型別,這就是Pair<?>和Pair方法的根本不同。
無限定萬用字元的出現是為了支援如下的函式定義。
public class PairAlg {    
    public static boolean hasNulls(Pair<?> p) {
        return p.getFirst() == null || p.getSecond() == null;
    }
}