欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

String字符串相加的原理

程序员文章站 2022-05-29 09:46:13
因为String是非常常用的类, jvm对其进行了优化, jdk7之前jvm维护了很多的字符串常量在方法去的常量池中, jdk后常量池迁移到了堆中 方法区是一个运行时JVM管理的内存区域,是一个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态常量等。 使用引号来创建字符串 单独(注意 ......

因为string是非常常用的类, jvm对其进行了优化, jdk7之前jvm维护了很多的字符串常量在方法去的常量池中, jdk后常量池迁移到了堆中

方法区是一个运行时jvm管理的内存区域,是一个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态常量等。

使用引号来创建字符串

  • 单独(注意是单独)使用引号来创建字符串的方式,字符串都是常量,在编译期已经确定存储在常量池中了。
  • 用引号创建一个字符串的时候,首先会去常量池中寻找有没有相等的这个常量对象,没有的话就在常量池中创建这个常量对象;有的话就直接返回这个常量对象的引用。

所以看这个例子:

string str1 = "hello";
string str2 = "hello";
system.out.println(str1 == str2);//true

new的方式创建字符串

string a = new string("abc");

new这个关键字,毫无疑问会在堆中分配内存,创建一个string类的对象。因此,a这个在栈中的引用指向的是堆中的这个string对象的。

然后,因为"abc"是个常量,所以会去常量池中找,有没有这个常量存在,没的话分配一个空间,放这个"abc"常量,并将这个常量对象的空间地址给到堆中string对象里面;如果常量池中已经有了这个常量,就直接用那个常量池中的常量对象的引用呗,就只需要创建一个堆中的string对象。

new构造方法中传入字符串常量, 会在堆中创建一个string对象, 但是这个对象不会再去新建字符数组(value) 来存储内容了, 会直接使用字符串常量对象中字符数组(value)应用,

具体方法参考

/**
* initializes a newly created {@code string} object so that it represents
* the same sequence of characters as the argument; in other words, the
* newly created string is a copy of the argument string. unless an
* explicit copy of {@code original} is needed, use of this constructor is
* unnecessary since strings are immutable.
*
* @param  original
*         a {@code string}
*/
public string(string original) {
   this.value = original.value;  //只是把传入对象的value和引用传给新的对象, 两个对象其实是共用同一个数组
   this.hash = original.hash;
}

value 虽然是private修饰的, 但是构造方法中通过original.value;还是可以直接获取另外一个对象的值. 因为这两个对象是相同的类的对象

String字符串相加的原理

所以有下面的结果

public static void main(string[] args) {
      string s1 = new string("hello");
      string s2 = "hello";
      string s3 = new string("hello");
      system.out.println(s1 == s2);// false
      system.out.println(s1.equals(s2));// true
      system.out.println(s1 == s3);//false
  }

关于“+”运算符

常量直接相加:

string s1 = "hello" + "word";
string s2 = "helloword";
system.out,println(s1 == s2);//true

这里的true 是因为编译期直接就把 s1 优化成了 string s1 = "helloword"; 所以后面相等

非常量直接相加

 public static void main(string[] args) {
        string s1 = "a";
        string s2 = "b";
        string s3 = new string("b");
        string s4 = s1 + s3;
        string s5="ab";
        string s6 = s1 + s2;
            string s66= s1 + s2;
        string s7 = "a" + s2;
        string s8 = s1 + "b";
        string s9 = "a" + "b";

        system.out.println(s2 == s3);  //false   
        system.out.println(s4 == s5);   //false   s4 是使用了stringbulider来相加了
        system.out.println(s4 == s6);  //false  s4和s6 两个都是使用了stringbulider来相加了
        system.out.println(s6 == s66);     //false   两个都是使用了stringbulider来相加了
          system.out.println(s5 == s7);    //false   s7是使用了stringbulider来相加了
        system.out.println(s5 == s8);  //false   s8是使用了stringbulider来相加了
        system.out.println(s7 == s8);  //false  两个都是使用了stringbulider来相加了
        system.out.println(s9 == s8);  //false  两个都是使用了stringbulider来相加了
    }

总结下就是:

  两个或者两个以上的字符串常量直接相加,在预编译的时候“+”会被优化,相当于把两个或者两个以上字符串常量自动合成一个字符串常量.

编译期就会优化, 编译的字节码直接就把加号去掉了, 直接定义一个常量

 其他方式的字符串相加都会使用到 stringbuilder的.

string的intern()方法.

这是一个native的方法,书上是这样描述它的作用的:如果字符串常量池中已经包含一个等于此string对象的字符串,则返回代表池中这个字符串的string对象;否则,将此string对象包含的字符添加到常量池中,并返回此string对象的引用。

并提到,在jdk1.6及其之前的版本,由于常量池分配在永久代内,我们可以通过-xx:permsize和-xx:maxpermsize限制方法区的大小从而间接限制常量池的容量。

不仅如此,在intern方法返回的引用上,jdk1.6和jdk1.7也有个地方不一样,来看看书本上给的例子:

public static void main(string[] args) {
    string str1 = new stringbuilder("计算机").append("软件").tostring();
    system.out.println(str1.intern() == str1);

    string str2 = new stringbuilder("ja").append("va").tostring();
    system.out.println(str2.intern() == str2);
}

这段代码在jdk1.6中,会得到两个false,在jdk1.7中运行,会得到一个true和一个false。

书上说,产生差异的原因是:在jdk1.6中,intern()方法会把首次遇到的字符串实例复制到永久代中,返回的也是永久代中这个字符串实例的引用,而由stringbuilder创建的字符串实例在java堆上,所以必然不是同一个引用,将返回false。

而jdk1.7的intern()不会再复制实例,只是在常量池中记录首次出现的实例的引用,因此intern()返回的引用和stringbuilder创建的那个字符串的实例是同一个。对str2比较返回false是因为"java"这个字符串在执行stringbuilder.tostring()之前就已经出现过,字符串常量池中已经有它的引用了,不符合“首次出现”的原则,而“计算机软件”这个字符串则是首次出现的,因此返回true。

jdk6和7 的差异是因为 7中常量池移动到堆中了, 并且对于常量池的处理也有差异, 6会把堆中的字符串复制一份副本到常量池中,

7 只是把堆中的字符串对象的引用放入常量池中, 所以第一个str1.intern()返回的也只是一个指向堆中对象的引用, 所以第一个出现false.

7的第二个false 是因为常量池中已经有了"java"对象了, 所以str2.intern()返回的是指向常量池中对象的引用, str2是指向堆中对象的引用, 所以false

stringtable的小说明

只是为了提高速度, 把常量池中的字符串常量维护了一个hashtable. 方便查找常量

这里先再提一下字符串常量池,实际上,为了提高匹配速度,也就是为了更快地查找某个字符串是否在常量池中,java在设计常量池的时候,还搞了张stringtable,这个有点像我们的hashtable,根据字符串的hashcode定位到对应的桶,然后遍历数组查找该字符串对应的引用。如果找得到字符串,则返回引用,找不到则会把字符串常量放到常量池中,并把引用保存到stringtable了里面。

在jdk7、8中,可以通过-xx:stringtablesize参数stringtable大小

jdk1.6及其之前的intern()方法

在jdk6中,常量池在永久代分配内存,永久代和java堆的内存是物理隔离的,执行intern方法时,如果常量池不存在该字符串,虚拟机会在常量池中复制该字符串,并返回引用;如果已经存在该字符串了,则直接返回这个常量池中的这个常量对象的引用。所以需要谨慎使用intern方法,避免常量池中字符串过多,导致性能变慢,甚至发生permgen内存溢出。

看一个图片来理解下:(图片来自https://blog.csdn.net/soonfly/article/details/70147205)

String字符串相加的原理

当然,这个常量池和堆是物理隔离的。

总之就是,要抓住“复制”这个字眼,常量池中存的是内容为"abc"的常量对象。

看个详细点的例子:

   public static void main(string[] args) {
        string a = new string("haha");
        system.out.println(a.intern() == a);//false
    }

首先,见到"haha",产量池中没有这个常量,所以会在常量池中放下这个常量对象,底层是通过ldc命令,"haha"被添加到字符串常量池,然后在stringtable中添加该常量的引用(引用好像是这个string对象中的char数组的地址),而a这个引用指向的是堆中这个string对象的地址,所以肯定是不同的。(而且一个在堆,一个在方法区中)。

jdk1.7的intern()方法

jdk 1.7后,intern方法还是会先去查询常量池中是否有已经存在,如果存在,则返回常量池中的引用,这一点与之前没有区别,区别在于,如果在常量池找不到对应的字符串,则不会再将字符串拷贝到常量池,而只是在常量池中生成一个对原字符串的引用。简单的说,就是往常量池放的东西变了:原来在常量池中找不到时,复制一个副本放到常量池,1.7后则是将在堆上的地址引用复制到常量池。

当然这个时候,常量池被从方法区中移出来到了堆中。

看个图:

(图片来自https://blog.csdn.net/soonfly/article/details/70147205)

String字符串相加的原理

所以再看回我们书上的那个例子

public static void main(string[] args) {
    string str1 = new stringbuilder("计算机").append("软件").tostring();
    system.out.println(str1.intern() == str1);

    string str2 = new stringbuilder("ja").append("va").tostring();
    system.out.println(str2.intern() == str2);

再看一个例子:

string str2 = new string("str")+new string("01");
str2.intern();
string str1 = "str01";
system.out.println(str2==str1);//true

这个返回true的原因也一样,str2的时候,只有一个堆的string对象,然后调用intern,常量池中没有“str01”这个常量对象,于是常量池中生成了一个对这个堆中string对象的引用。

然后给str1赋值的时候,因为是带引号的,所以去常量池中找,发现有这个常量对象,就返回这个常量对象的引用,也就是str2引用所指向的堆中的string对象的地址。

所以str2和str1指向的是同一个东西,所以为true。

jdk7中虽然是把引用复制到常量池中, 但是不影响常量池的功能, 常量池就是减少常量的创建, 增加性能. 常量池中是引用还是能起到减少常量的作用, 因为引用最终还是会指向真实的对象.

参考博客

https://www.cnblogs.com/wangshen31/p/10404353.html