《Effective Java》中文第三版,是一本关于Java基础的书,这本书不止一次有人推荐我看。其中包括我很喜欢的博客园博主五月的仓颉,他曾在自己的博文《给Java程序猿们推荐一些值得一看的好书》中也推荐过。加深自己的记忆,同时向优秀的人看齐,决定在看完每一章之后,都写一篇随笔。如果有写的不对的地方、表述的不清楚的地方、或者其他建议,希望您能够留言指正,谢谢。
《Effective Java》中文第三版在线阅读链接
不必要的对象,指的是当我们需要一个对象的时候,它的功能与之前创建过的对象时相同的,那么我们可以重用之前的对象,而不是去创建一个新的。如果此时我们仍创建一个新的对象,那么它就是不必要的对象。
‘对象是不可变的’,在这样的前提条件下,那它总是可以被重用的。
我们针对上方‘哪里用’中指的地方,一一列举实例,首先是正则表达式中的实现,我们先来看看它每次都会创建不必要的对象的情况,代码如下:
/**
*使用正则表达式来判断字符串中是否包含有罗马数字
*
* @Author GongGuoWei
* @Email [email protected]
* @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 [email protected]
* @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 [email protected]
* @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 [email protected]
* @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具有高度优化,廉价对象的回收,是轻松的。在总结这里,再提一个关键词防御性复制,指的是那些创建代价昂贵的对象,在保证它不可变的情况下,进行重复使用。
重用防御性复制的前提,所要求创建的代价,要远远大于一个廉价的对象。如果在不需要防御性复制的情况下重用,那么会导致潜在的错误和安全漏洞;而在需要重用不使用时,会影响程序的性能和风格。