一个示例
先看一段错误的代码
- public class GenericTest1 {
- public static void main(String[] args) {
- List arrayList = new ArrayList();
- arrayList.add("aaaa");
- arrayList.add(100);
- for (int i = 0; i <arrayList.size(); i++) {
- String item = (String) arrayList.get(i);
- System.out.println("测试" + "item =" + item);
- }
- }
- }
毫无疑问, 程序的运行结果会以崩溃结束:
测试 item = aaaa
- Exception in thread "main" java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
- at com.boxuegu.intermediate.language.sugar.generic.GenericTest1.main(GenericTest1.java:17)
ArrayList 可以存放任意类型, 例子中添加了一个 String 类型, 添加了一个 Integer 类型, 再使用时都以 String 的方式使用, 因此程序崩溃了.
为了解决类似这样的问题(在编译阶段就可以解决), 泛型应运而生.
泛型概念
泛型, 即 "参数化类型".
一提到参数, 最熟悉的就是定义方法时有形参, 然后调用此方法时传递实参. 那么参数化类型怎么理解呢?
顾名思义, 就是将类型由原来的具体的类型参数化, 类似于方法中的变量参数, 此时类型也定义成参数形式(可以称之为类型形参), 然后在使用 / 调用时传入具体的类型(类型实参).
泛型的本质是为了参数化类型(在不创建新的类型的情况下, 通过泛型指定的不同类型来控制形参具体限制的类型). 也就是说在泛型使用过程中, 操作的数据类型被指定为一个参数, 这种参数类型可以用在类, 接口和方法中, 分别被称为泛型类, 泛型接口, 泛型方法.
泛型定义
泛型类的定义形式如下:
class name<T1, T2, ..., Tn> { /* ... */ }
类型参数部分被一对尖括号 (<>) 划分, 紧跟类名, 它指定了类型参数(也叫作类型变量)T1, T2, ...., 和 Tn.
泛型示例
我们将第一行声明初始化 list 的代码更改一下, 编译器会在编译阶段就能够帮我们发现类似这样的问题.
- public class GenericTest2 {
- public static void main(String[] args) {
- List<String> arrayList = new ArrayList<String>();
- arrayList.add("aaaa");
- //arrayList.add(100); // 在编译阶段, 编译器就会报错
- for (int i = 0; i <arrayList.size(); i++) {
- String item = (String) arrayList.get(i);
- System.out.println("测试" + "item =" + item);
- }
- }
- }
泛型分类
泛型类
- public class GenericTest4<T> {
- T field;
- }
尖括号 <> 中的 T 被称作是类型参数, 用于指代任何类型. 事实上, T 只是一种习惯性写法. 但出于规范的目的, Java 还是建议我们用单个大写字母来代表类型参数. 常见的如:
T 代表一般的任何类.
E 代表 Element 的意思, 或者 Exception 异常的意思.
K 代表 Key 的意思.
V 代表 Value 的意思, 通常与 K 一起配合使用.
S 代表 Subtype 的意思.
如果一个类被 <T> 的形式定义, 那么它就被称为是泛型类.
在对泛型类创建实例的时候, 在尖括号中赋值相应的类型便是. T 就会被替换成对应的类型, 如 String 或者是 Integer.
- GenericTest4<String> test1 = new GenericTest4<String>();
- GenericTest4<Integer> test2 = new GenericTest4<Integer>();
泛型类也可接受多个类型参数.
diamond
在 Java SE 7 及以后版本, 可以省去类型参数调用泛型类的构造函数, 用一个空的类型参数(<>), 编译器可以通过上下文决定, 或推测 type arguments, 这个尖括号非正式得叫作 diamond(钻石), 你可以这样创建 Box<Integer > 的一个实例:
GenericTest4<String> test1 = new GenericTest4<>();
Type Parameter 和 Type Argument 术语(Terminology):
很多开发者交换使用这个两个术语, 但是这两个术语并不同. 敲代码时,
type argument 创建一个参数化类型, 因此, GenericTest4<T > 中的 T 是 type parameter,GenericTest4<String> 中的 String 是一个 type argument.
泛型方法
- public class GenericTest5 {
- public <T> void testMethod1(T t) {
- }
- public <T> T testMethod2(T t) {
- return null;
- }
- }
泛型方法与泛型类不同的地方是, 类型参数 (Type Parameter) 也就是尖括号那一部分是写在返回值前面的.<T> 中的 T 被称为类型参数, 而方法中的 T 被称为参数化类型(Type Argument), 它不是运行时真正的参数. 声明的类型参数, 也是可以当作返回值的类型的.
泛型类与泛型方法共存
- public class GenericTest6<T> {
- public static void main(String[] args) {
- GenericTest6<String> test = new GenericTest6<>();
- test.testMethod1("generic test1");
- Integer i = test.testMethod2(new Integer(1));
- }
- public void testMethod1(T t) {
- System.out.println(t.getClass().getName());
- }
- public <T> T testMethod2(T t) {
- System.out.println(t.getClass().getName());
- return t;
- }
- }
输出结果
- java.lang.String
- java.lang.Integer
上面代码中, GenericTest6<T> 是泛型类, testMethod1 是泛型类中的普通方法, 而 testMethod2 是一个泛型方法. 而泛型类中的类型参数与泛型方法中的类型参数是没有相应的联系的, 泛型方法始终以自己定义的类型参数为准.
但是, 为了避免混淆, 如果在一个泛型类中存在泛型方法, 那么两者的类型参数最好不要同名. 比如, GenericTest6<T> 代码可以更改为这样:
- public <E> E testMethod3(E e){
- return e;
- }
泛型接口
泛型接口和泛型类相差不大
- public interface GenericTest7<T> {
- }
通配符
除了用 <T> 表示泛型外, 还有 <?> 这种形式, ? 被称为通配符.
场景介绍
- public class GenericTest8 {
- public static void main(String[] args) {
- Sub sub = new Sub();
- Base base = sub;
- }
- }
- class Base {
- }
- class Sub extends Base {
- }
上面代码显示, Base 是 Sub 的父类, 它们之间是继承关系, 所以 Sub 的实例可以给一个 Base 引用赋值
- List<Sub> lsub = new ArrayList<>();
- List<Base> lbase = lsub;// 编译错误
最后一行代码不会编译通过, Sub 是 Base 的子类, 不代表 List<Sub> 和 List<Base> 有继承关系.
但是, 在现实编码中, 确实有这样的需求, 希望泛型能够处理某一范围内的数据类型, 比如某个类和它的子类, 对此 Java 引入了通配符这个概念.
所以, 通配符的出现是为了指定泛型中的类型范围.
通配符形式
通配符有 3 种形式.
- <?> 被称作无限定的通配符.
- <? extends T> 被称作有上限的通配符.
- <? super T> 被称作有下限的通配符.
限定通配符 <?>
- public void testMethod1(Collection<?> collection){
- }
上面的代码中, 方法内的参数是被无限定通配符修饰的 Collection 对象, 它隐略地表达了一个意图或者可以说是限定, 那就是 testMethod1() 这个方法内部无需关注 Collection 中的真实类型, 因为它是未知的. 所以, 只能调用 Collection 中与类型无关的方法. 当 <?> 存在时, Collection 对象丧失了 add() 方法的功能, 编译器不通过.
image.PNG
同样如下的代码也编译错误:
- public void testMethod2() {
- List<?> wildList=new ArrayList<String>();
- wildList.add(123);// 编译错误
- wildList.add("123");// 编译错误
- }
- <?> 提供了只读的功能, 也就是它删减了增加具体类型元素的能力, 只保留与具体类型无关的功能. 它不管装载在这个容器内的元素是什么类型, 它只关心元素的数量, 容器是否为空.
有上限的通配符<? extends T>
- <?> 代表着类型未知, 但是我们的确需要对于类型的描述再精确一点, 我们希望在一个范围内确定类别, 比如类型 A 及 类型 A 的子类都可以.
- public void testMethod3(Collection<? extends BaseClass> collection) {
- collection.add(new BaseClass());// 编译错误
- collection.add(new SubClass());// 编译错误
- }
上面代码中, collection 参数只接受 Base 及 Base 的子类的类型. 但是, 它仍然丧失了写操作的能力.
有下限的通配符<? super T>
这个和 <? extends T> 相对应, 代表 T 及 T 的超类.<? super T> 拥有一定程度的写操作的能力.
- public void testMethod4(Collection<? super SubClass> collection) {
- collection.add(new BaseClass());// 编译错误
- collection.add(new SubClass());// 编译通过
- }
通配符与类型参数的区别
一般而言, 通配符能干的事情都可以用类型参数替换. 比如
public void testWildCards(Collection<?> collection){}
可以被
public <T> void test(Collection<T> collection){}
取代. 如果用泛型方法来取代通配符, 那么上面代码中 collection 是能够进行写操作的. 只不过要进行强制转换.
- public <T> void test(Collection<T> collection){
- collection.add((T)new Integer(12));
- collection.add((T)"123");
- }
类型参数适用于参数之间的类别依赖关系:
- public class Test2 <T,E extends T>{
- T value1;
- E value2;
- }
- public <D,S extends D> void test(D d,S s){
- }
E 类型是 T 类型的子类, 显然这种情况类型参数更适合.
有一种情况是, 通配符和类型参数一起使用.
- public <T> void test(T t,Collection<? extends T> collection){
- }
类型擦除
这就是一个解语法糖的过程. 因为对于 Java 虚拟机来说, 他根本不认识 Map<String, String> map 这样的语法. 需要在编译阶段通过类型擦除的方式进行解语法糖. 可以通过如下代码来证明:
- public class GenericTest3 {
- public static void main(String[] args) {
- List<String> stringArrayList = new ArrayList<String>();
- List<Integer> integerArrayList = new ArrayList<Integer>();
- Class classStringArrayList = stringArrayList.getClass();
- Class classIntegerArrayList = integerArrayList.getClass();
- if (classStringArrayList.equals(classIntegerArrayList)) {
- System.out.println("泛型测试: 类型相同");
- }
- }
- }
泛型测试: 类型相同.
通过上面的例子可以证明, 在编译之后程序会采取去泛型化的措施. 也就是说 Java 中的泛型, 只在编译阶段有效. 在编译过程中, 正确检验泛型结果后, 会将泛型的相关信息擦出, 并且在对象进入和离开方法的边界处添加类型检查和类型转换的方法. 也就是说, 泛型信息不会进入到运行时阶段.
对此总结成一句话: 泛型类型在逻辑上看以看成是多个不同的类型, 实际上都是相同的基本类型.
虚拟机中没有泛型, 只有普通类和普通方法, 所有泛型类的类型参数在编译时都会被擦除, 泛型类并没有自己独有的 Class 类对象. 比如并不存在 List<String>.class 或是 List<Integer>.class, 而只有 List.class.
泛型的好处
与普通的 Object 代替一切类型这样简单粗暴而言, 泛型使得数据的类别可以像参数一样由外部传递进来. 它提供了一种扩展能力. 它更符合面向抽象开发的软件编程宗旨.
当具体的类型确定后, 泛型又提供了一种类型检测的机制, 只有相匹配的数据才能正常的赋值, 否则编译器就不通过. 所以说, 它是一种类型安全检测机制, 一定程度上提高了软件的安全性防止出现低级的失误.
泛型提高了程序代码的可读性, 不必要等到运行的时候才去强制转换, 在定义或者实例化阶段, 因为 Cache<String> 这个类型显化的效果, 程序员能够一目了然猜测出代码要操作的数据类型.
来源: http://www.jianshu.com/p/e4982dd2c870