关于java:由ifXXXTERM引发的连锁小故事

2次阅读

共计 8150 个字符,预计需要花费 21 分钟才能阅读完成。

1. 前言

判断语句“==”可能是 java 初学者因为了解不够深刻而最容易犯的最常见的谬误之一。不久前笔者就在代码中就犯了这个谬误。之前已看过 == 与 equals()办法的比拟,然而在本人写代码时却未然遗记了,而在对此问题的 debug 和钻研过程中发现竟还附带关联了不少其余问题,一个小小的 == 判断居然也有深挖的价值,笔者在感叹之余也决定把这次探险之旅记录下来。

2. 问题产生

问题呈现的情景十分常见,笔者试图应用 json 对象作为 http 传输的内容,应用了 alibaba 的 jsonObject 类,并将一个自定义对象的局部属性赋值至 jsonObject 对象中,再通过判断传输的 json 对象的属性值来开展不同的操作,于是写了如下的代码。

JSONObject jsonObject = new JSONObject();
jsonObject.put("command", messageReceive.getCommmand());
if(jsonObject.get("command") == "TERM"){doSomething();
}

测试时则发现判断语句 jsonObject.get(“command”)==”TERM” 一行的判断后果始终为 false,而我为此打印进去 jsonObject.get(“command”)的值又的确是“TERM”字符串。那么,问题到底呈现在哪里呢?

3. 问题排查

3.1 hashCode 和 identityHashCode

因为过后齐全遗记了 == 和 equals()办法的区别,我首先的排查办法是在 test 文件夹下新建测试类,简略地复现了下面的过程,代码如下:

JSONObject jsonObject = new JSONObject();
jsonObject.put("command", "TERM");
console.log(josnObject.put("command") == "TERM"); // 打印后果为 true!
if(jsonObject.get("command") == "TERM"){doSomething();
}

然而测试的后果居然为 true,这阐明在上述代码中 jsonObject.put(“command”)的后果的确是 TERM,这让我一度认为我找到了问题的本源,肯定是我自定义的类出了问题,可是为什么我应用 println 输入的后果又的确是指标字符串呢?就在我一愁莫展之际,冥冥之中的天意让我想起来了 hashcode 这一属性,想起来 ”==” 号,对于非根本类型,理论如同是比拟的二者的存储地址?而这个地址如同是基于 hashcode 属性来判断的?我连忙将我的 test 和 Source 文件夹下生成的这堆货色全副 hashcode 一下,失去的后果是惊人的,所有的 hashcode 竟全副雷同!肯定是哪里出了问题!果然,在度娘的帮忙下,我意识了一个更加高级的办法——System.identityHashCode(String s)。对于 String 类,它重写了 String 的 hashcode()办法,返回值只依据字符串的内容决定。而 System.identityHashCode()办法则返回重写前的依据对象存储地址所得的 hashcode 值!同时也学习到了 new 关键字申明会产生不同的援用地址,我决定在 test 文件夹下认真钻研一下新学习到的常识,于是有了如下运行后果。

TERM
============================================
s: String s = new String("TERM");
s2: String s2 = "TERM";
s3: String s3 = "TERM";
s4: String s4 = new String("TERM");
s5: String s5 = jsonObject.get("command");
============================================
s 的 identity-hashcode:1878246837
s2 的 identity-hashcode:929338653
s3 的 identity-hashcode:929338653
s4 的 identity-hashcode:1259475182
s5 的 identity-hashcode:929338653
"TERM" 的 identity-hashcode:929338653
============================================
s 的 hashcode:2571372
s2 的 hashcode:2571372
s3 的 hashcode:2571372
s4 的 hashcode:2571372
s5 的 hashcode:2571372
TERM 的 hashcode:2571372
=============================================
s==s4: false
s2==s3: true
s==s5: false
s2==s5: true
s==s2: false
=============================================

如下面代码结果显示所示,s~s5 通过不同的定义形式定义,而后我列出了它们的 identity-hashcode 和 hashcode,并拿不同的字符串进行了比拟,后果和我查阅的解释非常地吻合!

  • 通过各种形式失去的内容为“TERM”字符串对象的 hashcode 值均雷同!
  • 通过 new 关键字申明的 s 和 s4 即便内容雷同,它们的 identity-hashcode 也齐全不同,阐明每次 new 关键字申明都会生成一个新的援用地址;
  • 间接定义的 String 对象 s2,s3 则共享了雷同的 identity-hashcode,
  • 通过 jsonObject.get(“command”)办法失去的 identity-hashcode 也和间接定义的 s2, s3 统一;

对最初一条发现,其实也不难解释,因为 jsonObject 中定义 command 属性时的 put 办法也就是间接应用了“TERM”这个字符串,和间接的 String 申明本质是一样的,即便将其作为一个属性退出了对象中,也并没有扭转该对象的 identity-hashcode 值。阐明该 String 对象放入 jsonObject 时仅仅是一个建设了援用关系。

那么,我的 Source 源码中判断时报错就应该是 …

//MessageRecive.java
...
    this.command = new String(subBytes(startPos, endPos));

果然,我的自定义类中,command 属性的值就是通过 new String()申明的,因为须要人 byte[]中转换过去。到此,所有仿佛水落石出,new String()式申明会产生不同的 identity-hashcode 值,它代表的是对象的存储地址,而 == 的比拟就是通过判断 identity-hashcode 来比拟,而不是比拟的 String 的具体内容。

而这当然不能就此结束,因为此时摆在咱们背后的仍有一个关键问题:String str = “value” 和 String str = new String(“value”)到底差别在哪里??

3.2 java 内存的浅显了解

为什么 String str = “value” 和 String str = new String(“value”)失去的后果会有如此的区别呢,网上学习了一番,发现造成这一后果的差别与 java 的内存及 java 编绎运行的原理相干,对于一个 java 初学者,太浅近的底层常识目前还无奈了解,间接从源码剖析也临时超出了我的能力,甚至连官网文档的相干阐明也看不懂,只能从网上学习大神的解读并用本人能了解的办法稍作简化。

3.2.1 堆,栈和办法区

java 的内存分主堆,栈及办法区。其各局部的内容及性能蕴含如下:

  • 栈:栈用于寄存根本类型的数据及变量,线程公有,因而不同线程的变量个别不能共用。栈由一个个栈帧形成,一个办法的一次调用即产生一个栈帧,栈帧中的内容则包含一个局部变量区(寄存局部变量和参数)和一个操作数栈(办法的操作台,办法运行时相干变量在操作数栈中压入压出)。注:根本类型的成员变量存储在堆中对应的对象中,而不是在栈中。
  • 堆:用于寄存对象和数组的实例,全局共享。
  • 办法区:动态存储区域,包含类的相干信息,常量,动态变量,办法区中还有比拟重要的一部分内容就是常量池。常量池可分为全局字符串常量池,class 动态常量池,以及运行时常量池。
  • 常量池:

    • class 动态常量池。顾名思义,是类加载时生成的常量池,每个类都有公有的常量池,该常量池在编译时产生,蕴含编译期生成的字面量和符号援用,其中字面量次要就是通过双引号定义的字符串(也包含申明为 final 的常量)。在编译时,jvm 对字符串字面量有一个编译优化的过程,即对于 String str = “stu” + “de” + “nt”; 的命令,jvm 在编译时会主动将其拼接为一个残缺的”student“对象存储在常量池中,而不会存储 3 个字符串片段。但对于蕴含变量的拼接,jvm 则无奈进行这样的优化。
    • 全局字符串常量池。因为字符串是程序中应用较多的对象,而对象实例的创立是一个十分消耗工夫和空间的过程。为了进步程序效率,同时借助于字符串的不可扭转性,java 专门开拓了全局字符串常量池,在类加载时就可确认的字符串对象(个别都来自 class 动态常量池)退出全局字符串常量池。自 jdk1.8 及当前,全局字符串常量池是以一个 hash 表的模式存在,常量池中存储的不是具体的 string 对象实例,而是对应字符串对象的援用,这个实例被存储在堆中。一般来讲,全局字符串常量池只在类加载时会更新,特地地还能够通过 String 的 intern()办法在类加载实现当前再手动增加字符串至字符串常量池(如果该字符串曾经存在则不会增加而是返回以后常量池中的援用地址)。字符串常量池显然是全局共享的。全局字符串有说并不在办法区内,这块目前还未深刻了解,然而其地位并不影响整个性能流程的了解。
    • 运行时常量池。指程序运行时的常量池,程序运行时,class 动态常量池在类加载时也会将其中的大部分内容会导入到运行时常量池中,因为运行时常量池也是类公有的。对于字符串常量,其在导入运行时常量池时会先查问全局字符串常量池,若字符串常量池中已有,则间接将常量池中的援用替换给运行时常量池,若没有,则在堆中创立该字符串对象,并将其增加至全局字符串常量池,同时返回该援用给本身。须要留神的是,类加载的过程是懒加载 (lazy) 的,即每个字符串是否会进入运行时 or 全局字符串常量值只在对应语句须要被执行时才会进行检查和退出到对应常量值中(通过执行 ldc 命令)。后续程序在运行过程中,如果产生了新的字符串(如通过拼接或打断),将只会存储于运行时常量池,而不会再存储于全局字符串常量池中。

综合上述形容,能够总结 java 的堆栈及字符串定义的形式如下图。

上述过程可简略形容如下:

  1. 程序编译时生成的字面量包含”abc“,”abcd”(编译时优化),”ef”,”gh”;(s6 的拼接蕴含 new 的对象,无奈编译优化);
  2. 程序运行时,加载此段程序所在的类,String s1 = “abc”,在栈中创立变量 s1,此时全局字符串常量池中无内容,在堆中创立”abc“实例,并将此实例地址退出全局字符串常量池”abc“,运行时常量池中 ”abc” 间接指向全局字符串常量池中 ”abc” 对应的地址;
  3. String s2 = “abc”,在栈中创立变量 s2,此时全局字符串常量池中已有定义,间接复用,将字符串常量池中”abc“地址传递给变量 s2;
  4. String s3 = new String(“abc”),在栈中创立变量 s3,new 关键字创立的是一个对象,在堆中开拓空间存储该对象,空间地址指向 s3;字面量”abc“在全局变量字符串中已存在,间接拷贝一份至堆中 s3 指向的存储空间(疑难:此处拷贝是在堆中将值深拷贝一份还是在堆中创立一个新的援用依然指向原先的字符串实例?);
  5. String s4 = new String(“abc”),过程与 4 雷同,new 关键字必须开拓一个新的存储空间,创立一个新的对象实例;
  6. String s5 = “ab” + “cd”,因为编译优化,相当于执行 String s5 = “abcd”,过程同 1 统一;
  7. String s6 = “ef” + new String(“gh”),栈中创立变量 s6,无奈编译优化,加载过程中在常量池中减少“ef”和“gh”,而后运行时通过 StringBuilder.append()办法实现连贯,所得 s6 后果为 ”efgh”,在堆中开拓新的空间存储此字符串,留神此时“efgh”并未增加至运行时常量池和全局字符串常量池中;
  8. s6.intern(),执行 intern()办法,查看 s6 的字面量“efgh”发现不在全局字符串常量池中,则在全局字符串常量池中创立该常量,并指向以后对象对应的地址(疑难:此处增加时是否会一并退出到运行时常量池?
3.2.2 String 类型的字面量定义和对象定义

字符串的两种定义,间接应用双引号“value”定义的形式叫字面量定义,应用 new 关键字的中对象定义,两种定义方法由上一节的剖析曾经比拟清晰,此处再重点强调下其过程。

  • 字面量定义,String s = “value”,编译时”value“曾经存在于 class 常量池中;在运行阶段,通过类加载,运行到这句话时,发现字符串”value“不在以后全局字符串常量池中,则首先在堆中创立”value” 实例,并将实例地址退出全局字符串常量池和运行时常量池,生成一个 String 实例;
  • 对象定义,String s = new String(“value”),编译时”value“存在于 class 常量池中;在运行阶段,通过类加载,运行到这句话时,首先通过 new 关键字会在堆中创立一个对象,寄存字符串对象 s,而后查看”value“字面量,发现字符串”value“不在以后全局字符串常量池中,则在堆中再创立”value” 字面量实例,并将实例地址退出全局字符串常量池和运行时常量池,随后拷贝常量池中对应的”value“对象的内容至通过 new 关键字创立的字符串对象 s 中,因而共生成了 2 个 String 实例,此处存在的疑难是字符串 s 实例对应的内容是间接拷贝了常量池中的值还是也是通过援用的形式关联至了字面量”value“对应的 String 实例中。

3.3 == 和 equals()办法

依据 3.2 节的剖析能够看出,通过 new String()实例化的字符串对象在堆中均指向不同的地址。通过 == 进行的判断正是比拟地址,即比拟的后果是 2 者是否指向同一个对象;而 equals()办法则有所不同,对于 Object 父类,equals()办法与 == 号完全一致,然而在几种根本类型的包装类和 String 类中,均重写了该办法,如下:

// Object 类中的 equals()办法
public boolean equls(Object obj){return (this == obj);
}

//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;
}

由 String 类重写的办法可知,String 的 equals()办法比拟的是两个字符串的值完全相同。

问题到此仿佛圆满解决,然而仔细的我忽然想起来我在测试时曾试图用如下代码接管通过 jsonObject.get()办法返回的后果,然而代码提醒谬误:

String command = jsonObject.get("command"); //Reqired is String but Provided is Object

//jsonObject.get();
public Object get(Object key){Object val = this.map.get(ket);
    if(val == null && key instanceof Number){val = this.map.get(key.toString());
    }
}

// 上面语句执行的 equals 办法不应该是 Object 的 equals 办法吗?jsonObject.get("command").equals("TERM");// 后果为 true, 阐明执行的是 String 的 equals 办法!

查看对应代码的确如此,jsonObject.get()办法返回的是一个 Object 类型,因为其自身就反对你放入的类型能够是各种类型!对么当我对 get()办法的后果再应用 euqals()办法时,不是应该调用 Object 类的 equals 办法吗?毕竟取得的后果可是一个 Object 对象呀!

3.4 Java 的继承与多态

持续钻研,这个问题其实很简略,波及到的正是面向对象编程的外围特质,继承与多态。因为咱们在 jsonObject 中通过 put 办法放入的 ”command” 属性对应的值理论为一个 String 对象,其返回给 jsonObject 的理论只是一个隐式转换后的 Object 对象。

String value = new String("TERM");
JSONObject jsonObject = new JSONObject();
jsonObject.put("command", value);
/* 此处理论为:Object this.command = vaue = new String("TERM");
*/
Object command = jsonObject.get("command");

这里理论隐含了一个 Object command = new String(“TERM”)的一个多态写法,而且 String 类重写了 Object 类的 equals()办法,所以 Object 对象 command 调用 equals()办法时依据多态的个性必定会调用 String 类的 equals()办法。网上查阅的更深刻的无关多态办法调用存在一个优先级,这个能够日后再深刻开掘一下。

所以多态机制遵循的准则概括为:当超类对象援用变量援用子类对象时,被援用对象的类型而不是援用变量的类型决定了调用谁的成员办法,然而这个被调用的办法必须是在超类中定义过的,也就是说被子类笼罩的办法,然而它依然要依据继承链中办法调用的优先级来确认办法,该优先级为:this.show(O)、super.show(O)、this.show((super)O)、super.show((super)O)。

4. 总结

简略总结下本次钻研波及到的若干问题:

  1. hashcode()和 System.identityHashCode(),前者在局部类中会被重写,后者则忽视重写办法返回对象的存储地址对应计算的 hashcode 值。
  2. java 的堆、栈及办法区,3 个常量池。对象实例存储在堆中,字面量字符串在编译时存在在 class 常量池,程序运行过程中类加载时会将 class 常量池导入运行时常量池和全局字符串常量池。
  3. == 和 equals()办法,String 类重写了 equals()办法,比拟值,而非比拟地址;
  4. java 的继承和多态,父类援用变量援用子类对象时,若子类重写了父类的办法,则调用援用变量的办法时会调用子类重写后的办法。

4. 后记

至此无关”if(xxx == “TERM”)“这一行语句牵扯出的诸多问题总算告一段落,文中仍存留有局部问题留待后续学习过程中进一步弄清楚。一番总结下来不禁让人心生感叹,如此简略的一个问题一旦深刻探索上来竟也能牵扯出如此泛滥的内容,果然万事无外乎钻研二字!

正文完
 0