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:1878246837s2的identity-hashcode:929338653s3的identity-hashcode:929338653s4的identity-hashcode:1259475182s5的identity-hashcode:929338653"TERM"的identity-hashcode:929338653============================================s的hashcode:2571372s2的hashcode:2571372s3的hashcode:2571372s4的hashcode:2571372s5的hashcode:2571372TERM的hashcode:2571372=============================================s==s4: falses2==s3: trues==s5: falses2==s5: trues==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的堆栈及字符串定义的形式如下图。
上述过程可简略形容如下:
- 程序编译时生成的字面量包含”abc“,"abcd"(编译时优化),"ef","gh";(s6的拼接蕴含new的对象,无奈编译优化);
- 程序运行时,加载此段程序所在的类,String s1 = "abc",在栈中创立变量s1,此时全局字符串常量池中无内容,在堆中创立”abc“实例,并将此实例地址退出全局字符串常量池”abc“,运行时常量池中"abc"间接指向全局字符串常量池中"abc"对应的地址;
- String s2 = "abc",在栈中创立变量s2,此时全局字符串常量池中已有定义,间接复用,将字符串常量池中”abc“地址传递给变量s2;
- String s3 = new String("abc"),在栈中创立变量s3,new关键字创立的是一个对象,在堆中开拓空间存储该对象,空间地址指向s3;字面量”abc“在全局变量字符串中已存在,间接拷贝一份至堆中s3指向的存储空间(疑难:此处拷贝是在堆中将值深拷贝一份还是在堆中创立一个新的援用依然指向原先的字符串实例?);
- String s4 = new String("abc"),过程与4雷同,new关键字必须开拓一个新的存储空间,创立一个新的对象实例;
- String s5 = "ab" + "cd",因为编译优化,相当于执行String s5 = "abcd",过程同1统一;
- String s6 = "ef" + new String("gh"),栈中创立变量s6,无奈编译优化,加载过程中在常量池中减少“ef”和“gh”,而后运行时通过StringBuilder.append()办法实现连贯,所得s6后果为"efgh",在堆中开拓新的空间存储此字符串,留神此时“efgh”并未增加至运行时常量池和全局字符串常量池中;
- 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. 总结
简略总结下本次钻研波及到的若干问题:
- hashcode()和System.identityHashCode(),前者在局部类中会被重写,后者则忽视重写办法返回对象的存储地址对应计算的hashcode值。
- java的堆、栈及办法区,3个常量池。对象实例存储在堆中,字面量字符串在编译时存在在class常量池,程序运行过程中类加载时会将class常量池导入运行时常量池和全局字符串常量池。
- ==和equals()办法,String类重写了equals()办法,比拟值,而非比拟地址;
- java的继承和多态,父类援用变量援用子类对象时,若子类重写了父类的办法,则调用援用变量的办法时会调用子类重写后的办法。
4. 后记
至此无关”if(xxx == "TERM")“这一行语句牵扯出的诸多问题总算告一段落,文中仍存留有局部问题留待后续学习过程中进一步弄清楚。一番总结下来不禁让人心生感叹,如此简略的一个问题一旦深刻探索上来竟也能牵扯出如此泛滥的内容,果然万事无外乎钻研二字!