不定类型的公共框架
泛型
没有泛型时,只能用Object作为通用类型,引入泛型实现了类型约束和自动类型转换
比如List
可以接受任何对象,但是取出后需要自行转换,而List<E>
只能接受E类型的对象,并且取出后直接被转成E类型
泛型类与接口
以类为粒度的泛型,常见用法是实现定制参数类型, 返回值类型
1 | interface Sample<T> { |
方法泛型
可以脱离泛型类单独存在,常见用法是根据参数类型决定返回类型
1 | public <T> T method(T t) { |
不定类型返回值
特殊需求下,需要返回类型不固定
效果是自适应类型,按照赋值类型进行强制转换
1 | "unchecked") ( |
调用时看不到类型转换
只适合有明确约定确保类型匹配的场景,错误使用会导致强转失败
1 | String s = getValue("1"); |
List<?>
,List<Object>
和List
区别
表面上看,三者都可以容纳任何类型
List
不指定任何类型,称为Raw Type,同时具有读写能力,但是写入时会报Raw Type类型安全的警告List<Object>
明确了泛型类型,具备读写能力List<?>
通配符限制了写入能力(因为不确定类型,新增无法判定,删除可以),只能读取
不指定参数类型的方式只用于兼容没有泛型时的旧代码
从兼容上看,泛型可以接受老代码Raw类型赋值,编译器不会检查
1 | List<Object> list = new ArrayList(); |
如果都使用泛型,那么编译器检查类型,值得注意的是泛型赋值没有协变,类型不同就会报错
1 | List<Object> list = new ArrayList<Integer>(); //compile error |
使用Raw类型会丧失泛型功能,还有可能造成编译错误
- 无法调用泛型方法
1 | (ElementType.TYPE) |
- 无法类型转换
1 | public static List<Integer> method(List<String> param) { |
泛型约束
不能确定某个类型,但是能限制一些范围
考虑形状问题
1 | Element---Shape |
标准的赋值是父类可以容纳子类对象Shape s = square;
,不能反过来把父类对象赋给子类
extends
对象相容性
第一行声明表示列表中存的是某种形状,也就是说每个数据都是形状的子类实例,形状是上限
第二行赋值完全合法,因为都是容纳了某种具体形状
第三行报错,因为编译器不知道列表中到底存了啥类型,万一是正方形列表,那么就相当于让子类容纳了父类对象,不合法
1 | List<? extends Shape> list = new ArrayList<>(); |
extends
方便读取,适合作为方法返回值
容器相容性
根据声明这里链表是某种矩形子类,因此父类是不行的
1 | List<? extends Rectangle> list; |
super
对象相容性
第一行声明表示列表中存的是某种超越形状的存在,形状是下限
第二行赋值显然不合法,超越形状的数据显然不适合塞到形状对象里
之后的写入操作都是合法的,都符合子类加到父类容器
1 | List<? super Shape> list = new ArrayList<>(); |
super
方便写入,适合作为方法参数
容器相容性
根据声明这里链表是某种矩形父类,因此子类是不行的
1 | List<? super Rectangle> list; |
PECS(Producer Extends Consumer Super)原则
- 生产者输出内容,作为上游,使用extends方便下游读取
- 消费者写入内容,作为下游,使用super方便处理写入,作为下游的List
Collections类的copy方法使用了两种泛型约束声明
- 数据来源用于读取,使用extends
- 数据目的用于写入,使用super
1 | public static <T> void copy(List<? super T> dest, List<? extends T> src) { |
泛型类和泛型方法约束
针对泛型类和泛型方法,即类声明时的尖括号和方法前的尖括号,只能出现上界extends,不能出现下界super
指定上界后,类型擦除后就是上界,可以约束参数类型,可以访问上界中的方法,而下界擦除后就是Object,没什么好处
多个约束
使用&
可以加多个约束
1 | interface Sample<T extends Serializable & Comparable> { |
类型擦除
泛型在编译期进行约束,可以静态检查,但是考虑到老代码兼容性,类型信息在真正运行时会被抹去
虽然两个列表看上去是不同类型的,但是运行时都是加载同一个类java.util.ArrayList,没有类型信息
1 | List<Integer> intList = new ArrayList<>(); |
重载失败,因为类型抹掉后参数类型是一样的
1 | public void method(List<Integer> list) { |
类型擦除后,如果泛型参数没有上界,就用Object替代,如果有,就用第一个上界参数替代
运行时获取泛型类型
由于类型擦除,泛型类的方法是不能直接拿到泛型类型的,泛型类型只是一个类型参数,并不是真正的类
1 | class Sample<T> { |
方法1,既然拿不到,那就直接构造传入
1 | class Sample<T> { |
调用时有点冗余的感觉
1 | String s = new Sample<String>(String.class).newInstance(); |
方法2,如果有子类,可以利用抽象类提供额外方法,返回泛型类型
代价是每个子类实现额外的方法
1 | abstract class Sample<T> { |
方法3, 如果有子类,可以利用获取父类泛型参数的方法
原理是父类调用this实际上是子类对象
1 | abstract class Sample<T> { |
实现根据参数创建实例
1 | String s = new Child<String>().newInstance(); |
如果根本没有显式子类,还可以直接以匿名子类的形式,调用时有个空的实现
1 | Sample<String> p = new Sample<String>(){}; |
解析泛型参数,还可以采用Spring
的工具类GenericTypeResolver
1 | abstract class Sample<T> { |
集合间继承关系
虽然String是Object的子类,Object列表也显然能容纳String对象,但是直接把String列表进行赋值会报错
也就是说类型不同的集合不能直接赋值,因为两个列表之间没有直接的关系,传参时候要注意
1 | List<Object> objList = new ArrayList<>(); |
如果想赋值,需要建立容器间关系。使用通配符描述类型,字符串列表相当于类型具体化,在逻辑上等同子类
1 | List<? extends Object> someKindList = new ArrayList<>(); //等同于List<?> |
泛型数组
不能创建出泛型数组
1 | List<String>[] listArray = new ArrayList<String>[10]; //error |
可以使用无限定类型方式,但是牺牲了写入能力
1 | List<?>[] listArray = new ArrayList<?>[10]; |
枚举
描述enum类型的类定义为Class<? extends Enum<?>>
1 | public abstract class Enum<E extends Enum<E>> { |
定义枚举类型Color,内部会编译成
1 | class Color extends Enum<Color> { |
这种内部约束规定了泛型类型必须是枚举,不能用随便一个类作为泛型类型
泛型与静态
泛型类型擦除,相同类的不同泛型在底层是同一个类,因此静态变量只有一份
1 | class Sample<T> { |
静态只跟类有关,而类的不同实例可能采用不同泛型,因此泛型类的静态成员不能出现泛型
1 | class Sample<T> { |
静态内类和外围无关,因此外围的泛型无法作用于内类
1 | class Sample<T> { |
版权声明
This site by Linest is licensed under a Creative Commons BY-NC-ND 4.0 International License.
由Linest创作并维护的博客采用创作共用保留署名-非商业-禁止演绎4.0国际许可证。
本文永久链接:http://linest.github.io/2018/02/09/java-generic-design/