Java基础-泛型(上)

来源:互联网 发布:d3.min.js 下载 编辑:程序博客网 时间:2024/06/06 13:13


泛型由来的动机

通过Object转型问题引入.早期的Object类型可以接收任意的对象类型,但是在实际的使用中,会有类型转换的问题。也就存在这隐患,所以Java提供了泛型来解决这个安全问题。

好处

1.提高安全性(将运行期的错误转换到编译期)
2.省去强转的麻烦

未使用泛型

public static void main(String[] args) {ArrayList list = new ArrayList();list.add(123);//自动装箱list.add(456);list.add("789");//可以添加任何Object类型,存在安全隐患Iterator iterator = list.iterator();while (iterator.hasNext()) {Integer integer = (Integer) iterator.next();//需强制转换System.out.println(integer);}}/*outPut: * 123 * 456 * Exception in thread "main" java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Integer */

使用泛型后

public static void main(String[] args) {ArrayList<Integer> list = new ArrayList<>();list.add(123);//自动装箱list.add(456);//list.add("789"); 编译错误Iterator<Integer> iterator = list.iterator();while (iterator.hasNext()) {Integer integer = iterator.next();//无需强转System.out.println(integer);}}/*outPut: * 123 * 456 * 456 */

很明显,泛型的主要好处就是让编译器保留参数的类型信息,执行类型检查,执行类型转换操作:编译器保证了这些类型转换的绝对无误。

相对于依赖程序员来记住对象类型、执行类型转换——这会导致程序运行时的失败,很难调试和解决,而编译器能够帮助程序员在编译时强制进行大量的类型检查,发现其中的错误。

注意事项:

泛型类型必须是引用类型

前后的泛型必须一致,或者后面的泛型可以省略不写(1.7的新特性菱形泛型)

泛型的构成

由泛型的构成引出了一个类型变量的概念。根据Java语言规范,类型变量是一种没有限制的标志符,产生于以下几种情况:

泛型类声明

泛型接口声明

泛型方法声明

泛型构造器(constructor)声明


泛型类和接口

如果一个类或接口上有一个或多个类型变量,那它就是泛型。类型变量由尖括号界定,放在类或接口名的后面:

public interface List<T> extends Collection<T> {//TODO}

简单的说,类型变量扮演的角色就如同一个参数,它提供给编译器用来类型检查的信息。

Java类库里的很多类,例如整个Collection框架都做了泛型化的修改。例如,最开始使用的ArrayList就是一个泛型类。在那段代码里,list是一个ArrayList<Integer>对象,它是一个带有一个Integer类型变量的ArrayList的类实现的实例。编译器使用这个类型变量参数在get方法被调用、返回一个Integer对象时自动对其进行类型转换。

泛型方法和构造器

非常的相似,如果方法和构造器上声明了一个或多个类型变量,它们也可以泛型化

public static <T> T get(List<T> list) {//TODO}

这个方法将会接受一个List<T>类型的参数,返回一个T类型的对象。

泛型的子类

class Fruit {}class Apple extends Fruit {}class Strawberry extends Fruit {}class FujiApple extends Apple {}

看上面的继承体系,我们可以知道:FujiApple(富士苹果)是Apple的子类型,Apple是Fruit(水果)的子类型,那么FujiApple(富士苹果)当然也是Fruit(水果)的子类型

//编译通过Apple apple = new FujiApple();Fruit fruit = apple;

那么如果是泛型呢?

ArrayList<Apple> apples = new ArrayList<>();ArrayList<Fruit> fruits = apples; //编译错误

很遗憾,编译错误。答案可能会出乎你的意料:没有任何关系。用更通俗的话,泛型类型跟其是否子类型没有任何关系。

为什么一个苹果是一个水果,为什么一箱苹果不能是一箱水果?

在某些事情上,这种说法可以成立,但在类型(类)封装的状态和操作上不成立。如果把一箱苹果当成一箱水果会发生什么情况?

ArrayList<Apple> apples = new ArrayList<>();ArrayList<Fruit> fruits = apples; //编译错误fruits.add(new Strawberry());

如果可以这样的话,我们就可以在list里装入各种不同的水果子类型,这是绝对不允许的。

另外一种方式会让你有更直观的理解:一箱水果不是一箱苹果,因为它有可能是一箱另外一种水果,比如草莓(子类型)。

看到这里,你是否想起了数组的使用

//编译通过Apple[] apples = new Apple[5];Fruit[] fruits = apples;

数组和泛型类型上用法的竟然不一致?那么我们往里加入strawberrie(草莓)对象试试

public static void main(String[] args) {//编译通过Apple[] apples = new Apple[5];Fruit[] fruits = apples;fruits[0] = new Strawberry();}/*outPut: * Exception in thread "main" java.lang.ArrayStoreException: Strawberry */

真的可以编译通过,但是在运行时抛出ArrayStoreException异常(试图将错误类型的对象存储到一个对象数组时抛出的异常)。因为数组的这特点,在存储数据的操作上,Java运行时需要检查类型的兼容性。这种检查,很显然,会带来一定的性能问题,你需要明白这一点。

重申一下,泛型使用起来更安全,能“纠正”Java数组中这种类型上的缺陷。

也许你会感到很奇怪,为什么在数组上会有这种类型和子类型的关系?

如果它们不相关,你就没有办法把一个未知类型的对象数组传入一个方法里(不经过每次都封装成Object[])

public void sort(Object[] obj) {}

泛型出现后,数组的这个个性已经不再有使用上的必要了(下面的通配符),实际上是应该避免使用。


通配符

关键字说明

? 通配符类型

<?extends T> 表示类型的上界,表示参数化类型的可能是T 或是 T的子类

<? super T> 表示类型的下界(超类型限定),表示参数化类型是T或T的超类型(父类型),直至Object

extends示例:

ArrayList<Apple> apples = new ArrayList<>();ArrayList<? extends Fruit> fruits = apples; //编译通过fruits.add(new Strawberry()); //编译错误fruits.add(new Apple()); //编译错误fruits.add(new Fruit()); //编译错误fruits.add(null); //编译通过

Java编译器会阻止你往一个Fruit list里加入strawberry。在编译时我们就能检测到错误,在运行时就不需要进行检查来确保往列表里加入不兼容的类型了。即使你往list里加入Fruit对象也不行。

为什么呢?

这个? extends T 通配符告诉编译器我们在处理一个类型T的子类型,但我们不知道这个子类型究竟是什么。因为没法确定,为了保证类型安全,我们就不允许往里面加入任何这种类型的数据。可以添加null,因为null 可以表示任何类型

另一方面,因为我们知道,不论它是什么类型,它总是类型T的子类型,当我们在读取数据时,能确保得到的数据是一个T类型的实例

Fruit fruit = fruits.get(0); //编译通过Apple apple = fruits.get(0); //编译不通过


super示例:

ArrayList<Fruit> fruits = new ArrayList<>(); //编译通过ArrayList<? super Apple> apples = fruits;//编译通过apples.add(new FujiApple()); //编译通过apples.add(new Apple()); //编译通过apples.add(new Fruit()); //编译不通过apples.add(new Object()); //编译不通过apples.add(null); //编译通过

fruits指向的是一个装有Apple的某种超类(supertype)的List。ArrayList<? super Apple> 表示具有任何Apple超类型的列表”,列表的类型至少是一个 Apple类型,因此可以安全的向其中添加Apple及其子类型。如果往里面加入Apple的超类,编译器就会警告你,因为我们不知道它是怎样的超类,所有这样的实例就不允许加入。

从这种形式的类型里获取数据又是怎么样的呢?

Apple apple = apples.get(0); //编译不通过Fruit fruit = apples.get(0); //编译不通过Object obj = apples.get(0); //编译通过

结果表明,你只能取出Object实例:因为我们不知道超类究竟是什么,编译器唯一能保证的只是它是个Object,因为Object是任何Java类型的超类。

存取原则

如果你想从一个数据类型里获取数据,使用 ? extends 通配符

如果你想把对象写入一个数据结构里,使用 ? super 通配符

如果你既想存,又想取,那就别用通配符

同样的有如下总结:

extends 可用于的返回类型限定,不能用于参数类型限定。

super 可用于参数类型限定,不能用于返回类型限定。

带有super超类型限定的通配符可以向泛型对易用写入,带有extends子类型限定的通配符可以向泛型对象读取


0 0