概述
提及equals
和==
的区别,大多数的回答==
用于比较地址,equals
用于比较内容。这个回答没有错,但涉及的角度过于片面和狭隘,接下来让我们看看equals
和==
真正的区别所在。
1.概念
首先我们看一下二元比较运算符==的用法
对于基本数据类型,双等号用于比较二者的值是否相等。
int a = 1;
int b = 2;
System.out.println(a == b); // false
对于引用类型变量,双等号用于比较二者是否指向同一个对象。
Student john = new Student("john",19);
Student tomy = new Student("tomy",16);
System.out.println(john == tomy); // false
其次我们看一下Object类中equals方法的源码
public boolean equals(Object obj) {
return (this == obj);
}
看完源码后我们汇发现,耶( •̀ ω •́ )y,这是嘛呀!为什么equals
的方法体中用的也是二元比较运算符==
,这不是就跟双等号一样吗?没错,在Object
类中equals
和==
的作用是一模一样的,那既然作用都一样,equals
和==
在Java中同时存在的意义在哪呢?
我们再看一下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;
}
耶( •̀ ω •́ )y,这又是嘛呀!怎么这么多东西。没错,在String
类中我们看到了equals
的方法体要比Object
类中多了一些内容,这是因为Java在String
类中对equals
进行了重写。其实在与大多数的类中都会和String
类似的重写equals方法。所以,只有在重写了equals
方法的类中,比较equals
和==
的区别才有意义。
2.实例
接下来我们看几个例子
String a = "abc";
String b = "abc";
String c = new String("abc");
String d = new String("abc");
String e = new StringBuilder("abc").toString();
String f = new String("abc").intern();
String g = "ab" + "c";
String h = new String("ab") + new String("c");
System.out.println(a == b); // true
System.out.println(a == c); // false
System.out.println(a == e); // false
System.out.println(a == f); // true
System.out.println(a == g); // true
System.out.println(a == h); // false
怎么样?以上的结果和你们想象的结果一样吗?接下来让我们一起一一分析这些比较结果。
首先是字符串a和b比较的结果为true
。有人可能会问,双等号不是比较两个对象是不是同一个对象么?字符串a和b为什么指向同一个对象呢?这里就要引入字符串常量池的概念了。为了提高效率,Java在定义字符串的时候并不会每次都生成一个新的字符串,而是在内存中分配一块区域作为常量池,每当遇到一个新定义的字符串变量时,会优先到常量池中找是否有相同字面量的字符串。如果有,就直接复用当前字符串,如果没有,才会去创建新的字符串对象。
我们再看字符串a和c比较的结果为false
。这里会有一个疑问:不是每定义一个新的字符串变量就会优先在常量池中找相同字面量的字符串吗?a与c的字面量相同呀!在这里字符串变量c的创建用到了关键字new
,它表示c显式地调用了String
的构造器,这时字符串将无视常量池中是否含有相同字面量的字符串变量,强制重新创建一个字符串,新创建的字符串也不会加入常量池中,因此,a和c指向的不是同一个对象。
接下来看到字符串a和e比较的结果为false
。e中使用了StringBuilder
,字符串字面量"abc"
作为StringBuilder的
参数,按理说不会创建新的字符串呀。但问题出在后面的toString()
方法上,想要把StringBuilder
转换为String
对象,就需要调用toString()
方法。通过查看toString()
方法的源码,我们发现在该方法中调用了String
的构造器,因此和c一样,重新创建了字符串的对象。
@Override
public String toString() {
// Create a copy, don't share the array
return new String(value, 0, count);
}
字符串a和f的比较结果比较有意思。f明明显式地调用了String
的构造器,为什么比较结果为true
呢?关键在于后面的intern()
方法,这个方法大家可能不太熟悉,它的作用就是判断常量池中有没有对应的字符串,如果有的话,直接复用该字符串,如果没有的话,创建字符串并加入常量池中。所以,调用了intern()
方法之后,效果和直接使用字面量是相同的。
字符串a和g的比较结果为true
。字符串变量g创建时,将"ab"
和"c"
进行了拼接。Java在编译过程中,如果发现能够计算的结果,会在编译期直接将结果计算出来。也就是说在定义完字符串g的时候,编译完成时g的字面量已经是"abc"
了。所以直接复用了a的引用,因此a与e指向的是同一个字符串对象。再来看a与h的比较结果为false
,这是因为字符串h使用了String
的构造器进行字符串拼接,由于需要创建对象,在编译期编译器无法直接对该结果进行求值,因此h只能等待运行期创建两个字符串对象,再进行拼接,并创建新的对象。
接下来还有几行的比较例子,看小伙伴们思考的结果是否正确。
System.out.println(c == d); // false
System.out.println(c == e); // false
System.out.println(c == f); // false
System.out.println(c == g); // false
System.out.println(c == h); // false
System.out.println(e == f); // false
System.out.println(e == g); // false
System.out.println(e == h); // false
System.out.println(f == g); // true
System.out.println(f == h); // false
System.out.println(g == h); // false
由于jdk1.7中将字符串的常量池由方法区移到堆中。使用双等号比较字符串是否指向同一对象时,使用jdk1.7之前和之后的版本比较结果可能有所不同。
3.包装类
Java中数据类型可以分为两类,一种的基本数据类型,一种是引用数据类型。基本数据类型的数据不是对象,所以对于要将数据类型作为对象来使用的情况,Java提供了相对应的包装类。
例如:int
是基本数据类型,integer
是引用数据类型,是int
的包装类。
public final class Integer extends Number implements Comparable<Integer>
Integer
是final
类型的,表示不能被继承,同时实现了Number
类,并实现了Comparable
接口.
public static Integer valueOf(int i) {
assert IntegerCache.high >= 127;
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
assertion
语句的作用是对一个boolean
表达式进行检查,一个正确程序必须保证这个boolean
表达式的值为true
;如果该值为false
,说明程序已经处于不正确的状态下,系统将给出警告并且退出。一般来说,assertion
用于保证程序最基本、关键的正确性。
Java内部为了节省内存,IntegerCache
类中有一个数组缓存了值从-128~127
的Integer
对象。当我们调用Integer.valueOf(int i)
的时候,如果i的值时结余-128~127
之间的,会直接从这个缓存中返回一个对象,否则就new
一个新的Integer
对象。
接下来我们来看一下包装类应该使用双等号还是equals
进行比较。我们首先尝试像基本类型一样用双等号进行比较。
我们定义两个Integer
的范围在-128~127
之间,并且值相同的时候,用==
比较值为true
。当大于127或者小于-128的时候即使两个数值相同,也会new
一个integer
,那么比较的是两个对象,用==
比较的时候返回false
。
Integer a = 1;
Integer b = 1;
Integer c = 129;
Integer d = 129;
System.out.println(a == b); // true
System.out.println(c == d); // false
System.out.println(a.equals(b)); // true
System.out.println(c.equals(d)); // true
其他和整型相关的包装类也存在-128~127
之间的缓存,包括Character
、Byte
、Short
、Long
。但是缓存仅仅是为了提高效率的设计,并非为了进行比较。因此,对于包装类,我们应该使用equals
来比较两个对象包装的值是否相等而并非==
。
4.自定义类
在使用自定义类时,双等号用于比较两个变量是否指向同一对象,而equals
用于自定义规则的比较。如果我们想使用equals
方法对两个自定义类的对象进行比较,就必须要重写equals
方法,大多数情况下应该同时重写hashCode
方法。下面还是以Student
类为例,先看一下IDE自动生成的equals
方法(这里使用了Objects
工具类,Objects.equals
方法实际上就是封装了对象的equals
方法):
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
Student student = (Student) o;
return age == student.age &&
Objects.equals(name, student.name);
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
可以看出,默认情况下IDE生成的代码是对所有字段分别进行比较,如果全部相等,则两个对象相等。我们可以进行修改,选择其中一部分字段进行比较。
5.总结
总结一下,双等号可以用于比较基本类型的值是否相等,或者用于比较引用类型的变量是否指向同一对象;equals
是Object
类中的方法,默认与==
行为一致,可以通过重写该方法实现自定义规则的比较。常用的类中字符串和包装类应使用equals
来比较其内容是否相同。自定义类中要想自定义规则比较不仅要重写equals
方法还要重写 hashcode
方法。