欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Effective Java读书笔记、感悟——2.2 其余Object通用方法 博客分类: Java_SE effectivejavaobject 

程序员文章站 2024-02-15 09:08:22
...

 

最近一直在忙着完成作业,看书和写博客的进度大大受到折扣。假期回去把最近Nachos的实验重新做一遍,好好总结、测试过后写份博客,中间还是学到了很多东西,虽然学不到什么API的使用。不多说了,继续Java,这才是正道(要遭*的言论)。这次的博客中有很多问题都没有注意到,只是做了简单的笔记记录和一些小地方的注释,着重说明了hashCode,尤其clone没有更深入的探究,但是使用过程中有时候只是简单的clone,所以尽量也使用的时候详细的阅读以下该类的clone注释(还有个原因是clone使用需要谨慎,书中提出了推荐的方法,由于最近时间紧张也就没有选择深入探究clone)。

 

二:覆盖equals时总要覆盖hashCode

Object规范中提到:(非原文,仅理解和摘要)

à执行期间对象的equals比较操作用到的信息没有被修改,那么调用多次hashCode应该始终返回同一个整数,多次运行可以不同。

à两个对象根据equals比较相等,那么他们的hashCode返回相同的值。

àequals不等,hashCode不一定不等,但是给完全不同的对象产生不同的散列码提高性能

如果没有覆盖hashCode而违反的关键约定是第二条。

如果试图和HashMap(在应用程序中被非常广泛的用到)HashSetHashtable一起使用作为key(作为key之后会根据key产生的hashcode进行散列,如果很多不同的对象返回了相同的key就将散列表变成了很少的长链表,降低性能),不保证hashCode被覆盖将带来问题。

 

书中提到的散列函数(可以在其他自己需要构造散列表的时候使用):

 

1、把某个非零的常数值,比如说17,保存在一个名为result的int类型的变量中。

2、对于对象中每个关键域f(指equals方法中涉及的每个域),完成以下步骤:

  a. 为该域计算int类型的散列码c:

    i.    如果该域是boolean类型,则计算(f ? 1 : 0)。

    ii.   如果该域是byte、short、char或者int类型,则计算(int)f。

    iii.  如果该域是long类型,则计算(int)(f ^ (f >>> 32))。

    iv.  如果该域是float类型,则计算Float.floatToIntBits(f)。

    v.   如果该域是double类型,则计算Double.doubleToLongBits(f),然后照2.a.iii。

    vi.   如果该域是一个对象引用,并且该类的equals方法通过递归地调用equals方法来比较这个域,则同样为这个域递归地调用hashCode。如果需要更复杂的比较,则为这个域计算一个范式,然后针对这个范式调用hashCode。如果这个域的值为null,则返回0。

    vii.  如果该域是一个数组,则需要把每一个元素当做单独的域来处理,也就是说,递归地应用上述规则,对每个重要的元素计算一个散列码,然后根据步骤2.b中的做法把这些散列值组合起来。如果数组域中的每个元素都很重要,可以利用发行版本1.5中增加的其中一个Arrays.hashCode方法。

  b. 按照下面的公式,把步骤2.a中计算得到的散列码c合并到result中:result = 31 * result + c ;

3、返回result。

4、写完了hashCode方法之后,问问自己“相等的实例是否都具有相等的散列码”。

 

之所以采用31是因为它是一个奇素数。如果乘数是偶数,并且惩罚溢出的话信息会丢失,因为与2相乘等于移位运算。使用素数的好处并不明显大,但是习惯使用素数计算散列结果。31还有一个很好的特性,即用移位和减法来代替乘法可以得到更好的性能:31*i==(i<<5)-I。现代的VM可以自动完成这种优化。合理利用移位操作提高效率是好的编程习惯

 

这里先从外围,举最常用的Java的HashMap来说明一下HashCode的作用:

下面是put方法,其中是根据用户的hashCode产生的值的基础上做了一次散列计算。用户散列函数效果太差会直接影响到HashMap的使用性能。(尤其我们经常会将自己编写的Bean等作为value传入,如果没有覆盖hashCode)

 

 

public V put(K key, V value) {
        if (key == null)
            return putForNullKey(value);
        int hash = hash(key.hashCode());
        int i = indexFor(hash, table.length);
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }

        modCount++;
        addEntry(hash, key, value, i);
        return null;
}

 

 

下面看下HashMap的hash方法做了什么:

 

 

/**
     * Applies a supplemental hash function to a given hashCode, which
     * defends against poor quality hash functions.  This is critical
     * because HashMap uses power-of-two length hash tables, that
     * otherwise encounter collisions for hashCodes that do not differ
     * in lower bits. Note: Null keys always map to hash 0, thus index 0.
     */
    static int hash(int h) {
        // This function ensures that hashCodes that differ only by
        // constant multiples at each bit position have a bounded
        // number of collisions (approximately 8 at default load factor).
        h ^= (h >>> 20) ^ (h >>> 12);
        return h ^ (h >>> 7) ^ (h >>> 4);
    }

 

 

可以阅读两个部分的注释,使用null作为key不是好的做法。同时下面的注释也需要关注,我尝试翻译却总做不太好,就直接阅读英语注释吧,也不会出现翻译带来的偏差。

 

这里主要是说明一下hashcode的作用,上面函数对用户返回的hashcode做了不同的散列,使。Hashtable中是对hashcode做了如下的处理得到的数组索引

 

int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;

 

为什么用到hashCode的地方都要这样做呢,hashCode默认返回的值是相同的呢还是不同的呢,Object中并没有属性,我们在使用HashMap的时候没有切身的感觉到未覆盖hashcode带来的麻烦。如果相同上面的处理带不来任何用处,但是如果是基本都不同的那么上面的代码就能带来很大的性能优化了。直接看代码

 

/**
     * Returns a hash code value for the object. This method is
     * supported for the benefit of hash tables such as those provided by
     * {@link java.util.HashMap}.
     * <p>
     * The general contract of {@code hashCode} is:
     * <ul>
     * <li>Whenever it is invoked on the same object more than once during
     *     an execution of a Java application, the {@code hashCode} method
     *     must consistently return the same integer, provided no information
     *     used in {@code equals} comparisons on the object is modified.
     *     This integer need not remain consistent from one execution of an
     *     application to another execution of the same application.
     * <li>If two objects are equal according to the {@code equals(Object)}
     *     method, then calling the {@code hashCode} method on each of
     *     the two objects must produce the same integer result.
     * <li>It is <em>not</em> required that if two objects are unequal
     *     according to the {@link java.lang.Object#equals(java.lang.Object)}
     *     method, then calling the {@code hashCode} method on each of the
     *     two objects must produce distinct integer results.  However, the
     *     programmer should be aware that producing distinct integer results
     *     for unequal objects may improve the performance of hash tables.
     * </ul>
     * <p>
     * As much as is reasonably practical, the hashCode method defined by
     * class {@code Object} does return distinct integers for distinct
     * objects. (This is typically implemented by converting the internal
     * address of the object into an integer, but this implementation
     * technique is not required by the
     * Java<font size="-2"><sup>TM</sup></font> programming language.)
     *
     * @return  a hash code value for this object.
     * @see     java.lang.Object#equals(java.lang.Object)
     * @see     java.lang.System#identityHashCode
     */
    public native int hashCode();

 

够倒霉的,本地方法,没有在Object.java中实现,并且注释中提到的全部都是Effective Java中提到的规范。继续查看文章。这段代码找不到,但是可以根据Effective Java中提到的覆盖equals时要覆盖hashCode,且两者的关系可以知道查看equals也许有所帮助:

 

public boolean equals(Object obj) {
        return (this == obj);
}
 

内存地址?按照equals的内容应该是吧。编写测试程序吧,toString可以用来参考

 

public String toString() {
        return getClass().getName() + "@" + Integer.toHexString(hashCode());
}

 可以用equals和hashcode分配去验证,开始的时候发现应该是对的,但是随着数据量的增加,慢慢好像发生了问题,hashcode开始有重复(CHECKSIZE为1w时),下面是测试代码:

 

public static void main(String[] args) {
        List<Object> listHashCode =  new ArrayList<Object>();
        List<Object> listObj =  new ArrayList<Object>();
        
        //hashcode是否是内存地址
        for (int i = 0; i < CHECKSIZE; i++) {
            Object obj=new Object();
            if (listHashCode.contains(obj.toString())) {
                System.out.println(obj.toString() +"  exists in the listHashCode. "+ i+" ; Is obj contain in listObj :"+listObj.contains(obj));
            }
            else {
            	listHashCode.add(obj.toString());
            }
            listObj.add(obj);
        }
 }   
 
但是同时这里也说明了hashcode默认基本都是不同的,再细追究下去可能要走死胡同了,也没有意义。hashcod也没有必要说给不同对象都产生不同的值,上面的例子只会出现一次重复,可见已经可以取得较好的散列性能。
(还是补充说明一下Java开发人员给出的原因:hashCode()的缺省实施通过将对象的内存地址对映于一个整数值来生成。由于在某些架构上,地址空间大于int值的范围,两个不同的对象有相同的 hashCode()是可能的。如果您忽略了hashCode(),您仍旧可以使用System.identityHashCode()方法来接入这类缺省值。)

网上很多地方举了String的例子,我们看下

/**
     * Returns a hash code for this string. The hash code for a
     * <code>String</code> object is computed as
     * <blockquote><pre>
     * s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]
     * </pre></blockquote>
     * using <code>int</code> arithmetic, where <code>s[i]</code> is the
     * <i>i</i>th character of the string, <code>n</code> is the length of
     * the string, and <code>^</code> indicates exponentiation.
     * (The hash value of the empty string is zero.)
     *
     * @return  a hash code value for this object.
     */
    public int hashCode() {
        int h = hash;
        if (h == 0) {
            int off = offset;
            char val[] = value;
            int len = count;

            for (int i = 0; i < len; i++) {
                h = 31*h + val[off++];
            }
            hash = h;
        }
        return h;
}
 
可以看到, String类是使用它的 value值作为参数然后进行运算得出hashcode的。换句话说, 只要值相同的String不管是不是一个对象,hash值全部相等。虽然不得到所有不同对象不同的hashcode的最优情况,但是也有不错的性能了。
这部分就说到这了,再深入下去也没有意义了,使用规范就是开始说到的了,详细建议还是看下原书,指的一提的就是注意一同修改(可以直接忽略底层实现)和都不修改(直接使用底层实现,这种方式是针对Object的子类,如果其他的子类需要关注一下父类的实现是否对自己可用)是两种正确的使用方法。

 

三:始终要覆盖toString(很重要同样也很好理解)

默认的toString为:‘类名@散列码的无符号十六进制表示法’。

à在实际应用中,toString方法应该返回对象汇总包含的所有值得关注的信息,且这些信息是可以自描述的。

à无论是否制定格式,都应该在文档中明确的表明你的意图,且都要为信息提供一种编程式的访问途径。

 

四:谨慎的覆盖clone

 

àCloneable接口的作用是决定了Object中受保护的clone方法的实现行为:如果一个类实现了CloneableObjectclone方法就返回该对象的逐域拷贝,否则抛出CloneNotSupportedException。(极端的非典型接口实现方法)

à如果你覆盖了非final类中的clone方法,则应该返回一个通过调用super.clone而得到的对象。

àclone方法就是另一个构造器,你必须确保它不会伤害到原始的对象,并确保正确的创建被clone对象中的约束条件。

à实现对象拷贝的好方法是提供一个拷贝构造器或拷贝工厂。

 

五:考虑实现Comparable接口

àJava平台类库中的所有值类都实现了Comparable接口。

àequals不同的是,在跨越不同类的时候,compareTo可以不做比较,直接抛出ClassCastException异常。(较equal此处更方便实现,问题较少)

àcompareTo没有规定返回值的大小,只规定了正负,可以利用这个特性规定返回值的含义或用来简化代码(是指不需要过多的判断值的所属区域,只需要判断正负)。但是compareTo的返回值不应该被用于逻辑处理,这样就破坏了方法的单一职责,并且为继承该类的子类带来了不必要的麻烦,如果程序员没有详细阅读注释很容易造成错误。并且由于返回值对属性的依赖可能为以后的修改带来风险。