10

前言

最近读完了《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)返回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》的知识点就介绍到这里,最近在看《代码整洁之道》,后续可能也会来单独做个总结,若有不对的地方请多多指教。


超大只乌龟
882 声望1.4k 粉丝

区区码农