Object 类中的equals
2015-07-19 11:11
501 查看
先看几个基础的东西:
SDK: Software Development Kit, 软件开发工具包;
JDK: Java Development Kit,java开发工具包;
JRE: Java Runtime Environment,java运行时环境;
对于JRE,在安装JDK的时候会提示是否再安装一个JRE,安装完了之后会发现有两个JRE目录,一个是在jdk内部目录中,一个是后来提示安装的目录。其实这两个是不冲突的,内部的JRE是运行java内部程序的环境,其本身的程序也是用JAVA编写的,所以它也需要一个运行环境,而后来安装的JRE就属于外部的运行环境了,两个运行环境并不冲突,使用哪个都没有关系。
JDK目录:
bin 存放的是一些可执行的.exe程序及一些.dll文件,比如:java.exe,javac.exe等。
include 语言语言头文件 支持 用Java本地接口和Java虚拟机接口 来本机代码编程
lib 包目录,有着两个比较重要的工具包,dt.jar和tools.jar。
jre 运行环境的根目录
lib java运行时所要的代码包及一些资源文件,其中重要的有rt.jar ,java核心类库,运行时的基础类库。可以平时写代码所用到的java类几乎都出自这个jar包,包括下面讲的Object类。
-bin 这个目录里也是一些可执行的.exe文件和.dll文件,一个client目录和server目录,里面放的分别就是我们所熟悉的客户端和服务器版的jvm了。
Object 类存在于java.lang包中,是java所有类的基类,虽然说没有在代码层上显示的extends Object,但是在类创建的时候是默认其基类就是Object类。
jdk1.7中的Object源码中,首先其构造器:
然后是一些个本地化的方法
静态代码块,先注册上述的一些本地方法。
接一下就是一些普通的方法了,我们先看toString()方法:
该方法是把一个对象转换为字符串,基本思路就是强制类型转换,将其转换为StringBuilder非线程安全的字符集对象,再将其类名+@+hashCode值
接下来看两个wati方法
在前面也还一个带一个参数的本地化wait(long l) 方法,所以在Object类中共有三个wait方法,wait(),wait(long l),wait(long l,int i)。不同于equals和toString,这两个普通的wait方法是final的,也就是说它是不能覆盖的。wait方法实用于多线程的程序之中,其作用是暂停当前线程的执行,直到接到有信息(notify)来终止这个暂停。具体的多线程细节这里就先不介绍了。
Java 技术允许使用 finalize() 方法在垃圾收集器将对象从内存中清除出去之前做必要的清理工作。这个方法是由垃圾收集器在确定这个对象没有被引用时对这个对象调用的。它是在 Object 类中定义的,因此所有的类都继承了它。子类覆盖 finalize() 方法以整理系统资源或者执行其他清理工作。finalize() 方法是在垃圾收集器删除对象之前对这个对象调用的。
接下来我们重点看equals 方法:
这个方法也很简单,就是把this和传进来的这个对象引用比较一下,返回比较结果。但是在很多的时候我们是需要覆盖这个方法的,在覆盖equals方法的时候,必须要遵守它的通用约定:
自反性。对于任何非null的引用值x,x.equals(x)必须返回true。
对称性。对于任何非null的引用值x和y,当且仅当x.equals(y)返回true,y.equals(x)也必须返回true.
传递性。对于任何非null的引用值x,y,z,如果x.equals(y)返回true,并且y.equals(z)也返回true,那么x.equals(z)也必须返回true。
一致性。对于任何非null的引用值x和y,只要equals的比较操作在对象中所用的信息没有被修改,多次调用x.equals(y)就会一致地返回true,或者一致地返回false。
对于任何非null的引用值x,x.equals(null)必须返回false。
在对象类覆盖equals时要注意,总是要覆盖hashCode方法
因没有覆盖hashCode而违反的关键约定是:相等的对象必须具有相等的散列码
举个栗子:
对于jenny和jenny_sub它们所具有的内容完全相同,我们在业务上可认为这两个对象是相等的,但是上输出的结果却并不是我们希望的Jenny而是null。
由于PhoneNumber类没有覆盖hashCode方法,从而导致两个相等的实例具有不相等的散列码,违反了hashCode的约定。因此,put方法把电话号码对象放在一个散列桶(hash bucket)中,get方法却在另一个散列桶中查找这个电话号码。即使这两个实例正好被放在同一个散列桶中,get方法也必定会返回null,因为HashMap有一项优化,可以将与每个项相关的散列码缓存起来,如果散列码不匹配,也不必检验对象的等同性。
修正这个问题只要为PhoneNumber类提供一个适当的hashCode方法即可。然而一个好的散列函数通常倾向于”为不相等的对象产生不相等的散列码”。这正是hashCode约定中第三条的含义。理想情况下,散列函数应该把集合中不相等的实例均匀地分布到所有可能的散列值上。下面给出一个简单的解决办法:
把某个非零的常数值,比如说17,保存在一个名为result的int类型变量中。
对于对象中每个关键域f(指equals方法中涉及的每个域),完成以下步骤:
如果该域是boolean类型,则计算(f?1:0)。
如果该域是btye,char,short,int类型,则计算(int ) f。
如果该域是long类型,则计算(int)(f^(f>>>32))。
如果该域是float类型,则计算Float.floatToIntBits(f)。
如果该域是double类型,则计算Double.doubleToLongBits(f),然后按照步骤3,为得到的long类型值计算散列值。
如果该域是一个对象引用,并且该类的equals方法通过递归地调用equals的方式来比较这个域,则同样为这个域递归地调用hashCode。如果需要更复杂的比较,则为这个域计算一个”范式“,然后针对这个范式调用hashCode。如果这个域的值为null,则返回0。
如果该域是一个数组,则要把每一个元数当做单独的域来处理。也就是说,递归地应用上述规则,对每个重要元素计算一个散列码,然后再下面8的方法把这些散列组合起来。如果数组域中的每个元素都很重要,可以利用Arrays.hashCode方法。
把上述计算得到的散列码c合并到result:
result=31*result+c;
返回result;
所有要解决上述PhoneNumber的问题只要加上
最后我们来看看String类覆盖的hashCode方法
String本身就是一个字符数组value[],所以计算String的hashCode值要把每一个元素当做单独的域来处理,因些得循环整个数组来计算其散列码。但是它把散列值定义成局部变量,缓存了起来,再计算前先确定该散列值是否为存在,在不存在(为0)的情况下再进行计算,这种做法叫做”延迟初始化“,即要一直到hashCode被第一次调用的时候才初始化。
SDK: Software Development Kit, 软件开发工具包;
JDK: Java Development Kit,java开发工具包;
JRE: Java Runtime Environment,java运行时环境;
对于JRE,在安装JDK的时候会提示是否再安装一个JRE,安装完了之后会发现有两个JRE目录,一个是在jdk内部目录中,一个是后来提示安装的目录。其实这两个是不冲突的,内部的JRE是运行java内部程序的环境,其本身的程序也是用JAVA编写的,所以它也需要一个运行环境,而后来安装的JRE就属于外部的运行环境了,两个运行环境并不冲突,使用哪个都没有关系。
JDK目录:
bin 存放的是一些可执行的.exe程序及一些.dll文件,比如:java.exe,javac.exe等。
include 语言语言头文件 支持 用Java本地接口和Java虚拟机接口 来本机代码编程
lib 包目录,有着两个比较重要的工具包,dt.jar和tools.jar。
jre 运行环境的根目录
lib java运行时所要的代码包及一些资源文件,其中重要的有rt.jar ,java核心类库,运行时的基础类库。可以平时写代码所用到的java类几乎都出自这个jar包,包括下面讲的Object类。
-bin 这个目录里也是一些可执行的.exe文件和.dll文件,一个client目录和server目录,里面放的分别就是我们所熟悉的客户端和服务器版的jvm了。
Object 类存在于java.lang包中,是java所有类的基类,虽然说没有在代码层上显示的extends Object,但是在类创建的时候是默认其基类就是Object类。
jdk1.7中的Object源码中,首先其构造器:
public Object() { }
然后是一些个本地化的方法
private static native void registerNatives(); public final native Class getClass(); public native int hashCode(); public final native void notify(); public final native void notifyAll(); public final native void wait(long l) throws InterruptedException; protected native Object clone() throws CloneNotSupportedException;
静态代码块,先注册上述的一些本地方法。
static { registerNatives(); } }
接一下就是一些普通的方法了,我们先看toString()方法:
public String toString() { return (new StringBuilder()).append(getClass().getName()).append("@").append(Integer.toHexString(hashCode())).toString(); }
该方法是把一个对象转换为字符串,基本思路就是强制类型转换,将其转换为StringBuilder非线程安全的字符集对象,再将其类名+@+hashCode值
接下来看两个wati方法
public final void wait(long l, int i) throws InterruptedException { if(l < 0L) throw new IllegalArgumentException("timeout value is negative"); if(i < 0 || i > 999999) throw new IllegalArgumentException("nanosecond timeout value out of range"); if(i >= 500000 || i != 0 && l == 0L) l++; wait(l); } public final void wait() throws InterruptedException { wait(0L); }
在前面也还一个带一个参数的本地化wait(long l) 方法,所以在Object类中共有三个wait方法,wait(),wait(long l),wait(long l,int i)。不同于equals和toString,这两个普通的wait方法是final的,也就是说它是不能覆盖的。wait方法实用于多线程的程序之中,其作用是暂停当前线程的执行,直到接到有信息(notify)来终止这个暂停。具体的多线程细节这里就先不介绍了。
protected void finalize() throws Throwable { }
Java 技术允许使用 finalize() 方法在垃圾收集器将对象从内存中清除出去之前做必要的清理工作。这个方法是由垃圾收集器在确定这个对象没有被引用时对这个对象调用的。它是在 Object 类中定义的,因此所有的类都继承了它。子类覆盖 finalize() 方法以整理系统资源或者执行其他清理工作。finalize() 方法是在垃圾收集器删除对象之前对这个对象调用的。
接下来我们重点看equals 方法:
public boolean equals(Object obj) { return this == obj; }
这个方法也很简单,就是把this和传进来的这个对象引用比较一下,返回比较结果。但是在很多的时候我们是需要覆盖这个方法的,在覆盖equals方法的时候,必须要遵守它的通用约定:
自反性。对于任何非null的引用值x,x.equals(x)必须返回true。
对称性。对于任何非null的引用值x和y,当且仅当x.equals(y)返回true,y.equals(x)也必须返回true.
传递性。对于任何非null的引用值x,y,z,如果x.equals(y)返回true,并且y.equals(z)也返回true,那么x.equals(z)也必须返回true。
一致性。对于任何非null的引用值x和y,只要equals的比较操作在对象中所用的信息没有被修改,多次调用x.equals(y)就会一致地返回true,或者一致地返回false。
对于任何非null的引用值x,x.equals(null)必须返回false。
在对象类覆盖equals时要注意,总是要覆盖hashCode方法
因没有覆盖hashCode而违反的关键约定是:相等的对象必须具有相等的散列码
举个栗子:
public class PhoneNumber { private final short areaCode; private final short prefix; private final short lineNumber; public PhoneNumber(int areaCode,int prefix,int lineNumber){ rangeCheck(areaCode,999,"area code"); rangeCheck(prefix,999,"prefix"); rangeCheck(lineNumber,9999,"line number"); this.areaCode=(short)areaCode; this.prefix=(short)prefix; this.lineNumber=(short)lineNumber; } private void rangeCheck(int arg, int max, String name) { if(arg<0||arg>max){ throw new IllegalArgumentException(name +":"+arg); } } @Override public boolean equals(Object o){ if(o==this){ return true; } if(!(o instanceof PhoneNumber)){ return false; } PhoneNumber pn=(PhoneNumber) o; return pn.lineNumber==lineNumber && pn.prefix==prefix && pn.areaCode==areaCode; } public static void main(String[] args) { // TODO Auto-generated method stub Map<PhoneNumber,String> map=new HashMap<PhoneNumber,String>(); PhoneNumber jenny=new PhoneNumber(707, 867,5309); PhoneNumber jenny_sub=new PhoneNumber(707, 867,5309); map.put(jenny,"Jenny"); System.out.println(map.get(jenny_sub));//null } }
对于jenny和jenny_sub它们所具有的内容完全相同,我们在业务上可认为这两个对象是相等的,但是上输出的结果却并不是我们希望的Jenny而是null。
由于PhoneNumber类没有覆盖hashCode方法,从而导致两个相等的实例具有不相等的散列码,违反了hashCode的约定。因此,put方法把电话号码对象放在一个散列桶(hash bucket)中,get方法却在另一个散列桶中查找这个电话号码。即使这两个实例正好被放在同一个散列桶中,get方法也必定会返回null,因为HashMap有一项优化,可以将与每个项相关的散列码缓存起来,如果散列码不匹配,也不必检验对象的等同性。
修正这个问题只要为PhoneNumber类提供一个适当的hashCode方法即可。然而一个好的散列函数通常倾向于”为不相等的对象产生不相等的散列码”。这正是hashCode约定中第三条的含义。理想情况下,散列函数应该把集合中不相等的实例均匀地分布到所有可能的散列值上。下面给出一个简单的解决办法:
把某个非零的常数值,比如说17,保存在一个名为result的int类型变量中。
对于对象中每个关键域f(指equals方法中涉及的每个域),完成以下步骤:
如果该域是boolean类型,则计算(f?1:0)。
如果该域是btye,char,short,int类型,则计算(int ) f。
如果该域是long类型,则计算(int)(f^(f>>>32))。
如果该域是float类型,则计算Float.floatToIntBits(f)。
如果该域是double类型,则计算Double.doubleToLongBits(f),然后按照步骤3,为得到的long类型值计算散列值。
如果该域是一个对象引用,并且该类的equals方法通过递归地调用equals的方式来比较这个域,则同样为这个域递归地调用hashCode。如果需要更复杂的比较,则为这个域计算一个”范式“,然后针对这个范式调用hashCode。如果这个域的值为null,则返回0。
如果该域是一个数组,则要把每一个元数当做单独的域来处理。也就是说,递归地应用上述规则,对每个重要元素计算一个散列码,然后再下面8的方法把这些散列组合起来。如果数组域中的每个元素都很重要,可以利用Arrays.hashCode方法。
把上述计算得到的散列码c合并到result:
result=31*result+c;
返回result;
所有要解决上述PhoneNumber的问题只要加上
@Override public int hashCode(){ int result=17; result=31*result+areaCode; result=31*result+prefix; result=31* result+lineNumber; return result; }
System.out.println(map.get(jenny_sub));//Jenny
最后我们来看看String类覆盖的hashCode方法
private int hash; private final char value[]; public int hashCode() { int i = hash; if(i == 0 && value.length > 0) { char ac[] = value; for(int j = 0; j < value.length; j++) i = 31 * i + ac[j]; hash = i; } return i; }
String本身就是一个字符数组value[],所以计算String的hashCode值要把每一个元素当做单独的域来处理,因些得循环整个数组来计算其散列码。但是它把散列值定义成局部变量,缓存了起来,再计算前先确定该散列值是否为存在,在不存在(为0)的情况下再进行计算,这种做法叫做”延迟初始化“,即要一直到hashCode被第一次调用的时候才初始化。
相关文章推荐
- Equals和==的区别 公共变量和属性的区别小结
- java String 类的一些理解 关于==、equals、null
- hashCode方法的使用讲解
- C#使用Equals()方法比较两个对象是否相等的方法
- C#值类型、引用类型中的Equals和==的区别浅析
- java的equals和==的比较示例
- why在重写equals时还必须重写hashcode方法分享
- java中hashCode方法与equals方法的用法总结
- JAVA hashCode使用方法详解
- Java中==与equals的区别小结
- 基于Java字符串 "==" 与 "equals" 的深入理解
- Java对象的equals,hashCode方法
- hashmap重写key的hashcode问题
- Java中equals和==的区别
- Android 锁屏和黑屏的广播消息
- java 编写建议
- 重写equals方法和hashCode方法的套路
- 为什么equals(Object o)相等,hashCode()必须相等
- 13 Holding Your Objects课后练习
- WCF的配置参数maxItemsInObjectGraph所引发的的问题