JAVA / C# 详解之:运行时常量池 (string/stringBuilder)

一、什么是字符串常量池?

每当创建字符串对象时,首先会检查字符串常量池中是否存在面值相等的字符串:

  • 若有就不再创建,直接返回常量池中对该对象的引用;
  • 若没有,则创建,放到常量池中并且返回新建对象的引用;

所以推荐使用直接赋值(即String s=”aa”),除非有必要新建对象除外。

1.1 目的

为了提升性能减少内存开销,避免字符串重复创建。减少内存空间占用。

1.2 分类

无参构造与直接创建(直接赋值方式创建):

以“ ”方式给出的字符串,只要字符序列相同(顺序和大小写),无论在程序代码中出现几次,JVM 都只会建立一个String 对象,并在字符串池中维护。如图:

在执行a=“abc” 时,先在字符串池中查找是否存在字符串"abc",
若没有则将其存储进字符串池,同时使a的引用指向"abc",
再次执行b=“abc"时,重复前面操作,存在直接将b的引用指向"abc”,不需要再创造新值。(如图中,堆中少了一次创建“Hello”)

//例子一:无参构造
> string str1 = "Hello";
> string str2 = "Hello";

//ReferenceEquals用来比较引用地址是否是一个
> object.ReferenceEquals(str1,str2)
true

在这里插入图片描述

有参构造

通过 new 创建的字符串对象,每一次 new 都会申请一个内存空间,虽然内容相同,但是地址值不同

在调用其构造函数时便已经对引用赋值,使其跳过查找的过程,同时分配a,b两个不同指向空间,并存入字符串。

//例子一:有参构造
> string str1 = new string( "Hello".ToCharArray());
> string str2 = new string("Hello".ToCharArray());

//比较引用地址是否是一个
> object.ReferenceEquals(str1,str2)
false

区别

这里自然能知道两者区别:

一个省内存,但是有遍历过程。
一个省时间,但内存开销较前者大。

不过相比之下,好像推荐前者哦。


二、String str=”aa”与new String()区别

String类:内容是不可变
StringBuilder类:内容是可变

2.1 String str=“aa”

aa可能创建一个或者不创建对象

若“aa”在常量池中不存在,会在常量池中新建String对象(“aa”),然后str直接指向这个内存地址;后期无论用这中方式创建多少个值为(“aa”)的,始终只有一个内存地址被分配。
举例:

String s1 = "aa";
String s2 = "aa";
String s3 = "aa";

System.out.println(s1==s2);   结果:true
System.out.println(s1==s3);   结果:true

结果说明:JVM创建了三个引用s1、s2、s3,但在String Pool(字符串常量池中)中只创建了一个对象(“aa”),而且两个引用都指向了该对象。

对象 s1会在常量池中新建对象(“aa”);s2、s3 不再新建对象(“aa”),都同s1一样,指向一个 引用地址

2.2 String str = new String(“hello”)

至少会创建一个对象,也有可能创建两个

此方法用到了new关键字,肯定会在堆中创建一个String对象,然后str直接指向堆中new string( )这个对象的内存地址。

每一次 new 都会申请一个内存空间,虽然内容相同,但是地址值不同

举例

String s1 = new String("hello");
String s2 = new String("hello");

System.out.println(s1 == s2);  
>结果:false

三、String.intern()

当调用intern()方法时,首先会去字符串常量池中查找该字符串是否存在?

  • 若常量池已包含一个等于此String对象的字符串(用equals(Object)方法确定),则返回池中字符串;
  • 若不存在,将此String对象添加到常量池中,并返回此String对象的引用。这样就避免了重复创建字符串对象。
    String s1 = "abcd";
    String s2 = new String("abcd");
    String s3 = s2.intern();
    System.out.println(s1 == s3); 	结果:true
    System.out.println(s2 == s3);		结果:false

为什么要引入intern()这个函数呢?

就是因为,虽然"abcd"是被分配在constant pool里的,但是,一旦使用new String(“abcd”)就会在heap中新创建一个值为abcd的对象出来

试想,如果有100个这样的语句,岂不是就要在heap里创建100个同样值的对象?!这就造成了运行的低效和空间的浪费。
当程序中存在某个方法,可以根据不同的上下文环境创建并返回一个很长的字符串,而在程序运行的过程中它有会经常返回同样的字符串时,这时候可以 考虑使用Intern方法来提高内存的利用率。

但是,他有副作用:不存在其他引用的情况下,这个字符串仍然不会被垃圾回收
也就是说,即使常量池中的字符串已经没有用处了,他可能也要等着CLR终结时才能被销毁。


四、驻留池特殊机制——动态(运行时常量池)

前面我们讲到了直接赋值(字面值)的对象会放进常量池(也就是编译阶段的文本字符常量才会被自动添加到驻留池)。
那么问题来了,哪些不是编译阶段的字符常量?哪种情况不会被自动放入常量池?
我写了个例子,如图所示:
在这里插入图片描述

string dateTime1 = DateTime.Now.ToString(); 
string dateTime2 = DateTime.Now.ToString(); 
WriteLine("dateTime1:{0} \n dateTime2:{1}", dateTime1,dateTime2)
//输出时间
> dateTime1:2022/6/2 9:24:46 
> dateTime2:2022/6/2 9:24:46

//使用ReferenceEquals去对比两个变量的引用地址是否是一个
object.ReferenceEquals(dateTime1,dateTime2)
> false

以上可以看出来,动态创建的相比较 结果为false,这是为什么呢?接着往下走。
由于时间是程序运行时动态获取的,所以不会放在常量池(因为没必要,动态放常量池大多数都是不一样的,复用率低下,浪费资源)

所以,动态创建两个值相等的变量,他们的引用地址依然不同

也就是说,运行时期动态创建的字符串不会被加入到常量池中,因此即时两个动态字符串和常量池中的某个字符串的值相等引用不会相等

要想打破这种动态创建,其引用地址(指针)不相等的情况,可以使用静态方法String.Intern( );把动态创建的字符加入到常量池中。


小结

(1)单独使用”“引号创建的字符串都是常量,编译期就已经确定存储到String Pool(常量池)中;

(2)使用new String(” ")创建的对象会存储到heap中,是运行期新创建的;

new创建字符串时首先查看池中是否有相同值的字符串,如果有,则拷贝一份到堆中,然后返回堆中的地址;如果池中没有,则在堆中创建一份,然后返回堆中的地址(注意,此时不需要从堆中复制到池中,否则,将使得堆中的字符串永远是池中的子集,导致浪费池的空间)!

(3)形如"s"、“hello"等使用+拼接是在编译期间进行的,拼接后的字符串存放在字符串常量池中;

(4)而字符串引用的”+“拼接运算是在运行时进行的,新创建的字符串存放在堆中。

对于直接相加字符串,效率很高,因为在编译器便确定了它的值,也就是说形如"fri”+“end”; 的字符串相加,在编译期间便被优化成了"friend"。对于间接相加(即包含字符串引用),形如s1+s2+s3; 效率要比直接相加低,因为在编译器不会对引用变量进行优化。


扩展:判断字符串是否为空

String还提供了isEmpty()和isBlank()来判断字符串是否为空和空白字符串:

"".isEmpty(); // true,因为字符串长度为0
"  ".isEmpty(); // false,因为字符串长度不为0
"  \n".isBlank(); // true,因为只包含空白字符
" Hello ".isBlank(); // false,因为包含非空白字符

扩展:替换子串

要在字符串中替换子串,有两种方法。一种是根据字符或字符串替换:

String s = "hello";
System.out.println(s.replace('l', 'w'));// 结果:hewwo
System.out.println(s.replace("ll", "~~"));// 结果:he~~o

s.replace(‘l’, ‘w’) : 所有字符’l’被替换为’w’
s.replace(“ll”, “~ ~ “):所有子串"ll"被替换为”~ ~”

另一种是通过正则表达式替换:
replaceAll()

String s = "A,,B;C ,D";
 // 结果:"A,B,C,D"
System.out.println( s.replaceAll("[\\,\\;\\s]+", ","));



推荐内容








评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

元气小羊.

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值