6、避免创建不必要的对象
此条目下作者有这个观点:当你应该重用现有对象的时候,请不要创建新的对象
最突出的就是字符串常量创建的例子:
String str = new String("hello");
如上所述,本来在字符串常量池中就存在字符串"hello",但是我们又创建了一个新的字符串"hello"的实例,显然,这个新的str实例是并不需要的。
使用静态工厂方法:
构造方法一定会创建一个新的对象,但是其实很多时候并没有这个必要。对于一些可以重用现有对象的情况,我们可以使用静态的工厂方法来获取实例(因为静态工厂方法是九二一对实例进行严格控制的)。
缓存创建昂贵的对象:
对于那些可以复用的但是创建昂贵的对象,我们可以将其设置为公共静态的,这样既可以避免重复创建带来的性能开销,也可以避免持有这个对象的容器对象在失去引用后被GC清理掉。
注意,这里很容易进入一个误区,就是对那些(可能)会重用的对象都进行缓存。
这个条目并不代表对象的创建是昂贵的。其实通过构造方法创建和回收对象的性能开销并不高。
作者在此条目的下面提到:
除非池(对象缓存)中的对象非常重量级,否则通过维护自己的对象池来避免创建对象其实并不太好,因为者会显著的加大内存的占用。
回到条目的开始,我们应该将重点放在重用
和不要创建新的对象
上面。也就是说,我们更多应该考虑的是在重用某个对象的时候,尽可能的去避免创建新(没有必要)的对象。
7、消除过期的对象引用:
这里有一个误区,就是JAVA拥有垃圾收集机制,所以我们就不需要手动对内存进行管理了。
但是,其实这个是不一定正确的,内存泄漏在JAVA里面太频繁了。
作者在书中提到当一个类自己管理内存时,程序员应当警惕内存泄漏,书中举了一个栈弹出的例子:
public Object pop() {
if (size == 0) {
throw new EmptyStackException();
}
return elements[--size];
可以看到,这种实现实际上仅仅是将size的-1了。但是elements任然保留着对size外的元素的引用,也就是说那一部分的元素的内存并不会被GC释放掉。
解决方案:
public Object pop() {
if (size == 0) {
throw new EmptyStackException();
}
Object result = elements[--size];
elements[size] = null;
return result;
}
8、避免使用Finalizer和Cleaner机制
这可能对JAVA新手来说一脸懵逼(没错,我就是这种。因为压根就不知道什么是Finalizer,这里就不谈Cleaner机制了,因为我现在还是不会,不过功能应该和finalizer差不多)
对于C++程序员,我们知道有一个析构函数用于回收资源。
Finalizer机制:
GC在回收对象之前调用其finalize方法,但是这并不说明finalize就等于C++的析构函数。finalize方法的调用存在很大的不确定性,也就是说它的调用在不同设备上面的行为具有很大的差异。
-
当一个对象变得不可达一直到finalize方法的调用这一段时间是任意长的。所以我们千万不要在这个方法里面做一些关闭文件流这种释放非内存资源的操作。因为很有可能出现文件流没有被及时的关闭从而导致后面的文件流打开失败。
-
finalize机制已经被弃用了。
-
Java并不能保证Finalizer和Cleaner机制的及时运行,甚至不能够确保他们是否能够运行
-
System.gc和System.runFinalization 可能会增加finalize被执行的几率,但是不保证其一定被执行
-
在Finalizer机制中,未被捕获的异常会被忽略,cleaner机制不会有这个问题
-
Finalizer机制会严重影响垃圾收集的效率和性能
Finalizer机制的作用:
- 作为安全网,防止资源拥有者忽略了他的close方法。这个很好理解,当资源拥有者忘记close的时候,通过finalize至少可能会关闭资源,着总比不关闭好。也就是说,这个机制虽然不可信,但是聊胜于无。
- finalize方法适用于释放一些通过native方法释放的对象(比如说通过JNI创建的对象)
9、使用try-with-resources语句替代try-finally语句
可能你刚看到这个条目的时候会和我一样对于什么叫做try-with-resources语句一脸懵逼(本人太菜,之前确实没有见过)
书中举了这样一个例子:
static String firstLineOfFile(String path) throws IOException {
BufferedReader br = new BufferedReader(new FileReader(path));
try {
return br.readLine();
} finally {
br.close();
}
}
在上述的代码块中,有可能由于文件流的没有正常打开导致异常出现。同理,由于引用了空的对象,br无法正常调用close方法。那么在这次情况下第二个异常会完全覆盖掉第一个异常。如果使用了catch语句块捕获了异常,那么代码就会变成如下的样子:
static String firstLineOfFile(String path) throws IOException {
BufferedReader br = new BufferedReader(new FileReader(path));
try {
return br.readLine();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
br.close();
} catch (Exception e) {
e.printStackTrace();
}
}
return null;
}
这种代码,写出来简直辣眼睛。反复的异常捕捉显著的降低了代码的可读性。
使用try-with-resources语句:
static String firstLineOfFile(String path) throws IOException {
try (BufferedReader br = new BufferedReader(new FileReader(path))) {
return br.readLine();
} catch (IOException e) {
return defaultValue;
}
}
相较于之前的代码,显然可读性有所提高。
原理:
能够使用try-with-resources语句的资源类其实都自动实现了AutoCloseable接口这个接口提供了一个无参数的close方法。也就是说,使用了try-with-resources语句声明的资源只要实现了这个方法,那么都会在抛出异常之前调用close方法关闭资源。另外,用于资源对象的close方法是由其自己控制,所以我们也不需要考虑资源文件关闭所产生的异常。
10、重写euquals方法时应当遵守的通用约定
不需要重写equals方法的条件:
- 每一个类的实例都是一个固有且唯一的。
- 类并不需要提供一个逻辑相等的测试功能。
- 父类已经重写了equals方法。一个设计良好的可继承的父类应保证其子类完全实用于父类的equals方法。
- 类是private修饰的或者default修饰的,那么这个类的equals方法应该永远不会被调用。
需要重写equals方法的条件:
- 一个类包含逻辑相等的概念,并且父类并没有提供equals方法。这里特指一些表示值的类,比如Ineger或者String类。
- 使用equals方法的时候应当比较的是对象逻辑上面的相等,而并不是比较是否引用的是相同的对象。
- 当某一个类需要作为Map的key的时候。
equals方法应当满足的属性:
- 自反性:一个对象必须和自身相等
- 对称性:a.equals(b) == b.equals(a)
- 传递性:a.equals(b),b.equals(c),则a.equals(c)
- 一致性:相等的对象永远相等,不相等的对象永远不相等
- 非空性:所有的对象都不等于null
编写高质量的equals方法:
-
首先使用
==
运算符检查参数是否为该对象的引用,如果成立直接返回true,这是一种性能优化。 -
应当使用
instanceof
检查传递参数是否有正确的类型,因为equals方法默认传递的参数是Object类型的 -
将参数进行强制类型转化
-
对于参数的每一个值属性,应当验证值属性是否相等,注意,对于float或者double类型,不要用==而是使用Float.compare或者Double.compare方法比较。
-
对于参数的对象属性,应当递归的对持有的对象进行equals比较,由于对象引用可能为空,所以对引用对象进行比较的时候,应当使用Object.compare方法
-
使用equals方法进行比较的时候,应当首先考虑最有可能不同的属性,这也是一种性能优化。
-
不要忘记验证自己实现的equals方法是否满足上面提到的5个属性。
注意事项:
- 重写equals方法的时候,应该重写hashCode方法
- 不要对equals方法太过于吹毛求疵,这可能会因为考虑因素太多导致实现非常困难。
- 重写equals方法的时候,参数是Object类型的,不要将参数替换成其他的类型。
网友评论