EffectiveJava笔记[三]

11、重写equals时重写hashCode方法

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

规范:

  • 在程序执行的这段时间,如果对象equals方法进行的比较信息没有被改变的话,那么这个对象的hashCode该返回同一个整数。
  • 如果这个应用程序多次执行,那么没有必要保证每一次的hashCode都是一样的。
  • 如果两个对象通过equals方法比较是相等的,那么他们的hashCode应该一样
  • 如果两个对象通过equals方法比较是不相等的,那么他们的hashCode不应该一样。

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

hashCode的计算:

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

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

注意:

  • 如果一个域的值是由其他域演算出来的话,那么这个域不需要用于计算hashCode
  • 如果一个域并没有在equals方法中使用,那么这个域不应该用于计算hashCode
  • 不要排除一些关键的属性来提高hashCode的性能,这可能会降低这个方法的可靠性
  • 在编写完hashCode之后,应当测试hashCode功能的正确性
  • 如果一个对象计算hashCode的代价比较大的话,应当缓存这个hashCode

12、始终重写toString方法

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

  • toString方法应当返回对象中包含的所有需要关注的信息
  • 应当在文档中指定toString方法返回的格式
  • 在静态工具类中,编写toString方法是没有任何意义的。
  • 应当在任何抽象类型中定义toString方法,使得子类共享一个公共字符串表示形式
  • 除非父类已经实现了toString方法,否则应该在每一个实例化的子类中重写toString方法。

13、谨慎地重写clone

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

Cloneable接口:

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

  • 这是一种比较极端的写法,因为它违背了定义接口的作用。
  • 这是一种可以不调用构造器就创建对象的方法。

对于clone方法的一些约束:

  • (x.clone() != x) == true 
    
  • (x.clone().getClass() == x.getClass()) == true
    
  • x.clone().equals(x) == true
    

解读:

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

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

克隆的替代方案:

  • 对于那些确定是不可变的类,最好不要实现clone方法,因为这没有意义。
  • 使用拷贝构造器或者拷贝工厂
  • 使用基于接口的拷贝构造器(转换构造器)或者拷贝工厂(转换工厂)。

14、考虑实现Comparable接口

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

compareT方法

int compareTo(T t)

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

  • 负数:<
  • 0:=
  • 正数:>

好处:

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

要求:

  • sgn(x.compareTo(y)) == -sgn(y.compareTo(x)) == true
  • (x.compareTo(y) > 0 && y.compareTo(z) > 0) == (x.compareTo(z)>0) == true
  • x.compareTo(y) == 0 —> sgn(x.compareTo(z)) == sgn(y.compareTo(z))
  • (x.compareTo(y) == 0) == (x.equals(y)) 注意,与equals方法不同的是,equals方法有可能花月不同类型的对象,而compareTo遇到不同类型的对象的时候,会直接抛出异常。

注意:

  • 不要使用两个值之间的差值来返回compareTo的结果,这种写法可能会导致整数的最大长度溢出。
  • 使用compareTo方法的时候,应当避免使用 “<”(小于)或">"(大于)。在具体的值进行比较的时候,应当使用静态的compare或者Comparator接口中的构建方法。

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

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

封装的优势:

  • 将组成系统的组件分开,允许他们被独立的开发,测试,优化。
  • 拥有不同功能的组件可以并行开发。

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

  • 一个顶级类(.java文件中的直接定义的类)或接口的访问级别只能是 default或者public的。如果没有必要将其设置为public,那么我们就应该将其设置为default的。
  • 当我们把一个类设置为public的时候,那么它就拥有导出API的功能,在这种情况下,我们应该有义务维护这个类,让其保持着API的兼容性。当然如果是default的话就没有这个必要,因为它是包级私有的,并不拥有到处API的功能,仅仅是组件实现的一部分。
  • 如果一个default修饰的顶级类只被一个类使用的话,那么我们应该考虑将这个类作为其使用者的私有静态嵌套类。
  • 对于一个public修饰的类,我们在设计其成员的时候,应该尽可能的将其成员设置为私有的。

注意:

  • 如果一个类存在一个是public且非final的实例属性,那么我们就放弃了限制这个实例的属性被修改的能力,并且当这个实例的属性被修改的时候,我们也并没有解决的方案。
  • 即便是类中的实例属性是final的,也只能保证它引用的对象无法改变而无法保证它引用的对象的属性保持不变。
  • 对所有的静态属性也是如此。我们通过public static final 来表示一个常量。但是,如果其引用的是一个对象,虽然对象的引用无法被修改,但是我们我发保证对象的属性保持不变。
  • 所有的public static final 修饰的长度不为0的数组,我们也应当注意,因为它是可变的。

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270