泛型的extends和super通配符


extends通配符

首先定义一个类:

class User {
    private T t1;
    private T t2;

    public User() {
    }

    public User(T t1, T t2) {
        this.t1 = t1;
        this.t2 = t2;
    }

    public T getT1() {
        return t1;
    }

    public void setT1(T t1) {
        this.t1 = t1;
    }

    public T getT2() {
        return t2;
    }

    public void setT2(T t2) {
        this.t2 = t2;
    }
}

有一个add()方法,参数类型为User

public static int add(User user) {
    Number t1 = user.getT1();
    Number t2 = user.getT2();
    return t1.intValue() + t2.intValue();
}
  • 在主函数中调用add()方法

    public static void main(String[] args) {
        User user = new User<>(123, 356);
        System.out.println(add(user));
    }
    

    原因是:因为User并不是User的子类,所以编译阶段就会报User无法转换为javase.generics.upcasting.User错误。

  • 想要接收User,可以使用User<? extends Number>

    public static int add(User<? extends Number> user) {
        Number t1 = user.getT1();
        Number t2 = user.getT2();
        return t1.intValue() + t2.intValue();
    }
    

    这样改造后,不仅可以接收Integer,还可以传入DoubleBigDecimal等子类。这就称为extends通配符。

使用extends通配符,有什么要注意的点?

User类可以简化看作三种元素构成:T tT get()set(T t)

使用了extends通配符后,上述User类可以简化地理解为:

class User<? extends Number> {
    private <? extends Number> t;

    public <? extends Number> getT1() {
        return t1;
    }

    public void setT1(<? extends Number> t1) {
        this.t1 = t1;
    }
}

现在分别尝试调用get()set()方法。

public static int add(User<? extends Number> user) {
    Number t1 = user.getT1();
    Number t2 = user.getT2();
    user.setT1(new Integer(12321));
    user.setT2(new Integer(431243));
    return t1.intValue() + t2.intValue();
}

结果get()方法没有问题,set()方法编译无法通过。

原因

Java泛型擦拭机制:编译器总是将转为Object,并且在需要转型的时候,编译器会根据T的类型自动为我们实行安全地强制转型。

  • 对于get()方法来说

    在本例中,因为Java泛型擦拭机制的存在,所以编译器将<? extends Number>处理成Object,在返回结果时,自动安全的转型。

    但因为Java是一门静态语言,运行完成编译器会将结果自动转型为IntegerDoubleBigDecimalNumber的哪一种子类型,在运行完成前是不知道的,所以在此例中,想要安全的接收返回结果,只能用Number去接收。

  • 对于set()方法来说

    同样是因为泛型擦拭机制,在运行前编译器并不能识别出User<? extends Number> user是哪一种类型,但是很显然,除了User user类型之外的其他子类型的set()方法都不能传入Integer类型参数。

    所以在编译阶段,set()方法就会报java: 不兼容的类型: java.lang.Integer无法转换为capture#1, 共 ? extends java.lang.Number错误。

super通配符

extends通配符User<? extends Number>可以接收IntegerDoubleBigDecimalNumber的子类型。

super通配符和extends通配符刚好相反。User<? super Integer>可以接收NumberObject等父类型。

同样User类可以简化看作三种元素构成:T tT get()set(T t)

使用了extends通配符后,上述User类可以简化地理解为:

class User<? super Integer> {
    private <? super Integer> t;

    public <? super Integer> getT1() {
        return t1;
    }

    public void setT1(<? super Integer> t1) {
        this.t1 = t1;
    }
}

同样是调用get()set()

public static int test(User<? super Integer> user) {
    Integer t1 = user.getT1();
    Integer t2 = user.getT2();
    user.setT1(12321); //Integer自动装箱
    user.setT2(12321);
    return t1 + t2;
}

结果set()方法没有问题,get()方法编译无法通过。

原因

  • 对于get()方法来说

    和extends通配符同样的道理,同样是由于擦拭机制,编译器会将<? super Integer>处理成Object,在返回结果时,自动地安全转型。

    由于Java的静态语言特性,在运行前只能知道返回值可能是Integer类型、NumberObject等超类型。

    因为无法具体知道是哪一种类型,所以在使用Integer去接收参数时,在编译阶段就会报java: 不兼容的类型: capture#1, 共 ? super java.lang.Integer无法转换为java.lang.Integer错误。

  • set()方法来说

    一个子类对象自然也是一个父类对象。即无论User<? super Integer> user是哪一种Integer的超类,都不妨碍一个Integer对象自然也是一个超类(如是一个Number对象)。

    因此一个Integer对象总是可以作为传入参数并生效的。

总结

无论是<? extends Number>还是<? super Integer>都可以理解为就是某一种类型。

  • <? extends Number>中,这种类型一定是Number的一个子类型,只不过目前还不知道这种类型是什么,所以用?代替。

  • <? super Integer>中,这种类型一定是Integer的一个超类型,只不过目前还不知道这种类型是什么,所以用?代替。

上两节描述的两种情况,其实背后都是因为Java泛型的擦拭机制导致的,同时这种Java泛型的擦拭机制的制定是因为Java需要先编译后执行的静态语言特性,为了让编译器能在早期来帮助静态校正程序和提前找到错误,必须牺牲一定的灵活性——编译器无法准确判断泛型类型,从而实现安全转型的都报错误。