Java编程思想__泛型(五)

边界

  • 边界使得你可以在用于泛型的参数类型上设置限制条件。
  • 尽管这使得你可以强制规定泛型可以应用的类型,但是其潜在的一个重要的效果是你可以按照自己的边界类型来调用方法。
  • 因为擦除移除了类型信息,所以,可以用无边界泛型参数调用的方法只是那些可以用 Object调用的方法。但是如果能够将这个参数限制为某个类型子集,那么你就可以用这些类型子集来调用方法。
  • 为了执行这种限制,Java 泛型重用了 extends 关键字。对你来说有一点很重要,既要理解 extends 关键字在泛型边界上下文环境中和在普通情况下所具有的意义是完全不同的。
public interface HasColor {

   public void getColor();
}

class Colored{
    T t;
    Colored(T t){
        this.t=t;
    }
    T getT(){
        return t;
    }
    //界限允许您调用方法
    void color(){
        t.getColor();
    }
}
  1. 这只是单一界限,如果多重限制的话,这样写是行不通的, extends 后面第一个必须是类,然后在是接口。如下
class Dimension{
    public int x,y,z;
}

class ColoredDimension{
    T t;

    public ColoredDimension(T t) {
        this.t = t;
    }

    public T getT() {
        return t;
    }
    void color(){
        t.getColor();
    }

    int getX(){
        return t.x;
    }
    int getY(){
        return t.y;
    }

    int getZ(){
        return t.z;
    }
}
  1. 界限的 extends 后面是否可以跟多个 类(class),这样做是不可以的。与继承一样,您只能拥有一个具体的类,但是可以有多个接口。
interface Weight{
    int weight();
}

class Solid{
    T t;

    public Solid(T t) {
        this.t = t;
    }
    public T getT() {
        return t;
    }
    void color(){
        t.getColor();
    }
    int getX(){
        return t.x;
    }
    int getY(){
        return t.y;
    }
    int getZ(){
        return t.z;
    }
    int weight(){
        return t.weight();
    }
}
  1. 接下来我们创建测试一下创建一个 即继承某个类,又实现多个接口的类。
class Bounded extends Dimension implements HasColor,Weight{
    @Override
    public void getColor() {
        System.out.println("Bounded getColor method");
    }
    @Override
    public int weight() {
        return 0;
    }
}
class BasicBounds{
    public static void main(String[] args) {
        Solid solid=new Solid<>(new Bounded());
        solid.color();
        solid.getX();
        solid.weight();
    }
}
  1. 你可能已经观察到了, BasicBounds.java 看上去包含可以通过继承可以消除冗余。下面,可以看到如何在继承的每个层次上添加边界限制。
public class HoldItem {
    T t;

    public HoldItem(T t) {
        this.t = t;
    }
    T getT(){
        return t;
    }
}

class Colored2  extends HoldItem{

    public Colored2(T t) {
        super(t);
    }
    void color(){
        t.getColor();
    }
}


class ColoredDimesion2  extends Colored2{

    public ColoredDimesion2(T t) {
        super(t);
    }
    int getX(){
        return t.x;
    }
    int getY(){
        return t.y;
    }
    int getZ(){
        return t.z;
    }
}

class Solid2  extends ColoredDimesion2{
    public Solid2(T t) {
        super(t);
    }
    int weight(){
        return t.weight();
    }
}

class InheritBounds{
    public static void main(String[] args) {
        Solid2 solid2=new Solid2<>(new Bounded());
        solid2.color();
        solid2.getX();
        solid2.weight();
    }
}
  1. HoldItem 直接持有一个对象,因此这种行为被继承到了 Colored2 中,它也要求其参数与 HoldColor 一致。ColoredDimension2 和 Solid2 进一步扩展了这个层次结构,并且每个层次上都添加了边界。现在这些方法被继承,因而不必在每个类中重复。
public interface SuperPower {}

interface XRayVision extends SuperPower{
    void seeThroughWalls();
}

interface SuperHearing extends SuperPower{
    void hearThroughNoises();
}

interface SuperSmell extends SuperPower{
    void trackBySmell();
}

class SuperHerp{
    T t;

    public SuperHerp(T t) {
        this.t = t;
    }

    public T getT() {
        return t;
    }
}

class SuperSleuth extends SuperHerp {

    public SuperSleuth(T t) {
        super(t);
    }
    void see(){
        t.seeThroughWalls();
    }
}

class CanineHero extends SuperHerp{

    public CanineHero(T t) {
        super(t);
    }
    void hear(){
        t.hearThroughNoises();
    }
    void smell(){
        t.trackBySmell();
    }
}

class SuperHearSmell implements SuperHearing,SuperSmell{

    @Override
    public void hearThroughNoises() {}
    @Override
    public void trackBySmell() {}
}

class DogBoy extends CanineHero{

    public DogBoy() {
        super(new SuperHearSmell());
    }
}

class EpicBattle{
    //bounds in generic methods 泛型方法的界限
    static  void useSuperHearing(SuperHerp tSuperHerp){
        tSuperHerp.getT().hearThroughNoises();
    }

    static   void superFind(SuperHerp superHerp){
        superHerp.getT().hearThroughNoises();
        superHerp.getT().trackBySmell();
    }

    public static void main(String[] args) {
        DogBoy dogBoy=new DogBoy();
        useSuperHearing(dogBoy);
        superFind(dogBoy);

        //you can do this  你可以这样做
        //List audioBoys
        //But you can't do this 但是你做不到
        //List dogBoys

    }
}
  1. 注意,通配符被限制为单一边界。

通配符

  • 我们开始入手的示例要展示数组的一种特殊行为: 可以向导出类型的数组赋予基类型的数组引用。
public class Fruit {
}

class Apple extends Fruit {
}

class Jonathan extends Apple {
}

class Orange extends Fruit {
}

class CovariantArrays {
    public static void main(String[] args) {
        Fruit[] fruits = new Apple[10];
        fruits[0] = new Apple();
        fruits[1] = new Jonathan();
        //运行时类型是 apple[] 而不是 Fruit[] 或 Orange
        try {
            //编译器允许您添加 Fruit
            //ArrayStoreException 异常
            fruits[2] = new Fruit();
        } catch (Exception e) {
            System.err.println(e);
        }
        try {
            //编译器不允许你添加 Orange
            //ArrayStoreException 异常
            fruits[3] = new Orange();
        }catch (Exception e){
            System.err.println(e);
        }
    }
}

//运行结果为
java.lang.ArrayStoreException: generic.Fruit
java.lang.ArrayStoreException: generic.Orange
  1. main() 中的第一行创建了一个Apple 数组,并将其赋值给了一个 Fruit 数组的引用。这是有意义的,因为 Apple 也是一种 Fruit ,因此Apple 数组应该也是一个 Fruit 数组。
  2. 但是,如果实际的数组类型是 Apple[] ,你应该只能在其中放置 Apple 和 Apple 的子类型,这在编译期和运行时都可以工作。但是请注意,编译器允许你将 Fruit 放置到这个数组中,这对于编译器来说是有意义的,因为它有一个 Fruit[] 引用___它有什么理由不允许将 Fruit 对象或者任何从 Fruit 集成出来的对象(如 Orange),放置到这个数组中呢?
  3. 因此,在编译期, 这是允许的,但是,运行时的数组机制知道它处理的是 Apple[] ,因此会在向数组中放置异构类型时抛出异常(ArrayStoreException)
  4. 实际上,向上转型不适合在这里。你真正做的是将一个数组赋值给另一个数组。数组的行为应该是它可以持有其他对象,这里只是因为我们能够向上转型而已,所以很明显,数组对象可以保留有关它们包含的对象类型的规则。就好像数组对它们持有的对象时有意识的,因此在编译期检查和运行时检查之间,你不能滥用它们。
  5. 对数组的这种赋值并不是那么可怕,因为在运行时可以发现你已经插入了不正确的类型。但是泛型的主要目标之一是将这种错误监测移入到编译期。因此当我们试图使用泛型容器来代替数组时,会发生什么呢?
class NonCovariantGenerics{
    //编译异常 不兼容的类型
    List fruitList=new ArrayList();
}
  1. 尽管你在第一次阅读这段代码时会认为 不能将一个Apple 容器赋值给一个 Fruit容器。 别忘了,泛型不仅和容器相关正确的说法是 不能把一个涉及 Apple 泛型赋值给一个涉及 Fruit 的泛型。
  2. 如果就像在数组的情况中一样,编译器对代码的了解足够多,可以确定所涉及到的容器,那么它可能会留下一些余地。但是它不知道任何有关这方面的信息,因此它拒绝向上转型。
  3. 然而实际上这根本不是向上转型___Apple 的List 不是 Fruit 的List 。Apple 的List 将持有Apple 和 Apple 的子类型,而Fruit 的List 将持有任何类型的 Fruit ,诚然这包括 Apple 在内,但是它不是一个 Apple 的List,它人就是 Fruit 的List 。Apple 的List 在类型上不等价 Fruit 的List , 即使 Apple 是一种Fruit类型。
  • 真正的问题是我们再谈论容器的类型,而不是容器持有的类型。与数组不同,泛型没有内建的协变类型。这是因为数组在语言中是完全定义的,因此可以内建了编译期和运行时的检查,但是在使用泛型时,编译期和运行时系统都不知道你想用类型做些什么,以及应该采用什么样的规则。
class GenericAndGCovariange{
    public static void main(String[] args) {
        //wildcards allow covariance 通配符允许协方差
        List list=new ArrayList();
        //编译错误:无法添加任何类型的对象
        
        //list.add(new Apple());
        //list.add(new Orange());
        //list.add(new Fruit());
        
        //合法但无趣
        list.add(null);
        //我们知道它至少返回 fruit
        Fruit fruit = list.get(0);
    }
}
  1. list 类型现在是 List , 你可以将其读作 : 具有任何从 Fruit继承的类型的列表。但是,这实际上并不意味着这个List 将持有任何类型的 Fruit。
  2. 通配符引用的是明确的类型,因此它意味着 某种list引用没有指定的具体类型。因此这个被复制的List 必须持有诸如 Fruit 或 Apple 这样的某种执行类型,但是为了向上转型为 list ,这个类型是什么并没有人关心。
  3. 如果唯一的限制是这个List 要持有某种具体的 Fruit 或 Fruit 子类型,但是你实际上并不关心它是什么,那么你能用这样的List 做什么呢?如果不知道List持有什么类型,那么你怎么才能安全地向其中添加对象呢? 就像在 CovariantArrays.java中向上转型数组一样,你不能,除非编译器而不是运行时系统可以阻止这种操作的发生。你很快就会发现这一问题。
  4. 你可能会认为,事情变得有点走极端了,因为现在你甚至不能向刚刚声明过将持有 Apple 对对象的List 中放置一个 Apple 对象了。是的,但是编译器并不知道这一点。 List 可以合法地指向一个 List 。一旦执行这种类型的向上转型,你就将丢失掉向其中传递任何对象的能力,甚至是传递Object 也不行。
  5. 另一方面,如果你调用一个返回Fruit 的方法,则是安全的,因为你知道在这个List中的任何对象至少具有Fruit 类型,因此编译器将允许这么做。

 

编译器有多聪明

  • 现在,你可能会猜想自己被阻止去调用任何接受参数的方法,请考虑如下程序。
class CompilerIntelligence{
    public static void main(String[] args) {
        List list= Arrays.asList(new Apple());
        //没有警告
        Apple apple= (Apple) list.get(0);
        //Argument is Object
        list.contains(new Apple());
        list.indexOf(new Apple());

    }
}
  1. 你可以看到,对contains() 和 indexOf() 调用,这俩个方法都接受 Apple 对象作为参数,而这些调用都可以正常执行。这意味着编译器实际上将检查代码,以查看是否有某个特定的方法修改了它的对象?
  2. 通过查看ArrayList文档,我们可以发现,编译器并没有这么聪明。尽管 add() 将接受一个具有泛型参数类型的参数,但是 contains() 和 indexOf() 将接受Object 类型的参数。因此当你指定一个 ArrayList 时, add() 的参数就变成了 ? extends Fruit 。
  3. 从这个描述中,编译器并不了解这里需要Fruit 的那个具体子类型,因此它不会接受任何类型的 Fruit。如果先将 Apple 向上转型为 Fruit ,也无关紧要___编译器将直接拒绝对参数列表中涉及通配符的方法(如 add()) 的调用。
  • 在使用 contains() 和 indexOf() 时,参数类型是Object , 因此不涉及任何通配符,而编译器也将允许这个调用。这意味着将由泛型类的涉及者来决定哪些调用是安全的,并使用Object类型作为其参数类型。为了在类型中使用了通配符的情况下禁止这类调用,我们需要在参数列表中使用类型参数。
public class Holder {
    private T t;
    public Holder(T t) {
        this.t = t;
    }
    public Holder() {
    }
    public T getT() {
        return t;
    }
    public void setT(T t) {
        this.t = t;
    }
    @Override
    public boolean equals(Object object) {
        return t.equals(object);
    }

    public static void main(String[] args) {
        Holder holder = new Holder<>(new Apple());
        Apple apple = holder.getT();
        holder.setT(apple);

        //Holder fruitHolder=holder;  无法向上转型
        Holder fruit = holder;
        Fruit fruit1 = fruit.getT();
        //返回的结果是 object
        apple= (Apple) fruit.getT();
        try {
            Orange orange = (Orange) fruit.getT();
        }catch (Exception e){
            System.out.println(e);
        }
        //fruit.setT(new Apple());
        //fruit.setT(new Fruit());
        System.out.println(fruit.equals(apple));
    }
}

//运行结果为
java.lang.ClassCastException: generic.Apple cannot be cast to generic.Orange
true
  1. Holder 有一个接受 T类型对象的set() 方法,一个 get() 方法,以及一个接受 Object 对象的 equals() 方法。 
  2. 正如你已经看到的,如果创建了一个 Holder ,不能将其向上转型为 Holder ,但是可以将其向上转型为 Holder
  3. 如果调用 getT() ,它只会返回一个 Fruit ___这就是在给定 任何扩展自 Fruit 的对象 这一边界之后,它所能知道的一切了。
  4. 如果能够了解更多的信息,那么你可以转型到某种具体的Fruit 类型,而这不是导致任何警告,但是你存在着得到 ClassCastException 的风险。
  5. setT() 方法不能工作于 Apple 或 Fruit ,因为 setT() 的参数也是 ? extends Fruit 这意味着它可以是任何事物,而编译器无法验证任何事物的类型安全性。
  6. 但是 , equals() 方法工作良好,因为它将Object 类型而并非T 类型的参数。因此,编译器只关注传递进来和要返回的对象类型,它并不会分析代码,以查看是否执行了任何实际的写入和读取操作。

 

逆变

  • 还可以走另外一条路,即使用 超类型通配符。这里声明通配符是由某个特定类的任何基类来界定的,方法是指定   甚至或者使用类型参数: (尽管你不能对泛型参数给出一个超类型边界,即不能声明 )。 这使得你可以安全地传递一个类型对象到泛型类型中。
  • 因此,有了超类型通配符,就可以向 Collection写入了如下。
public class SuperTypeWildcards {
    public static void main(String[] args) {
        List apples=new ArrayList<>();
        apples.add(new Apple());
        apples.add(new Jonathan());
        
        //apples.add(new Fruit()); 编译失败
    }
}
  1. 参数 Apple 是 Apple 的某种基类型的List , 这样你就知道向其中添加 Apple 或Apple 子类型是安全的。
  2. 但是,既然Apple 是下界,那么你可以知道向这样的 List 中添加 Fruit是不安全的,因为这将使这个 List 敞开口子,从而可以向其中添加非Apple类型的对象,而这时违反静态类型安全的。
  3. 因此你可能会根据如何能够向一个泛型类型 写入(传递给一个方法), 以及如何能够从一个泛型类型中 读取(从一个方法中返回) , 来着手思考子类型和超类型边界。
  4. 超类型边界放松了在可以向方法传递的参数上所作的限制。
public class GenericWriting {
    static List apples=new ArrayList<>();
    static List fruits=new ArrayList<>();


    static void f1(){
        writeExact(apples,new Apple());
        writeExact(fruits,new Apple());
    }
    static  void writeWithWildcard(List list,T item){
        list.add(item);
    }


    static  void writeExact(List list,T item){
        list.add(item);
    }

    static void f2(){
        writeWithWildcard(apples,new Apple());
        writeWithWildcard(fruits,new Apple());
    }

    public static void main(String[] args) {
        f1();
        f2();
    }
}
  1. writeExact() 方法使用了一个确切参数类型(无通配符) 。 
  2. 在 writeWithWildcard() 中,其参数现在是 List ,因此这个List 将持有从T 导出的某种具体;类型,这样就可以安全地将一个T 类型的对象或者从T 导出的任何对象作为参数传递给List 的方法。
  3. 在 f2() 中可以看到这一点,在这个方法中我们仍旧可以像前面那样,将Apple 放置到List 中,但是现在我们可以如你所期望的那样,将Apple放置到List
public class GenericReading {

    static  T readExact(List list) {
        return list.get(0);
    }

    static List apples = Arrays.asList(new Apple());
    static List fruits = Arrays.asList(new Fruit());


    //静态方法适应每个调用
    static void f1() {
        Apple apple = readExact(apples);
        Fruit fruit = readExact(fruits);
        fruit = readExact(apples);
    }

    //如果您有一个类,那么它的类型是在实例化类时建立
    static class Reader {
        T readExact(List list) {
            return list.get(0);
        }
    }

    static void f2() {
        Reader reader = new Reader<>();
        Fruit fruit = reader.readExact(fruits);

        // reader.readExact(apples); 错误
        //readExact(List) 不可能是  applied to (List)
    }

    static class CovariantReader{
        T readCovariant(List list){
            return list.get(0);
        }
    }

    static void f3(){
        CovariantReader covariantReader=new CovariantReader<>();
        Fruit fruit = covariantReader.readCovariant(fruits);
        Fruit fruit1 = covariantReader.readCovariant(apples);

    }
    
    public static void main(String[] args) {
        f1();
        f2();
        f3();
    }
}
  1. 与前面一样,第一个方法 readExact() 使用了精确的类型。因此如果使用这个没有任何通配符的精确类型,就可以向 List 中写入和读取这个精确类型。
  2. 另外,对于返回值,静态的泛型方法 readExact() 可以有效地 适应每个方法调用,并能够从 List 中返回一个Apple,从List 中返回一个 Fruit ,就像 f1() 中看到的那样。
  3. 因此,如果可以摆脱静态泛型方法,那么当只是读取时,就不需要协变类型了。
  4. 但是,如果有一个泛型类,那么当你创建这个类的实例时,要为这个类确定参数。就像在 f2() 中看到的, reader 实例可以从List 中读取一个 Fruit,因为这就是它的确切类型。但是从 List 还应该产生 Fruit 对象,而 reader 不允许这么做。
  5. 为了修正这个问题 covariantReader.readCovariant() 方法将接受 List ,因此,从这个列表中读取一个T 是安全的(你知道在这个列表中的所有对象至少是一个 T ,并且可能是T倒垂的某种对象)。在 f3() 中,你可以看到现在可以从 List 中读取 Fruit了。

 

无界通配符

  • 无界通配符 看起来意味着任何事物,因此使用无界通配符好像等价于使用原生类型。事实上,编译器初看起来是支持这种判断的。

public class UnboundedWildcards1 {

    static List list1;
    static List list2;
    static List list3;


    static void assign1(List list) {
        list1 = list;
        list2 = list;
        list3 = list;
    }

    static void assign2(List list) {
        list1 = list;
        list2 = list;
        list3 = list;
    }

    static void assign3(List list) {
        list1 = list;
        list2 = list;
        list3 = list;
    }

    public static void main(String[] args) {
        assign1(new ArrayList());
        assign2(new ArrayList());
        assign3(new ArrayList());

        assign1(new ArrayList());
        assign2(new ArrayList());
        assign3(new ArrayList());

        List lists = new ArrayList<>();
        lists=new ArrayList();

        assign1(lists);
        assign2(lists);
        assign3(lists);
    }

}
  1. 有很多情况都和你在这里看到的情况类似,即编译器很少关系使用的是原生类型还是
  2. 在这些情况中, 可以被认为是一种装饰,但是它仍旧是很有价值的,因为,实际上,它是在声明 我是想用Java泛型来编写这段代码,我在这里并不是要用原生类型, 但是在当前这种情况下,泛型参数可以持有任何类型。
  3. 当你在处理多个泛型参数时,有时允许一个参数可以是任何类型,同时为其他参数确定某种特定类型的这种能力会显得很重要。
public class UnboundedWildcards {

    static Map map;
    static Map map1;
    static Map map2;
    
    static void assign1(Map map){
        map=map;
    }

    static void assign2(Map map){
        map1=map;
    }

    static void assign3(Map map){
        map2=map;
    }

    public static void main(String[] args) {
        assign1(new HashMap());
        assign2(new HashMap());
        assign3(new HashMap());

        assign1(new HashMap());
        assign2(new HashMap());
        assign3(new HashMap());
    }
}
  1. 但是,当你拥有的全部都是无界通配符时,就像在 Map 中看到那样,在JDK1.8 是可以将其与原生Map区分开了。

 

 

 

 

 

 

你可能感兴趣的:(java编程思想,java编程思想)