Equals和==的简单比较
1. equals和 == 的区别
1.1 == 比较的是内存地址。
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) ;
System. out. println ( apple1== apple3) ;
}
}
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) ) ;
}
}
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) ) ;
System. out. println ( apple1. hashCode ( ) ) ;
System. out. println ( apple2. hashCode ( ) ) ;
}
}
我们重写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< Fruit, String> fruits = new HashMap < > ( ) ;
fruits. put ( apple1, "Unrelated!" ) ;
System. out. println ( fruits. containsKey ( apple1) ) ;
System. out. println ( fruits. containsKey ( apple2) ) ;
}
}
我们重载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 ( ) {
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 ( ) ) ;
System. out. println ( apple2. hashCode ( ) ) ;
Map< Fruit, String> fruits = new HashMap < > ( ) ;
fruits. put ( apple1, "Unrelated!" ) ;
System. out. println ( fruits. containsKey ( apple1) ) ;
System. out. println ( fruits. containsKey ( apple2) ) ;
}
}
这样,结果与我们的预期便一样了! 这是因为HashMap的containsKey方法实际上调用的是HashMap的getNode(int hash, Object key)
方法:
final Node< K, V> getNode ( int hash, Object key) {
Node< K, V> [ ] tab; Node< K, V> first, e; int n; K k;
if ( ( tab = table) != null && ( n = tab. length) > 0 &&
( first = tab[ ( n - 1 ) & hash] ) != null) {
if ( first. hash == hash &&
( ( k = first. key) == key || ( key != null && key. equals ( k) ) ) )
return first;
if ( ( e = first. next) != null) {
if ( first instanceof TreeNode )
return ( ( TreeNode< K, V> ) 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< Fruit, String> fruits = new HashMap < > ( ) ;
fruits. put ( apple1, "First!" ) ;
fruits. put ( apple2, "Second!" ) ;
System. out. println ( fruits. size ( ) ) ;
System. out. println ( fruits. get ( apple1) ) ;
System. out. println ( fruits. get ( apple2) ) ;
}
}
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 ( ) ) ;
Map< Fruit, String> fruits = new HashMap < > ( ) ;
fruits. put ( apple1, "First!" ) ;
System. out. println ( fruits. containsKey ( apple1) ) ;
apple1. setNumber ( 12 ) ;
System. out. println ( apple1. hashCode ( ) ) ;
System. out. println ( fruits. containsKey ( apple1) ) ;
}
}
当我们修改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 ( ) {
return name. hashCode ( ) ;
}
}
public class Main {
public static void main ( String[ ] args) {
Fruit apple1 = new Fruit ( "apple" , 10 ) ;
System. out. println ( apple1. hashCode ( ) ) ;
Map< Fruit, String> fruits = new HashMap < > ( ) ;
fruits. put ( apple1, "First!" ) ;
System. out. println ( fruits. containsKey ( apple1) ) ;
apple1. setNumber ( 12 ) ;
System. out. println ( apple1. hashCode ( ) ) ;
System. out. println ( fruits. containsKey ( apple1) ) ;
}
}
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) ;
System. out. println ( System. identityHashCode ( s1) ) ;
System. out. println ( System. identityHashCode ( s2) ) ;
}
}
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) ;
System. out. println ( s3 == s4) ;
System. out. println ( System. identityHashCode ( s1) ) ;
System. out. println ( System. identityHashCode ( s2) ) ;
System. out. println ( System. identityHashCode ( s3) ) ;
System. out. println ( System. identityHashCode ( s4) ) ;
}
}
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) ;
System. out. println ( s1 == s2. intern ( ) ) ;
System. out. println ( System. identityHashCode ( s1) ) ;
System. out. println ( System. identityHashCode ( s2) ) ;
System. out. println ( System. identityHashCode ( s2. intern ( ) ) ) ;
}
}
若字符串不存在于常量池 中,intern()方法首先会在常量池中保存该字符串对象的引用 ,然后返回一个对象引用 ,该对象引用 指向常量池中的引用 。
public class Main {
public static void main ( String[ ] args) {
String s1 = new String ( "ab" ) + new String ( "cd" ) ;
System. out. println ( s1 == s1. intern ( ) ) ;
System. out. println ( System. identityHashCode ( s1) ) ;
System. out. println ( System. identityHashCode ( s1. intern ( ) ) ) ;
String s2 = new String ( "ab" ) + new String ( "cd" ) ;
System. out. println ( s2 == s2. intern ( ) ) ;
System. out. println ( System. identityHashCode ( s2) ) ;
System. out. println ( System. identityHashCode ( s2. intern ( ) ) ) ;
System. out. println ( 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" ;
String s4 = "abcd" ;
System. out. println ( s2== s3) ;
System. out. println ( s2== s4) ;
System. out. println ( System. identityHashCode ( s1) ) ;
System. out. println ( System. identityHashCode ( s2) ) ;
System. out. println ( System. identityHashCode ( s3) ) ;
System. out. println ( System. identityHashCode ( s4) ) ;
}
}