最近读完了《Effective Java》
这本书,笔者这里对一些比较重点的准则作个总结。数组
String s = new String("wugui");
上面这句每次执行都会建立一个新的String实例,改进后的版本以下:性能优化
String s = "wugui";
改进后只用了一个String实例,而不是每次执行的时候都建立一个新的实例。编辑器
还有一个很重要的点:要优先使用基本类型而不是装箱基本类型,要小心无心识的自动装箱。ide
上面这句话是什么意思呢,看下面这个例子就知道了:性能
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
实例。测试
所谓的过时引用,是指永远也不会被再被解除的引用。 优化
举个例子:从栈中弹出来的对象不会被当作垃圾回收,即便使用栈的程序再也不引用这些对象,它们也不会被回收。 栈内部维护着对这些对象的过时引用。ui
所以,一旦对象引用已通过期,只需清空这些引用便可。this
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));
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
(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方法。
示例以下
public int hashCode(){ int result = 17; result = 31 * result + x; result = 31 * result + y; result = 31 * result + z; return result; }
若是一个类是不可变的,而且计算哈希值的开销比较大,就应该考虑把哈希值保存在对象内部,而不是每次请求的时候都从新计算哈希值(好比String类内部就有个int类型的hash变量来保存哈希值)。
先看下面这个反例:
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注解,这样的话编译器就能够帮你防止大量的错误。
Java1.5发行版本中引入的for-each循环,经过彻底隐藏迭代器或者索引变量,避免了混乱和出错的可能,以下:
for(Element e : elements){ doSomething(e) }
注意:利用for-each循环不会有性能损失,实际上在某些状况下比起普通的for循环,它还有些性能优点,由于它对数组索引的边界值只计算一次。
总之,for-each循环在简洁性和预防BUG方面有着传统的for循环没法比拟的优点,而且没有性能损失。应该尽量地使用for-each循环。遗憾的是,有三种常见的状况没法使用for-each循环:
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》的知识点就介绍到这里,最近在看《代码整洁之道》,后续可能也会来单独作个总结,如有不对的地方请多多指教。