Java泛型小结

所谓泛型,就是在定义类、接口、方法的时候使用“类型形参”,而在真正使用类、接口、方法的时候,动态的指定实参(也就是指定某一种类型,或者用通配符来通配多种类型)。

泛型类

例如,使用泛型定义 Foo 类:

class Foo<T> {
    private T t;

    public T getT() {
        return t;
    }

    public void setT(T t) {
        this.t = t;
    }
}

使用 Foo 类:

public class Test0830 {
    public static void main(String[] args) {
        Foo<String> foo = new Foo<String>(); // 可以省略为: Foo<String> foo = new Foo<>();
        foo.setT("abc");
        System.out.println(foo.getT());
        
        Foo<Integer> fo = new Foo<>();
        fo.setT(123);
        System.out.println(fo.getT());
    }
}

需要注意的是, Foo<A>Foo<B> 是不兼容的,即使A和B是父类和子类的关系。

例如:假定有如下继承关系:

在这里插入图片描述
下面的代码编译报错:

        Foo<Fruit> foo = new Foo<Apple>(); // 编译报错

究其原因,假如可以这么做,那么 foo 可以动态的指向 Foo<Apple> 或者 Foo<Banana> 对象。既然fooFoo<Fruit> ,那么它一定可以调用 setT() 方法,传入一个 Fruit (或其子类)对象,下面的代码一定是work的:

        foo.setT(new Apple());

但是 foo 可能指向的是 fooBanana 对象,AppleBanana 是不兼容的,这就矛盾了。

(其实要这么设计也行,大不了到运行期再报错,只不过Java泛型的设计原则是,只要编译期没有问题,那么运行期就不会有类型兼容的问题。)

不使用泛型

在不使用泛型的时候,就是这样,编译期干啥都行,如果有问题,到运行期再报错。看下面的例子:

        Foo foo = new Foo();
        foo.setT(123);
        System.out.println(foo.getT()); // 123
        foo.setT("abc");
        System.out.println(foo.getT()); // abc

这个例子没有任何问题,运行正常,再看下面的例子:

        Foo foo = new Foo<Apple>();
        foo.setT(123);
        System.out.println(foo.getT());
        foo.setT("abc");
        System.out.println(foo.getT());

编译期或者运行期会有问题吗?看起来编译期应该没问题,毕竟定义foo时没有指定泛型,foo.setT(123) 应该能通过编译,大概到运行期的时候才会报错吧,因为foo实际指向的是 Foo<Apple> 对象。

实际上,在运行期也没有报错!这就奇怪了,明明指向的是 Foo<Apple> 对象, setT() 方法怎么能传入 123 或者 "abc" 而不报错呢?

这就涉及到泛型擦除了,anyway,这样做是允许的,下面的代码才会报错:

        Foo<Apple> foo1 = new Foo<>();
        
        Foo foo = foo1;
        
//        foo1.setT(123); //编译期报错
        foo.setT(123); // OK
        
        System.out.println(foo.getT()); // OK
        System.out.println(foo1.getT()); // OK

        foo1.getT().doSth(); //运行期报错,java.lang.ClassCastException

类型通配符

回到前面的例子, Foo<Fruit>Foo<Apple> 不兼容,那如果希望 setT() 方法能接受任意水果,那怎么办呢,总不至于写N个重载方法吧。解决办法就是,使用类型通配符 ?

        Foo <? extends Fruit> foo = null;
        
        foo = new Foo<Apple>(); // OK
        foo = new Foo<Banana>(); // OK

此处的 Foo <? extends Fruit> 可以看做 Foo <Apple>Foo <Banana> 的父类,所以就可以兼容二者了。

但是,这还是不能解决 setT() 的问题啊。如果你这么想,那就对了,这个问题确实无法解决。由于“只要编译期没有问题,那么运行期就不会有类型兼容的问题”的原则,为了不在运行期报错,只好在编译期报错了。

        foo.setT(new Apple()); // 编译期报错

实际上,此处不管传什么参数都会报错,也就是说根本无法调用foo的setT()方法。

更典型的例子如下:

        List<? extends Fruit> list = new ArrayList<Apple>();
        
        list.add(new Apple()); //编译报错

请注意,上面编译报错是因为定义foo时,指定了其上限(协变)。如果是指定其下限(逆变),或者指定明确的类型,就可以调用 setT() 方法。

        Foo <? super Apple> foo = null;
        
        foo = new Foo<Apple>(); // OK
        foo = new Foo<Fruit>(); // OK
        foo = new Foo<Object>(); // OK
        foo = new Foo<Banana>(); // 编译期报错,因为Banana不是Apple的父类
        foo = new Foo<GreenApple>(); // 编译期报错,因为GreenApple不是Apple的父类

那么,调用 setT() 方法,都可以传哪些类型的参数呢?

        foo.setT(new Apple()); // OK
        foo.setT(new GreenApple()); // OK
        foo.setT(new Fruit()); //
        foo.setT(new Object()); //
        foo.setT(new Banana()); // 

简言之,可以传下限(本例中是 Apple )或其子类。道理很简单,不管foo实际指向什么对象,其泛型一定是Apple或其某一种父类,而“Apple或其父类”一定兼容Apple或者子类(向上转型是安全的)。

类型通配符上限( Foo<? extends Xxx>

  • 兼容 Foo<Yyy> ,其中 YyyXxx 或其子类。
  • 不可调用“泛型类型作为参数”的方法(因为无法确定类型实参的真正类型)。
  • 可以调用“泛型类型作为返回值”的方法,泛型类型都可看作 Xxx (当然也可看做 Xxx 的父类)。

类型通配符下限( Foo<? super Xxx>

  • 兼容 Foo<Zzz>,其中 ZzzXxx 或其父类。
  • 可以调用“泛型类型作为参数”的方法,可传入 Xxx 或其子类(因为类型实参的真正类型是 Xxx 或其父类,一定会兼容 Xxx 或其子类)。
  • 可以调用“泛型类型作为返回值”的方法,不过只能当做 Object (因为类型实参的真正类型是 Xxx 或其父类,所以一定都可以看作 Object )。

泛型方法

前面的例子中使用了泛型类,其实泛型方法也类似,如果只是在某一个方法上需要使用泛型,那就没必要用泛型类,只需使用泛型方法。

在用法上,泛型类是在类名后面加上 <T> ,而泛型方法则是把 <T> 放在方法返回值类型前面。

class Bar {
    public <T> void printT(T t) {
        System.out.println("printing " + t);
    }
}

使用 Bar 类:

        Bar bar = new Bar();

        bar.printT(123);
        bar.printT("abc");

注意,在使用泛型方法时,不需要显式指定其实际类型,编辑器是根据实际传入的值来推断其类型的。

对于 xxx(T t1, T t2) 这样的泛型方法,调用该方法时,两个实参的类型不需要相同。

class Bar {
    public <T> void printT(T t1, T t2) {
        System.out.println("printing " + t1 + ", " + t2);
    }
}
        Bar bar = new Bar();

        bar.printT(123, 456); // OK
        bar.printT("abc", "def"); // OK
        bar.printT(789, "ghi"); // OK

本例中,编译期应该是把 T 推断为 Object 了(兼容Integer和String)。

再看下面的例子:

class Bar {
    public <T> void test(Collection<T> t1, Collection<T> t2) {
        // do something
    }
}
        Bar bar = new Bar();

        bar.test(new ArrayList<Object>(), new ArrayList<Object>()); // OK
        bar.test(new ArrayList<Object>(), new ArrayList<String>()); // 编译期报错

本例中,编译期报错,是因为形参( Collection<T> )和实参( ArrayList<String> )不兼容。

要想改为兼容,可使用类型通配符 ?

    public <T> void test(Collection<T> t1, Collection<? extends T> t2) {
        // do something
    }

这样, T 可被推断为 Object ,上面报错的那行代码就OK了。不过要注意,t2使用了类型通配符指定了上限,所以无法调用其参数包含了该泛型的方法。

<T><?> 的区别

前者是形参,后者是实参。也就是说, <T> 出现在类或者方法的定义里面, <?> 出现在类或者方法的使用里面。

定义方法 printT()

    public <T> void printT(T t1, T t2) {
        System.out.println("printing " + t1 + ", " + t2);
    }

使用类型 List

        List<? extends Fruit> list = null;

再看下面的代码:

    public void doSth(List<?> list) {
        // do something
    }

虽然是在定义 doSth() 方法,但是 <?> 修饰的是List,也就是对List的使用。

<T><?> 可以同时出现:

    public <T> void test(Collection<T> t1, Collection<? extends T> t2) {
        // do something
    }

这个例子中, <?> 是对Collection的使用, <T> 是对test()方法的定义。

如果 <T> 在泛型方法定义中只使用了一次,那它唯一的作用,就是可以在此处传入不同的实际类型,对于这种情况,无需用 <T> ,只要用 <?> 即可。

换句话说,使用 <T> 是为了在多处使用,互相依赖。比如前面的例子:

    public <T> void test(Collection<T> t1, Collection<? extends T> t2) {
        // do something
    }

通配符可用于定义变量:

        List<? extends Fruit> list = null;

此处, <? extends Fruit> 是泛型类 List 的类型实参。

思考题

ListList<Object>List<?>List<? extends Object> 有何区别?

  • List 没有泛型的限制,可以“想做什么就做什么”。
  • List<Object> 不兼容其它 List<Xxx>
  • List<?> 兼容其它的 List<Xxx> ,但是不能调用“泛型类型作为参数”的方法。
  • List<? extends Object> 个人感觉它等同于 List<?>
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值