Java中的== 和equals
引言
笔者之前参加过一些Java的面试,事先hr都会给一套笔试题做一做。而笔试题里出现频率最高的一道题就是Java里面的= =和equals相关的东西。
注:==在markdown编译器有其他意义,显示有问题,所以此处使用= =代替
出题的方式一般两种:
- 第一种:选择题形式;给一小段代码,里面多个String变量或者Integer(int)变量定义相同的值,使用 = =或者equals进行比较然后给出各种选项判断true或者false。
- 第二种:问答题形式;简单粗暴,考题形式如下:= =和equals的关系和区别。
无论是这两种题型的哪一种,都埋着各种坑,都不会是简单的送分题,如果对Java虚拟机数据的存储方式或者Java基础知识没有清醒的认识,可能很难回答正确。
笔者在未完全了解这两种比较方式的时候,曾经给出的答案:
== 比较的是两个变量的地址,equals比较的是两个变量的值。
当时还深以为然,不觉得有什么错。随着 知识的增进,发现自己的答案是漏洞百出,笔者会在本文最后给出一个相对完整准确的答案。
Java常量池
了解Java常量池是解答上述问题的一个关键知识点
简单回顾一下《Java虚拟机规范》中对运行时数据区的内存区域的划分
- 程序计数器是jvm执行程序的流水线,存放一些跳转指令。
- 本地方法栈是jvm调用操作系统方法(本地方法native)所使用的栈。
- 虚拟机栈是jvm执行java代码所使用的栈。
- 方法区存放了一些常量、静态变量、类信息等,可以理解成class文件在内存中的存放位置。
- 堆是存放对象实例和被垃圾收集器管理的内存区域。
由上我们可以得知Java常量池存在于方法区中。
Java中的常量池分为两种:静态常量池和运行时常量池。
- 静态常量池,即*.class文件中的常量池,class文件中的常量池不仅仅包含字符串(数字)字面量,还包含类、方法的信息,占用class文件绝大部分空间。这种常量池主要用于存放两大类常量:字面量(Literal)和符号引用量(Symbolic References),字面量相当于Java语言层面常量的概念,如文本字符串,声明为final的常量值等,符号引用则属于编译原理方面的概念,包括了如下三种类型的常量:类和接口的全限定名;字段名称和描述符;方法名称和描述
- 运行时常量池,则是jvm虚拟机在完成类装载操作后,将class文件中的常量池载入到内存中,并保存在方法区中,我们常说的常量池,就是指方法区中的运行时常量池。
运行时常量池相对于class文件常量池的另外一个重要特征是具备动态性,Java语言并不要求常量一定只有编译期才能产生,也就是并非预置入class文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中,这种特性被开发人员利用比较多的就是String类的intern()方法。
String的intern()方法会查找在常量池中是否存在一份equal相等的字符串,如果有则返回该字符串的引用,如果没有则添加自己的字符串进入常量池。
字符串常量池
JVM为了提高性能和减少内存开销,在实例化字符串常量的时候进行了一些优化。
- 为 了减少在JVM中创建的字符串的数量,字符串类维护了一个字符串池,每当代码创建字符串常量时,JVM会首先检查字符串常量池。如果字符串不在池中,就会实例化一个字符串并放到池中。并在堆中创建该对象实例。
- 如果字符串已经存在池中, 就返回池中的实例引用。不再在堆中重复创建。
Java能够进行这样的优化是因为字符串是不可变的,可以不用担心数据冲突进行共享。
关于Java中的 ==
案例
话不多说,上代码
package test36;
public class Main {
public static void main(String[] args) {
System.out.println("**********第一组************");
String abc = "abc";
String def = "abc";
System.out.println(abc == def);
System.out.println("**********第二组************");
String abcd = new String("def");
String defg = new String("def");
System.out.println(abcd == defg);
System.out.println("**********第三组************");
String abcde = "abcde";
String defgh = new String("abcde");
System.out.println(abcde == defgh);
System.out.println("**********第四组************");
int a=1;
Integer b = new Integer(1);
System.out.println(a==b);
System.out.println("**********第五组************");
Integer c = new Integer(1);
Integer d = new Integer(1);
System.out.println(c==d);
System.out.println("**********第六组************");
Integer e = new Integer(1);
Integer f = 1;
System.out.println(e==f);
System.out.println("**********第七组************");
Integer g = 100;
Integer h = 100;
System.out.println(g==h);
System.out.println("**********第八组************");
Integer j = 128;
Integer k = 128;
System.out.println(j==k);
System.out.println("**********第九组************");
Integer m = Integer.valueOf(128);
Integer n = 128;
System.out.println(m==n);
System.out.println("**********第十组************");
Integer q = Integer.valueOf(100);
Integer p = 100;
System.out.println(q==p);
}
}
可以先试着猜一猜,后面给出运行结果
答案解析
第一组:Java中的字符串定义变量的方式有两种,一种是直接赋值,一种使用new关键字实例化。如下:
- 直接赋值:String str = “Hello World”;
- 构造方法实例化:String str = new String(“Hello World”);
在Java中,String不属于8种基本数据类型之一,所以其定义的变量属于对象,所以= =比较的是内存地址。
在了解完字符串常量池后,在两个String定义变量使用直接赋值的情况下,答案就很清晰了。所以为true。
第二组:使用new关键字实例化,都会在堆中开辟一块内存。地址不同,所以为false。同理,第五组也是。
第三组:一个是方法区中字符串常量池中对象的地址,另一个是堆中对象的地址。因此是false。第六组同理也是。
第四组:Integer是int的包装类,int则是java的一种基本数据类型 。包装类Integer和基本数据类型int比较时,java会自动拆包装为int,然后进行比较,实际上就变为两个int变量的比较。(基本数据类型 = = 比较的数据的值,引用数据类型使用 = =比较的对象地址的值)
第七,八,九,十组:
java在编译Integer i = 100 ;时,会翻译成为Integer i = Integer.valueOf(100);
java对于-128到127之间的数,会进行缓存,Integer i = 127时,会将127进行缓存,下次再写Integer j = 127时,就会直接从缓存中取,就不会new了
关于Java中的 equals
案例
package test37;
import java.util.HashSet;
import java.util.Set;
public class Main {
public static void main(String[] args) {
String s1 = new String("abc");
String s2 = new String("abc");
System.out.println(s1 == s2);
System.out.println(s1.equals(s2));
Set<String> set01 = new HashSet<>();
set01.add(s1);
set01.add(s2);
System.out.println(set01.size());
System.out.println("===========");
Person p1 =new Person("abc");
Person p2 =new Person("abc");
System.out.println(p1 == p2);
System.out.println(p1.equals(p2));
Set<Person> set02 = new HashSet<>();
set02.add(p1);
set02.add(p2);
System.out.println(set02.size());
}
}
package test37;
public class Person {
private String name;
public Person(String name) {
this.name = name;
}
public Person() {
}
}
运行结果如下:
答案解析
s1 == s2为false已经很明确了。
由String的equals源码可知,它比较的是两个变量的值。所以为true。
set01的 size大小为何是1?
先瞅瞅HashSet的源码
由HashSet的add()方法源码可以得到两个结论:
1.HashSet的add()底层调的是HashMap的put方法
2.HashMap的put方法的key唯一性(不重复)的确定是以key值的hashcode为准。
所以再看看String的hashcode方法
相同的变量值通过公式计算出的hashcode是一样的。
所以答案就很明确了
第二组Person的“= =”也很明确了
equals方法使用的Object类的equals方法。咱们看看Object类的equals方法的源码
那答案就很明确了。
聊一聊==和equals的区别
结论
- 构造方法实例化字符串会存在一部分垃圾空间,便是堆内存中重复创建的字符串字段,因此直接赋值的方式确实优于构造方法的方式,因此字符串在使用时都使用的是直接赋值的方法。
- Java常量池存在于方法区
- 使用new关键字实例化,都会在堆中开辟一块内存
- 基本数据类型 = = 比较的数据的值,引用数据类型使用 = =比较的对象地址的值
- java在编译Integer i = 100 ;时,会翻译成为Integer i = Integer.valueOf(100);java对于-128到127之间的数,会进行缓存,Integer i = 127时,会将127进行缓存,下次再写Integer j = 127时,就会直接从缓存中取,就不会new了。
- HashSet的add()底层调的是HashMap的put方法
- HashMap的put方法的key唯一性(不重复)的确定是以key值的hashcode为准。不是key值本身
- String类重写了equals方法和hashCode()方法。因此String类的equals比较的是变量值是否相等。
- 普通类如果未重写equals方法的话,那就使用Object类的equals()方法,其实和 = =是相同的。