写在前面
最初接触java的时候,都会有涉及equals和==的区别,最经典的案例就是用String类型的数据作类比。最常见的说法就是:equals比较的是值,==比较的是引用地址。
首先这种说法是错误的,也有人认为这种说法是不完全正确的(至少对于String这个类来说这种说法是没问题的)。
之所以说这种说法是错误的,是因为本人真的觉得这个总结实在是误人子弟。
1、equals和==
1.1、==
首先我们要先清楚java中有哪些数据类型,及他们的存储位置。
java中的数据类型:
(1)基本数据类型:整数类型(byte,short,int,long)、浮点类型(float,double)、字符型(char)、布尔型(Boolean)
(2)引用数据类型:类(class)、接口(interface)、数组
存储位置:
基础数据类型:存储在常量池中(JDK8之后去除了元方法区,改为存在堆内存中的元空间)
引用数据类型:存储于堆中。
在java中,==比较的内容也跟数据所处的位置相关:
(1)基础数据类型:比较的是值。
(2)引用数据类型:比较的是是对象的引用地址。
为什么会这样分?我们就需要来看一个例子。
示例代码:
public class StringTest { String str1 = "字符串"; String str2 = "字符串"; }
public class StringTest1 { String str3 = new String("字符串"); String str4 = new String("字符串"); }
首先来看一上述两段代码有什么不同?一个是直接赋值,一个是给对象赋值。我们通过javac把上述两段代码编译为.class文件,再用javap方法来反编译看一下:
StringTest对应的反编译文件
public StringTest(); descriptor: ()V flags: ACC_PUBLIC Code: stack=2, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: aload_0 5: ldc #2 // String 字符串 7: putfield #3 // Field str1:Ljava/lang/String; 10: aload_0 11: ldc #2 // String 字符串 13: putfield #4 // Field str2:Ljava/lang/String; 16: return LineNumberTable: line 1: 0 line 3: 4 line 4: 10
StringTest1 对应的反编译文件
public StringTest1(); descriptor: ()V flags: ACC_PUBLIC Code: stack=4, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: aload_0 5: new #2 // class java/lang/String 8: dup 9: ldc #3 // String 字符串 11: invokespecial #4 // Method java/lang/String."<init>":(Ljava/lang/String;)V 14: putfield #5 // Field str3:Ljava/lang/String; 17: aload_0 18: new #2 // class java/lang/String 21: dup 22: ldc #3 // String 字符串 24: invokespecial #4 // Method java/lang/String."<init>":(Ljava/lang/String;)V 27: putfield #6 // Field str4:Ljava/lang/String; 30: return LineNumberTable: line 1: 0 line 3: 4 line 4: 17 }
分析结果:
从反编译文件可以看出,StringTest并没有新建对象,仅做了赋值。StringTest1这段代码中new了两个String对象。也就是说:
StringTest中的str1 和str2直接指向常量池中的"字符串"这个值;
StringTest1中的str3 和str4分别指向堆中的两个String对象,String对象再指向常量池中的值。如下图:
结论:
基础数据类型是加载完成后,直接赋值,指向常量池中的数据。所以==方法比较的是常量池中的值;
引用数据加载完成之后,会在堆中创建一个对象,其对象本身指向常量池中的数据,对象不一样,结果也就不一样。所以说==比较的是引用地址。
1.2 equals方法
java中,equals方法存在的作用是允许程序员自己根据需要定义比较方法。也就是说由程序员决定什么情况两个对象是相等的。为什么String中的equals()方法比较的是值,这种说法是正确的呢?
首先我们来了解一下java中所有类的祖先:Object的equals方法
1.2.1 Object的equals方法
Object是Java中最原始的类,在Object中,有默认的hashCode和equals方法实现,二者是相互关联的。JDK中Object.java定义了这两个方法:
public class Object { ...... public native int hashCode(); public boolean equals(Object obj) { return (this == obj); } ...... }
其中hashCode方法是native方法,具体是实现是返回一个对象的内存地址作为其hashcode。equals方法则是简单的直接比较两个对象的地址。
1.2.2 String的equals方法
String类继承自Object类,并覆写了其equals方法,仅对值做了循环匹配,所以才会有String的equals方法是比较值这一说法。String类的源码如下:
public boolean equals(Object anObject) { if (this == anObject) { return true; } if (anObject instanceof String) { String anotherString = (String)anObject; int n = count; if (n == anotherString.count) { char v1[] = value; char v2[] = anotherString.value; int i = offset; int j = anotherString.offset; while (n-- != 0) { if (v1[i++] != v2[j++]) return false; } return true; } } return false; } public int hashCode() { int h = hash; if (h == 0 && value.length > 0) { char val[] = value; for (int i = 0; i < value.length; i++) { h = 31 * h + val[i]; } hash = h; } return h; }
看过String源码就会发现,String类不仅覆写了equals代码,还覆写了hashCode的值。这是因为java常规协定关于equals方法的协定,规定了覆写equals方法的同时,必须覆写hashCode。协定内容想看第2章总结。
1.2.3 示例代码
代码中Student2 这个类覆写了equals方法,但是没有覆写hashCode。
public class EqualsTest { public static void main(String[] args) { //String类 System.out.println("常量直接赋值====="); String str1 = new String("字符串"); String str2 = new String("字符串"); System.out.println(str2.hashCode()); System.out.println(str1.hashCode()); System.out.println(str1.equals(str2)); //未重写equals方法 System.out.println("未重写equals方法====="); Student1 student1 = new Student1("name"); Student1 student2 = new Student1("name"); System.out.println(student2.hashCode()); System.out.println(student1.hashCode()); System.out.println(student1.equals(student2)); //重写equals方法 System.out.println("重写equals方法====="); Student2 student3 = new Student2("name"); Student2 student4 = new Student2("name"); System.out.println(student3.hashCode()); System.out.println(student4.hashCode()); System.out.println(student3.equals(student4)); } } class Student1 { private String name; public Student1(String name) { this.name = name; } public String getName() { return name; } public void setName(String name) { this.name = name; } } class Student2 { private String name; public Student2(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 (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; Student2 other = (Student2) obj; if (name != other.name) return false; return true; } }
执行结果
String类===== 23468387 23468387 true 未重写equals方法===== 366712642 1829164700 false 重写equals方法===== 2018699554 1311053135 true
分析:从结果可以看到Student2 未覆写hashCode所以两个对象的hash值不一样,但因为重写了equals方法所以equals比较结果为true.
2、总结
(1)==针对基础数据类型比较的是值,针对引用数据类型比较的是引用地址;
(2)equals()方法由开发人员自己定义对象的相等方式,重写equals方法需要遵循协定:
①两个对象相等,hashcode一定相等
②两个对象不等,hashcode不一定不等
③hashcode相等,两个对象不一定相等
④hashcode不等,两个对象一定不等