了解 java 中 == 运算符和 equals() 方法之间的主要区别,并了解在 Java 编程中如何有效地使用它们进行对象比较。
public class Demo {
public static void main(String args[]){
String a = "a" "b" 1;
String b = "ab1";
System.out.println(a == b);
}
}
根据上面的代码,请看下列问题:
a和b在内存中的表示是什么?
编译时有什么优化策略吗?
“==”在Java语言中,==用于比较两个内存单元的内容是否相同。
对于诸如 byte、boolean、short、char、int、long、float 和 double 等基本数据类型,比较的是它们的值。
对于引用类型,比较的是引用的值,可以将其视为对象的逻辑地址。
当 == 运算符应用于两个引用类型时,它会比较两个对象各自的内存地址是否相同。
也就是说,如果两个引用指向同一个对象,则返回true;否则,返回 false。
public class Demo {
public static void main(String args[]){
List<String> a = null;
List<String> b = null;
System.out.println(a == b);
}
}
结果是:true
“equals”
下面是Object类中equals()的源代码。
public boolean equals(Object obj) {
return (this == obj);
}
换句话说,如果没有重写 equals() 方法,并且相应类的父类也没有重写 equals() 方法,则 equals() 默认就是比较对象的地址。
equals()方法的存在是为了鼓励子类重写它,从而实现基于值的比较。
掌握了基础知识后,现在让我们解决文章开头提出的两个问题。
a和b在内存中的表示是什么?由于 Java 的编译优化策略,a 和 b 都引用相同的内存位置。 我们可以使用JD-GUI反编译工具来检查反编译后的代码
public class Demo
{
public static void main(String[] args)
{
String a = "ab1";
String b = "ab1";
System.out.println(a == b);
}
}
结果是:true
到这里,结果应该就很清楚了。(==比较的内存地址) JVM在编译时对常量折叠进行了优化,因为常量折叠的结果是一个固定值,不需要运行时计算,所以通过这种方式进行优化。
不过,不要急于下结论。 JVM 只优化它能优化的部分。它并没有优化所有方面。
例如,前面提到的字符串连接。如果字符串的连接过程中有变量,意味着编译时不知道具体值,则 JVM 不会执行这种编译时合并。
如果你已经理解了上一段的意思,我们再来看另一个例子:
public class Demo {
public static void main(String args[]){
String a = "a";
final String c ="a";
String b = a "b";
String d = c "b";
String e = getA() "b";
String compare = "ab";
System.out.println( b == compare);
System.out.println( d == compare);
System.out.println( e == compare);
}
private static String getA(){
return "a";
}
}
结果是:
false
true
false
根据我们之前的解释,很容易理解为什么 b == Compare 和 e == Compare 会产生错误结果。
这是因为 a 和 getA() 不是常量,所以并且没有对此进行编译时优化。相反,c是一个final的不可变的常量,所以进行了编译优化。
我们可以通过使用 JD-GUI 检查反编译代码来验证这一点:
import java.io.PrintStream;
public class Demo
{
public static void main(String[] args)
{
String a = "a";
String c = "a";
String b = a "b";
String d = "ab";
String e = getA() "b";
String compare = "ab";
System.out.println(b == compare);
System.out.println(d == compare);
System.out.println(e == compare);
}
private static String getA()
{
return "a";
}
}
从反编译的代码中,我们可以确认b和e没有经过JVM编译优化。
看到的是变量 d 已经被 JVM 优化了。 关键的区别在于变量 c 用 Final 修饰符修饰。 该声明对 c 的不可变性施加了严格的约束,并且由于 Final 意味着不可变性,因此编译器自然地知道结果也是不可变的。
字符串在内存里怎么存储?字符串对象内部使用字符数组进行存储。
现在,让我们看一下以下示例:
String m = "hello,world";
String n = "hello,world";
String u = new String(m);
String v = new String("hello,world");
这段代码在内存中怎么分配?大致是这样的:
- 它会分配一个长度为11的char数组,并在字符串池中创建一个由该char数组组成的字符串。然后,m 引用该字符串。
- 使用n引用字符串池中的字符串,因此n和m引用同一个对象。
- 生成一个新字符串,但其内部字符数组引用与 m 相同的数组。
- 同样,创建了一个新字符串,但其内部字符数组引用了字符串池中的字符数组,即与u相同。
结论是m和n是同一个对象,而m、u和v是不同的对象,但它们都共享相同的字符数组。
使用equals()方法进行比较时,也会返回true。
我们可以使用反射来修改字符数组来验证效果。
public class Demo {
public static void main(String args[]) throws NoSuchFieldException, IllegalAccessException {
String m = "hello,world";
String n = "hello,world";
String u = new String(m);
String v = new String("hello,world");
Field f = m.getClass().getDeclaredField("value");
f.setAccessible(true);
char[] cs = (char[]) f.get(m);
cs[0] = 'H';
String p = "Hello,world";
System.out.println(m.equals(p));
System.out.println(n.equals(p));
System.out.println(u.equals(p));
System.out.println(v.equals(p));
}
}
结果是:
true
true
true
true
从上面的例子中,我们可以看到,通常说字符串是不可变的时,这意味着对字符串的引用是不可更改的,类似于其他final类。
虽然String类没有暴露它的value字段,但是仍然可以通过反射来修改它。
关于String中的intern()方法public class Demo {
public static void main(String args[]){
String a = "a";
String b = a "b";
String c = "ab";
String d = new String(b);
System.out.println(b == c);
System.out.println(c == d);
System.out.println(c == d.intern());
System.out.println(b.intern() == d.intern());
}
}
结果是:
false
false
true
true
String引用指向的对象存储在常量池中,保证具有相同值的字符串全局唯一。
如何确保这种全局唯一性?
当调用 intern() 方法时,JVM 使用 equals() 方法检查常量池中是否存在具有相等值的 String。
如果找到,则返回常量池中 String 对象的引用。如果没有找到匹配的字符串,它会创建一个具有相同值的新字符串,并在常量池中返回对此新字符串的引用。
只要两个字符串相同,对它们调用 intern() 就会产生对常量池中相应 String 的引用。 因此,在调用intern()之后,可以使用相等运算符来匹配两个字符串