从“==”谈起

写在前面
以前觉得写技术博客是一产出比不高的事情,但是最近发现写写博客能帮助自己整理思路,理清脉络,加深印象。若有浅陋愚笨,以管窥天之言,能得大方之家指 出,或有幸只言片语能幸解他人一时之惑,凡此种种,于人于己都有裨益的,所以今天开始,在这小小的自留地里洒下种子,望他日能有一处风景,自得其乐。


最近盘算着换个城市呆呆,所以比较留意相关面试题,今天看到了这样一道题:
下面程序运行结果是:


//程序一
String str1 = "hello"; //line 1
String str2 = "he" + new String("llo");//line 2
System.err.println(str1 == str2);//line 3

A:    true

B:    false

C:   exception

D:   无输出


此题很常见,也很简单,答案自然是 B。
在 Java中str1 和str2 只是两个declared,只是一块内存区域的标识,而==比较的是保存在这两块内存中的数据是否相同。我们可以这样理解:str1,str2只是两间房 子的门牌号,而==比较的是房间里面装的东西是否相同。对于基本数据类型 (boolean,byte,char,short,short,float,long,double)来说房间里面装的就是本身的值,而对于引用类型来说,房间里面装的并不是对象本身,根据不同的JVM实现,它可能是代表对象的一个句柄,也可能是指向对象起始地址的引用指针,或者是returnAddress(字节码指令地址)。下面一个程序很好说明了引用类型并不是对象本身:

  String str = null;
        if (str instanceof Object) // NULLCHK
            System.out.println("str is Object");
        else
            System.out.println("str is not Object");

程序输出:str is not Object,因为str只是String类型一个声明而不是对象本身。再看下面一个程序:

String str1 = null; 
String str2 = null;
Object obj = null;
System.err.println(str1 == str2);
System.err.println(str1 == obj);

程序输出:true true,因为“==”比较的是房间里面的内容,而str1,str2,obj里面保存的都是“null”。‘null’并不表示什么都没保存,正如 一间空房间不能等同于一间“真空”的房间,事实上编译器是不允许出现“真空”的情况。对于类的成员变量来说可以不显示的赋初始值,是因为JVM在 给对象分配内存空间时会对内存进行一次清理,等效于隐式的给对象的成员变量赋值,基本类型默认为对应的0,boolean 类型默认为false,引用类型为null。但是如果该成员变量被final修饰,即是一个常量,需要显示的赋值,否则编译器会报错。
我们对程序一略作修改:

  // 程序二
String str1 = "hello"; // line 1
String str2 = "he" + new String("llo");// line 2
System.err.println(str1.equals(str2));// line 3

程序输出:true
与程序一相比修改的只是在line 3 处,改用了equals方法,对于任何一本Java相关的书籍都会提到,比较两个对象是否相同应该使用equals方法,而非“==”。
我 们知道Java中任何一个类类型都有一个共同的祖先Object ,而equals方法是Object中一个成员方法,我们通过查看SDK源代码知道,一个类若未override equals方法,则和“==”比较是等效的。


//Object类    
public boolean equals(Object obj) {
        return (this == obj);
    }
Java语言对equals()的要求如下,这些要求是必须遵循的:
1.自反性:对任意引用值X,x.equals(x)的返回值一定为true.
2.对称性:对于任何引用值x,y,当且仅当y.equals(x)返回值为true时,x.equals(y)的返回值一定为true;
3.传递性:如果x.equals(y)=true, y.equals(z)=true,则x.equals(z)=true
4.一致性:如果参与比较的对象没任何改变,则对象比较的结果也不应该有任何改变
5.非空性:任何非空的引用值X,x.equals(null)的返回值一定为false

    在任何一个地方提到equals方法,必然会涉及到另一个方法:hashCode(),它也是属于Object类的成员方法,而且是一个本地方法,该方法返回的是“a hash code value for this object”。

 public native int hashCode();
    那么为什么equals方法和hashCode方法总是形影不离呢?因为在Java语言编程规范中规定:
    任何两个对象若equals方法返回true,则它们的调用hashCode方法的返回值也一定相同。
    所以我们在override equals方法时,常常需要override hashCode方法,以确保任何该类型对象都满足以上规定。
但是我们不能由两个对象的hashcode相同,来推定它们是equals的。也就是说两个对象 hashCode相同时,它们也可能equals方法返回的是false。其实不难理解,对于任何一个保证一定内存利用率的散列函数来说都不可能完全避免冲突的发生,发生冲突就意味着不同的对象映射到了同一hashcode。
    我们在override 其中任意一个函数后,编译器并不会强制要求我们override 掉另一个函数,但是如果我们破坏掉了上面的原则在某些情况下会导致错误的发生。
    在Java中有一个HashSet类型,它在执行put操作时会先检查集合 中是否存在与待插入对象hashCode相等的元素,若不存在则会直接插入待插入对象,若存在,则会调用equals方法比较hashCode相等的对 象,若返回false则插入对象,若返回true则不插入该对象。在默认的实现下hashCode返回的值实际上是对象的内存地址的一个映射,如果我 们未正确override hashCode方法,则每一个new的对象都会被插入其中。
现在让我们回头来观察程序一,我们发现对一个字符串类型的引用赋值有两种方式,它们有什么区别和联系呢?
我们先来看下面一个程序:

        String s1 = "hi";
        String s2 = new String("hi");
        String s3 = "hi";
        String s4 =  new String ("hi");
        if (s1 == s2) {
            System.out.println("s1 and s2 equal");
        } else {
            System.out.println("s1 and s2 not equal");
        }
        if (s1 == s3) {
            System.out.println("s1 and s3 equal");
        } else {
            System.out.println("s1 and s3 not equal");
        }
        if (s2 == s4) {
            System.out.println("s2 and s4 equal");
        } else {
            System.out.println("s2 and s4 not equal");
        }
    


程序的运行结果为:s1 and s2 not equal   s1 and s3 equal   s2 and s4 not equal
由前面的分析我们知道对于引用类型来说“==”实际比较的是引用保存的值只是否相等,也就是说如果两个引用类型“==”结果为true,则表明它们指向的是内存中同一个对象,若为false则指向内存中不同的对象。那么为什么s1和s3是指向的同一个对象,而s1(s3),s2,s4指向的是不同的对象呢?
    在
JVM运行时内存区域 中存在一块叫作constant pool(常量池)的区域。它保存了若干种常量:编译器可知的数值字面量到必须运行期解析后才能获得的方法或字段的引用。
    我们知道Java 中String是不可变类,所以对于 String s1 = "hi";此种方式来说,编译前就可知道对象的内容是“hi”,所以“hi”就是一个常量,编译器会检查在 constant pool 中是否存在“hi”对象,若不存在则加入该对象,当有引用需要指向 “hi” 对象时,直接返回常量池中“hi”对象的引用,所以s1和s3指向的是同一 对象。
    但当采用new方式创建字符串对象时,JVM中又会发生什么呢?
    在Java中所有的new操作都是在heap(堆)中进行的,执行 String s2 = new String("hi"); 时,不管常量池中是否存在“hi”这个对象都会在堆中创建一个新的“hi”对象,而对于每一次new操作在堆中产生的对象是一定不相同的,所以这也是s2不等于s4的原因。
    与此同时被当作构造器参数传入的“hi”对象,对于JVM来说仍是一个常量,若 constant pool 中不存在“hi”对象,则会在常量池加入该对象。正因为此种副作用,所以一般情况下,我们应该避免使用new关键字来创建字符串对象。
    在 String类中有一个与上述情况相关的名为intern 的成员方法,当执行String str = new String ("hello").intern(); JVM中的实际流程是:在堆中创建一个“hello”对象,检查常量池中是否存在"hello"对象,若常量池中不存在,则在常量池中加入“hello”对 象,然后将常量池中“hello”对象的引用返回给str,将堆中的“hello”对象变为垃圾对象,等待GC的收集。注意,不能略去在堆中创建对象这一 步,因为intern方法为非静态方法。
    在Java中与String类具有相似性质的还包括基本数据类型的封装类。
    最后,让我们回到最初的地方,对于程序一来说到底在内存中创建了几个字符串对象呢?
//程序一
String str1 = "hello"; //line 1
line 1 创建了一个对象“hello”,位于常量池中;
String str2 = "he" + new String("llo"); //line 2
line 2 :创建了一个“he”的对象位于常量池中。new 关键字创建了一个位于堆中的“llo”对象,同样在常量池中创建了一个“llo”对象,“+”操作创建了一个“hello”对象位于堆中,而常量池中已经存在“hello”对象所以不再重复加入。
line1 +line 2 = 4个,因为字符串对象的这种特性,我们在处理经常变化的字符串时,通常会借助于,StringBuffer/StringBuild类,以避免产生多个不需要的字符串对象。




评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值