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

举报
菜菜的后端私房菜 发表于 2024/07/26 09:34:59 2024/07/26
【摘要】 掌握8条泛型规则,打造优雅通用的Java代码在Java开发中泛型以类型安全和代码复用的特性扮演着关键角色掌握好泛型能够确保类型安全、提升代码复用、降低维护成本,打造出优雅通用的代码本文基于 Effective Java 泛型章节汇总出8条泛型相关习惯 不要使用原生态类型在早期的JDK中,从集合中获取对象时都需要强制转换如果在添加对象时,不小心将不同类型的对象加入集合,那么获取对象强制转换时...

掌握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>

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

  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三种情况只能使用原生态类型,其他情况下都建议使用泛型

泛型能够带来安全、灵活的特点,当无法预估对象类型时可以使用<Object>或无限制通配符<?>

使用泛型可能带来警告,需要确保类转换安全,使用注解@SuppressWarnings抑制警告并说明理由

列表能够使用泛型,列表与数组选型时优先使用列表List

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

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

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

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

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

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

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

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

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

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

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

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

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。