深入理解Java泛型

概述

泛型的本质是参数化类型,通常用于输入参数、存储类型不确定的场景。相比于直接使用 Object 的好处是:编译期强类型检查、无需进行显式类型转换

类型擦除

正确理解泛型概念的首要前提是理解类型擦除(type erasure)。 Java中的泛型基本上都是在编译器这个层次来实现的。在生成的Java字节代码中是不包含泛型中的类型信息的。使用泛型的时候加上的类型参数,会被编译器在编译的时候去掉。这个过程就称为类型擦除。如在代码中定义的List和List等类型,在编译之后都会变成List。JVM看到的只是List,而由泛型附加的类型信息对JVM来说是不可见的。Java编译器会在编译时尽可能的发现可能出错的 地方,但是仍然无法避免在运行时刻出现类型转换异常的情况。类型擦除也是Java的泛型实现方式与 C++模板机制 实现方式之间的重要区别。

很多泛型的奇怪特性都与这个类型擦除的存在有关,包括:

1、 泛型类并没有自己独有的 Class 类对象。比如并不存在 List.class 或是List.class,而只有 List.class。

2、 静态变量是被泛型类的所有实例所共享的。对于声明为 MyClass的类,访问其 中的静态变量的方法仍 然是 MyClass.myStaticVar。不管是通过 new MyClass还是 new MyClass创建的对象,都是共享一个静态变量。

3、 泛型的类型参数不能用在 Java 异常处理的 catch 语句中。因为异常处理是由 JVM在运行时刻来进行的。由于类型信息被擦除,JVM 是无法区分两个异常类型MyException和 MyException的。对于 JVM 来说,它们都是MyException 类型的。也就无法执行与异常对应的 catch 语句。类型擦除的基本过程也比较简单,首先是找到用来替换类型参数的具体类。这个具体类一般是 Object。如果指定了类型参数的上界的话,则使用这个上界。把代码中的类型参数都替换成具体的类。同时去掉出现的类型声明,即去掉<>的内容。比如T get()方法声明就变成了 Object get();List就变成了 List。接下来就可能需要生成一些桥接方法(bridge method)。这是由于擦除了类型之后的类可能缺少某些必须的方法。比如考虑下面的代码:

当类型信 息被擦 除之后 ,上述类的声明变成了 class MyString implementsComparable。但是这样的话,类 MyString 就会有编译错误,因为没有实现接口Comparable 声明的 int compareTo(Object)方法。这个时候就由编译器来动态生成这个方法。

实例分析

了解了类型擦除机制之后,就会明白编译器承担了全部的类型检查工作。编译器禁止某些泛型的使用方式,正是为了确保类型的安全性。以上面提到的 List和 List为例来具体分析:

这段代码中,inspect方法接受List作为参数,当在test方法中试图传入List的 时候,会出现编译错误。假设这样的做法是允许的,那么在inspect方法就可以通过list.add(1)来向集合中添加一个数字。这样在test方法看来,其声明为List的集合中却被添加了一个Integer类型的对象。这显然是违反类型安全的原则的,在某个时候肯定会 抛出 ClassCastException。因此,编译器禁止这样的行为。编译器会尽可能的检查可能存在的类型安全问题。对于确定是违反相关原则的地方,会给出编译错误。当编译器无法判断类型的使用是否正确的时候,会给出警告信息。

通配符与上下界

在使用泛型类的时候,既可以指定一个具体的类型,如 List就声明了具体的类型是 String;也可以用通配符?来表示未知类型,如 List就声明了 List 中包含的元素类型是未知的。 通配符所代表的其实是一组类型,但具体的类型是未知的。List所声明的就是所有类型都是可以的。但是 List并不等同于 List。List实际上确定了 List 中包含的是 Object 及其子类,在使用的时候都可以通过 Object 来进行引用。而 List则其中所包含的元素类型是不确定。其中可能包含的是 String,也可能是 Integer。如果它包含了 String 的话,往里面添加 Integer 类型的元素就是错误的。正因为类型未知,就不能通过 new ArrayList()的方法来创建一个新的 ArrayList 对象。因为编译器无法知道具体的类型是什么。但是对于 List中的元素确总是可以用Object 来引用的,因为虽然类型未知,但肯定是 Object 及其子类。考虑下面的代码:

如上所示,试图对一个带通配符的泛型类进行操作的时候,总是会出现编译错误。其原因在于通配符所表示的类型是未知的。因为对于 List中的元素只能用 Object 来引用,在有些情况下不是很方便。在这些情况下,可以使用上下界来限制未知类型的范围。如 List说明 List中可能包含的元素类型是 Number 及其子类。而 List则说明 List 中包含的是 Number 及其父类。当引入了上界之后,在使用类型的时候就可以使用上界类中定义的方法。比如访问 List的时候,就可以使用 Number类的 intValue 等方法。

泛型中 ? 可以用来做通配符,单纯 ? 匹配任意类型。< ? extends T > 表示类型的上界是 T,参数化类型可能是 T 或 T 的子类:

从上面代码中可以看出来,赋值是参数化类型为 Fruit 和其子类的集合都可以成功,通配符类型无法实例化。为啥上面代码中的 add 全部编译失败了呢?因为 fruits 集合并不知道实际类型是 Fruit、Apple 还是 Food,所以无法对其赋值。

除了 extends 还有一个通配符 super,< ? super T > 表示类型的下界是 T,参数化类型可以是 T 或 T 的超类:

看上面代码可知,super 通配符类型同样不能实例化,Fruit 和其超类的集合均可赋值。这里 add 时 Fruit 及其子类均可成功,为啥呢?因为已知 fruits 的参数化类型必定是 Fruit 或其超类 T,那么 Fruit 及其子类肯定可以赋值给 T。

归根到底,还是“子类对象可以赋值给超类引用,而反过来不行”这一规则导致 extends 和 super 通配符在 add 操作上表现如此的不同。同样地,也导致 super 限定的 fruits 中 get 到的元素不能赋值给 Fruit 引用,而 extends 则可以。

总结一下就是:

1、extends 可用于的返回类型限定,不能用于参数类型限定。即作为返回值的时候是可以的,但是作为参数类型限定的时候是不可以的,就像上面的代码写的,add方法全部不可以用,但是super却可以用。

2、super 可用于参数类型限定,不能用于返回类型限定。super和extend正好相反

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

类型系统

在Java中,大家比较熟悉的是通过继承机制而产生的类型体系结构。比如String继承自Object。根据 Liskov替换原则 ,子类是可以替换父类的。当需要Object类的引用的时候,如果传入一个String对象是没有任何问题的。但是反过来的话,即用父类的引用替换子类引用 的时候,就需要进行强制类型转换。编译器并不能保证运行时刻这种转换一定是合法的。这种自动的子类替换父类的类型转换机制,对于数组也是适用的。String[]可以替换Object[]。但是泛型的引入,对于这个类型系统产生了一定的影响。正如前面提到的List是 不能替换掉List的。引入泛型之后的类型系统增加了两个维度:一个是类型参数自身的继承体系结构,另外一个是泛型类或接口自身的继承体系结构。第一个指的是对于 List和List这样的情况,类型参数 String 是继承自 Object 的。而第二种指的是 List接口继承自 Collection 接口。对于这个类型系统,有如下的一些规则:

1、相同类型参数的泛型类的关系取决于泛型类自身的继承体系结构。即List 是 Collection 的 子 类 型 , List 可 以 替 换Collection。这种情况也适用于带有上下界的类型声明。

2、 当泛型类的类型声明中使用了通配符的时候,其子类型可以在两个维度上分别展开。如对 Collection来说,其子类型可以在 Collection 这个维度上展开,即 List和 Set等;也可以在Number 这个层次上展开,即 Collection和 Collection等。如此循环下去,ArrayList和 HashSet等也都算是 Collection的子类型。

3、 如果泛型类中包含多个类型参数,则对于每个类型参数分别应用上面的规则。理解了上面的规则之后,就可以很容易的修正实例分析中给出的代码了。只需要把List改成 List即可。List是 List的子类型,因此传递参数时不会发生错误。

开发自己的泛型类

泛型类与一般的 Java 类基本相同,只是在类和接口定义上多出来了用<>声明的类型参数。一个类可以有多个类型参数,如 MyClass。 每个类型参数在声明的时候可以指定上界。所声明的类型参数在 Java 类中可以像一般的类型一样作为方法的参数和返回值,或是作为域和局部变量的类型。但是由于类型擦除机制,类型参数并不能用来创建对象或是作为静态变量的类型。考虑下面的泛型类中的正确和错误的用法。

泛型的规则

1、泛型的参数类型只能是类(包括自定义类),不能是简单类型。

2、同一种泛型可以对应多个版本(因为参数类型是不确定的),不同版本的泛型类实例是不兼容的。

3、泛型的类型参数可以有多个

4、泛型的参数类型可以使用 extends 语句,习惯上称为“有界类型”

5、泛型的参数类型还可以是通配符类型,例如 Class

泛型的使用场景

当类中要操作的引用数据类型不确定的时候,过去使用 Object 来完成扩展,JDK 1.5后推荐使用泛型来完成扩展,同时保证安全性。

最佳实践

在使用泛型的时候可以遵循一些基本的原则,从而避免一些常见的问题。

1、 在代码中避免泛型类和原始类型的混用。比如 List和 List 不应该共同使用。这样会产生一些编译器警告和潜在的运行时异常。当需要利用 JDK 5 之前开发的遗留代码,而不得不这么做时,也尽可能的隔离相关的代码。

2、 在使用带通配符的泛型类的时候,需要明确通配符所代表的一组类型的概念。由于具体的类型是未知的,很多操作是不允许的。

3、 泛型类最好不要同数组一块使用。你只能创建 new List[10]这样的数组,无法创建 new List[10]这样的。这限制了数组的使用能力,而且会带来很多费解的问题。因此,当需要类似数组的功能时候,使用集合类即可。

4、 不要忽视编译器给出的警告信息。

注意事项

1、在泛型类中,可以T t;这样声明,但是不能在前面加上static,因为静态变量在类实例化之前就存在了,会造成编译错误。同理在泛型接口中连T t;这样都不可以定义,因为在接口中,默认是由public static final 修饰的

2、类型没有确定的时候,不能创建实例化对象。如T t=new T();这样是编译错误的,首先是语法上不支持这样写,其次是实例化对象的分配内存的时候,实例化对象的内存多大应该是确定的,而T是不确定的,所以内存的大小也是不确定的。

引用(本文章只供本人学习以及学习的记录,如有侵权,请联系我删除)

Java 深度历险(五)——Java 泛型

深入理解 Java 泛型

你可能感兴趣的:(深入理解Java泛型)