Java中的==与equals

Integer的比较

       执行如下代码:

1
2
3
4
5
6
7
8
public static void main(String[] args) {
Integer i1 = 100;
Integer i2 = 100;
System.out.println(i1==i2);
Integer i3 = 1000;
Integer i4 = 1000;
System.out.println(i3==i4);
}

       结果如下:

1
2
true
false

       根据Java编译机制,.java文件在编译以后会生成.class文件给JVM加载执行,于是找到.class文件,反编译看了一下,发现编译器在编译我们的代码时,在我们声明的变量加上了valueOf方法 ,代码变成了如下:

1
2
3
4
5
6
7
8
public static void main(String[] args) {
Integer i1 = Integer.valueOf(100);
Integer i2 = Integer.valueOf(100);
System.out.println(i1==i2);
Integer i3 = Integer.valueOf(1000);
Integer i4 = Integer.valueOf(1000);
System.out.println(i3==i4);
}

       看一下valueOf方法的实现:

1
2
3
4
5
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}

       Integer的作者在写这个类时,为了避免重复创建对象,对Integer值做了缓存,如果这个值在缓存范围内,直接返回缓存好的对象,否则new一个新的对象返回,那究竟这个缓存到底缓存了哪些内容呢?看一下IntegerCache这个类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
private static class IntegerCache {
static final int low = -128;
static final int high;
static final Integer cache[];

static {
// high value may be configured by property
int h = 127;
String integerCacheHighPropValue =
sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
if (integerCacheHighPropValue != null) {
try {
int i = parseInt(integerCacheHighPropValue);
i = Math.max(i, 127);
// Maximum array size is Integer.MAX_VALUE
h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
} catch( NumberFormatException nfe) {
// If the property cannot be parsed into an int, ignore it.
}
}
high = h;

cache = new Integer[(high - low) + 1];
int j = low;
for(int k = 0; k < cache.length; k++)
cache[k] = new Integer(j++);

// range [-128, 127] must be interned (JLS7 5.1.7)
assert IntegerCache.high >= 127;
}

private IntegerCache() {}
}

       这是一个内部静态类,该类只能在Integer这个类的内部访问,这个类在初始化的时候,会去加载JVM的配置,如果有值,就用配置的值初始化缓存数组,否则就缓存-128到127之间的值。

       之前的代码给i1和i2赋值时在-128到127之间,取同一个数组里的对象返回,i1和i2指向同一个对象,因此i1==i2返回true,而i3和i4相当于new了两个对象,因此i3==i4返回false

       在比较两个Integer对象的值时,无论是怎么声明的,都一定要使用equals去比较,不能用==,在Java中没有重载操作符。如下所示:

1
System.out.println(i3.equals(i4));

字符串的比较

       再来看一段代码:

1
2
3
4
5
6
7
8
9
public static void main(String[] args) {
String i1 = "100";
String i2 = "100";
System.out.println(i1==i2);

String i3 = new String("100");
String i4 = new String("100");
System.out.println(i3==i4);
}

       结果如下:

1
2
true
false

       Java代码是运行在JVM里的,在JVM中,当代码执行到String s1 = “100” 时,会先看常量池里有没有字符串刚好是“100”这个对象,如果没有,在常量池里创建该对象,并把引用指向它,如下字符串在内存中的示意图,绿色部分为常量池,存在于堆内存中。

字符串在内存中的示意图1

       当执行到String s2 = “100” 时,发现常量池已经有了100这个值,于是不再在常量池中创建这个对象,而是把引用直接指向了该对象,如下图:

字符串在内存中的示意图2

       这时候我们打印System.out.println(s1 == s2)时,由于==是判断两个对象是否指向同一个引用,所以这儿打印出来的就应该是true。

       继续执行到Strings3 = new String(“100”) 这时候我们加了一个new关键字,这个关键字呢就是告诉JVM直接在堆内存里开辟一块新的内存,如下图所示:

字符串在内存中的示意图3

       继续执行String s4 = new String(“100”)

字符串在内存中的示意图4

       这时候再打印System.out.println(s3 == s4) 那一定便是false了,因为s3和s4不是指向对一个引用(对象)。

       在比较两个String对象内容时,无论是怎么声明的,都一定要使用equals去比较,不能用==。

       上面表明了Java字符串两种声明方式在堆内存中不同的体现,在写代码过程中,为了避免重复的创建对象,尽量使用String s1 =”123” 而不是String s1 = new String(“123”),因为JVM对前者给做了优化。

Java里的equals

       看如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Person{
private String name;
public Person(String name){
this.name = name;
}
}

public static void main(String[] args) {
String i3 = new String("100");
String i4 = new String("100");
System.out.println(i3.equals(i4));

Integer itr = new Integer("123");
Long lon = new Long("123");
System.out.println(itr.equals(lon));

Person person1 = new Person("张三");
Person person2 = new Person("张三");
System.out.println(person1.equals(person2));
}

       执行结果如下:

1
2
3
true
false
false

       ”==”与equals两者之间没有必然的联系,在引用类型中,”==”是比较两个引用是否指向堆内存里的同一个地址(同一个对象),而equals是一个普通的方法,该方法返回的结果依赖于自身的实现。我们先看一下Person这个类,在Java中,如果一个类没有继承其它类,那么它默认继承Object这个类,打开Object这个类看一下,发现如下代码,Person这个类的equals方法就继承自这里:

1
2
3
public boolean equals(Object obj) {
return (this == obj);
}

       很简单,判断两个引用是否指向同一个对象,两个Person对象在堆内存中的表现如下图所示:

Person在内存中的示意图

       所以代码person1.equals(person2)等同于person1 == person2,当然打印出来的结果是false。我们再来看看Integer这个类, equals的实现如下:

1
2
3
4
5
6
public boolean equals(Object obj) {
if (obj instanceof Integer) {
return value == ((Integer)obj).intValue();
}
return false;
}

       代码中会判断传入比较的对象是不是Integer类型,如果是,则比较值是否相等,如果不是直接返回false。当代码执行到System.out.println(itr.equals(lon))时,会判断传入的lon这个对象是否是Integer类型,这里的lon是Long类型,所以打印出来的结果当然是false了。

       最后是String的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public boolean equals(Object anObject) {
if (this == anObject) {
//如果两个对象指向同一个引用,直接返回true
return true;
}
if (anObject instanceof String) {//判断传入的对象是否是String类型
String anotherString = (String)anObject;
int n = length();
if (n == anotherString.length()) {//判断这两个字符串底层char数组的长度是否一致
int i = 0;
while (n-- != 0) {//循环判断底层数组里的每一个char字符,看值是否相等
if (charAt(i) != anotherString.charAt(i))
return false;
i++;
}
return true;
}
}
return false;
}

       当代码执行到:System.out.println(s3.equals(s4)),由于字符串底层char数组里存的都是{‘1’,’0’,’0’},打印出来是true。

再谈两个类的比较

       将面向对象中的Person类精简,保留了isSame()方法,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Person {
private String name;
public Person(String name) {
this.name = name;
}

public boolean isSame(Person person) {
//如果名字相同返回true,否则返回false
if (this.name.equals(person.name) ) {
return true;
}
return false;
}
}

       执行如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static void main(String[] args) {
String str1 = new String("张三");
String str2 = new String("张三");
Set<String> strSet = new HashSet<>();
strSet.add(str1);
strSet.add(str2);
System.out.println("strSet.size()" + strSet.size());

Person person1 = new Person("张三");
Person person2 = new Person("张三");
Set<Person> personSet = new HashSet<>();
personSet.add(person1);
personSet.add(person2);
System.out.println("personSet.size()" + personSet.size());
}

       结果如下:

1
2
strSet.size()1
personSet.size()2

       同样是对象,都是放到了set中,一个打印size()是1,另一个打印size()是2。

       在Java程序中,有很多的“公约”,我们称之为编程规范,遵守这些规范实现代码,会避开很多坑。要判断两个对象的内容是否相等,不要自己写方法(isSame())去判断,而是应该重写父类的equals方法(这里的父类是Object),String重写了equals()方法,所以打印size结果是1,而Person没有重写,因此Set没法判断这两个”张三”是否是同一个人,打印size结果是2。

       像String一样,重写一下Person的equals方法:

1
2
3
4
public boolean equals(Person other) {
//重用了String的equals方法,如果两个人名字相同,那我们认为这两个对象代表同一个人。
return name.equals(name);
}

       如果是重写方法,我们在方法上要加上@Override注解,加上该注解,编译器会帮助检查是否真的覆盖了父类的方法。编译一下,居然报错了。原来我们跟本就不是重写(覆盖)了父类的equals方法,而是自己又写了一个参数为Person的equals方法,根本不是重写,只是重载了父类的方法而已。

1
2
3
public boolean equals(Person other) {//重写equals方法这里的参数类型是Object,不能写Person
return name.equals(name);
}

重载:就是在同一个类中,方法的名字相同,但参数个数、参数的类型不同。

重写:它是指子类和父类的关系,子类重写了父类的方法,但方法名、参数类型、参数个数必须相同。

       正确重写代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Override
public boolean equals(Object other) {
//先判断是否是同一个对象,如果是直接返回true
if (this == other) {
return true;
}
//判断other是否是Person的实例
if (other instanceof Person) {
Person otherPerson = (Person) other;//转换类型
return name.equals(otherPerson.name);
}
return false;
}

       代码测试如下,引入了List:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static void main(String[] args) {
Person person1 = new Person("张三");
Person person2 = new Person("张三");
//引入List辅助测试
List<Person> perList = new ArrayList<>();
//把person1放入了List中,但是并没有放Person2
perList.add(person1);
//set
Set<Person>pSet = new HashSet<>();
//把person1放入了Set中,但是并没有放Person2
pSet.add(person1);
System.out.println(person1.equals(person2));
System.out.println(perList.contains(person2));
System.out.println(pSet.contains(person2));
}

       运行一下,perList里面我们只添加person1,并没有添加person2,但执行perList.contains(person2)打印的结果居然是true(List里面包含了person2),只因为重写了equals()方法,但pSet.contains(person2))依旧是false。

1
2
3
person1.equals(person2)true
perList.contains(person2)true
pSet.contains(person2)false

       再次执行本节开始的代码,结果依旧:

1
2
strSet.size()1
personSet.size()2

       很明显,Person这个类在重写equals()方法后,虽然已经支持List,但还不支持Set。要完美支持HashMap、HashSet、LinkedHashMap、ConcurrentHashMap等这些类,不但要重写equals方法,还需要重写hashCode()方法。

       现在我们在Person类里重写一下hashCode()方法:

1
2
3
4
5
@Override
public int hashCode() {
//重写hashCode方法,这里直接返回name的hashCode
return name.hashCode();
}

       再次执行代码:

1
2
3
person1.equals(person2)true
perList.contains(person2)true
pSet.contains(person2)true

       再次执行本节开始的代码,结果已经是我们期望的:

1
2
strSet.size()1
personSet.size()1

       当我们在实际业务中需要重写(覆盖)equals方法时,根据规范,我们一定要重写(覆盖)hashCode方法。在实际开发过程中,不建议一上来就重写equals方法,除非你有特殊的需求。

       hash的相关内容请参考Hash

参考资料:
清浅池塘 让人疑惑的Java代码Java字符串那些事儿说说Java里的equals(上)说说Java里的equals(中)

Fork me on GitHub