泛型的基本概念
- 泛型: 参数化类型
- 参数:
- 定义方法时有形参
- 调用方法时传递实参
- 参数化类型: 将类型由原来的具体的类型参数化,类似方法中的变量参数
- 类型定义成参数形式, 可以称为类型形参
- 在使用或者调用时传入具体的类型,可以称为类型实参
- 参数:
- 泛型的本质是为了参数化类型
- 在不创建新的类型的情况下,通过泛型指定的不同类型来控制形参具体限制的类型
- 在泛型使用过程中,操作的数据类型被指定为一个参数,这种参数类型可以用在:
- 类 - 泛型类
- 接口 - 泛型接口
- 方法 - 泛型方法
- 泛型示例:
List arrayList = new ArrayList(); arrayList.add("aaaa"); arrayList.add(100); arrayList.forEach(i -> { String item = (String) arrayList.get(i); Log.d("泛型", "item = " + item); });
- 这样的写法会导致程序出现异常崩溃结束:
java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
- 这里的ArrayList可以存放任意类型,添加了一个String类型,添加了一个Integer类型,再使用时都以String的方式使用,因此程序崩溃
- 泛型就是解决这样的问题
- 再讨论另一种情况,如果将第一行声明初始的代码修改一下,那么在编译阶段就能发现问题:
List arrayList = new ArrayList<String>(); arrayList.add("aaaa"); arrayList.add(100); // 这一步在编译阶段,编译器就会报错 arrayList.forEach(i -> { String item = (String) arrayList.get(i); Log.d("泛型", "item = " + item); });
- 泛型只在编译阶段有效
可以发现,在编译过后,程序会采取去泛型化措施.也就是说,Java中的泛型,只在编译阶段有效.在编译过程中,正确检验泛型结果后,会将泛型的相关信息擦除,并且在对象进入和离开方法的边界处添加类型检查和类型转换方法
- 泛型类型在逻辑上可以看成多个不同的类型,实际上都是相同的基本类型
泛型的使用
- 泛型有三种使用方式:
- 泛型类
- 泛型接口
- 泛型方法
泛型类
- 泛型类: 泛型类型用于类定义中
- 通过泛型类可以完成对一组类的操作对外开发相同的接口
- 最典型的就是各种容器类:
- List
- Set
- Map
- 泛型类的最基本写法:
class 类名称 <泛型标识: 标识号,标识指定的泛型的类型> {
private 泛型标识 成员变量类型 成员变量名;
}
- 示例:
/*
* 这里的T可以为任意标识,通常使用T,E,K,V等形式的参数表示泛型
* 在实例化泛型时,必须指定T的具体类型
*/
public class Generic<T> {
// key这个成员变量的类型为T,T的类型由外部指定
private T key;
// 泛型构造方法形参key的类型也为T,T的类型由外部指定
public Generic(T key) {
this.key = key;
}
// 泛型构造方法getKey的返回值类型为T,T的类型由外部指定
public T getKey() {
}
}
- 泛型类中不一定要传入泛型类型的实参:
- 如果传入泛型实参,会根据传入的泛型实参做相应的限制,此时泛型才会起到本应起到的限制作用
- 如果不传如泛型类型的实参,在泛型类中使用泛型的方法或者成员变量的定义可以为任何类型
- 泛型的类型参数只能是类类型,不能是简单类型
- 不能对确切的泛型类型使用instanceof操作,编译时会出错
泛型接口
- 泛型接口与泛型类的定义及使用基本相同
- 泛型接口常常被用在各种类的生产器中
- 示例:
// 定义一个泛型接口
public interface Generator<T> {
public T next();
}
泛型通配符
- Integer是number的一个子类 ,Generic< Integer > 与**Generic< number ** 实际上是相同的一种类型
- 由此,产生如下问题:
- 在使用Generic< number > 作为形参的方法中,能否使用Generic< Integer > 的实例传入?
- 在逻辑上类似于Generic< number >和Generic< Integer >是否可以看成是具有父子关系的泛型类型呢?
由此可以看到Generic< Integer >不能看作是Generic< Number >的子类.
- 由此可见:
- 同一种泛型可以对应多个版本,因为参数类型是不确定的
- 不同版本的泛型类型实例是不兼容的
- 为了解决这样的问题,又不能为了定义一个新的方法来处理Generic< Integer >,这与Java中多态的理念违背.因此,需要一个在逻辑上可以表示同时是Generic< Integer >和Generic< Number >父类的引用类型.这样的类型就是类型通配符:
- 使用通配符表示泛型:
public void showKeyValueWildcard(Generic<?> obj) {
Log.d("泛型测试", "key value is" + obj.getKey());
}
- 类型通配符一般使用 ? 代替具体的类型实参:
- 此处的 ? 是类型实参, 而不是类型形参.
- 和Number,String,Integer一样,都是一种实际的类型
- 可以把 ? 看作是所有类型的父类,是一种真实的类型
- 类型通配符的使用场景:
- 当具体类型不确定的时候,这个通配符就是 ?
- 当操作类型时,不需要使用类型的具体功能,只使用Object类中的功能,那么可以使用 ? 通配符来表示未知的类型
泛型方法
- 泛型类: 在实例化类的时候指明泛型的具体类型
- 泛型方法: 在调用方法的时候指明泛型的具体类型
静态方法与泛型
- 注意在类中的静态方法使用泛型:
- 静态方法无法访问类上定义的泛型
- 如果静态方法操作的引用数据类型不确定的时候,必须要将泛型定义在方法上
- 如果静态方法要使用泛型的话,必须将静态方法定义成泛型方法:
public class StaticGenerator<T> {
...
...
/*
* 如果在类中定义使用泛型的静态方法,需要添加额外的泛型声明 - 将这个方法定义成泛型方法
* 否则会报错: StaticGenerator cannot be refrenced from static context
*/
public static <T> void show(T t) {
}
}
泛型方法总结
- 泛型方法能使方法独立于类而产生变化,使用原则:
- 无论何时,如果能做到,就尽量使用泛型方法
- 如果使用泛型方法将整个类泛型话,就应该使用泛型方法
- 对于一个static方法,无法访问泛型类型的参数.如果static方法要使用泛型,就必须使之成为泛型方法
泛型的上下边界
- 在使用泛型的时候,可以为传入的泛型类型实参进行上下边界的限制:
- 比如: 类型的实参只准传入某种类型的父类或者某种类型的子类
- 为泛型方法添加上边界,即传入的类型实参必须是指定类型的子类型
- 为泛型类添加上边界,即类中泛型必须是指定类型的子类型
- 从上面可以看出 : 泛型的上下边界添加,必须与泛型的声明在一起
泛型数组
- 在Java中,不能创建一个确切的泛型类型的数组
/*
* 这个数组创建的方式是不允许的
* List<String>[] ls = new ArrayList<String>[10];
*/
// 使用通配符创建泛型数组是可以的
List<?>[] ls = new ArrayList<?>[10];
// 下面的这个方法也是可以的
List<String> ls = new ArrayList[10];
- 由于JVM的擦除机制,在运行时JVM是不知道泛型信息的:
- 所有可以给oa[1] 赋值一个ArrayList却不会出现异常
- 但是在取出数据的时候要做一次类型转换,就会出现ClassCastException
- 如果可以进行泛型数组的声明,那么上面的这种情况在编译期将不会出现任何警告和错误,只有在运行时才会报错
- 通过对泛型数组的声明进行限制,对于这样的情况,可以在编译期提示代码有类型安全问题
- 数组的类型不可以是类型变量,除非是采用通配符的方式: 因为对于通配符的方式,最后取出数据是要做显式的类型转换的