掌握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三种情况只能使用原生态类型,其他情况下都建议使用泛型
泛型能够带来安全、灵活的特点,当无法预估对象类型时可以使用或无限制通配符<?>
使用泛型可能带来警告,需要确保类转换安全,使用注解@SuppressWarnings抑制警告并说明理由
列表能够使用泛型,列表与数组选型时优先使用列表List
如果必须要使用数组,并且搭配泛型满足通用性,那么可以考虑使用以下两种方案:
- 定义泛型数组,实例化由Object数组进行强转
- 使用Object数组存储,读取数据时再强转为泛型
考虑使用泛型方法,它能够给方法带来通用性、安全、灵活
有限通配符能够提升灵活性,上限通配符只允许读不允许写、下限通配符允许写和只允许读Object
使用有限通配符时遵循PECS原则,生产使用上限通配符、消费使用下限通配符
泛型与可变参数一起使用时,确保类安全要用注解@SafeVarargs
如果想存储多个不同类型对象,考虑使用泛型Class<?>作为Key,存储对象作为Value
最后(不要白嫖,一键三连求求拉~)
本篇文章被收入专栏 Effective Java,感兴趣的同学可以持续关注喔
本篇文章笔记以及案例被收入 Gitee-CaiCaiJava、 Github-CaiCaiJava 感兴趣的同学可以stat下持续关注喔~
有什么问题可以在评论区交流,如果觉得菜菜写的不错,可以点赞、关注、收藏支持一下~
关注菜菜,分享更多干货,公众号:菜菜的后端私房菜