前言
《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具有高度优化,廉价对象的回收,是轻松的。在总结这里,再提一个关键词防御性复制,指的是那些创建代价昂贵的对象,在保证它不可变的情况下,进行重复使用。
重用防御性复制所要求创建的代价,要远远大于一个廉价的对象。如果在不需要防御性复制的情况下重用,那么会导致潜在的错误和安全漏洞;而在需要重用不使用时,会影响程序的性能和风格。