Java核心技术卷I:基础知识(原书第8版):12.8 通配符类型

铁文整理

12.8 通配符类型

    固定的泛型类型系统使用起来并没有那么令人愉快,类型系统的研究人员知道这一点已经有一段时间了。Java的设计者发明了一种巧妙的(仍然是安全的)“解决方案”:通配符类型。例如,通配符类型

        Pair<? extends Employee>

    表示任何泛型Pair类型,它的类型参数是EmPloyee的子类,如Pair<Manager>,但不是Pair<String>

    假设要编写一个打印雇员对的方法,像这样:

      public static void printBuddies(Pair<Employee> p) {

           Employee first = p.getFirst();

           Employee second = p.getSecond();

           System.out.println(first.getName() + " and " + second.getName() + " are buddies.");

      }

    正如前面讲到的,不能将Pair<Manager>传递给这个方法,这一点很受限制。解决的方法很简单:使用通配符类型:

    public static void printBuddies(Pair<? extends Employee> p)

    类型Pair<Manager>是Pair<? extends Employee>的子类型(参看图12-3)

    使用通配符会通过Pair<? extends Employee>的引用破坏Pair<Manager>吗?

        Pair<Manager> managerBuddies = new Pair<Manager>(ceo, cfo);

        Pair<? extends Employee> wildcardBuddies = managerBuddies; // OK

        wildcardBuddies.setFirst(lowlyEmployee); // compile-time error

    这可能不会引起破坏。对setFirst的调用有一个类型错误。要了解其中的缘由,请仔细看一看类型Pair<? extends Employee>。其方法似乎是这样的:

    ? extends Employee getFirst()

    void setFirst(? extends Employee)

    这样将不可能调用setFirst方法。编译器只知道需要某个Employee的子类型,但不知道具体是什么类型。它拒绝传递任何特定的类型。毕竟,?不能用来匹配。

    使用getFirst就不存在这个问题:将getFirst的返回值赋给一个Employee的引用完全合法。

    这就是引入有限定的通配符的关键之处。现在己经有办法区分安全的访问器方法和不安全的更改器方法了。

12.8.1 通配符的超类型限定

    通配符限定与类型变量限定十分类似,但是,还有一个附加的能力,即可以指定一个超类型限定(supertype bound),如下所示:

    ? super Manager

    这个通配符限制为Manager的所有超类型。(已有的super关键字十分准确地描述了这种联系,这一点十分令人感到欣慰。)

    为什么要这样做呢?带有超类型限定的通配符的行为与前面介绍的相反。可以为方法提供参数,但不能使用返回值。例如,Pair<? super Manager>有方法

    void setFirst(? super Manager)

    ? super Manager getFirst()

    编译器不知道setFirst方法的确切类型,但是可以用任意Manager对象(或子类型,例如,Executive)调用它,而不能用Employee对象调用。然而,如果调用getFirst,返回的对象类型就不会得到保证。只能把它赋给一个Object

    下面是一个典型的示例。有一个经理的数组,并且想把奖金最高和最低的经理放在一个Pair对象中。Pair的类型是什么?在这里,Pair<Employee>是合理的,Pair<Object>也是合理的(如图12-4所示)。下面的方法将可以接受任何适当的Pair

    public static void minmaxBonus(Manager[] a, Pair<? super Manager> result) {

        if (a == null || a.length == 0)

            return;

        Manager min = a[0];

        Manager max = a[0];

        for (int i = 0; i < a.length; i++) {

            if (min.getBonus() > a[i].getBonus())

                min = a[i];

            if (max.getBonus() < a[i].getBonus())

                max = a[i];

        }

        result.setFirst(min);

        result.setSecond(max);

    }

    直观地讲,带有超类型限定的通配符可以向泛型对象写入,带有子类型限定的通配符可以从泛型对象读取。

    下面是超类型限定的另一种应用。Comparable接口本身就是一个泛型类型。声明如下:public interface Comparable<T> {

    public int compareTo(T other);

}

    在此,类型变量指示了other参数的类型。例如,String类实现Comparable<String>,它的compareTo方法被声明为public int compareTo(String other);

    很好,显式的参数有一个正确的类型。在了Java SE 5.0之前,other是一个Object,并且这个方法的实现需要强制类型转换。

    由于Comparable是一个泛型类型,也许可以把ArrayAlg类的min方法做得更好一些?可以这样声明:

    public static <T extends Comparable<T>> T min(T[] a);

    看起来,这样写比只使用T extends Comparable更彻底,并且对许多类来讲,工作得更好。例如,如果计算一个String数组的最小值,T就是String类型的,而StringComparable<String>的子类型。但是,当处理一个GregorianCalendar对象的数组时,就会出现问题。GregorianCalendarCalendar的子类,并且Calendar实现了Comparable<Calendar>。因此GregorianCalendar实现的是Comparable<Calendar>,而不是Comparable<GregorianCalendar>

    在这种情况下,超类型可以用来进行救助:

    public static <T extends Comparable<? super T>> T min(T[] a);

    现在compareTo方法写成

    public int compareTo(? super T);

    有可能被声明为使用类型T的对象,也有可能使用T的超类型(例如,当TGregorianCalendar)。无论如何,传递一个T类型的对象给compareTo方法都是安全的。

    对于初学者来说,<T extends Comparable<? super T>>这样的声明看起来有点吓人。很遗憾,因为这一声明的意图在于帮助应用程序员排除调用参数上的不必要的限制。对泛型没有兴趣的应用程序员很可能很快就学会掩盖这些声明,想当然地认为库程序员做的都是正确的。如果是一名库程序员,一定要习惯于通配符,否则,就会受到用户的责备,还要在代码中随意地添加强制类型转换直至代码可以编译。

12.8.2 无限定通配符

    还可以使用无限定的通配符,例如,Pair<?>。初看起来,这好像与原始的Pair类型一样。实际上,有很大的不同。类型Pair<?>有方法如:

    ? getFirst() void setFirst(?)

    getFirst的返回值只能赋给一个ObjectsetFirst方法不能被调用,甚至不能用Object调用。Pair<?>Pair本质的不同在于:可以用任意Object对象调用原始的Pair类的setObject方法。

    为什么要使用这样脆弱的类型?它对于许多简单的操作非常有用。例如,下面这个方法将用来测试一个pair是否包含了指定的对象,它不需要实际的类型。

    public static boolean hasNulls(Pair<?> p) {

        return p.getFirst() == null || p.getSecond() == null;

    }

    通过将contains转换成泛型方法,可以避免使用通配符类型:

public static< T> boolean hasNulls(Pair<T> p)

    但是,带有通配符的版本可读性更强。

12.8.3 通配符捕获

    编写一个交换pair元素的方法;

    public static void swap(Pair<?> p)

    通配符不是类型变量,因此,不能在编写代码中使用“?”作为一种类型。也就是说,下述代码是非法的:

        ? t = p.getFirst(); // ERROR

        p.setFirst(p.getSecond());

        p.setSecond(t);

    这是一个问题,因为在交换的时候必须临时保存第一个元素。幸运的是,这个问题有一个有趣的解决方案,我们可以写一个辅助方法swapHelper,如下所示:

    public static <T> void swapHelper(Pair<T> p) {

        T t = p.getFirst();

        p.setFirst(p.getSecond());

        p.setSecond(t);

    }

    注意,swapHelper是一个泛型方法,而swap不是,它具有固定的Pair<?>类型的参数。

    现在可以由swap调用swapHelper

    public static void swap(Pair<?> p) {

        swapHelper(p);

    }

    在这种情况下,swapHelper方法的参数T捕获通配符。它不知道是哪种类型的通配符,但是,这是一个明确的类型,并且<T>swapHelper的定义只有在T指出类型时才有明确的含义。

    当然,在这种情况下,并不是一定要使用通配符。我们已经直接实现了没有通配符的泛型方法<T> void swap(Pair<T> p)。然而,下面看一个通配符类型出现在计算中间的示例:

    public static void maxminBonus(Manager[] a, Pair<? super Manager> resu1t) {

        maxminBonus(a, result);

        PairAlg.swapHelper(result); // OK-swapHelper captures wildcard type

    }

    在这里,通配符捕获机制是不可避免的。

    通配符捕获只有在有许多限制的情况下才是合法的。编译器必须能够确信通配符表达的是单个、确定的类型。例如,ArrayList<Pair<T>>中的T永远不能捕获ArrayList<Pair<?>>中的通配符。数组列表可以保存两个Pair<?>,分别针对?的不同类型。

    12-3中的测试程序将前几节讨论的各种方法综合在一起,读者从中可以看到它们彼此之间的关联。

12-3 PairTest3.java

import java.util.*;

 

/**

 * @version 1.00 2004-05-10

 * @author Cay Horstmann

 */

public class PairTest3 {

    public static void main(String[] args) {

        Manager ceo = new Manager("Gus Greedy", 800000, 2003, 12, 15);

        Manager cfo = new Manager("Sid Sneaky", 600000, 2003, 12, 15);

        Pair<Manager> buddies = new Pair<Manager>(ceo, cfo);

        printBuddies(buddies);

 

        ceo.setBonus(1000000);

        cfo.setBonus(500000);

        Manager[] managers = { ceo, cfo };

 

        Pair<Employee> result = new Pair<Employee>();

        minmaxBonus(managers, result);

        System.out.println("first: " + result.getFirst().getName()

                + ", second: " + result.getSecond().getName());

        maxminBonus(managers, result);

        System.out.println("first: " + result.getFirst().getName()

                + ", second: " + result.getSecond().getName());

    }

 

    public static void printBuddies(Pair<? extends Employee> p) {

        Employee first = p.getFirst();

        Employee second = p.getSecond();

        System.out.println(first.getName() + " and " + second.getName()

                + " are buddies.");

    }

 

    public static void minmaxBonus(Manager[] a, Pair<? super Manager> result) {

        if (a == null || a.length == 0)

            return;

        Manager min = a[0];

        Manager max = a[0];

        for (int i = 1; i < a.length; i++) {

            if (min.getBonus() > a[i].getBonus())

                min = a[i];

            if (max.getBonus() < a[i].getBonus())

                max = a[i];

        }

        result.setFirst(min);

        result.setSecond(max);

    }

 

    public static void maxminBonus(Manager[] a, Pair<? super Manager> result) {

        minmaxBonus(a, result);

        PairAlg.swapHelper(result); // OK--swapHelper captures wildcard type

    }

}

 

class PairAlg {

    public static boolean hasNulls(Pair<?> p) {

        return p.getFirst() == null || p.getSecond() == null;

    }

 

    public static void swap(Pair<?> p) {

        swapHelper(p);

    }

 

    public static <T> void swapHelper(Pair<T> p) {

        T t = p.getFirst();

        p.setFirst(p.getSecond());

        p.setSecond(t);

    }

}

 

class Employee {

    public Employee(String n, double s, int year, int month, int day) {

        name = n;

        salary = s;

        GregorianCalendar calendar = new GregorianCalendar(year, month - 1, day);

        hireDay = calendar.getTime();

    }

 

    public String getName() {

        return name;

    }

 

    public double getSalary() {

        return salary;

    }

 

    public Date getHireDay() {

        return hireDay;

    }

 

    public void raiseSalary(double byPercent) {

        double raise = salary * byPercent / 100;

        salary += raise;

    }

 

    private String name;

    private double salary;

    private Date hireDay;

}

 

class Manager extends Employee {

    /**

     * @param n

     *            the employee's name

     * @param s

     *            the salary

     * @param year

     *            the hire year

     * @param month

     *            the hire month

     * @param day

     *            the hire day

     */

    public Manager(String n, double s, int year, int month, int day) {

        super(n, s, year, month, day);

        bonus = 0;

    }

 

    public double getSalary() {

        double baseSalary = super.getSalary();

        return baseSalary + bonus;

    }

 

    public void setBonus(double b) {

        bonus = b;

    }

 

    public double getBonus() {

        return bonus;

    }

 

    private double bonus;

}


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值