字符串常量池
一、什么是字符串常量池?
每当创建字符串对象时,首先会检查字符串常量池中是否存在面值相等的字符串:
- 若有就不再创建,直接返回常量池中对该对象的引用;
- 若没有,则创建,放到常量池中并且返回新建对象的引用;
所以推荐使用直接赋值
(即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]+", ","));
推荐内容
- string/StringBuilder/ToString()底层代码解析( JAVA / C# )
C# /JAVA: 字符串构建利器StringBuilder区别,附有C# /JAVA 底层源码分析。
- ToString底层代码解析(C#/JAVA)
分别浅谈 C# / JAVA 中 stringBuilder.ToString()方法底层原理以及区别
附有C# /JAVA 底层源码分析。
- string/stringBuilder常量池(驻留池) java/C#学习
JAVA / C# 详解之:运行时常量池