《Effective Java》看这一篇就够了

前言

最近读完了《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));

覆盖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)返回truey.equals(z)返回true,那么x.equals(z)也应该返回true
(4)一致性
对于任何非null的引用值x,只要对象没有修改,那么x.equals(y)会一直返回true
(5)非空性
对于任何非null的引用值x,x.equals(null)必须返回false

  1. 实现高质量equals方法的诀窍:

(1)使用 == 操做符检查 "参数是否为这个对象的引用"

若是是,则返回true。这只不过是一种性能优化,若是比较操做有可能很昂贵,就值得这么作。

(2)使用 instanceof 操做符检查 "参数是否为正确的类型"

若是不是,则返回false。所谓的正确类型是指equals方法所在的那个类。
某些状况是指该类实现的改进了equals方法的接口,此接口容许实现了该接口的类进行比较。

(3)把参数转换为正确的类型

由于转换以前进行过instanceof测试,因此确保会成功。

(4)检查参数中的域是否与该对象中对应的域相匹配

若是这些测试所有成功,则返回true,不然返回false
对于不是floatdouble类型的基本类型域,可使用==操做符进行比较。
对于对象引用域,能够递归地调用euqals方法。
对于floatdouble域,应该使用Float.compareDouble.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

floatdouble类型主要是为了科学计算和工程计算而设计的,然而塔门并无提供彻底精确的结果,因此不该该被用于须要精确结果的场合。float和double类型尤为不适合于货币计算,由于要让一个float和double精确地表示0.1是不可能的。

好比下面这个例子

public class Test {
    public static void main(String[] args) {
        System.out.println(1.0 - 0.9);
    }
}

输出结果为:0.09999999999999998

解决这个问题的正确方法时使用BigDecimalint或者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》的知识点就介绍到这里,最近在看《代码整洁之道》,后续可能也会来单独作个总结,如有不对的地方请多多指教。

相关文章
相关标签/搜索