String的比较,编译优化以及intern引申

  java string比较时必须使用equals,这是一个定论,但是要了解这个规则确实需要不少知识。不少书上告诉你==只能比较地址,地址不一样,那就是false,string存在着相同字面量不同地址的问题。知道的多一些的,还知道有常量池,但是什么样的string在常量池呢,本文就详细的说一下各种情况。

string常量池

  大家都知道string能通过==返回true是因为常量池,他们的地址是相同的,在两种情况下我们会用到,第一种是编译阶段,java在编译成字节码的同时,会把常量标志出来,在编译阶段确定的字符串会进入常量池;第二种情况就是string的intern方法,这个方法会把调用的字符串放入常量池(jdk1.7以后中会直接获取堆中string对象的引用,文章后面会具体说),并且返回常量池的地址,如果常量池有相同的字符串,则返回常量池的地址,不再创建。

常见场景

String a = "xxx";
String b = "xxx";

这种情况a==b为true,因为在编译时已经确定了xxx的字符串

String a ="xxx1";
final int  value =1;
String b="xxx"+value;

这种情况下a==b为true,在编译时final的值已经确定,编译器进行了宏替换。

String a="xxx1";
int value =1;
String b="xxx"+value;

这种情况下a==b为flase,在编译是value作为一个变量出现,无法确定value的值,所以b对象的内容不会进入常量池。

String a ="xxx1";
final int value=getNum();// getNum()是根据各种变量计算返回1
String b="xxx"+value;

这种情况下a==b为false,此处的final 修饰的value只是一个不可变的值,在编译时期根本不知道数据是多少,此时编译不会出现宏替换的情况,b对象的内容也不会进入常量池。

String a= "xxx";
String b= new String("xxx");

这种情况下a==b为false,因为b是堆中新建的地址,a的地址在常量池,地址不同。

String a = "xxx";
String b=new String("xxx").intern();

这种情况下a==b为true,a对象的地址在常量池,新建的string虽然在堆,但是执行了intern方法,在运行时,获取到了常量池xxx的地址,并且给b,此时a和b的地址是相同的。

加号的解析

String x ="aa"+"bb";

上面情况常量池有几个String对象,猜测很多,这里就不列举了,最终只有一个对象进入常量池,就是aabb,至于aa,bb在编译时已经做了优化,并不会放入常量池中。

int value =10;
String x ="xxx"+value;

x引用的对象在哪里?堆还是栈(上面已经验证过常量池中没有),实际是在堆中。


javap.png

下面就是用Javap查看的执行过程(sunjdk),String x="xxx"+value;的过程被解析成为Stringbuilder.append(“xxx”),Stringbuilder.append(value),Stringbuilder.toString(),三个步骤。

Stringbuilder.toString()就是新建了一个String对象。

jdk变动

随着jdk的变动,有一些代码执行的结果也就不一样了。1.7开始移动常量池进入堆中了。

              String str = new String(new char[]{'a','b'});
              str.intern();
              String str1 ="ab";

如果在你熟悉API和string常量池的解析后,认为是str==str1为flase的请往下看。
在1.7之前这个代码str和str1是不相等的,但是从1.7开始,这个str和str1地址相等了。在1.7之前,字符串常量池是在方法区中,new出来的对象在堆,调用Intern方法肯定是在字符串常量池中新建了一个对象,但是jvm为了避免字符串常量池那边oom,1.7开始把字符串常量池移入到了堆内存,在常量池没有ab字符串的时候,str.intern()方法会让常量池拿到堆里对象的引用,并没有新建一个对象,所以str1指向的对象就和str指向的对象相同了。这个可以直接在调试的时候查看对象ID来检测,或者使用hsdb来查看内存来验证。下面推荐一篇解析了intern,native代码的博客,帮助理解。http://brokendreams.iteye.com/blog/2260870

常见考题

java为什么不是第一次出现
String  sb = new StringBuilder("ja").append("va").toString();

上面java是我们第一次写代码出现,但是常量池中早已有这个关键字,所以Intern返回的地址是早已进入常量池中的地址。

产生对象个数问题
String  sb = new StringBuilder("hello").toString();

此时产生了2个hello对象,一个在常量池,一个在内存。具体请查看 StringBuilder的构造过程。已经返回对象的过程,下面是stringbuilder构造时调用的一个方法

  public void getChars(int srcBegin, int srcEnd, char dst[], int dstBegin) {
        if (srcBegin < 0) {
            throw new StringIndexOutOfBoundsException(srcBegin);
        }
        if (srcEnd > value.length) {
            throw new StringIndexOutOfBoundsException(srcEnd);
        }
        if (srcBegin > srcEnd) {
            throw new StringIndexOutOfBoundsException(srcEnd - srcBegin);
        }
        System.arraycopy(value, srcBegin, dst, dstBegin, srcEnd - srcBegin);
    }

代码的最后做了一次char[]数据拷贝。把传入的string转化成char[]保持成成员变量。然后看tostring的过程,就是新建了一个string对象。

    public String toString() {
        // Create a copy, don't share the array
        return new String(value, 0, count);
    }
这里说的两个Hello对象,一个是作为StringBuilder的参数传入的(常量池),一个是作为返回值新建的(堆中)。
上一篇:java core dump分析实战


下一篇:jvm程序执行慢诊断手册