Java中,只有8种基本类型不是对象,例如:4种整形类型(byte, short, int,long),2种浮点类型(flout, double),boolean, char不是对象,其他的所有类型,不论是对象数组,列表等都扩展了Object类.了解学习Object中方法的设计原理和实现方式有助于更好的学习理解java语言.
下面,我们首先学习一下Object中的equals方法.判断两个对象相等时,JVM首先查找两个对象的hashCode, 如果两者hashCode不同,则返回false;如果两者的hashCode相同,则调用equals方法进行比较.
Object中equals方法
Object中的equals方法用于检测一个对象是否等于另外一个对象. Object类中,判断两个对象是否引用同一个对象实现equals方法,源码如下:
return ( this == obj);
}
实际处理中,若要比较两个对象是否相等,一般需要重写equals方法.
重写equals方法的要点:
1. 使用==操作符检查“实参是否为指向对象的一个引用”。
2.判断实参是否为null
3. 使用instanceof操作符检查“实参是否为正确的类型”。
4. 把实参转换到正确的类型。
5. 对于该类中每一个“关键”域,检查实参中的域与当前对象中对应的域值是否匹
配。对于既不是float也不是double类型的基本类型的域,可以使用==操作符
进行比较;对于对象引用类型的域,可以递归地调用所引用的对象的equals方法;
对于float类型的域,先使用Float.floatToIntBits转换成int类型的值,
然后使用==操作符比较int类型的值;对于double类型的域,先使用
Double.doubleToLongBits转换成long类型的值,然后使用==操作符比较
long类型的值。
6. 当你编写完成了equals方法之后,应该问自己三个问题:它是否是对称的、传
递的、一致的?(其他两个特性通常会自行满足)如果答案是否定的,那么请找到
这些特性未能满足的原因,再修改equals方法的代码。
例1:雇员对象比较
如果两个雇员对象的姓名, 薪水和雇佣一样,就认为它们相等.重写equals方法如下:
private String name;
private double salary;
private Date hireDay;
...
@Override
public boolean equals(Object obj) {
// 如果为同一对象的不同引用,则相同
if (this == obj) {
return true;
}
// 如果传入的对象为空,则返回false
if (obj == null) {
return false;
}
// 如果两者属于不同的类型,不能相等
if (getClass() != obj.getClass()) {
return false;
}
// 类型相同, 比较内容是否相同
Employee other = (Employee) obj;
return Objects.equals(name, other.name) && salary == other.salary && Objects.equals(hireDay, other.hireDay);
}
}
注意,比较通过Objects中静态函数equals比较两个对象是否相等.该方法源码如下:
return (a == b) || (a != null && a.equals(b));
}
这样,当两个对象都为null时,返回true,例如,两个Employee对象的name都为null, 返回true; 如果第一个对象不为null,则调用a.equals(Object obj)方法
对象比较与继承
如果隐式和显式的对象所属类型不一致,equals方法该如何处理呢?在例1中,如果两个对象的类型不一致,直接返回false;多数程序员直接使用instanceof检测:
这样做不但没有解决继承子类的错误,还可能引发其他的一些错误,例1中,采用强制类型转换的方式.
要实现子类对象比较,先了解一下java规范中对equals方法的约束.
java规范中equals方法特征
- 自反性
对于任何非空引用x, x.equals(x) 返回true;
- 对称性
对于任何引用x, y, 当且仅当y.equals(x) 返回true, x.equals(y)返回true;
- 传递性
对于任何引用x, y, z, 若x.equals(y)返回true, y.equals(z)返回true; 则 x.equals(z)返回true;
- 一致性
若x和y引用的对象没有发生改变, 则反复调用x.equals(y)应该返回同样的结果.
- 对任意非空引用x, x.equals(null) 返回false;
下面可以通过两个不同的情况看待这个问题:
- 如果子类能够拥有自己的相等概念, 则对称性需求强制采用getClass进行检测
- 如果由超类决定相等的概念, 那么就用instanceof进行检测,这样可以在不用子类的对象之间进行相等的比较
注释:
- 在标准的java库中包含150多个equals方法的实现,包括instanceof检测, 调用getClass检测, 捕获ClassCastException检测或者什么都不做. 在java.sql.TimeStamp实现人员指出, Timestamp类继承Date类,而后者的equals方法使用了一个instanceof检测,这样重写equals方法时,就无法同时做到对称性.
- 在由超类决定相等时,可以考虑final关键字修改比较函数,若考虑到子类equals方法灵活性,可以不加修饰,例如AbstractSet.equals方法,应该申明为final, 这样就可以比较子类HashSet和TreeSet, 但是考虑到子类的灵活性,没有添加任何修饰.
编写equals方法的建议:
- 显示参数命名为otherObject, 稍后转化成other变量
public boolean equals(Object otherObject)
- 检测this和otherObject是否是同一个对象的引用,是,返回true;
if( this==otherObject){
return true;
} - 检测otherObject是否为null, 是, 返回false;
if(otherObject == null){
return false;
} - 比较this和otherObject是否属于同一个类. 如果equals的语义在每个子类中有所改变,就使用getClass检测:
if(getClass() != otherObject.getClass()){
return false;
}if(!(otherObject instanceof Employee)){
return false;
} - 将otherObject转化为相对应的类型变量other
Employee other = (Employee)otherObject;
- 对所需要的比较的数据域进行比较. 如果是基本数据类型,使用a==b比较; 如果是对象比较,调用Objects.equals(a, b)进行比较
return Objects.equals(name, other.name) && salary == other.salary && Objects.equals(hireDay, other.hireDay);
整个流程可以参照例1;
常见equals方法实现错误
public boolean equals(Employee other) {
return Objects.equals(name, other.name) && salary ==other.salary && Objects.equals(hireDay, other.hireDay);
}
这个方法声明的显示参数类型是Employee. 其结果并没有覆盖Object中的equals方法,而是定义了一个完全无关的方法.为了避免发生类型错误,可以使用@override对覆盖超类的方法进行标记.
在Java中,问什么说重写了equals方法都要进而重写Hashcode方法呢?
首先,想要明白hashCode的作用,你必须要先知道Java中的集合。
总的来说,Java中的集合(Collection)有两类,一类是List,再有一类是Set。
你知道它们的区别吗?前者集合内的元素是有序的,元素可以重复;后者元素无序,但元素不可重复。
那么这里就有一个比较严重的问题了:要想保证元素不重复,可两个元素是否重复应该依据什么来判断呢?
这就是Object.equals方法了。但是,如果每增加一个元素就检查一次,那么当元素很多时,后添加到集合中的元素比较的次数就非常多了。
也就是说,如果集合中现在已经有1000个元素,那么第1001个元素加入集合时,它就要调用1000次equals方法。这显然会大大降低效率。
于是,Java采用了哈希表的原理。哈希(Hash)实际上是个人名,由于他提出一哈希算法的概念,所以就以他的名字命名了。
哈希算法也称为散列算法,是将数据依特定算法直接指定到一个地址上。如果详细讲解哈希算法,那需要更多的文章篇幅,我在这里就不介绍了。
初学者可以这样理解,hashCode方法实际上返回的就是对象存储的物理地址(实际可能并不是)。
这样一来,当集合要添加新的元素时,先调用这个元素的hashCode方法,就一下子能定位到它应该放置的物理位置上。
如果这个位置上没有元素,它就可以直接存储在这个位置上,不用再进行任何比较了;如果这个位置上已经有元素了,
就调用它的equals方法与新元素进行比较,相同的话就不存了,不相同就散列其它的地址。
所以这里存在一个冲突解决的问题。这样一来实际调用equals方法的次数就大大降低了,几乎只需要一两次。
原因如下:当equals此方法被重写时,通常有必要重写 hashCode 方法,以维护 hashCode 方法的常规协定,该协定声明相等对象必须具有相等的哈希码。如下:
(1)当obj1.equals(obj2)为true时,obj1.hashCode() == obj2.hashCode()必须为true
(2)当obj1.hashCode() == obj2.hashCode()为false时,obj1.equals(obj2)必须为false
hashcode是用于散列数据的快速存取,如利用HashSet/HashMap/Hashtable类来存储数据时,都是根据存储对象的hashcode值来进行判断是否相同的。
这样如果我们对一个对象重写了euqals,意思是只要对象的成员变量值都相等那么euqals就等于true,但不重写hashcode,那么我们再new一个新的对象,当原对象.equals(新对象)等于true时,两者的hashcode却是不一样的,由此将产生了理解的不一致。
——————————————————————————————————————————
正确重写hashCode的办法
[1]. hashCode重写成相同的值的缺点
将所有对象的hashCode都返回一样的值是不科学的。比如a1和a3这两个根本不同的对象,就没有必要去比较equals,增加无谓的计算量。所以应该对象本身的内容 (属性)来重写hashCode。
一旦两个对象内部不一样,就直接判定出hashCode不一样,不用再调用equals进行比较。
[2]. 正确书写hashCode的办法:
【原则】按照equals( )中比较两个对象是否一致的条件用到的属性来重写hashCode()。
{1}. 常用的办法就是利用涉及到的的属性进行线性组合。
{2}. 线性组合过程中涉及到的组合系数自定义即可。
注意,拼接之后的数值不能超过整形的表达范围。
{3}. 公式:属性1的int形式+ C1*属性2的int形式+ C2*属性3的int形式+ …
【技巧】当属性是引用类型的时候,如果已经重写过hashCode(),那么这个引用属性的int形式就是直接调用属性已有的hashCode值。
最典型的就是这个属性是字符串类型的,String类型已经重写了hashCode()方法,所以直接拿来使用即可。
——————————————————————————————————————————
要重写自己的hashCode方法并没有什么绝对正确的答案,但是我们的目标是:不相等的对象尽可能有不同的hashCode,而且必须满足的一个通用约定是:相等的对象应该具有相同的hashCode。下面介绍一种hashCode的实现方式,这种实现方式对一般的程序来说足够了,至于如何实现更完美的hashCode方法就留给数学家或者理论家去讨论吧。
第一步:定义一个初始值,一般来说取17
int result = 17;
第二步:分别解析自定义类中与equals方法相关的字段(假如hashCode中考虑的字段在equals方法中没有考虑,则两个equals的对象就很可能具有不同的hashCode)
情况一:字段a类型为boolean 则[hashCode] = a ? 1 : 0;
情况二:字段b类型为byte/short/int/char, 则[hashCode] = (int)b;
情况三:字段c类型为long, 则[hashCode] = (int) (c ^ c>>>32);
情况四:字段d类型为float, 则[hashCode] = d.hashCode()(内部调用的是Float.hashCode(d), 而该静态方法内部调用的另一个静态方法是Float.floatToIntBits(d))
情况五:字段e类型为double, 则[hashCode] = e.hashCode()(内部调用的是Double.hashCode(e), 而该静态方法内部调用的另一个静态方法是Double.doubleToLongBits(e),得到一个long类型的值之后,跟情况三进行类似的操作,得到一个int类型的值)
情况六:引用类型,若为null则hashCode为0,否则递归调用该引用类型的hashCode方法。
情况七:数组类型。(要获取数组类型的hashCode,可采用如下方法:s[0]*31 ^ (n-1) + s[1] * 31 ^ (n-2) + ..... + s[n-1], 该方法正是String类的hashCode实现所采用的算法)
第三步:对于涉及到的各个字段,采用第二步中的方式,将其依次应用于下式:
result = result * 31 + [hashCode];
补充说明一点:如果初始值result不取17而取0的话,则对于hashCode为0的字段来说就没有区分度了,这样更容易产生冲突。比如两个自定义类中,一个类比另一个类多出来一个或者几个字段,其余字段全部一样,分别new出来2个对象,这2个对象共有的字段的值全是一样的,而对于多来的那些字段的值正好都是0,并且在计算hashCode时这些多出来的字段又是最先计算的,这样的话,则这两个对象的hashCode就会产生冲突。还是那句话,hashCode方法的实现没有最好,只有更好。
- public int hashCode() {
- int hash = 7;
- hash = 31 * hash + num;
- hash = 31 * hash + (null == data ? 0 : data.hashCode());
- return hash;
- }
先调用hashCode,若相同,再调用equals!!!