• 06.避免创建不必要的对象


    前言

    《Effective Java》中文第三版,是一本关于Java基础的书,这本书不止一次有人推荐我看。其中包括我很喜欢的博客园博主五月的仓颉,他曾在自己的博文《给Java程序猿们推荐一些值得一看的好书》中也推荐过。加深自己的记忆,同时向优秀的人看齐,决定在看完每一章之后,都写一篇随笔。如果有写的不对的地方、表述的不清楚的地方、或者其他建议,希望您能够留言指正,谢谢。

    《Effective Java》中文第三版在线阅读链接:https://github.com/sjsdfg/effective-java-3rd-chinese/tree/master/docs/notes

    是什么

    不必要的对象,指的是当我们需要一个对象的时候,它的功能与之前创建过的对象时相同的,那么我们可以重用之前的对象,而不是去创建一个新的。如果此时我们仍创建一个新的对象,那么它就是不必要的对象。

    '对象是不可变的',在这样的前提条件下,那它总是可以被重用的。

    哪里用

    • 正则表达式
    • 自动装箱
    • 初始化配置

    怎么实现

    我们针对上方‘哪里用’中指的地方,一一列举实例,首先是正则表达式中的实现,我们先来看看它每次都会创建不必要的对象的情况,代码如下:

    /**
     *使用正则表达式来判断字符串中是否包含有罗马数字
     *
     * @Author GongGuoWei
     * @Email GongGuoWei01@yeah.net
     * @Date 2020/1/14
     */
    public class RomanNumerals {
        static boolean isRomanNumeral(String s) {
            return s.matches("^(?=.)M*(C[MD]|D?C{0,3})"
                    + "(X[CL]|L?X{0,3})(I[XV]|V?I{0,3})$");
        }
    }

    我们使用String.matches方法,来检查字符串是否与正则表达式匹配,但它不适合在性能临界的情况下重复使用,因为matches的内部为正则表达式创建了一个Pattern实例,并且只使用它一次,它就有资格被进行垃圾收集。创建Pattern实例的代价是昂贵的,因为Patter需要将正则表达式编译成有限状态机。

    为了提高性能,我们将它作为类初始化的一部分,将正则表达式显式编译为一个Pattern实例(不可变),缓存它,并在isRomanNumeral 方法的每个调用中重复使用相同的实例,代码如下:

    /**
     * @Author GongGuoWei
     * @Email GongGuoWei01@yeah.net
     * @Date 2020/1/14
     */
    public class RomanNumerals02 {
        private static final Pattern ROMAN = Pattern.compile(
                "^(?=.)M*(C[MD]|D?C{0,3})"
                        + "(X[CL]|L?X{0,3})(I[XV]|V?I{0,3})$");
    
        static boolean isRomanNumeral(String s) {
            return ROMAN.matcher(s).matches();
        }
    }

    如果经常调用,我们上方的改进版本的性能会显著提升。速度提高了6.5倍。性能上不仅有所提升,而且为之前不可见的Pattern实例创建了一个fianl修饰的属性,并允许给它一个名字,这个名字比正则表达式本身更具有可读性。但是,如果包含isRomanNumeral的类被初始化,但是从未被调用,则ROMAN属性没必要初始化。我们可以通过延时初始化属性来排除初始化,但一般不建议这么做。因为延时初始化会导致实现复杂化,而性能也没有衡量的改进空间。

    下面我们继续看看,自动装箱时,我们怎么避免。我们都知道,Java允许混用基本类型和包装类型,自动进行装箱和拆箱。但是我们不要模糊的同时使用,例如下面的例子,我们需要计算所有正整数的总和,代码如下:

        private static long sum() {
            Long sum = 0L;
            for (long i = 0; i <= Integer.MAX_VALUE; i++) {
                sum += i;
            }
            return sum;
        }

    这段代码运行的结果是正确的,但是我们却写错一个字符,将变量sum的long,写成了Long。这意味着程序大约构造了2的31次方不必要的Long实例。当我们把sum变量类型改为long时,在我的机器上运行时间从5.5秒降低到0.42秒!!!优先使用基本类型而不是装箱的基本类型,也要注意无意识的自动装箱。

    下面是初始化配置,我们拿JDBC获取数据库连接对象来举例,代码如下:

    /**
     * @Author GongGuoWei
     * @Email GongGuoWei01@yeah.net
     * @Date 2020/1/14
     */
    public class demo02 {
        private static final String URL = "";
    
        private static final String USERNAME = "";
    
        private static final String PASSWORD = "";
    
        static Connection getConnection() {
            Connection connection = null;
    
            try {
                 Class.forName("com.mysql.jdbc.Driver");
                 connection = DriverManager.getConnection(URL, USERNAME, PASSWORD);
                  } catch (ClassNotFoundException e) {
                         e.printStackTrace();
                  } catch (SQLException e) {
                         e.printStackTrace();
                  }
                 return connection;
             }
    }

    我们在每次获取数据库连接对象时,都会创建一个connection的对象,但是往往数据库的连接配置是不变的,没必要每次都去创建,因为这个对象的构建代价是昂贵的,并且在JVM垃圾回收时,也会增加内存的占用,并损害性能。我们将它作为类初始化的一部分,代码实现如下:

    /**
     * @Author GongGuoWei
     * @Email GongGuoWei01@yeah.net
     * @Date 2020/1/15
     */
    public class demo03 {
        private static final String URL = "";
    
        private static final String USERNAME = "";
    
        private static final String PASSWORD = "";
    
        private static Connection connection = null;
    
        static {
            try {
                Class.forName("com.mysql.jdbc.Driver");
                connection = DriverManager.getConnection(URL, USERNAME, PASSWORD);
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
    
        public static Connection getConnection() {
            return connection;
        }
    }

    总结

    避免反复创建对象,是正确的,但是对象我们需要弄清楚,它创建的代价是不是昂贵的。当创建的代价是廉价的,这个时候我们通过构造方法创建它,是更好的选择,因为创建额外的对象,增强程序的清晰度、简单性、功能性,这是一件好事,尤其在现代JVM具有高度优化,廉价对象的回收,是轻松的。在总结这里,再提一个关键词防御性复制,指的是那些创建代价昂贵的对象,在保证它不可变的情况下,进行重复使用。

    重用防御性复制所要求创建的代价,要远远大于一个廉价的对象。如果在不需要防御性复制的情况下重用,那么会导致潜在的错误和安全漏洞;而在需要重用不使用时,会影响程序的性能和风格。

  • 相关阅读:
    linux获取日志指定行数范围内的内容
    python解决open()函数、xlrd.open_workbook()函数文件名包含中文,sheet名包含中文报错的问题
    robot framework添加库注意事项
    robot framework取出列表子元素
    Vue 及框架响应式系统原理
    响应式布局和自适应布局的不同
    前端综合学习笔记---异步、ES6/7、Module、Promise同步 vs 异步
    前端综合学习笔记---变量类型、原型链、作用域和闭包
    doT.js模板引擎及基础原理
    Spring Boot入门第五天:使用JSP
  • 原文地址:https://www.cnblogs.com/gongguowei01/p/12194720.html
Copyright © 2020-2023  润新知