很多Java的初学者对于String的使用细节不是太关心,但是我在这里想通过对Inside Java 2 Virtual Machine的学习和部分手动试验向大家展示String在所有Java Object中最特别的地方。
关键词: ==, equals, hashCode, intern, interned string reference list
首先最重要的是区分String中“==”, “equals()”以及“hashCode()”。
在Java中,很多时候两个相同Class的实例对象就算内容一样,通过equals对比后还是不一样,比如,
Car car1 = new Car();
Car car2 = new Car();
car1.equals(car2); // false
其根本原因还是两个对象在内存中是不同的对象,放在Java虚拟机的堆中不同的位置上。这样,你在对比两个对象的时候,当然就是不同的东西了。而equals有个必要条件,就是只要两个对象equals不一样的话,hashCode就必须不一样;反之,hashCode必须是一样的。因此我们可以知道当两个内容相同的实例对象equals不同的时候,他们的hashCode也必然是不同的。
String很特别,他的equals以及hashCode实现的方法是充分的利用了已有的对象,能够非常有效的节约空间。其实现方法就是两个不同的String对象,假如内容一样,那么equals是一样的,即他们的hashCode也是相同的。从已有的代码来看,其hashCode计算方法即是对每一个放在String内部数据结构所带的char数组中的字符进行计算和相加。比如,
String s1 = "hello";
其可能的hashCode计算方法是将h, e, l, l, o的每个ASCII码取出来然后乘以一个数字,最后相加。这样做的结果就是,只要你两个String字符串是一样的,最后hashCode就是一样的。而String的equals实现方法还是比较两个String对象中的字符串是否一样,一样的则返回为真。这也同样说明了两个String对象的字符串相同则就是同一个对象。
而“==”比较的是调用它的String引用所指向的String对象是否相同,这是什么意思呢?就是说,我用了==符号就是去比较位于==两边String引用所指向的,放在Java虚拟机的堆中的String对象是否是一样的。比如,
String s1 = new String("123");
String s2 = new String("234");
大家一看,必然知道这两者不仅equals下来是不一样的,==后肯定也是不一样的,毕竟这两个字符串内容都不一样,所以对应的String对象也肯定是不一样的。那么我们看这样呢?
String s1 = new String("123");
String s2 = new String("123");
首先可以确定的是s1.equals(s2);结果肯定是true,两者分别查看hashCode()后结果也是一样的,都是48690。但是如果你用s1 == s2进行比较的话,结果肯定是false!
为什么呢?
原因很简单,因为你使用了动态分配对象new,所以Java虚拟机在自己的堆上分别为两个String对象都分配了空间,因此你用==进行比较的时候,它比较的是两个String对象堆中实际的位置,结果当然就是不一样的了。而你用equals因为比较的是String实际的字符串是否相等,所以结果是相同的。
同样的,我们有,
String s1 = "123";
String s2 = new String("123");
这个==下来结果也是不同的,equals和hashCode是相同的。
神秘的“=”符号和new的区别。
可能大部分同学在使用String生成一个新的String对象的时候,都没有去想过使用动态分配对象new和直接在“=”符号后赋上一个字符串生成String对象的区别。实际上两者区别非常大。
使用=号后直接赋上字符串是告诉Java虚拟机,你在Java编译器将Java源代码编译成为bytecode的时候就让bytecode所在的class file知道String内容是什么,并且“潜规则”预订好了将String生成为一个对象的时间和后续动作。
上面两个黑体字符串表示什么特别的意思呢?
就是说,在这个String s1 = "123";所在的方法被调用的时候,Java虚拟机会对编译时候已经知道的“123”进行一些动作以便生成String对象。第一个动作就是resolution,第二个动作是检查interned string reference list中是否有改对象的引用了(即reference),第三个可能的动作是在interned string reference list中没有该String对象的引用的时候对该对象intern()一下,将其加入到list中。
问题是上面几句话是什么意思?
因为我们知道在编译完的时候Java虚拟机就已经知道了我们以后会用到123这个String对象,所以将其暂时放到class file中的constant pool表中。其实说白了,就是说因为我知道以后要用你,现在就暂时以文字的方式将你存储起来,以后以便将你实例化。这就像XML一样,你先可以将一些对象的内容存储在XML文件中,到时候遇到它要用它的时候再将其deserialize读出来,并放到构造器中实例化。Java虚拟机也是这样做的。好这样我们知道通过第一步resolution后,我们会将其实例化。
但实际上,Java虚拟机中为了节省空间,暂时并没有将其实例化,而是检查放在其method area中所维护的一个叫做interned string reference list中是否有该对象存在了。该list中保持的是指向Java虚拟机堆中的String对象的引用。所以你在进行检查的时候,实际上是调用了这个即将创建的String对象的equals和该list中所有引用比较。这样做的好处就是,因为equals会在发现两者字符串相同的时候返回true,所以你能保持同样的String在Java虚拟机堆中永远只有这一个对象存在。
继续刚才的1,2,3。假如Java虚拟机发现了相同的String内容已经存在了,就返回该对象的引用给s1。所以这样s1指向了这个已存在的对象。而假如Java虚拟机发现没有同样的内容存在,那么就调用new String();创建该对象,并放在堆中。
第三步是在list中没有该对象的时候,将刚创建的String对象intern(),即将其加入到这个维护的list中。
new会做什么呢?new使得Java虚拟机在调用一个class file的时候无从得知该对象的存在,即在method具体走到该步的时候,才会在堆中分配该对象。仅此而已。这样做的一个缺点是,假如该堆中已经有了同样内容的String对象,即“123”已经存在,虚拟机还是会为你再次分配个一样的对象。这叫做浪费革命粮食。因此我们可以比较一下,
String s1 = "123";
String s2 = new String("123");
String s3 = new String("123");
s1在method调用到这句话的时候在堆中创建了一个String对象,内容是123,并将这个对象的引用添加到了interned string reference list中。
s2在堆中创建了一个同样为123的对象。s3也是在堆中创建了一个s3的对象。
s1 == s2; // false
s2 == s3; // false
s1.equals(s2); //true
s2.equals(s3); //true
intern()以及优化String。
我们在有了下面的语句后,
String s1 = "123";
String s2 = new String("123");
String s3 = new String("123");
分别用,
s2 = s2.intern();
s3 = s3.intern();
这样就会让Java虚拟机去寻interned string reference list中寻找是否有相同内容的String对象存在。结果是它会发现s1所指向的对象的内容和s2以及s3所指向的对象的内容是一样的。于是乎,s2以及s3会被赋予s1。这样s2和s3原来所指向的堆中的对象就是没有reference的对象了。迟早,他们会被垃圾回收器所回收掉空间。
上面两步完成后你再测试,
s1 == s2; // true
s2 == s3; // true
希望以上的文字对于大家进行Java或者Java虚拟机的学习有帮助。
Subscribe to:
Post Comments (Atom)
No comments:
Post a Comment