掌握8条泛型规则,打造优雅通用的Java代码
掌握8条泛型规则,打造优雅通用的Java代码
在Java开发中泛型以类型安全和代码复用的特性扮演着关键角色
掌握好泛型能够确保类型安全、提升代码复用、降低维护成本,打造出优雅通用的代码
本文基于 Effective Java 泛型章节汇总出8条泛型相关习惯
不要使用原生态类型
在早期的JDK中,从集合中获取对象时都需要强制转换
如果在添加对象时,不小心将不同类型的对象加入集合,那么获取对象强制转换时会发生报错
这种报错并不会在编译期间提示,而是运行时才会出现
//原生态泛型
List list = new ArrayList();
//加入时不会报错
list.add("123");
list.add(456);
Iterator iterator = list.iterator();
while (iterator.hasNext()) {
//读数据 强转时报错
String next = (String) iterator.next();
System.out.println(next);
}
在JDK 5 后加入泛型,使用泛型可以指定对象的类型,在编译期将泛型擦除并完成强制转换
在编译期间当发生这种情况时会在编译期间报错,从而尽早的发现错误
为了对历史版本兼容,也可以不需要指定泛型,这种情况称为原生态泛型
原生态泛型只是为了兼容历史版本,它会丢失使用泛型的所有优点:安全(提早报错)、灵活(不需要手动强转)
当无法预估集合中对象的类型时,可以使用泛型Object或无限制通配符<?>
如果使用泛型Object则可以存放任何对象,因为Object是所有类的父类
但是对象从集合中取出时,只能转换为Object,如果需要转换为其他类型则还是需要强制转换
List<Object> list = new ArrayList<>();
list.add("123");
list.add(456);
list.add(new int[]{});
Iterator iterator = list.iterator();
while (iterator.hasNext()) {
Object next = iterator.next();
System.out.println(next);
}
如果使用无限制通配符<?>,则无法添加对象
List<?> list = new ArrayList<>();
//编译报错
// list.add("123");
// list.add(456);
如果想要添加对象则要使用有限制通配符 <? super X>
在某些情况下只能使用原生态泛型:
- 兼容历史版本
- 获取Class对象时只能使用原生态泛型(由于泛型运行时会擦除,因此不能通过泛型获取Class对象)
//合法
Class<List> listClass = List.class;
//不合法
List<Object>.class
- 使用interface时只能使用原生态泛型(因为运行时已经将类型擦除)
List<Object> arrayList = new ArrayList<>();
//合法
if (arrayList instanceof List){
List<?> lists = arrayList;
}
//不合法
if (arrayList instanceof List<Object>){
}
消除非受检的警告
不合理的使用泛型会出现抑制警告,抑制警告并不影响编译,但消除抑制警告泛型会越用越好
类型需要使用<>
List<Integer> integer = new ArrayList<>();
//未检查赋值
List<Integer> integers = new ArrayList();
当需要进行转换能够确保不会出现错误,可以使用@SuppressWarnings注解并说明理由进行抑制警告(作用范围越小越好)
List list = new ArrayList();
list.add(1);
list.add(11);
list.add(111);
@SuppressWarnings("确保list中类型为Integer")
List<Integer> integerList = list;
列表优于数组
数组只提供运行时安全,并未提供编译时安全
Object[] objects = new Long[2];
//运行时 ArrayStoreException
objects[0] = "1233123";
使用数组时不能使用泛型,运行时报错泛型数组,但是使用无限制通配符?的泛型是允许的
//允许
List<?>[] lists2 = new List<?>[2];
//报错 创建泛型数组
List<Integer>[] lists = new List<Integer>[5];
当泛型与数组混用时应该使用列表代替数组
平时使用也应该优先使用列表,因为它能够得到使用泛型的好处
优先考虑泛型
部分情况下是无法使用列表的而必须使用数组的,比如实现列表时需要使用数组
在这种情况下为了通用性也会选择使用泛型,但需要注意无法创建泛型数组
第一种方案:定义泛型数组,实例化时使用Object数组强制转换
public class Stack<E> {
private E[] elements;
private int size = 0;
private static final int DEFAULT_INITIAL_CAPACITY = 16;
@SuppressWarnings("unchecked")
public Stack() {
elements = (E[]) new Object[DEFAULT_INITIAL_CAPACITY];
}
}
这种方案在强制时会有抑制警告,需要保证强转时是安全的(不要泄漏引用)
第二种方案:使用Object数组,读取数据时进行强转(ArrayList就是使用的这种方案)
transient Object[] elementData;
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
this.elementData = EMPTY_ELEMENTDATA;
} else {
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}
在读取数据时强转,使用注解禁止抑制警告
public E get(int index) {
rangeCheck(index);
return elementData(index);
}
@SuppressWarnings("unchecked")
E elementData(int index) {
return (E) elementData[index];
}
把组件设计成泛型的更安全,如果即要使用泛型又要使用数组可以参考以上两种方案
优先考虑泛型方法
使用泛型方法的好处:安全、调用方法不需要强转、提升通用性
比如策略工厂中通过key获取不同的策略实现
public static <T extends Strategy> T getStrategyByKey(String key) {
Strategy strategy = strategyFactory.get(key);
if (Objects.isNull(strategy)) {
return null;
}
//确保工厂中的实现类都实现策略,否则强转会抛出异常
return (T) strategy;
}
使用泛型方法后强转为泛型T,其中T需要实现策略
并且要使用注解抑制警告,确保工厂strategyFactory中的value都实现策略,否则强转会抛出异常
利用有限通配符提升API灵活性
有限通配符分为上限、下限通配符
上限通配符:? extends T
确定上限为类型T,但不确定下限,只能读不能写
// 上限通配符
List<? extends Number> numbers = new ArrayList<>();
//无法写
//numbers.add(1);
numbers = Arrays.asList(1,2,3);
//只能读
numbers.forEach(System.out::println);
下限通配符:? super T
确定下限为类型T,上限为Object,可以读写,但只能读到Object
// 下限通配符
List<? super Number> superNumbers = new ArrayList<>();
superNumbers.add(new Integer(123));
superNumbers.add(new Long(123));
superNumbers.add(new BigDecimal("123.33"));
//只能读到Object类型
for (Object o : superNumbers) {
System.out.println(o);
}
由上限决定是否能读,由下限决定是否能写
遵循PECS(Producer Extends, Consumer Super)原则,生产(读)使用extends,消费(写)使用 super
这里的生产、消费是对集合来说的,读取数据时相当于提供给外界,写数据相当于消费外界传入的数据
泛型和可变参数谨慎同时使用
可变参数是一种语法糖,实际上会转换为数组
当泛型与可变参数同时使用时,实际上可以理解为泛型数组
但是JDK允许这么使用,在很多JDK方法中也会这么去使用,但会使用注解@SafeVarargs标识类型安全
@SafeVarargs
@SuppressWarnings("varargs")
public static <T> List<T> asList(T... a) {
return new ArrayList<>(a);
}
在这种情况下,编译能够通过,但运行时会报错
static void dangerous(List<String>... stringLists) {
List<Integer> intList = Arrays.asList(42);
Object[] objects = stringLists;
// Heap pollution
objects[0] = intList;
// 报错ClassCastException
String s = stringLists[0].get(0);
}
可变参数与泛型同时使用可能会造成类型转换失败,如果确保类型安全则使用注解@SafeVarargs
优先考虑类型安全异构容器
在集合中使用泛型会固定集合存储的对象类型
如果需要存储多个不同类型的对象时,可以考虑使用泛型Class<?>作为Key、Value存储对象的方式构建类型安全异构容器
private Map<Class<?>, Object> map = new HashMap<>();
public <T> void put(Class<T> type, T instance) {
map.put(Objects.requireNonNull(type), instance);
}
public <T> T get(Class<T> type) {
return type.cast(map.get(type));
}
需要注意的是Class的泛型不能是List<?>,因为获取Class对象时泛型被擦除
IsomerismTest f = new IsomerismTest();
f.put(String.class, "Java");
f.put(Class.class, IsomerismTest.class);
f.put(Double[].class, new Double[]{1.1, 2.2});
//Java
String string = f.get(String.class);
System.out.println(string);
//IsomerismTest
Class<?> cClass = f.get(Class.class);
System.out.println(cClass.getSimpleName());
//1.1
//2.2
Double[] doubles = f.get(Double[].class);
for (Double aDouble : doubles) {
System.out.println(aDouble);
}
总结
使用泛型能够指定对象类型,在编译期间进行类型擦除并强制转换为对应类型
除了兼容历史版本、获取Class对象、使用interface三种情况只能使用原生态类型,其他情况下都建议使用泛型
泛型能够带来安全、灵活的特点,当无法预估对象类型时可以使用<Object>或无限制通配符<?>
使用泛型可能带来警告,需要确保类转换安全,使用注解@SuppressWarnings抑制警告并说明理由
列表能够使用泛型,列表与数组选型时优先使用列表List
如果必须要使用数组,并且搭配泛型满足通用性,那么可以考虑使用以下两种方案:
- 定义泛型数组,实例化由Object数组进行强转
- 使用Object数组存储,读取数据时再强转为泛型
考虑使用泛型方法,它能够给方法带来通用性、安全、灵活
有限通配符能够提升灵活性,上限通配符只允许读不允许写、下限通配符允许写和只允许读Object
使用有限通配符时遵循PECS原则,生产使用上限通配符、消费使用下限通配符
泛型与可变参数一起使用时,确保类安全要用注解@SafeVarargs
如果想存储多个不同类型对象,考虑使用泛型Class<?>作为Key,存储对象作为Value
最后(不要白嫖,一键三连求求拉~)
本篇文章被收入专栏 Effective Java,感兴趣的同学可以持续关注喔
本篇文章笔记以及案例被收入 Gitee-CaiCaiJava、 Github-CaiCaiJava 感兴趣的同学可以stat下持续关注喔~
有什么问题可以在评论区交流,如果觉得菜菜写的不错,可以点赞、关注、收藏支持一下~
关注菜菜,分享更多干货,公众号:菜菜的后端私房菜
- 点赞
- 收藏
- 关注作者
评论(0)