掘金 后端 ( ) • 2024-04-09 18:03

theme: smartblue highlight: a11y-dark

12条通用编程原则✨全面提升Java编码规范性、可读性及性能表现

基于Effective Java 通用编程章节总结12条通用编程原则

从最基础的局部变量使用、控制结构编写,到集成类库、明智选择数据类型,慎用反射和本地方法,以及如何对待优化和遵循命名惯例等方面,全面提升编码的规范性、可读性以及性能表现

将局部变量作用域最小化

将局部变量作用域最小化可以增强可读性、可维护性,降低出错可能

要将局部变量作用域最小化,最好在第一次使用它时进行声明

几乎每个局部变量声明时都需要初始化

如果局部变量作用域只在循环中,那么for循环优于while循环

        //for循环局部变量作用域只在循环中
        for (Iterator iterator = list.iterator(); iterator.hasNext(); ) {
            System.out.println(iterator.next());
        }

        //while循环作用域更大
        Iterator<Integer> i1 = list.iterator();
        while (i1.hasNext()) {
            System.out.println(i1.next());
        }

并且使用for循环能够降低CV大法出错的可能性

        Iterator<Integer> i1 = list.iterator();
        while (i1.hasNext()) {
            System.out.println(i1.next());
        }

        Iterator<Integer> i2 = list.iterator();
        //CV大法忘记修改 i1
        while (i1.hasNext()) {
            System.out.println(i1.next());
        }

遇到方法实现代码较多导致局部变量作用域变大可以拆分方法

foreach循环优于for循环

foreach循环又叫增强for循环是一种语法糖,实际上使用迭代器和for循环实现

foreach循环能够隐藏迭代器和for循环中的索引(只展示元素)

如果只需要元素而不需要迭代器和索引,那么foreach会更加简洁,并且不会产生性能消耗

        List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6);
        //for循环
        for (Iterator iterator = list.iterator(); iterator.hasNext(); ) {
            System.out.println(iterator.next());
        }
        for (int index = 0; index < list.size(); index++) {
            System.out.println(list.get(index));
        }

        //foreach
        for (Integer integer : list) {
            System.out.println(integer);
        }

如果需要使用迭代器或索引那么就无法使用foreach

了解和使用类库

使用类库的好处是不必自己造轮子性能会提高(由专业的算法工程师实现)、会维护(随着版本升级,开发者提出的缺点会被解决)、让代码更易维护

优先使用JDK中的类库,无法满足需求时使用第三方类库如guava,三方类库也无法满足时再自己实现

精确计算避免使用浮点型

浮点型flout、double在计算时可能出现精度溢出

        //拥有的钱 1.00
        double funds = 1.00;
        int itemsBought = 0;
        //每次都买商品递增 0.1
        //每次价格 0.1、0.2、0.3、0.4 期望买四个商品 剩下的钱为0
        for (double price = 0.10; funds >= price; price += 0.10) {
            funds -= price;
            itemsBought++;
        }
        //3 items bought.
        System.out.println(itemsBought + " items bought.");
        //Change: $0.3999999999999999
        System.out.println("Change: $" + funds);

如果需要精确计算不要使用浮点型,可以转换为最小单位使用整形int、long计算(注意值不要超过范围)

        //去除小数、使用整形
        int funds = 100;
        int itemsBought = 0;
        for (int price = 10; funds >= price; price += 10) {
            funds -= price;
            itemsBought++;
        }
        //4 items bought.
        System.out.println(itemsBought + " items bought.");
        //Cash left over: 0 cents
        System.out.println("Cash left over: " + funds + " cents");

如果必须有小数计算或超过整形的范围上限可以使用BigDecimal计算

(BigDecimal不熟悉API就使用容易踩坑)

基本数据类型优于包装类

Java提供8种基本数据类型:boolean、byte、char、short、int、long、float、double

每种基本数据类型都有其对应的包装类(基本数据类型的对象版本)

基本数据类与包装类的区别:

  1. 包装类可以值相同但对象引用不同,比较要用equals (如果使用 == 可能会导致结果错误)

        Comparator<Integer> naturalOrder = (i, j) -> (i < j) ? -1 : (i == j ? 0 : 1);
        int result1 = naturalOrder.compare(new Integer(42), new Integer(42));
        // 1
        System.out.println(result1);
    
  2. 包装类还可以为null

  3. 基本数据类型更省时间、空间

基本数据类型与包装类混用时,会将包装类自动拆箱为基本数据类型

        Comparator<Integer> naturalOrder = (iBoxed, jBoxed) -> {
            //自动拆箱
            int i = iBoxed, j = jBoxed;
            return i < j ? -1 : (i == j ? 0 : 1);
        };
        int result2 = naturalOrder.compare(new Integer(42), new Integer(42));
        // 0
        System.out.println(result2);

如果基本数据类型需要转换为包装类使用,则会使用自动装箱

注意自动拆箱、装箱,循环操作会有大量性能开销

        Long sum = 0L;
        for (long i = 0; i < Integer.MAX_VALUE; i++) {
            sum += i;
            System.out.println(sum);
        }

当使用集合、反射时需要使用包装类,其他情况考虑使用基本数据类型

如果其他类型更适合,尽量避免使用字符串

字符串一般只用于文本存储

如果存储类型为整形数值、确定是否的布尔类型或是其他更适合该场景的类型,都避免使用字符串代替

字符串可能带来更大的开销,如存储数值99999

注意字符串拼接性能问题

使用字符串时注意拼接性能问题

需要频繁拼接使用StringBuilder,需要线程安全使用StringBuffer

通过接口引用对象

对于字段、局部变量、方法入参、方法返回值的声明,如果有合适的接口类型就用接口类型进行声明

接口类型能够更灵活,后续如果想增强功能、替换实现不需要太大改动 (注意使用的方法为实现类都共享的方法)

        Map<String, String> map = new HashMap<>();
        Map<String, String> map = new LinkedHashMap<>();

如果没有合适的接口类型,则尽量使用类结构上层的类,如抽象类、公共实现父类等

谨慎使用反射

使用反射会带来很多缺点:

  1. 失去编译期间的类型检查(运行时可能抛出异常)
  2. 反射实现的代码可读性不好且繁多
  3. 性能更差

使用反射能够在编译期间引用对应的接口,而不需要其实现类已经实现

比如编译时引用Set,运行时再去加载其实现类HashSet或TreeSet

        Class<? extends Set<String>> cl = (Class<? extends Set<String>>) Class.forName("java.util.HashSet");
//        Class<? extends Set<String>> cl = (Class<? extends Set<String>>) Class.forName("java.util.TreeSet");
        Constructor<? extends Set<String>> cons = cl.getDeclaredConstructor();
        Set<String> s = cons.newInstance();
        s.addAll(Arrays.asList("a", "aa", "bbb", "qqq", "asd"));
        //使用TreeSet:[a, aa, asd, bbb, qqq]
        //使用HashSet:[aa, qqq, a, bbb, asd]
        System.out.println(s);

使用JDBC获取MySQL的连接,在编译期间不需要MySQL实现JDBC连接的实现类

通过引用连接的接口Connection,运行时使用Class.forName加载实现类

类似这种服务提供者框架都可以使用反射来实现

谨慎使用本地方法

本地方法就是非Java(其他语言)实现的方法/函数

通过JNI/JNA使用本地方法时,无法得到JVM分配内存、垃圾回收等好处,稍有不小心就会导致出现错误,并且不好进行调试

如果非要使用本地方法一定要做好测试

谨慎进行优化

设计系统时考虑性能因素,不要为了性能而牺牲设计的合理结构

构建完系统后测试性能,如果未满足要求再去通过工具(如arthas)分析性能瓶颈,解决后再进行压测,直到满足要求

遵守普遍接受的命名规范

遵守常规的命名规范:包名小写、驼峰命名、常量大写...

可实例化的类通用单数名词,不可实例的工具类用复数 (Collection、Collections)

类型转换通用toXX(toString、toArray)

返回包装类对应基本类型通用xxValue (intValue、doubleValue)

命名规范尽量使用大家公认的方法,见名知意

总结

最好让局部变量作用域最小化,在第一次使用时声明,作用域只在循环时优先使用for循环

foreach是迭代器与for循环实现的语法糖,只展示元素屏蔽迭代器与索引,优先使用foreach

优先使用JDK、第三方类库,不要自己造轮子

精确计算不使用浮点型,可以转换为最小单位使用整形,如果必须要小数部分或计算量超出整形范围使用BigDecimal

只有集合、反射时才用包装类,其他情况使用基本数据类型,使用包装类注意空指针和用equals比较

字符串适合文本,其他类型合适避免使用字符串

字符串大量拼接时使用StringBuilder,线程安全使用StringBuffer

声明入参、变量、返回时使用接口声明更加灵活,如果没合适接口就选择抽象类/公共父类

反射会导致丢失编译类型检测、性能损耗、代码不好阅读,可以实现服务提供者框架,在编译期间引用接口,而不需要其实现类已经实现

使用本地方法会丢失JVM分配内存、垃圾回收等益处,尽量不用,如果使用要做好测试

设计系统时考虑性能,不要为了性能破坏合理的设计结构,性能不达标分析性能瓶颈再优化

遵守常规、公认的命名规范,见名知意

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

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

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

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

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