zoukankan      html  css  js  c++  java
  • 关于覆盖Object中的hashCode, equals和toString

    最近在看《Effective Java》,里面看到了关于重载hashCode、equals和toString方法的篇章,顿时觉得视野开拓了不少,而且正结合自己工作、项目中的实例,觉得有必要总结一下,并分享给其它人。

    首先,我准备了一个Bean,里面有几种数据类型的变量,算是各自举了个例子:

     1 public class Instance {
     2     public byte parameter1;
     3     public boolean parameter2;
     4     public char parameter3;
     5     public short parameter4;
     6     public int parameter5;
     7     public long parameter6;
     8     public float parameter7;
     9     public double parameter8;
    10     public int[] intArr;
    11     public String string;
    12
    View Code

    首先是toString吧,这个方法比较独立。toString方法的通用约定要求对本类提供一个“简洁且内容丰富”字符类型。如果没有重写toString的话,调用该方法并打印出来,字符串是这个样子的:

    1 Instance@d5682s78
    View Code

    其中@前面的字符表示该类的名称,同时还会包括该类的包名。@后面的则是该类在内存中的位置地址的16进制表示。

    使用Eclipse的应该都知道,source-->Generate toString。按照这种方式书写的toString方法如下:

    1     @Override
    2     public String toString() {
    3         return "Instance [parameter1=" + parameter1 + ", parameter2="
    4                 + parameter2 + ", parameter3=" + parameter3 + ", parameter4="
    5                 + parameter4 + ", parameter5=" + parameter5 + ", parameter6="
    6                 + parameter6 + ", parameter7=" + parameter7 + ", parameter8="
    7                 + parameter8 + ", intArr=" + Arrays.toString(intArr)
    8                 + ", string=" + string + ", hashcode=" + hashcode + "]";
    9     }
    View Code

    这是覆盖toString方法的非格式化方式。覆盖toString有两种方式,接下来是一种格式化的方式:

    1 @Override
    2     public String toString() {
    3         return String
    4                 .format("Instance [parameter1=%d, parameter2=%b, parameter3=%c, parameter4=%x, parameter5=%o, parameter6=%d, parameter7=%f, parameter8=%a, intArr=%s, string=%s]",
    5                         parameter1, parameter2, parameter3, parameter4,
    6                         parameter5, parameter6, parameter7, parameter8,
    7                         Arrays.toString(intArr), string);
    8     }
    View Code

    其中的格式化符号的含义如下:

    转换符 说明 示例

    %s 字符串类型 "mingrisoft"

    %c 字符类型 'm'

    %b 布尔类型 true

    %d 整数类型(十进制)99

    %x 整数类型(十六进制)FF

    %o 整数类型(八进制)77

    %f 浮点类型 99.99

    %a 十六进制浮点类型 FF.35AE

    %e 指数类型 9.38e+5

    %g 通用浮点类型(f和e类型中较短的)

    %h 散列码

    %% 百分比类型 %

    %n 换行符

    %tx 日期与时间类型(x代表不同的日期与时间转换符

    当然在格式化的时候格式由开发人员确定,同时一旦确定就最好不要再次修改,因为这样会导致已有产品与后期的更新不匹配的情况。

    然后下面是equals方法和hashCode方法。在Object的通用约定中,有以下的原则:

    1,生成的同一个对象,多次调用hashCode,返回的值是一样;同样类型的对象,在多次运行时,产生的值可以不同。

    2,equals返回true的对象,两者的hashCode返回值是相同的。

    3,hashCode返回值相同的对象,equals未必返回true。

    因此,在重写equals方法的同时,必须重写hashCode方法。

    重写equals方法时,要求两个对象逻辑上相等。但未必在内存中的位置相等,亦即未必是相同的对象才能equals为true。通常equals方法比较类中的一些或全部变量在值上相等。下面是一个equals重写的示例:

     1 @Override
     2     public boolean equals(Object obj) {
     3         // TODO Auto-generated method stub
     4         if (obj == null) {
     5             return false;
     6         }
     7         if (obj == this) {
     8             return true;
     9         }
    10         if (obj instanceof Instance) {
    11             Instance instance = (Instance) obj;
    12             return instance.parameter1 == parameter1
    13                     && instance.parameter2 == parameter2
    14                     && instance.parameter3 == parameter3
    15                     && instance.parameter4 == parameter4
    16                     && instance.parameter5 == parameter5
    17                     && instance.parameter6 == parameter6
    18                     && instance.parameter7 == parameter7
    19                     && instance.parameter8 == parameter8
    20                     && instance.string.equals(string);
    21         }
    22         return false;
    23     }
    View Code

    这个重写equals示例中,选取了parameter1-8和string作为是否相等的判断元素之一。

    因为覆盖equals方法,必须要覆盖hashCode方法,而且,hashCode的计算中,equals中参与equals判断的元素必须出现在hashCode的计算过程中。

    因而,hashCode的覆盖可以采用以下的方式:

     1         @Override
     2     public int hashCode() {
     3         // TODO Auto-generated method stub
     4         int result = 17;
     5         result = 31 * result + parameter1;
     6         result = 31 * result + (parameter2 ? 1 : 0);
     7         result = 31 * result + parameter3;
     8         result = 31 * result + parameter4;
     9         result = 31 * result + parameter5;
    10         result = 31 * result + (int) (parameter6 ^ (parameter6 >>> 32));
    11         result = 31 * result + Float.floatToIntBits(parameter7);
    12         long dLong = Double.doubleToLongBits(parameter8);
    13         result = 31 * result + (int) (dLong ^ (dLong >>> 32));
    14         result = 31 * result + string.hashCode();
    15         return result;
    16     }
    View Code

    这里hashCode的计算有几个通用的原则:

    1,result的初始值为17。其实这个值可以随意取,但最好是素数。

    2,参数因子31。之所以选择31,原因有二:一,31是一个传统的奇数。二,31*x==x<<5-x. 这两者决定了31作为hash计算时的参数因子,而且,很多的hash计算方法都会采用31。

    3,对于byte, char, short,int等小整型,通常要将其转化为int类型跟result相加。

    3,对于boolean类型,通常要转化为b ? 1:0;

    4,对long类型,通常要进行(int)(f^(f>>>32))转换。

    5,对于float类型,通常要进行Float.floatToIntBits(f)转化;

    6,对于double类型,通常要进行Double.doubleToLongBits(d),之后再按照4进行转化后与result相加;

    7,对于String类型,则要进行s.hashCodd()转化。

    8,对于数组,则要对数组中的每一个元素都要进行相应的转化。

    9,对于类类型,则要进行o.hashCode()转化。同时,该类类型的hashCode也要进行覆盖。

    同时,这种计算hash的方法,参数的顺序不会对值产生影响;字符串中的字符顺序也不会产生影响。在实践中,这种hash计算方式具有较好的散列性质。

    然而,这种方式覆盖的hashCode在每次调用时,都会进行一次计算,显然有些浪费资料并延缓了程序的运行。自然,这种方式还有等改进的地方。

    下面的这种方式采用了预存hash值的方式:

     1         private volatile int hashcode = 0;
     2 
     3     @Override
     4     public int hashCode() {
     5         // TODO Auto-generated method stub
     6         int result = hashcode;
     7         if (hashcode == 0) {
     8             result = 17;
     9             result = 31 * result + parameter1;
    10             result = 31 * result + (parameter2 ? 1 : 0);
    11             result = 31 * result + parameter3;
    12             result = 31 * result + parameter4;
    13             result = 31 * result + parameter5;
    14             result = 31 * result + (int) (parameter6 ^ (parameter6 >>> 32));
    15             result = 31 * result + Float.floatToIntBits(parameter7);
    16             long dLong = Double.doubleToLongBits(parameter8);
    17             result = 31 * result + (int) (dLong ^ (dLong >>> 32));
    18             result = 31 * result + string.hashCode();
    19         }
    20         return result;
    21     }
    View Code

    这种方式预存了一个volatile类型的hash值,只有在hashCode方式在第一次调用时进行了hash值的计算,其余的时候只要直接获取hash值就可以了,明显地提高了程序的运行效率。

    在这种计算hash的方法中,volatile关键字原义是“不稳定、变化”的意思,volatile告诉jvm, 它所修饰的变量不保留拷贝,直接访问主内存中的。由于使用volatile屏蔽掉了VM中必要的代码优化,所以在效率上比较低,因此一定在必要时才使用此关键字。

    最后,也可以对hashCode进行“延迟加载”,由于“延迟加载”通常伴随着代码上复杂性的增加和可读性的损失,因而不再对此方法赘述。

  • 相关阅读:
    [POJ 3253] Fence Repair
    [POJ 1422] Air Raid
    [POJ 2195] Going Home
    [POJ 1273] Drainage Ditches
    [BZOJ 1718] Redundant Paths
    [POJ 1041] John's Trip
    [NOI 2003] 逃学的小孩
    __attribute__((noreturn))的用法
    回味经典——uboot1.1.6 之 第二阶段 第三阶段
    回味经典——uboot1.1.6 之 第一阶段
  • 原文地址:https://www.cnblogs.com/littlepanpc/p/3853112.html
Copyright © 2011-2022 走看看