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具有高度优化,廉价对象的回收,是轻松的。在总结这里,再提一个关键词防御性复制,指的是那些创建代价昂贵的对象,在保证它不可变的情况下,进行重复使用。

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

你可能感兴趣的