EffectiveJava笔记[三]

2019-07-22  本文已影响0人  pigrange

11、重写equals时重写hashCode方法

在每一个重写了equals方法的类中,应当重写这个类的hashCode方法。如果不这么做,那么将会违背Object.hashCode的通用约定,从而导致某一些基于hash散列的集合无法正常运行,比如说HashSetHashMapHashTable

规范:

对于基于hash散列的类来说,如果不重写hashCode方法的话,那么即便他们通过equals方法得到的结果是true,那么将他们作为hashMap的key也无法拿到正确的值,因为他们只是逻辑上相等,而hashMap是基于hashCode寻址的。

hashCode的计算:

  1. 为对象中的每一个关键域f,计算其散列码c

  2. result = 31 * result + c;
    
  3. 返回 result

注意:


12、始终重写toString方法

虽然Object类提供了toString方法的实现,但是在某些情况它返回的toString并没有什么乱用。提供一个良好的toString方法可以使类更易于使用和调试。


13、谨慎地重写clone

首先需要指出,这个条目是基于Cloneable接口或者说clone方法而列出的。Cloneable接口是一个空的接口,它仅仅用来表明这个对象是允许被克隆的。真正的clone方法的提供是在Object类中。这一点也被作者在书中描述为设计上面的缺陷。

Cloneable接口:

这是一个并未包含任何方法的接口,它的唯一作用就是决定Object类中clone方法实现的行为。换句话说,如果一个类实现了Cloneable接口,那么它就应该在clone方法里面返回对该对象的逐域拷贝,否则就会抛出CloneNotSupportedException异常。

对于clone方法的一些约束:

解读:

不得不说,这本书的翻译是真的烂。_ (:з)∠) _

  1. 上面提到的不调用构造方法就可以创建对象的规定其实太过于强硬,或者享有了太大的特权,想象你好不容易实现了一个单例,并通私有构造方法并抛出异常,甚至提供了readResolve这个方法来保证反序列化也是单例。但是一旦你实现了Cloneable,接口并重写了clone方法,那么你好不容易设计的单例会因为clone而失效。作者指出行为良好的clone方法,应该是可以调用构造器来创建对象的,但是遗憾,clone并没有这么做。
  2. 作者强调(通常情况下)(x.clone().getClass() == x.getClass()) == true这个规定太过于软弱。这里我理解了半天。为什么叫过于软弱,因为它不是强制要求的,因为clone指出了不通过构造器就创建对象,但是却允许了(x.clone().getClass() == x.getClass()) != true。举个例子:比如我有一个child类,他继承了他的父类,但是并没有重写clone方法,那么这个方法将最终调用父类的clone。但是很遗憾,父类的设计者并未考虑到这个问题,因为他约定clone的一些约束,允许上面的不等条件出现,所以他使用了构造方法来clone了对象的副本。那么当子类调用clone方法的时候,你会惊讶的发现居然返回的是父类的对象,甚至你无法通过强制类型转化将其转化过来。
  3. 为了避免上面的情况,保证子类通过调用super.clone()也返回自己的实例。我们的父类也应该调用自己的super.clone()(父类的父类的clone方法)。在这种层层传递下,所有的super.clone()最终都会调用Object.clone()方法,这样就能保证在整个类的层级结构中,所有的子类的clone()方法最终均会返回Object的对象,由于 Objcet.clone()返回的是对象的逐域拷贝(也就是对整块内存的复制),所以最终我们只需要进行强制类型转化即可。
  4. 很遗憾的是,Cloneable接口并没有清楚的指出一个类实现这个接口应该承担什么责任。

浅克隆:

这里作者列举了一个Stack类的例子:

public class Stack {
    private Object[] elements;
    private int size = 0;
    private static final int DEFAULT_INITIAL_CAPACITY = 16;

    public Stack() {
        this.elements = new Object[DEFAULT_INITIAL_CAPACITY];
    }

    public void push(Object e) {
        ensureCapacity();
        elements[size++] = e;
    }

    public Object pop() {
        if (size == 0)
            throw new EmptyStackException();
        Object result = elements[--size];
        elements[size] = null; // Eliminate obsolete reference
        return result;
    }

    // Ensure space for at least one more element.
    private void ensureCapacity() {
        if (elements.length == size)
            elements = Arrays.copyOf(elements, 2 * size + 1);
    }
}

我们可以看到,Stack类持有Object数组的应用。当我们克隆这个对象的时候,理想情况下,Stack的elements也会跟随一起被克隆,然而很遗憾,克隆出来的的对象和原来的对象持有相同element对象的引用,也就是说,克隆的时候,只传递了引用。

深克隆:

其实就是针对上面问题的一个解决方案罢了。

对于对象持有的对象引用,我们应当递归(或迭代)地调用这些对象的clone方法。

@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();
    }
}

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;
}

克隆的替代方案:


14、考虑实现Comparable接口

compareTo方法并没有在Object类中声明,它是Comparable接口中的唯一方法,其实这个是很好理解的,因为我们并不需要每一个对象都是可以比较大小的。

compareT方法

int compareTo(T t)

可以看到,这个方法是泛型的,并且返回的结果是int类型。其值和比较的结果关系如下:

好处:

通过实现Comparable接口,可以让类与所有依赖此接口的通用算法和集合实现来进行相互操作。并且几乎所有的JAVA平台类库中的所有值类都实现了这个接口。

要求:

注意:


15、使类和成员的可访问性最小化

一个良好设计的类应该隐藏它的所有实现细节,仅仅对外暴露API,这样会比较干净。其他的组件,通过API和他们进行交互,并且对他们的内部工作应当一无所知。这个概念被称为封装

封装的优势:

可访问性最小化原则:让每一个类或成员尽可能的不可访问,即尽可能的降低访问级别。

注意:


相关链接:
EffectiveJava笔记[一]
EffectiveJava笔记[二]

上一篇 下一篇

猜你喜欢

热点阅读