Effective Java读书笔记(2)—— 所有对象通用的方法

这一章写的是对所有对象都通用的一些方法,换言之就是Object带有的一些方法(Object类是所有类的父类),在这些方法中非final的类都是被设计为需要重写的,重写时需要遵从一些通用的约定。

第一条:使用try-with-resources语句代替try-finally语句

在进行IO操作和JDBC数据库操作的时候,最终都要手动调用close()关闭资源(用finalizer机制也可以,但是上一篇文章也讲过了,finalizer和cleaner能不用就不用)。

一个常见的操作是用try-finally语句来解决,在finally语句块中调用close(),如下:

	FileInputStream in = null;
    try{
            in = new FileInputStream("path/filename");
            // process input
    }catch (IOException e){
        e.printStackTrace();
    }finally {
        try{
            in.close();
        }catch (IOException e){
            e.printStackTrace();
        }
    }

只有一个IO资源的时候可能还好,但是当第一个try块里需要再打开一个IO资源的时候代码就会变得很冗长、可读性很差。具体如下:

		FileInputStream in = null;
        FileOutputStream out = null;

        try{
            in = new FileInputStream("path");
            try{
                out = new FileOutputStream("path");
                // process out
            }catch (IOException e){
                e.printStackTrace();
            }finally {
                try {
                    if (out != null)
                        out.close();
                }catch (IOException e){
                    e.printStackTrace();
                }
            }
            // process in
        }catch (IOException e){
            e.printStackTrace();
        }finally {
            try {
                if (in != null)
                    in.close();
            }catch (IOException e){
                e.printStackTrace();
            }
        }

可以看出来非常麻烦,上面的代码里try块嵌套try块,也可以finally嵌套finally,不管怎样都很不精简。

更好的一种做法是使用try-with-resource,如下:

		try(FileInputStream in = new FileInputStream("path");
        FileOutputStream out = new FileOutputStream("path")){
            // process
        }catch (IOException e){
            e.printStackTrace();
        }

同样是打开两个资源,try-with-resouce的做法比上面try-finally的做法精简太多了,不需要自己手动调用close()。

当然,这样的做法有一个前提,就是IO类实现了AutoClosable接口,Java中常见的IO类都实现了AutoClosable接口,对于自己定义的类也要记得实现,才可以这么写。

第二条:重写equals方法时遵守通用约定

equals方法在不重写的情况下等同于“==”,即每个对象只和自己相等。在一下几种情况下不需要重写equals方法:

  • 单例模式
  • 不需要提供“逻辑相等”功能,所谓逻辑相等就是:两个人,各个属性如姓名和家庭住址都一样我们就认为他们相等。
  • 父类已经重写equals方法,并完全适用于子类。
  • 类是私有的或包级私有,它的equals方法永远不会被调用,为了防止意外调用可以重写equals方法,方法体为throw new AssertionError();

第四点才开始没太看明白,类只能声明为public,除非这是一个内部类。对于private的内部类,外部类也可以实例化并调用equals方法,如下:

public static void main(String[] args) {
        Student stu = new Student(1, "mike");
        Secret s1 = stu.new Secret();
        Secret s2 = stu.new Secret();
        System.out.println(s1.equals(s2));    // false
    }

Secret类被声明为Student类的private内部类,调用equals是没问题的。所以书里的意思应该是在我们确保equals不会被调用的情况下,可以重写,抛出断言错误。

重写equals方法要实现的属性:

  • 自反性:x.equals(x)为true
  • 对称性:x.equals(y) == y.equals(x)
  • 传递性: x.equals(y)为true,y.equals(z)为true,则x.equals(z)为true
  • 一致性:两个对象只要没被修改,equals返回结果始终一致
  • 非空性:对于所有非空引用x, x.equals(null)返回false

比较通用的equals方法重写做法如下:

public class Student{
	int id;
	String name;
	Address address;
	
	public Student(int id, String name, Address address){
		this.id = id;
		this.name = name;
		this.address = address;
	}
	
	@Override
	public boolean equals(Object o){
		if (o == this)
			return true;
		if (!(o instance of Student))
			return false;
		Student s = (Student)o;
		return this.id==s.id && this.name.equals(s.name) && this.address.equals(s.address);
	}
}

第三条:重写equals时记得重写hashcode方法

1. 为什么重写hashCode

根绝Java规范,如果两个对象equals返回true,那么他们的hashcode一定是相同的;反之,如果hashcode相同,equals不一定返回true。

最容易理解上述规定的场景就是HashMap,两个对象的hashcode相同说明他们会被分配到哈希表的同一个桶里,但是一个桶里可能会有多个键值对,每个键值对的key是不同的,也就是说这些hashcode相同的对象时不相等的。但是如果两个对象相等(key相同),那么他们一定在同一个桶里,也就是hashcode一定相同。

那么如果只重写equals,不重写hashcode会出现什么问题呢?看下面的代码:

Map<Student, String> m = new HashMap<>();
m.put(new Student(1, "mike"), "new student");
String s = m.get(new Student(1, "mike"));

对于字符串s,我们期望得到“new student”,而事实上m.get(new Student(1, “mike”))会返回null。原因在于,get方法会根据hashcode会寻找相应的桶,而这个hashcode和put时放入的对象的hashcode不同,桶也就不同(当然也有一定几率,两个对象被分配到了同一个桶,但是大多数情况下并不能得到我们期望的结果)。

2. 重写hashCode方法

那么对于刚才重写equals方法的Student类,需要继续重写hashcode方法,简单写法如下:

@Overrride
public int hashCode(){
	int result = Integer.hashCode(id);
	result = result*31 + name.hashCode();
	result = result*31 + address.hashCode();
	return result;
}

在重写hashCode的时候需要考虑所有在equals方法中参与比较的属性,这些属性结合的方式的先乘31再相加,比起直接相加,更加不容易发生hash冲突,而且与31相乘可以优化成与运算:31 * i ==(i << 5) - i

另一种更省事但是效率更低的写法(底层的实现和上面的写法是一样的):

@Overrride
public int hashCode(){
	return Objects.hash(id, name, address);
}

更先进、复杂的方法参照Guava 框架的com.google.common.hash.Hashing [Guava] 方法。

3. 缓存hashcode

如果一个类是不可变的,并且需要频繁计算hashcode、或计算hashcode代价比较大,可以缓存hashcode,也就是延迟hashcode的计算,并且只计算一次。还是以上面的Student类为例,缓存hashcode的做法如下:

private int hashcode = 0;

@Override
public int hashCode(){
	int result = hashcode;
	if (hashcode == 0){
		result = Integer.hashCode(id);
		result = result*31 + name.hashCode();
		result = result*31 + address.hashCode();
		hashcode =  result;
	}
	return hashcode;
}

可以看到,在Student类里加入了一个int变量,变量名为hashcode,作为哈希码的缓存 (引入一个result而不是直接在hashcode上操作是为了防止多线程下的数据不安全问题)。

第三条:始终重写toString方法

当我们用System.out.println() 来打印一个对象时,会调用这个对象的toString方法,如果不重写这个方法,我们得到的结果是:类名@哈希码的无符号十六进制表示,可读性不强,因此需要重写toString方法,打印对象必要的信息。

第四条:谨慎地重写clone方法

Cloneable接口内部并没有声明一个clone方法,只是作为一个mixin接口公布这样的类允许克隆。

根据Object规范,通常情况下,对于任何对象 x,表达式 x.clone() != x返回 true, x.clone().getClass() == x.getClass()也返回 true,x.clone().equals(x) 返回 true,但它们不是绝对的要求。

不可变类不需要提供clone方法,直接多个引用指向同一个对象就可以了,避免内存和复制开销的浪费。

如果希望重写clone方法,而父类提供了一个行为娘好的clone方法,就先调用super.clone()。简单的clone方法重写如下所示:

@Override
public Student clone(){
	try{
		return (Student)super.clone();
	}catch(CloneNotSupportedException e){
		throw new AssertionError();
	}
}
深拷贝与浅拷贝

在进行clone时,要注意实现深拷贝,即所有引用类型的成员变量都在内存中复制了一份新的,拷贝对象和原对象互不影响。如果直接赋值,则实现的是浅拷贝,拷贝对象和原对象的引用类型成员变量共享同一块内存空间,会发生意想不到的错误。

深拷贝的做法就是递归调用clone方法:

@Override
public Student clone(){
	try{
		Student result = (Student)super.clone();
		result.id = this.id;
		result.name = this.name;
		result.address = this.address.clone();
		return result;
	}catch(CloneNotSupportedException e){
		throw new AssertionError();
	}
}

在某些情况下仅仅递归调用clone方法是不够的,比如:

// Recursive clone method for class with complex mutable state
public class HashTable implements Cloneable {
    private Entry[] buckets = ...;

    private static class Entry {
        final Object key;
        Object value;
        Entry  next;

        Entry(Object key, Object value, Entry next) {
            this.key   = key;
            this.value = value;
            this.next  = next;  
        }

        // Recursively copy the linked list headed by this Entry
        Entry deepCopy() {
            Entry result = new Entry(key, value, next);
   			for (Entry p = result; p.next != null; p = p.next)
      			p.next = new Entry(p.next.key, p.next.value, p.next.next);
   			return result;
        }

    @Override public HashTable clone() {
        try {
            HashTable result = (HashTable) super.clone();
            result.buckets = new Entry[buckets.length];
            for (int i = 0; i < buckets.length; i++)
                if (buckets[i] != null)
                    result.buckets[i] = buckets[i].deepCopy();
            return result;
        } catch (CloneNotSupportedException e) {
            throw new AssertionError();
        }
    }
}

应用场景

通常使用复制构造函数或参数为同类型对象的静态工厂方法是拷贝对象的更好方法。

唯一的例外是数组,最好用clone方法复制。

第五条:考虑实现Comparable接口

Comparable接口代表某个类的对象互相之间是可以比较的,那么也就可以进行排序操作。实现了Comparable接口的类需要实现compareTo方法,a.compareTo(b)方法返回负数代表a<b,零表示a==b,正数代表 a>b。

compareTo需要遵循的一些约定(和eqauls有点类似):

  • 对称性: sgn(x.compareTo(y)) == -sgn(y. compareTo(x)),若其中一个抛出异常,另一个必须也抛异常。
  • 传递性:(x. compareTo(y) > 0 && y.compareTo(z) > 0),则x.compareTo(z) > 0
  • (x.compareTo(y) == 0) == (x.equals(y)),非必须但推荐。(BigDecimal类就不遵守这条约定)
☆☆☆ 不要用>,<重写compareTo!!

打五角星是因为之前一直习惯这么写,原来是错的:

// 重写compareTo:
@Override
public int compareTo(Integer x){
	return this - x;
}

// 实现Comparator接口重写compare:
@Override
public void compare(Integer x1, Integer x2){
	return x1 - x2;
}

这种写法的问题在于有可能会发生数值溢出和IEEE 754浮点数失真。比较好的做法是使用静态compare方法:

// 重写compareTo:
@Override
public int compareTo(Integer x){
	return Integer.compare(this, x);
}

// 实现Comparator接口重写compare:
@Override
public void compare(Integer x1, Integer x2){
	return Integer.compare(x1, x2);
}

对于Student类的重写:

// 重写compareTo:
@Override
public int compareTo(Student s){
	int result = Integer.compare(id, s.id);
	if (result == 0){
		result = String.compare(name, s.name);
		if (result == 0){
			result = address.compareTo(s.address);
		}
	}
	return result;
}

注意比较的顺序由最重要的属性开始,类似于字典序。

此外Java8中Comparator接口提供了一系列的比较器方法,更加简洁但是性能一般。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值