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

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

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

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

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

本文基于 Effective Java 泛型章节汇总出8条泛型相关习惯

image.png

不要使用原生态类型

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

相关文章
|
11天前
|
存储 安全 Java
Java Map新玩法:探索HashMap和TreeMap的高级特性,让你的代码更强大!
【10月更文挑战第17天】Java Map新玩法:探索HashMap和TreeMap的高级特性,让你的代码更强大!
33 2
|
4天前
|
XML 安全 Java
Java反射机制:解锁代码的无限可能
Java 反射(Reflection)是Java 的特征之一,它允许程序在运行时动态地访问和操作类的信息,包括类的属性、方法和构造函数。 反射机制能够使程序具备更大的灵活性和扩展性
15 5
Java反射机制:解锁代码的无限可能
|
6天前
|
存储 安全 Java
系统安全架构的深度解析与实践:Java代码实现
【11月更文挑战第1天】系统安全架构是保护信息系统免受各种威胁和攻击的关键。作为系统架构师,设计一套完善的系统安全架构不仅需要对各种安全威胁有深入理解,还需要熟练掌握各种安全技术和工具。
31 10
|
1天前
|
分布式计算 Java MaxCompute
ODPS MR节点跑graph连通分量计算代码报错java heap space如何解决
任务启动命令:jar -resources odps-graph-connect-family-2.0-SNAPSHOT.jar -classpath ./odps-graph-connect-family-2.0-SNAPSHOT.jar ConnectFamily 若是设置参数该如何设置
|
8天前
|
搜索推荐 Java 数据库连接
Java|在 IDEA 里自动生成 MyBatis 模板代码
基于 MyBatis 开发的项目,新增数据库表以后,总是需要编写对应的 Entity、Mapper 和 Service 等等 Class 的代码,这些都是重复的工作,我们可以想一些办法来自动生成这些代码。
19 6
|
8天前
|
Java
通过Java代码解释成员变量(实例变量)和局部变量的区别
本文通过一个Java示例,详细解释了成员变量(实例变量)和局部变量的区别。成员变量属于类的一部分,每个对象有独立的副本;局部变量则在方法或代码块内部声明,作用范围仅限于此。示例代码展示了如何在类中声明和使用这两种变量。
|
9天前
|
存储 Java API
优雅地使用Java Map,通过掌握其高级特性和技巧,让代码更简洁。
【10月更文挑战第19天】本文介绍了如何优雅地使用Java Map,通过掌握其高级特性和技巧,让代码更简洁。内容包括Map的初始化、使用Stream API处理Map、利用merge方法、使用ComputeIfAbsent和ComputeIfPresent,以及Map的默认方法。这些技巧不仅提高了代码的可读性和维护性,还提升了开发效率。
28 3
|
7天前
|
Java API
[Java]泛型
本文详细介绍了Java泛型的相关概念和使用方法,包括类型判断、继承泛型类或实现泛型接口、泛型通配符、泛型方法、泛型上下边界、静态方法中使用泛型等内容。作者通过多个示例和测试代码,深入浅出地解释了泛型的原理和应用场景,帮助读者更好地理解和掌握Java泛型的使用技巧。文章还探讨了一些常见的疑惑和误区,如泛型擦除和基本数据类型数组的使用限制。最后,作者强调了泛型在实际开发中的重要性和应用价值。
11 0
[Java]泛型
|
9天前
|
存储 Java 开发者
Java中的Map接口提供了一种优雅的方式来管理数据结构,使代码更加清晰、高效
【10月更文挑战第19天】在软件开发中,随着项目复杂度的增加,数据结构的组织和管理变得至关重要。Java中的Map接口提供了一种优雅的方式来管理数据结构,使代码更加清晰、高效。本文通过在线购物平台的案例,展示了Map在商品管理、用户管理和订单管理中的具体应用,帮助开发者告别混乱,提升代码质量。
18 1
|
11天前
|
Java
Java代码解释静态代理和动态代理的区别
### 静态代理与动态代理简介 **静态代理**:代理类在编译时已确定,目标对象和代理对象都实现同一接口。代理类包含对目标对象的引用,并在调用方法时添加额外操作。 **动态代理**:利用Java反射机制在运行时生成代理类,更加灵活。通过`Proxy`类和`InvocationHandler`接口实现,无需提前知道接口的具体实现细节。 示例代码展示了两种代理方式的实现,静态代理需要手动创建代理对象,而动态代理通过反射机制自动创建。