掘金 后端 ( ) • 2024-03-27 18:56

theme: smartblue highlight: a11y-dark

掌握8条泛型规则,打造优雅通用的Java代码

在Java开发中泛型以其类型安全和代码复用的特性扮演着关键角色

掌握好泛型能够确保类型安全、提升代码复用、降低维护成本,打造出优雅通用的代码

本文基于 Effective Java 泛型章节汇总出8条泛型相关习惯(文末附案例链接)

临时 (1).jpg

不要使用原生态类型

在早期的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);

如果想要添加对象则要使用有限制通配符 <? extends X> 或 <? super X>

在某些情况下只能使用原生态泛型:

  1. 兼容历史版本
  2. 获取Class对象时只能使用原生态泛型(由于泛型运行时会擦除,因此不能通过泛型获取Class对象)
        //合法
        Class<List> listClass = List.class;

        //不合法
        List<Object>.class
  1. 使用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

如果必须要使用数组,并且搭配泛型满足通用性,那么可以考虑使用以下两种方案:

  1. 定义泛型数组,实例化由Object数组进行强转
  2. 使用Object数组存储,读取数据时再强转为泛型

考虑使用泛型方法,它能够给方法带来通用性、安全、灵活

有限通配符能够提升灵活性,上限通配符只允许读不允许写、下限通配符允许写和只允许读Object

使用有限通配符时遵循PECS原则,生产使用上限通配符、消费使用下限通配符

泛型与可变参数一起使用时,确保类安全要用注解@SafeVarargs

如果想存储多个不同类型对象,考虑使用泛型Class<?>作为Key,存储对象作为Value

最后(不要白嫖,一键三连求求拉~)

本篇文章被收入专栏 Effective Java,感兴趣的同学可以持续关注喔

本篇文章笔记以及案例被收入 Gitee-CaiCaiJavaGithub-CaiCaiJava 感兴趣的同学可以stat下持续关注喔~

有什么问题可以在评论区交流,如果觉得菜菜写的不错,可以点赞、关注、收藏支持一下~

关注菜菜,分享更多干货,公众号:菜菜的后端私房菜