String、StringBuffer、StringBuilder
先大致了解一下常量池在jdk版本有什么变化
jdk1.8 中永久代向元空间的转换,方法区的实现改为了元空间,使用本地内存,静态变量(引用和变量对象实体都在堆空间)和StringTable放在对空间中。
为什么jdk1.8要把方法区从JVM里(永久代)移到直接内存(元空间)?
- 字符串存在永久代中,容易出现性能问题和内存溢出
- 类及方法的信息等比较难确定大小,因此对于永久代的大小指定比较困难,在某些场景下,如果动态加载过多的类就会造成
Full gc(甚至OOM)造成STW,阻塞用户线程。而元空间使用本地内存,因此默认情况下,元空间大小受本地内存限制。 - 永久代会为
GC带来不必要的复杂度,并且回收效率偏低

这里大概说一下什么是常量池和运行时常量
常量池:程序通过javac生成字节码文件,该文件记录了当前这个类的所有相关的信息,其中有一部分被称为常量池,常量池存放编译器生成的各种字面量和符号引用
字面量:文本字符串、被final修饰的常量和八种基本类型的值
符号引用:则是类和接口的全限定名、方法的名称和描述符和字段的名称和描述符
运行时常量池
运行时常量是相对于常量来说的,它具备一个重要特征是:动态性。当然,值相同的动态常量与我们通常说的常量只是来源不同,但是都是储存在池内同一块内存区域。
Java语言并不要求常量一定只能在编译期产生,运行期间也可能产生新的常量,这些常量被放在运行时常量池中。这里所说的常量包括:基本类型包装类(包装类不管是浮点型,整型只会管理-128到127(下面会讲到))和String(也可以通过String.intern()方法可以强制将String放入常量池)
补充
jdk1.6及之前有永久代,常量池在方法区;jdk1.7,有永久代,逐步开始抛弃方法区,常量池在堆(这里jdk文档并没有说运行时常量是否在也跟着移到堆);jdk1.8及之后,去除了永久代,常量池在在元空间(也就是说将本地内存用来存储,容量取决系统的是64位还是32位的操作系统的可用虚拟内存大小)

再补充一下上面说到的(包装类不管浮点型,整型只会管理-128到127)
Java中的基本数据类型实现了常量池技术(除了Float和Double这两种浮点型的包装类)其他5种包装类默认创建了数值,也就是上面说到的-128到127的相应类型的缓存数据(注意:如果超过该数值,则仍然会创建新的对象)直接上代码
@Test
public void test(){
//首先是2种浮点型
Double a1=2.0;
Double a2=2.0;
System.out.println(a1 == a2);
Integer b1=20;
Integer b2=20;
System.out.println(b1 == b2);
Integer c1=200;
Integer c2=200;
System.out.println(c1 == c2);
}
/*
输出结果:
false
true
false
*/
//注意:如果Integer去new仍然也会创建对象(这里使用了自动装箱,从而使用常量池中的对象)
然后在了解一下String在jdk版本有什么变化
String在jdk1.8及以前内部定义了final char[] value用于存蓄字符串数据。jdk9时改为byte[]数组存储 (从两个字节改为一个字节)。同时String(不可变性)底层是final关键字修饰的(除了hash这个属性,其他都声明为final),一旦创建,值是不可改变的。而StringBuffer(线程安全)、StringBuilder(线程不安全)两者是可变性的
在
jdk1.8String底层是用char[]数组存储
后来在
jdk9改为byte[]数组存储(字符串功能没有受到影响)(StringBuffer和StringBuilder在jdk9以后也使用了byte数组)
String
说到String,这里简单了解一下什么是字符串常量池(为了避免系统产生大量的String对象,所以引入了字符串常量池)
字符串常量池,即为了避免多次创建字符串对象,而将字符串在
jvm中开辟一块空间,储存不重复的字符串在直接使用双引号
""声明字符串的时候, java都会去字符串常量池找有没有这个相同的字符串,如果有,则将常量池的引用返回给变量,没有的话它会在池中创建一个新的字符串,然后返回新字符串的引用。@Test public void test(){ String str="abc";//String可以直接赋值;这种就是字面量赋值(在字符串常量池中创建了一个字面量为"str"的字符串。 ) str="cdf"; System.out.println(str); String str1="abc"; String str2="abc"; System.out.println(str1==str2);//比较的是str1和str2的地址值(true) } /* 输出结果:cdf true 注意:这里我们就可以看出str再次被赋值时,首先常量池会查找有没有对应的字符串,有则引用,没有创建 */
String str="hello"通过字面量的方式(区别于new)给一个字符串赋值,此时的字符串值声明的字符串在常量池中(方法区)字符串常量池不会存储相同内容(equals)的字符串的(如果字符串内容相同会指向同一块内存区域)(在字符串常量池中当对字符重新赋值时,需要重写指定内存区域赋值,不能使用原来的value进行赋值)
- 使用
new关键字创建,比如String str= new String("imooc");,这里可能创建两个对象. 一个是用双引号括起来的imooc,按照上面的逻辑, 如果常量池没有,创建一个对象. 另一个是必须会创建对象,然后返回的是new关键词创建的对象的地址(简单来说一个是在堆空间中new出来的结构,另一个是对应常量池中的数据("imooc")) 
补充
如果使用new创建字符串,则会强制String类在堆空间中创建一个新的String对象。我们可以使用intern()方法将其放入字符串常量池或从字符串常量池中查找具有相同的值字符串对象并返回其引用,下面会讲到intern()方法
String
String 实现了Serializable接口:表示字符串是支持序列化的;也实现了Comparable接口:表示String可以比较大小
通过上面铺垫我们知道String(不可变性)String对象是不可变的(底层被final修饰),直到这个String对象被销毁。拼接或者替换都会在字符串常量池中新创建一个String对象的
@Test
public void test2(){
String str1="abc";//在字符串常量池中创建了一个字面量为"abc"的字符串
String str2="def";
String str5="abc"+"def";
String str3=str1+"def";//实际上原来的“abc”字符串对象已经丢弃了,现在在堆空间中产生了一个字符 串str1+"def"(也就是"abcdef")。如果多次执行这些改变串内容的操作,会导致大量副本 字符串对象存留在内存中,降低效率。如果这样的操作放到循环中,会极大影响 程序的性能
String str4=str1+str2;
String str6=(str1+str2).intern();
String str7="abc";
String str8="abc";
String str9=new String("abc");
String str10=new String("abcdef");
final String str11="abc";//对于final修饰的变量,它在编译时被解析为常量池的一个本地拷贝存储到自己的常量池中或嵌入到它的字节码流中
System.out.println(str5==str3);//false
System.out.println(str5==str4);//false
System.out.println(str3==str4);//false
System.out.println(str5==str6);//true
System.out.println(str7==str8);//true(比较的是str1和str2的地址值)
System.out.println(str1 == str9);//false
System.out.println(str4==str10);//false
System.out.println(str6==str10);//false
System.out.println(str1==str11);//true
System.out.println(str9 == str11);//false
}
/*
注意:
1.常量与常量的拼接结果在常量池。且常量池中不会存在相同内容的常量。
2.拼接只要其中有一个是变量,结果就在堆中
3.如果拼接的结果调用intern()方法,返回值就在常量池中(intern()用来返回常量池中的某字符串,如果常量池中已经存在该字符串,则直接返回常量池中该对象的引用。否则,在常量池中加入该对象,然后 返回引用)
4.当对现有的字符串连接操作时,也需要重新指定内存区域赋值,不能在原有的value进行赋值
*/
补充
Java语言为“
+”连接符以及对象转换为字符串提供了特殊的支持,字符串对象可以使用“+”连接其他对象。其中字符串连接是通过StringBuilder(或StringBuffer)类及其append方法实现的,对象转换为字符串是通过toString方法实现的,该方法由Object类定义,并可被Java中的所有类继承。Java中使用"+"连接字符串对象时,会创建一个StringBuilder()对象(JVM会隐式创建StringBuilder对象),并调用append()方法将数据拼接,最后调用toString()方法返回拼接好的字符串。由于append()方法的各种重载形式会调用String.valueOf方法
String主要方法
| 构造方法 | 说明 |
|---|---|
String(String original) | 把字符串数据封装成字符串对象 |
String(char[] value) | 把字符数组的数据封装成字符串对象 |
String(char[] value, int index, int count) | 把字符数组中的一部分数据封装成字符串对象 |
| 方法 | 说明 |
|---|---|
int length() | 返回字符串的长度(也就是字符个数) |
char charAt(int index) | 返回某索引处的字符 |
indexOf(String str) | 返回指定字符第一次出现的字符串内的索引。 |
isEmpty() | 判断指定字符串是否为空 |
substring(int start) | 从start开始截取字符串 |
String substring(int start,int end) | 从start开始,到end结束截取字符串。包括start,不包括end |
equalsIgnoreCase(String anotherString) | 比较字符串的内容是否相同,忽略大小写 |
toUpperCase()(toLowerCase()) | 将字符串转换为大写(将字符串转换为小写) |
replace(char oldChar, char newChar) | 将指定字符替换成另一个指定的字符 |
replaceAll(String regex,String replasement) | 用新的内容替换全部旧内容 |
contains(CharSequence s) | 查看字符串中是都含有指定字符 |
startsWith(String prefix,int toffset) | 判断字符串对象是否以指定的字符开头,参数toffset为指定从哪个下标开始 |
endsWith(String str) | 判断字符串对象是否以指定的字符结尾 |
contains(CharSequence s) | 当且仅当此字符串包含指定的char值序列时才返回true |
replace(char oldChar, char newChar) | 使 用指定的字面值替换序列替换此字符串所有匹配字面值目标序列的子字符串。 |
String replaceAll(String regex, String replacement) | 使用给定的 replacement 替换此字符串所有匹配给定的正则表达式的子字符串 |
PS:正则表达式是使用单个字符串来描述、匹配一系列匹配某个句法规则的字符串
还有很多方法就不一一举例了,详情请见API
小结
String底层是final关键字修饰,所以是不可变性的- 常量与常量的拼接结果在常量池。且常量池中不会存在相同内容的常量(常量池中一定不存在两个相同的字符串)
- 拼接只要其中有一个是变量,结果就在堆 (简单来说如果2个字面量连接的话,相等;如果是用变量名去连接字面量的话,是在堆空间上(不相等))
- 如果拼接的结果调用
intern()方法,返回值就在常量池中(intern()用来返回常量池中的某字符串,如果常量池中已经存在该字符串,则直接返回常量池中该对象的引用。否则,在常量池中加入该对象,然后 返回引用) - 当对现有的字符串连接操作时,也需要重新指定内存区域赋值,不能在原有的
value进行赋值
StringBuffer
StringBuffer在jdk1.8及以前内部char数组用于存蓄字符串数据。jdk9时改为byte[]数组存储
StringBuffer
可变的字符序列(线程安全的,效率低)它继承了AbstractStringBuilder本质上是通过一个可变的byte数组进行数据存储。StringBuffer把所有修改的数据的方式都加上了synchronized,保证了线程安全
扩容
StringBuffer类不同于String,其对象必须使用构造器生成。有三个常用构造器:
StringBuffer()
初始容量为16的字符串缓冲区
无参构造方法实例化时,初始化大小的容量为16,在进行
append方法时,会有容量大小的判断,如果append添加的字符长度大于初始化16,则按新算法计算容量大小,默认情况下底层源码扩容为原来的2倍+2(也就是34=(16+2+2)),同时将原有的数组中的元素复制到新的数组中;如果字符长度超过16,并且大于34,则直接使用字符串长度
String str=new String();
//char[] value=new char[0];
//初始容量为16的字符串缓冲区
StringBuffer(String str)
将内容初始化为指定字符串内容
有参构造方法实例化时实例化时,初始容量大小为实例化参数字符串的长度+16
StringBuffer str=new StringBuffer("abc");
//char[] value=new char["abc".length()+16];构造一个初始化为指定字符串内容的字符串缓冲区。字符串缓冲区的初始容量为16加上字符串参数的长度
StringBuffer(int size)
构造指定容量的字符串缓冲区
实例化的时候自定义初始化容量大小
补充
由于
String的不可变性,当字符串的拼接会产生很多无用的中间对象,不仅浪费空间,而且还效率低下。所以后面引入StringBuffer和StringBuilder,两者的对象都可以被多次修改,并不产生新的对象(所以说StringBuffer对象是一个字符序列可变的字符串,它没有重新生成一个对象,而且在原来的对象中可以连接新的字符串)
StringBuffer常用方法(很多方法与String相同)
StringBuffer对象则代表一个字符序列可变的字符串,当一个StringBuffer被创建以后,通过StringBuffer提供的append()、insert()、reverse()、setCharAt()、setLength()等方法可以改变这个字符串对象的字符序列。一旦通过StringBuffer生成了最终想要的字符串,就可以调用它的toString()方法将其转换为一个String对象。
StringBuffer可变的字符序列。实现可修改的字符串。它在任何时间点都包含一些特定的字符序列,但是可以通过某些方法调用来更改序列的长度和内容。除非另有说明,否则将null参数传递给此类中的构造函数或方法将导致引发NullPointerException。(详情请看到文章末尾)
@Test
public void test(){
StringBuffer stringBuffer = new StringBuffer(null);
System.out.println(stringBuffer);
}
//编译会报错java.lang.NullPointerException
| 方法 | 说明 |
|---|---|
append(xxx) | 提供了很多的append()方法,用于进行字符串拼接 |
delete(int start,int end) | 删除指定位置的内容 |
replace(int start, int end, String str) | 把[start,end)位置替换为str |
r insert(int offset, xxx) | 在指定位置插入xxx |
reverse() | 把当前字符序列逆转 |
StringBuilder
StringBuilder类也代表可变字符串对象。实际上,StringBuilder和StringBuffer基本相似,两个类的构造器和方法也基本相同。不同的是:StringBuffer是线程安全的,而StringBuilder则没有实现线程安全功能,所以性能略高。
StringBuilder:可变的字符序列(线程不安全的,效率高)底层使用byte[]数组存储

StringBuilder
相同
扩容方式和
StringBuffer差不多,它们共同之处就是都继成了AbstractStringBuilder这个抽象类,实现了CharSequence接口。其append方法也差不多都是super.append(str),调用了父类AbstractStringBuilder的append(String str)方法
不同
StringBuffer比StringBuilder多了一个toStringCache字段,用来在toString方法中进行缓存,每次append操作之前都先把toStringCache设置为null,若多次连续调用toString方法,可避免每次Arrays.copyOfRange(value, 0, count)操作,节省性能。
StringBuilder方法都和上面差不多这里就不举例了
总结
- 由于
StringBuilder相较于StringBuffer有速度优势,所大多数建议使用StringBuilder类。如果在应用程序要求线程安全的情况下,则必须使用StringBuffer类(高并发场景下,若有用到二者,还是建议优先使用StringBuilder的) - 当对字符串进行修改的时候,需要使用
StringBuffer和StringBuilder类
最后关于StringBuffer中append的时候能不能传入null,输出还是报错看下面这位博主的文章

