前言
最近读完了《Effective Java》
这本书,笔者这里对一些比较重点的准则做个总结。
避免创建不必要的对象
String s = new String("wugui");
上面这句每次执行都会创建一个新的String实例,改进后的版本如下:
String s = "wugui";
改进后只用了一个String实例,而不是每次执行的时候都创建一个新的实例。
还有一个很重要的点:要优先使用基本类型而不是装箱基本类型,要当心无意识的自动装箱。
上面这句话是什么意思呢,看下面这个例子就知道了:
public static void main(String[] args){
Long sum = 0L;
for(long i = 0;i < Integer.MAX_VALUE; i++){
sum += i;
}
System.out.println(sum);
}
这段程序算出的答案是正确的,但是比实际情况要更慢一些,只因为打错了一个字符。变量sum
被声明为Long
而不是long
,每次循环i
都会进行自动装箱
升级为Long
类型,意味着程序构造了大约2的31次幂个多余的Long
实例。
消除过期的对象引用
所谓的过期引用,是指永远也不会被再被解除的引用。
举个例子:从栈中弹出来的对象不会被当做垃圾回收,即使使用栈的程序不再引用这些对象,它们也不会被回收。 栈内部维护着对这些对象的过期引用。
因此,一旦对象引用已经过期,只需清空这些引用即可。
element[size]=null;
使类和成员的可访问性最小化
尽可能地使每个类或者成员不被外界访问
除了public static final
变量的特殊情形之外,任何类都不应该包含public
变量,并且要确保public static final
变量所引用的对象都是不可变的(比如String
)
public static final
变量要么指向基本类型,要么指向不可变对象。因为虽然引用本身不能修改,但是它引用的对象却可以被修改。
许多编辑器会返回指向私有数组域的访问方法,可以用下面方法解决:
private static final Employee[] PRIVATE_VALUES = {......}
public static final List<Employee> VALUES = Collections.unmodifiableList(Arrays.asList(PRIVATE_VALUES));
覆盖equals时请遵守通用约定
1.equals方法实现了等价关系:
(1)自反性
对于任何非null的引用值x,x.equals(x)
必须返回true
(2)对称性
对于任何非null的引用值x,当x.equals(y)
返回true
时,y.equals(x)
也必须返回true
(3)传递性
对于任何非null的引用值x,如果x.equals(y)
返回true
,y.equals(z)
返回true
,那么x.equals(z)
也应该返回true
(4)一致性
对于任何非null的引用值x,只要对象没有修改,那么x.equals(y)
会一直返回true
(5)非空性
对于任何非null的引用值x,x.equals(null)
必须返回false
- 实现高质量equals方法的诀窍:
(1)使用 ==
操作符检查 "参数是否为这个对象的引用"。
如果是,则返回true
。这只不过是一种性能优化,如果比较操作有可能很昂贵,就值得这么做。
(2)使用 instanceof
操作符检查 "参数是否为正确的类型"
如果不是,则返回false
。所谓的正确类型是指equals
方法所在的那个类。
某些情况是指该类实现的改进了equals
方法的接口,此接口允许实现了该接口的类进行比较。
(3)把参数转换为正确的类型
因为转换之前进行过instanceof
测试,所以确保会成功。
(4)检查参数中的域是否与该对象中对应的域相匹配
如果这些测试全部成功,则返回true
,否则返回false
。
对于不是float
和double
类型的基本类型域,可以使用==
操作符进行比较。
对于对象引用域,可以递归地调用euqals
方法。
对于float
和double
域,应该使用Float.compare
和Double.compare
方法。因为存在Float.NaN、-0.0f
等常量
对于数组,则可以使用Arrays.equals
方法。
有些对象引用域包含null是合法的。为了避免空指针异常,可以这样比较:
(field == null ? o.field==null : field.equals(o.field))
(5)当写完equals方法后,应该测试它们是否是对称的、传递的、一致的
示例如下:
public boolean equals(Object o){
if(o == this)
return true;
if(!(o instanceof MyClass))
return false;
Myclass mc=(MyClass)o;
return mc.x == x && mc.y == y;
}
3.注意:
(1)覆盖equals时总要覆盖hashCode
(2)不要让equals方法过于智能
(3)不要将equals声明中的Object对象替换成其它类型
public boolean equals(MyClass o){
..........
}
问题在于,这个方法并没有覆盖Object.equals,因为它的参数类型应该是Object。相反,它重载了Object.equals。
覆盖equals时总要覆盖hashCode
在每个覆盖了equals方法的类中,也必须覆盖hashCode方法。
- 如果x.euqals(y)返回true,那么x和y的哈希值一定相等。
- 如果x.equals(y)返回false,那么x和y的哈希值也有可能相等。
- 如果x和y的哈希值不相等,那么x.equals(y)一定返回false。
示例如下
public int hashCode(){
int result = 17;
result = 31 * result + x;
result = 31 * result + y;
result = 31 * result + z;
return result;
}
如果一个类是不可变的,并且计算哈希值的开销比较大,就应该考虑把哈希值保存在对象内部,而不是每次请求的时候都重新计算哈希值(比如String类内部就有个int类型的hash变量来保存哈希值)。
坚持使用Override注解
先看下面这个反例:
public class Bigram {
private final char first;
private final char second;
public Bigram(char first, char second) {
this.first = first;
this.second = second;
}
public boolean equals(Bigram b) {
return b.first == first && b.second == second;
}
public int hashcode() {
return 31 * first + second;
}
public static void main(String[] args) {
Set<Bigram> s = new HashSet<>();
for (int i = 0; i < 10; i++) {
for (char ch = 'a'; ch <= 'z'; ch++) {
s.add(new Bigram(ch, ch));
}
}
System.out.println(s.size());
}
}
上面这个例子你可能以为程序打印出的大小为26,因为集合不能包含重复对象,但是运行后你会发现打印的不是26而是260,到底是哪里出错了呢?
很显然,Bigram类的创建者原本想要覆盖equals
方法,同时还记得覆盖了hashcode
方法,可惜这个程序没能覆盖到equals
方法,而是重载了Object
类的equals
方法。因为如果想要覆盖Object
类的equals
方法你必须定义一个Object类型的equals方法,而在上面的例子中只是做了重载操作。
只有当你使用@Override标注Bigram类时,编译器才能帮你发现这个错误,如果加上这个注解并且试着运行程序,编译器会产生一条下面这样地错误信息:method does not override or implement a method from a supertype,这样的话你会马上意识到自己哪里错了,并且用正确的来取代错误的方法,如下:
@Override
public boolean equals(Object o) {
if (!(o instanceof Bigram)) {
return false;
}
Bigram b = (Bigram) o;
return b.first == first && b.second == second;
}
因此,你应该在你想要覆盖父类的每个方法中加上@Override注解,这样的话编译器就可以帮你防止大量的错误。
for-each循环优先于传统的for循环
Java1.5发行版本中引入的for-each循环,通过完全隐藏迭代器或者索引变量,避免了混乱和出错的可能,如下:
for(Element e : elements){
doSomething(e)
}
注意:利用for-each循环不会有性能损失,实际上在某些情况下比起普通的for循环,它还有些性能优势,因为它对数组索引的边界值只计算一次。
总之,for-each循环在简洁性和预防BUG方面有着传统的for循环无法比拟的优势,并且没有性能损失。应该尽可能地使用for-each循环。遗憾的是,有三种常见的情况无法使用for-each循环:
- 过滤——如果需要遍历集合,并删除指定的元素,就需要使用显示的迭代器,以便可以调用它的remove方法
- 转换——如果需要遍历列表或者数组,并取代它部分或者全部的元素值,就需要列表迭代器或者索引数组,以便设定元素的值
- 平行迭代——如果需要并行地遍历多个集合,就需要显示的控制迭代器或者索引变量。以便所有迭代器啊或者索引变量都可以得到同步移动
如果需要精确的答案,请避免使用float和double
float
和double
类型主要是为了科学计算和工程计算而设计的,然而塔门并没有提供完全精确的结果,所以不应该被用于需要精确结果的场合。float和double类型尤其不适合于货币计算,因为要让一个float和double精确地表示0.1是不可能的。
比如下面这个例子
public class Test {
public static void main(String[] args) {
System.out.println(1.0 - 0.9);
}
}
输出结果为:0.09999999999999998
解决这个问题的正确方法时使用BigDecimal
、int
或者long
进行货币计算。
基本类型优先于装箱基本类型
Java中变量主要由两部分组成,一个是基本类型,如int、double和boolean等,另外一个是引用类型,如String和List等。每个基本类型都有一个对应的引用类型,称作装箱基本类型。比如int对应Integer、boolean对应Boolean等。
Java1.5版本增加了自动装箱和自动拆箱,但是这两种类型之间是有差别的。
看下面这个比较器
Comparator<Integer> comparator = new Comparator<Integer>(){
public int compare(Integer first,Integer second){
return first < second ? -1:(first == second ? 0:1);
}
}
这个比较器表面上看起来不错,它可以通过许多测试。但是当你打印comparator.compare(new Integer(42),new Integer(42))时,本应该打印出0,但是最后结果却是1,这表明第一个Integer值大于第二个。
问题出在哪里呢?方法中的第一个测试做的很好,当执行first < second 时会使first和second引用的Integer类型被自动拆箱,但是后面再计算first == second时,因为是对象之间使用==比较,这时候比较的是对象的内存地址,如果是两个不同的对象就会返回false,所以不应该用==来比较两个对象的值。
正确的程序应该是下面这样
Comparator<Integer> comparator = new Comparator<Integer>(){
public int compare(Integer first,Integer second){
int f = first;
int s = second;
return f < s ? -1:(f == s ? 0:1);
}
}
那么什么时候使用装箱基本类型呢?它们有几个合理的用处:
第一个是作为集合中的元素,你不能将基本类型放在集合中,如List<Integer>
而不是List<int>
。
第二个是在泛型中必须使用装箱基本类型为类型参数,如ThreadLocal<Integer>。
总之,当可以选择的时候,基本类型要优先于装箱基本类型。基本类型更加简单,也更加快速。
总结
有关《Effective Java》的知识点就介绍到这里,最近在看《代码整洁之道》,后续可能也会来单独做个总结,若有不对的地方请多多指教。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。