目录
Equals和==的简单比较
1. equals和 == 的区别
1.1 == 比较的是内存地址。
- 举例分析:
//首先自定义一个Fruit类 public class Fruit { private String name; private int number; public Fruit(String name, int number) { this.name = name; this.number = number; } }
- 验证:
public class Main { public static void main(String[] args) { Fruit apple1 = new Fruit("apple", 10); Fruit apple2 = new Fruit("apple", 10); Fruit apple3 = apple1; System.out.println(apple1==apple2); //false System.out.println(apple1==apple3); //true } }
- 明显
==
比较的是内存地址。
1.2 equals 比较的是可重载的等价关系
- equals方法实现了的是对一个对象的非空引用的等价关系的判定。
- 接着上面的例子:
public class Main { public static void main(String[] args) { Fruit apple1 = new Fruit("apple", 10); Fruit apple2 = new Fruit("apple", 10); System.out.println(apple1.equals(apple2)); //false } }
- 依然是false。查看equals方法:
public boolean equals(Object obj) { return (this == obj); }
- 内部默认调用的是
==
比较,apple1和apple2显然不是指向同一内存。- 接下来重载equals方法:
public class Fruit { private String name; private int number; public Fruit(String name, int number) { this.name = name; this.number = number; } @Override public boolean equals(Object obj) { //内存地址相同肯定相同 if (this == obj) { return true; } //内容相同,则认为相同 if (obj instanceof Fruit) { Fruit aFruit = (Fruit) obj; return name.equals(aFruit.name) && number == aFruit.number; } return false; } }
- 再次判断
apple1.equals(apple2)
结果即为true。
2.重载equals时要重载hashCode()方法
- hashCode()主要作用在我们使用哈希表(散列表)时。
- 重载hashCode()是为了使我们定义的等价关系更为完整。
2.1为什么需要重载hashCode()?
- 一般来说,在我们重载equals方法时,也要同时重载hashCode()方法。
- 当我们只重载了equals方法时:
public class Main { public static void main(String[] args) { Fruit apple1 = new Fruit("apple", 10); Fruit apple2 = new Fruit("apple", 10); System.out.println(apple1.equals(apple2)); //true System.out.println(apple1.hashCode()); //189568618(不同机器不一定相同) System.out.println(apple2.hashCode()); //793589513(不同机器不一定相同) } }
- 我们重写equals方法后,希望的是:apple1和apple2在最大程度上是等价的。但是,显而易见在hashCode层面上apple1和apple2并不相同。
- 当我们用到HashMap时会出现以下这种问题:
public class Main { public static void main(String[] args) { Fruit apple1 = new Fruit("apple", 10); Fruit apple2 = new Fruit("apple", 10); Map fruits = new HashMap(); fruits.put(apple1,"Unrelated!"); System.out.println(fruits.containsKey(apple1)); //true System.out.println(fruits.containsKey(apple2)); //false } }
- 我们重载equals定义的等价关系是期望在最大程度上将apple1和apple2视为相等(除了内存角度,内存角度上apple1肯定不等apple2)。因此,我们期望的结果是,在HashMap中添加apple1后,通过containsKey来判断是否含有apple2时,应该返回true。
- 重载hashCode()方法:
public class Fruit { private String name; private int number; public Fruit(String name, int number) { this.name = name; this.number = number; } @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (obj instanceof Fruit) { Fruit aFruit = (Fruit) obj; return name.equals(aFruit.name) && number == aFruit.number; } return false; } @Override public int hashCode() { //这里只是展示重载hashCode,并不严谨 return name.length()+number*1000; } }
- 再来看之前的结果:
public class Main { public static void main(String[] args) { Fruit apple1 = new Fruit("apple", 10); Fruit apple2 = new Fruit("apple", 10); System.out.println(apple1.hashCode());//10005 System.out.println(apple2.hashCode());//10005 Map fruits = new HashMap(); fruits.put(apple1,"Unrelated!"); System.out.println(fruits.containsKey(apple1)); //true System.out.println(fruits.containsKey(apple2)); //true } }
- 这样,结果与我们的预期便一样了!
- 这是因为HashMap的containsKey方法实际上调用的是HashMap的
getNode(int hash, Object key)
方法://上述例子中hash对应参数传入的是:根据apple2的hashCode计算出的值 final Node getNode(int hash, Object key) { Node[] tab; Node first, e; int n; K k; if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) {//first是根据hashCode计算出来的 //在hash值相等的情况下才会去进一步比较相等! if (first.hash == hash && // always check first node ((k = first.key) == key || (key != null && key.equals(k)))) return first; if ((e = first.next) != null) { if (first instanceof TreeNode) return ((TreeNode)first).getTreeNode(hash, key); do { if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) return e; } while ((e = e.next) != null); } } return null; }
- 同理在向HashMap加入键值对的时候,apple1和apple2也会被视为相同的键:
public class Main { public static void main(String[] args) { Fruit apple1 = new Fruit("apple", 10); Fruit apple2 = new Fruit("apple", 10); Map fruits = new HashMap(); fruits.put(apple1,"First!"); fruits.put(apple2,"Second!"); System.out.println(fruits.size()); //1 System.out.println(fruits.get(apple1)); //Second! System.out.println(fruits.get(apple2)); //Second! } }
- HashMap的put方法,实际上调用的是HashMap的
putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict)
方法。该方法也会先根据键的hashCode判断,再结合==
和equals
判断在HashMap中是否已经存在该键。
2.2 重载hashCode()应注意什么?
- hashCode()方法会为Object返回一个哈希值。该方法是为了支持哈希表的优点。
- 哈希表的主要特点就是访问速度快,这得益于其散列的特点。HashMap正式根据插入键值对的键的hashCode来完成散列存储的操作。因此,等价的对象(比如apple1和apple2)应该存储在哈希表相同的位置,而对象位置是决定于对象的hashCode,因此等价的对象的hashCode需要相同(即重载
equals
后,A.equals(B)为true,那么A和B的hashCode()应该相等)。- 哈希表的优势同样受制于碰撞的情况,应该尽量减少碰撞现象的发生。不等价的对象应该散列在不同位置,因此不等价的对象的hashCode应尽量不同(即重载
equals
后,A.equals(B)为false,那么A和B的hashCode()应该尽量不相等)。如果所有对象的hashCode返回相同的值,那么HashMap中存储的所有对象都互相碰撞,HashMap将优势全无。- hashCode()的计算尽量稳定。示例会出现这样一种情况:
public class Main { public static void main(String[] args) { Fruit apple1 = new Fruit("apple", 10); System.out.println(apple1.hashCode()); //10005 Map fruits = new HashMap(); fruits.put(apple1,"First!"); System.out.println(fruits.containsKey(apple1)); //true apple1.setNumber(12); System.out.println(apple1.hashCode()); //12005 System.out.println(fruits.containsKey(apple1)); //false } }
- 当我们修改apple1对象number属性值时,会使hashCode()返回值改变,这会导致使用HashMap的containsKey方法判断apple1的键是否存在时出现错误。(新的hashCode值导致散列表查找操作命中位置改变,从而错过HashMap中存储的对象,误以为不存在)。
- 改进hashCode()方法,使之相对稳定:
public class Fruit { private String name; private int number; public Fruit(String name, int number) { this.name = name; this.number = number; } public void setNumber(int number) { this.number = number; } @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (obj instanceof Fruit) { Fruit aFruit = (Fruit) obj; return name.equals(aFruit.name) && number == aFruit.number; } return false; } @Override public int hashCode() { /** * number会经常变动,而name不会变动。 * 通过不易变动的值计算的hash值会更稳定 */ return name.hashCode(); } }
- 再看示例:
public class Main { public static void main(String[] args) { Fruit apple1 = new Fruit("apple", 10); System.out.println(apple1.hashCode()); //93029210 Map fruits = new HashMap(); fruits.put(apple1,"First!"); System.out.println(fruits.containsKey(apple1)); //true apple1.setNumber(12); System.out.println(apple1.hashCode()); //93029210 System.out.println(fruits.containsKey(apple1)); //true } }
- 结果符合预期!
3.有关字符串的一些比较
3.1 创建字符串 = 和 new 的区别
- 通过
s1="abc";
创建字符串时,字符串被存在常量池中,s1指向常量池中字符串的地址。- 通过
s2=new String("abc");
创建字符串时,首先字符串被存在常量池中,然后在堆中保存字符串的副本(副本存的是常量池中字符串的地址),最后s2指向堆中字符串副本的地址。
public class Main {
public static void main(String[] args) {
String s1 = "abc";
String s2 = new String("abc");
System.out.println(s1==s2); //false
System.out.println(System.identityHashCode(s1));//284720968
System.out.println(System.identityHashCode(s2));//189568618
}
}
3.2 字符串在常量池中和堆中的区别
- 常量池中的字符串对象是唯一的。
- 堆中的字符串对象是不唯一的。
public class Main { public static void main(String[] args) { String s1 = "abc"; String s2 = "abc"; String s3 = new String("abc"); String s4 = new String("abc"); System.out.println(s1 == s2); //true System.out.println(s3 == s4); //false System.out.println(System.identityHashCode(s1));//284720968 System.out.println(System.identityHashCode(s2));//284720968 System.out.println(System.identityHashCode(s3));//189568618 System.out.println(System.identityHashCode(s4));//793589513 } }
3.3 intern()方法
- 若字符串存在于常量池中,intern()方法会返回一个对象引用,该对象引用指向常量池中的字符串。
public class Main { public static void main(String[] args) { String s1 = "abc"; String s2 = new String("abc"); System.out.println(s1 == s2); //false System.out.println(s1 == s2.intern()); //true System.out.println(System.identityHashCode(s1));//284720968 System.out.println(System.identityHashCode(s2));//189568618 System.out.println(System.identityHashCode(s2.intern()));//284720968 } }
- 若字符串不存在于常量池中,intern()方法首先会在常量池中保存该字符串对象的引用,然后返回一个对象引用,该对象引用指向常量池中的引用。
public class Main { public static void main(String[] args) { String s1 = new String("ab")+new String("cd");//1 System.out.println(s1 == s1.intern());//true System.out.println(System.identityHashCode(s1));//1349393271 System.out.println(System.identityHashCode(s1.intern()));//1349393271 String s2 = new String("ab")+new String("cd");//2 System.out.println(s2 == s2.intern()); //false System.out.println(System.identityHashCode(s2));//1338668845 System.out.println(System.identityHashCode(s2.intern()));//1349393271 System.out.println(s1 == s2.intern());//true /** * 执行完1时,常量池中存在"ab"和"cd";堆中存在"abcd";s1指向堆中"abcd"的地址。 * s1.intern()执行,因为常量池中并不存在"abcd",所以会将堆中"abcd"的引用保存在常量池中。 * 此时常量池中存在的是:"ab"和"cd"和一个象征"abcd"引用,这个引用指向的是堆中保存的"abcd"。 * 所以: s1 和 s1.intern() 实际指向同一个对象。 */ /** * 执行2后,常量池中存在的是:"ab"和"cd"和一个象征"abcd"引用(s1的); * 堆中存在两个"abcd"对象(一个s1的,一个s2的)。 * s2.intern()执行,因为常量池中存在一个象征"abcd"引用, * 会返回一个对象引用,该对象引用指向那个象征"abcd"引用; * 实际上,s2.intern()返回的对象引用指向的是堆中的"abcd"(s1的)。 * 所以: s2 和 s2.intern() 实际指向非同一个对象; * s1 和 s2.intern() 实际指向同一个对象。 */ } }
3.4 使用中间变量也会返回指向堆中对象的引用
- 使用” “形式声明的字符串相加,在编译阶段会被直接合并。eg.”ab”+”cd”—>”abcd”。
- 通过中间变量,字符串相加,在编译阶段不会被合并处理,会返回堆中对象的引用。(类似new)
public class Main { public static void main(String[] args) { String s1 = "ab"; String s2 = "ab"+"cd"; String s3 = s1 +"cd"; //使用中间变量s1,相当于s3 = new String("XXXX") String s4 = "abcd"; System.out.println(s2==s3); //false System.out.println(s2==s4); //true System.out.println(System.identityHashCode(s1));//960604060 System.out.println(System.identityHashCode(s2));//1349393271 System.out.println(System.identityHashCode(s3));//1338668845 System.out.println(System.identityHashCode(s4));//1349393271 } }
转载请注明:汪明鑫的个人博客 » Equals和==的简单比较
说点什么
您将是第一位评论人!