一、初步了解在JVM中的内存分配机制
在JVM中,内存可分为堆内存和栈内存,它们两者的区别是:当我们创建一个对象 (new Object) 时,会调用对象的构造方法来开辟空间,将对象数据存储到堆内存中,同时在栈内存中生成对应的引用,当我们在后续代码中调用的时候用的都是栈内存中的引用。而对于方法中声明的基本类型变量 (局部变量),每当程序调用方法时,都会将该变量存储到方法栈内存中,如果是在类中声明的基本类型变量 (全局变量),每当创建对象时,都会将该变量存储到堆内存中。
二、equals() 与 == 的区别
在面试中,我们经常会被问到 equals()方法 与 "=="运算符有什么区别?而我们常常会回答道:"如果使用基本数据类型进行 '=='运算符比较,则比较的是值。如果使用引用类型(对象) 进行 '=='运算符比较,则比较的是地址。而equals()方法比较的是对象的值。",看起来好像是这么回事,但其实我们回答的并不完全正确,接下来就让我们带着这个疑问继续往下看。
首先说起equals()方法,我们都知道它是超类Object中的一个基本方法,用来判断一个对象与另外一个对象是否相等,而在超类Object的equals()方法中,实际上也是通过 '=='运算符来判断两个对象是否具有相同的引用,其源码如下所示:
public boolean equals(Object obj) {
return (this == obj);
}
实际上我们知道,所有的对象都拥有标识(内存地址) 和 状态(数据),而对象使用"=="运算符比较的是两个对象的内存地址,所以说超类Object 的 equals()方法比较的是两个对象的内存地址是否相等,如果object.equals(otherObject) 结果为true,则表示 object 和 otherObject 实际上是引用的是同一个对象。
而为什么我们在上述的回答中,会认为equals()方法比较的是对象的值呢?那是因为在日常的项目开发中,我们大多数都是使用equals()方法来对 字符串(String类型) 进行相等判断,而对于String、Integer、Date等类都对超类Object中的 equals()方法进行了重写,比较的是其对象的值,而不再是对象的内存地址,其String类的 equals()方法 源码如下所示:
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof String) {
String anotherString = (String)anObject;
int n = value.length;
if (n == anotherString.value.length) {
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
while (n-- != 0) {
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
return false;
}
所以对于上述的问题,最终的回答是:"如果使用基本数据类型进行 '=='运算符比较,则比较的是值。如果使用引用类型(对象) 进行 '=='运算符比较,则比较的是地址。而 equals()方法如果比较对象的类没有对其进行重写,则比较的是地址,否则比较的是对象的值。"
接下来我们来编写一个例子,证明我们上述的结论,代码如下所示:
package com.reflex.test;
public class People {
private String name;
public People(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public static void main(String[] args) {
People people1 = new People("张三");
People people2 = new People("张三");
System.out.println("people1.equals(people2)的结果为:" + people1.equals(people2));
System.out.println("people1 == people2的结果为:" + (people1 == people2));
}
}
输出结果为:
people1.equals(people2)的结果为:false
people1 == people2的结果为:false
对于上述的代码,我们来分析一下:首先对于使用 "=="运算符比较两个People对象,返回的结果为false,这点很容易理解,因为对象使用"=="运算符 比较的是内存地址,而people1 和 people2 使用过new People类的构造方法创建的两个对象,所以people1 和 people2的内存地址自然也就不一样。
而对于equals()方法比较的两个People对象,我们希望两个人名字相同的情况下即认为他们是同一个人,但是从运行结果来看,people1.equals(people2)返回的结果确是false,对于这个结果我们也能理解,这是因为我们写的People类并没有重写超类Object的 equals()方法,所以People调用的equals() 方法是从超类Object 继承过来的,根据我们之的分析 超类Object的 equals()方法内部实现使用的是 "=="运算符,比较的是两个对象的内存地址是否相同,所以运行结果返回了false。
因此为了达到我们的期望 (即两个人名字相同的情况下即认为他们是同一个人),我们必须对超类Object的 equals()方法进行重写,让其比较的是对象的名称 (即对象的内容),而不是比较内存地址,于是修改如下:
package com.reflex.test;
public class People {
private String name;
public People(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public boolean equals(Object obj) {
if(obj instanceof People){
People people = (People)obj;
return name.equals(people.getName());
}
return false;
}
public static void main(String[] args) {
People people1 = new People("张三");
People people2 = new People("张三");
System.out.println("people1.equals(people2)的结果为:" + people1.equals(people2));
}
}
输出结果为:
people1.equals(people2)的结果为:true
对超类Object的 equals()方法进行重写,使用 instanceof来判断引用obj所指向的对象的类型,如果obj是People类的对象,则将其强制转为People对象,然后比较两者People的名字,相等返回true,否则返回false。当然如果obj不是 People对象,自然也得返回false。
三、重写equals()时是否必须重写hashCode()
最初遇到这个问题时,我也很好奇,我们已经可以通过重写超类Object的 equals()方法来判断两个对象的内容是否相等,还有必要重写hashCode()方法吗,而hashCode()方法是干嘛的又能帮助我们进行什么样的操作呢?带着这个问题我们继续往下看,hashCode()方法主要是针对映射相关的操作 (HashTable、HashMap和HashSet) 使用的,学过数据结构的朋友都知道,类似HashMap这样的结构都会使用到键对象的哈希码,当我们调用put()方法或者get()方法时,都会根据键对象的哈希码来计算值的存储位置。在java中 hashCode()方法同样也是超类Object中的一个基本方法,是返回对象存储在内存地址的编号,由于是超类Object中的基本方法,因此所有的子类都有该方法。接下来就让我们认识一下hashCode()方法吧。
package com.reflex.test;
public class HashCodeTest {
public static void main(String[] args) {
String str = "num";
StringBuilder sBuilder = new StringBuilder(str);
System.out.println("str的哈希码为:" + str.hashCode() + ",sBuilder的哈希码为:" + sBuilder.hashCode());
String myStr = new String("num");
StringBuilder stBuilder = new StringBuilder(myStr);
System.out.println("myStr的哈希码为:" + myStr.hashCode() + ",stBuilder的哈希码为:" + stBuilder.hashCode());
}
}
输出结果为:
str的哈希码为:109446,sBuilder的哈希码为:356573597
myStr的哈希码为:109446,stBuilder的哈希码为:1735600054
可以看到,字符串str 与 myStr拥有相同的哈希码,这是因为String类对超类Object的 hashCode()方法进行了重写,其字符串的哈希码是由内容导出的 (即内容相同,则哈希码相同),而sBuilder 和 stBuilder却拥有不同的哈希码,这是因为StringBuilder没有重写hashCode()方法,使用的是超类Object的 hashCode()方法,而该方法时通过对象的存储地址计算出来的,自然哈希码也就不同。
如果我们不重写超类Object的 hashCode()方法,在进行集合操作时又会发生怎么样的情况呢?请看下面这个例子
package com.reflex.test;
import java.util.HashMap;
public class HashCodeTest {
private String name;
public HashCodeTest(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "HashCodeTest{" +
"name='" + name + '\'' +
'}';
}
public static void main(String[] args) {
HashMap<String, HashCodeTest> stringHashMap = new HashMap<String, HashCodeTest>(2);
String s1 = new String("key");
String s2 = new String("key");
HashCodeTest hashCodeTest = new HashCodeTest("my HashCode");
stringHashMap.put(s1, hashCodeTest);
System.out.println("s1.equals(s2)的结果为:" + s1.equals(s2));
System.out.println("hashMap.get(s1)的结果为:" + stringHashMap.get(s1));
System.out.println("hashMap.get(s2)的结果为:" + stringHashMap.get(s2));
//-------------------------------------------------------------------------
HashMap<Key, HashCodeTest> keyHashMap = new HashMap<Key, HashCodeTest>(2);
Key k1 = new Key("key");
Key k2 = new Key("key");
keyHashMap.put(k1, hashCodeTest);
System.out.println("k1.equals(k2)的结果为:" + k1.equals(k2));
System.out.println("hashMap.get(k1)的结果为:" + keyHashMap.get(k1));
System.out.println("hashMap.get(k2)的结果为:" + keyHashMap.get(k2));
}
}
class Key {
private String value;
public Key(String value) {
this.value = value;
}
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
@Override
public boolean equals(Object obj) {
if (obj instanceof Key) {
Key key = (Key) obj;
return value.equals(key.getValue());
}
return false;
}
}
输出结果为:
s1.equals(s2)的结果为:true
hashMap.get(s1)的结果为:HashCodeTest{name='my HashCode'}
hashMap.get(s2)的结果为:HashCodeTest{name='my HashCode'}
k1.equals(k2)的结果为:true
hashMap.get(k1)的结果为:HashCodeTest{name='my HashCode'}
hashMap.get(k2)的结果为:null
对于s1和s2的结果,很好理解,因为通过相同的键s1 和 s2,在HashMap集合中获取相同内容为HashCodeTest{name='my HashCode'}这很正常,因为String类重写了equals()方法 和 hashCode()方法,使其比较的是内容 和 获取内容的哈希码,但是对于k1 和 k2的结果就让人很意外了,为什么k1.equals(k2)返回的结果为 true,但是k1可以获取到内容,而k2获取的内容却为null,这是为什么呢?想必大家已经发现了,Key类只重写了equals()方法,但是并没有重写hashCode()方法,所以equals()方法比较的是内容,而hashCode()方法呢?由于Key类没有重写,那肯定调用的是超类Object的 hashCode()方法,返回对象存储在内存地址的编号,又因为k1 和 k2是两个不同的对象,所以地址肯定不一样,所以导致我们在通过 keyHashMap.get(k2) 获取值时为null。那该如何修改呢?其实也很简单,在Key类中重写超类Object的 hashCode()方法即可,代码如下:
package com.reflex.test;
import java.util.HashMap;
public class HashCodeTest {
private String name;
public HashCodeTest(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "HashCodeTest{" +
"name='" + name + '\'' +
'}';
}
public static void main(String[] args) {
HashMap<Key, HashCodeTest> keyHashMap = new HashMap<Key, HashCodeTest>(2);
Key k1 = new Key("key");
Key k2 = new Key("key");
HashCodeTest hashCodeTest = new HashCodeTest("my HashCode");
keyHashMap.put(k1, hashCodeTest);
System.out.println("k1.equals(k2)的结果为:" + k1.equals(k2));
System.out.println("hashMap.get(k1)的结果为:" + keyHashMap.get(k1));
System.out.println("hashMap.get(k2)的结果为:" + keyHashMap.get(k2));
}
}
class Key {
private String value;
public Key(String value) {
this.value = value;
}
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
@Override
public boolean equals(Object obj) {
if (obj instanceof Key) {
Key key = (Key) obj;
return value.equals(key.getValue());
}
return false;
}
@Override
public int hashCode() {
return value.hashCode();
}
}
输出结果为:
k1.equals(k2)的结果为:true
hashMap.get(k1)的结果为:HashCodeTest{name='my HashCode'}
hashMap.get(k2)的结果为:HashCodeTest{name='my HashCode'}
好了,到此hashCode()就介绍完了,回归到我们之前的问题,重写equals()时必须重写hashCode(),同时在Java API文档中关于hashCode()方法有以下介绍和几点规定:
介绍:public int hashCode()返回该对象的哈希码值。支持此方法是为了提高哈希表(例如 Java.util.Hashtable 提供的哈希表)的性能。
规定:
1、在 Java 应用程序执行期间,在对同一对象多次调用 hashCode 方法时,必须一致地返回相同的整数,前提是将对象进行 equals 比较时所用的信息没有被修改。从某一应用程序的一次执行到同一应用程序的另一次执行,该整数无需保持一致。
2、如果根据 equals(Object) 方法,判断两个对象是相等的,那么对这两个对象中的每个对象调用 hashCode() 方法都必须生成相同的整数结果。
3、如果根据 equals(java.lang.Object) 方法,两个对象不相等,那么对这两个对象中的任一对象上调用 hashCode 方法不要求一定生成不同的整数结果。但是,程序员应该意识到,为不相等的对象生成不同整数结果可以提高哈希表的性能。
总结起来就是说:equals()相等的两个对象,它们的hashCode()肯定相等,但是hashCode()相等的两个对象,它们的equals()不一定相等。
在使用ORM( 对象关系映射(Object Relational Mapping,简称ORM)是通过使用描述对象和数据库之间映射的元数据,将面向对象语言程序中的对象自动持久化到关系数据库中 )时,需要注意的是:
1、如果你使用ORM处理一些对象的话,你要确保在类的 hashCode()和equals()中使用getter和setter而不是直接引用成员变量。因为在ORM中有的时候成员变量会被延时加载,这些变量只有当getter方法被调用的时候才真正可用。
2、例如:如果我们在类的hashCode()和equals()中使用的是 name == people.name则可能会出现这个问题,但是我们使用this.getName() == people.getName()就不会出现这个问题。