#Java学了这么久,项目也做了?基础知识还不巩固?快来关注我的这篇系列博客——Java基础复习巩固吧;#
引言:
在Java中 String、StringBuffer 和 StringBuild 都是用于操作字符串的三种不同的类,在选择使用 String
、StringBuffer
还是 StringBuilder
时,需要根据具体的应用场景和线程安全性要求来决定,以达到最佳的性能和功能平衡。那么他们三者之间又有什么区别?使用过程中又该如何选择呢?
一、String类
- String 类代表字符串常量。Java 程序中的所有字符串字面值(如
"abc"
)都作为此类的实例实现。 - String类实现的接口:Serializable(序列化接口)、Comparable<String>(比较大小的接口);表示String类的实例可以被序列化或者互相比大小。
-
字符串是常量;它们的值(对象)在创建之后,内容不能被更改,但是可以共享。例如:
String str1 = "abc";
String str2 = "abc";
//str1和str2这两个引用,指向同一个String对象:"abc";
//也可说str1和str2这两个引用共享同一个String实例:"abc";
且 String str = "abc" ;这种创建方式等效于:
String str = new String("abc");
优点:线程安全,由于其不可变性,String
对象可以被安全地共享和缓存。适合在多线程环境中使用,不用担心数据被意外修改。
什么是线程安全?(可参考以下博客):什么是线程安全?如何保证线程安全?-CSDN博客
缺点:每次对 String
对象进行修改操作(如拼接)时,实际上都会创建一个新的 String
对象,这会导致性能开销较大。
例如,如果在一个循环中频繁地拼接 String
:
String str = "";
for (int i = 0; i < 1000; i++) {
str += i;
}
在上述代码中,每次循环都会创建一个新的 String
对象,会创建1000次 String
对象,效率低下。
如何理解String类的不可变性:
- 当字符串重新赋值时,不会修改原来内存地址中的字符串值,而是重新分配新的内存地址进行赋值。
- 当字符串进行拼接时,也不会对原来内存地址中的字符串进行修改,而是重新分配新的内存地址进行赋值。
- 当调用String类中replace()方法修改字符串中指定的字符或子字符串时,也不会在原本的字符串上进行修改,也是重新分配新的内存地址进行赋值。
String类中的常用方法:
int length():返回字符串的长度
char charAt(int index):返回指定索引处的字符
boolean isEmpty():判断字符串是否为空
String toLowerCase():将字符串中的所有字符转换为小写
String toUpperCase():将字符串中的所有字符转换为大写
String trim():返回字符串的副本,去掉前导空白和尾部空白,中间的空白不会被去掉
boolean equals(Object obj):比较字符串的内容是否相同
boolean equalsIgnoreCase(String anotherString):忽略大小写,比较字符串的内容是否相同
String concat(String str):将指定字符串连接到此字符串的结尾,等价于用“+”
int compareTo(String anotherString):比较两个字符串的大小
String substring(int beginIndex):返回从beginIndex到末尾的子字符串
String substring(int beginIndex, int endIndex):返回从beginIndex到endIndex前一位的子字符串,不包括endIndex
boolean endsWith(String suffix): 判断字符串是否以指定的后缀结束
boolean startsWith(String prefix):判断字符串是否以指定的前缀开始
boolean startsWith(String prefix, int toffset):判断字符串在指定索引开始的子字符串是否以指定前缀开始
boolean contains(CharSequence s):判断当前字符串中是否包含指定的字符串
int indexOf(String str):返回指定子字符串在当前字符串中第一次出现处的索引
int indexOf(String str, int fromIndex):返回从指定的索引后,指定子字符串在当前字符串中第一次出现处的索引
int lastIndexOf(String str):返回指定子字符串在当前字符串中最后一次出现处的索引
int lastIndexOf(String str, int fromIndex):返回从指定的索引后,指定子字符串在当前字符串中最后一次出现处的索引
注:indexOf和lastIndexOf方法如果未查找到指定子字符串时,返回值都为-1。
String replace(char oldChar, char newChar):替换当前字符串中指定的子字符串
String[] split(String regex):根据指定的符号拆分当前字符串,然后返回一个String数组
String类的常考面试题:问以下代码输出结果是true还是false?
第一个:
public static void main(String[] args) {
final String s1="Hello";
String s2="HelloWorld";
String s3=s1+"World";
System.out.println(s2==s3);
}
第二个:
public static void main(String[] args) {
String s1="Hello";
String s2="HelloWorld";
String s3=s1+"World";
System.out.println(s2==s3);
}
第一个为true,第二个为false;原因如下:
由于 s1
被声明为 final 是常量,而字符串"world"也是常量,由于Java存在常量优化机制,因此在编译时,等号右边s1 + "World"
不会被当成字符串拼接操作,而是直接被看作一个常量字符串"HelloWorld"
。并且jvm会判断字符串常量池中是否已存在字符串"HelloWorld"
,若存在,便不会在创建新的对象,而是让s3直接指向该"HelloWorld"
,而 s2
也指向字符串常量池中的 "HelloWorld"
,所以 s2
和 s3
指向的是同一个字符串对象,s2 == s3
结果为 true
。
对于第二个代码片段;这里的 s1
不是 final
,所以 s1 + "World"
在运行时会被当作字符串拼接操作,会直接创建一个新的字符串对象,用来接收拼接后的字符串"HelloWorld"
,s3
指向这个新创建的对象,而 s2
指向的是字符串常量池中的 "HelloWorld"
,它们不是同一个对象,所以 s2 == s3
结果为 false
。
下面再出两个类似的,看大家是否掌握了呢
public static void main(String[] args) {
String s1="abcd";
String s2="ab"+"cd";
System.out.println(s1==s2);
}
结果为:true
解析:Java常量优化机制,同上题的第二个;"ab"、"cd"都是常量;
public static void main(String[] args) {
String s1="abc";
String s2=new String("abc");
System.out.println(s1==s2);
}
结果为:false
解析:因为只要用到new来创建String字符串,一定是在堆区开辟了新的地址空间(字符串拼接操作背后就是new了一个新的字符串)
String与char[]之间相互装换
1、char[]转String,利用String类的有参构造方法:String(char[] data)
传入参数为字符数组,返回一个String类型的字符串:
char[] data={'a','b','c'};
String str = new String(data);
System.out.println(str); //"abc"
2、String转char[] ,调用String类的toCharArray()方法
String str="Hello";
char[] data = str.toCharArray();
for (int i = 0; i < data.length; i++) {
System.out.println(data[i]);
}
/*
H
e
l
l
o
*/
资料来源:深入理解String、StringBuffer和StringBuilder_stringbiluder-CSDN博客
二、StringBuffer类
StringBuffer
是一个可变的字符序列,线程安全。这意味着多个线程可以同时访问和操作同一个 StringBuffer
对象,而不会导致数据不一致或错误。
它提供了一些方法,如 append
、insert
、delete
等,用于直接修改字符串的内容,而无需再像String一样创建一个新的对象。
优点:
- 适用于多线程环境下对字符串进行修改操作。
- 提供了一系列方法,如
append
、insert
等,用于高效地修改字符串内容。 - 例如:
StringBuffer str = new StringBuffer(); for (int i = 0; i < 1000; i++) { str.append(i); }
这种方式在性能上要优于使用
String
的拼接操作。
StringBuffer常用方法:
append("xxx"):在当前字符串后面拼接上新字符串:"xxx"
delete(int start,int end):删除字符串中指定范围的内容,左开右闭
replace(int start, int end, String str):替换指定范围的内容
insert(int n, "xxx"):在下标为n的位置插入指定的内容:xxx
reverse() :把当前字符序列逆转
三、StringBuilder类
StringBuilder
的功能和 StringBuffer
类似,也是用于操作可变的字符序列。但不同的是,StringBuilder
不是线程安全的。
优点:
- 在单线程环境下,性能通常比
StringBuffer
更好。 -
例如:
StringBuilder str = new StringBuilder(); for (int i = 0; i < 10000; i++) { str.append(i); }
这种单线程的字符串拼接在性能上要比StringBuffer更好一些。
在大多数情况下,如果是在单线程中操作字符串,优先选择 StringBuilder
。如果是多线程环境,需要保证线程安全,则使用 StringBuffer
。
四、补充
1. StringBuffer和StringBuilder背后的具体实现
StringBuffer、StringBuilder和String类似,底层也是用一个数组来存储字符串的值,并且数组的默认长度为16,即一个空的StringBuffer对象数组长度为16。当实例化一个StringBuffer对象后即创建了一个大小为16个字符的字符串缓冲区。
但是当我们调用有参构造函数创建一个StringBuffer对象时,数组长度就不再是默认16了,而是长度为“当前所创建的字符串的长度+16”。所以一个 StringBuffer 或 StringBuilder 字符串创建完成之后,有16个字符的空间可以用于对其值进行修改。如果修改的值范围超出了16个字符,则Jvm会先检查StringBuffer对象的原char数组的容量能不能装下新的字符串,如果装不下则会对 char 数组进行扩容。
扩容的逻辑就是创建一个新的 char 数组,将现有容量扩大一倍再加上2,如果还是不够大则直接等于需要的容量大小。扩容完成之后,将原数组的内容复制到新数组,最后将指针指向新的 char 数组。
2. String、StringBuffer和StringBuilder的异同
相同点:都用于处理字符串相关的操作,且底层都是通过char数组实现的
不同点:
-
可变性:
String
是不可变的,一旦创建,其内容不能被修改;如果要修改,则会重新开辟内存空间创建一个新的对象来存储修改之后的字符串;StringBuffer
和StringBuilder
是可变的,可以通过相关方法直接修改其内部的字符序列。
-
线程安全性:
StringBuffer
是线程安全的,适合在多线程环境中使用。StringBuilder
是非线程安全的,在单线程环境中性能更好。
-
性能:
- 在字符串操作方面,StringBuffer和StringBuilder的性能都要优于String
- 在单线程环境下,对字符串进行频繁修改操作时,
StringBuilder
的性能通常优于StringBuffer
。
然而,如果在一个多线程环境中,多个线程同时对一个字符串进行修改,拿就应该使用 StringBuffer
3. String、StringBuffer和StringBuilder的性能对比
(1)String
public static void main(String[] args) {
Date date01 = new Date();
long t1 = date01.getTime();
String str ="" ;
for (int i = 0; i < 100000; i++) {
str += i;
}
Date date02 = new Date();
long t2 = date02.getTime();
System.out.println("总耗时为:"+(t2-t1)+"毫秒");
}
输出 总耗时为:15302毫秒
仅10万次的拼接操作,String类型的字符串就耗时15302毫秒(15秒左右)
(2)StringBuffer
public static void main(String[] args) {
Date date01 = new Date();
long t1 = date01.getTime();
StringBuffer str = new StringBuffer();
for (int i = 0; i < 10000000; i++) {
str.append(i);
}
Date date02 = new Date();
long t2 = date02.getTime();
System.out.println("总耗时为:"+(t2-t1)+"毫秒");
}
输出 总耗时为:336毫秒
1000万次的字符串拼接,StringBuffer类型的字符串耗时为336毫秒左右
(3)StringBuilder
public static void main(String[] args) {
Date date01 = new Date();
long t1 = date01.getTime();
StringBuilder str = new StringBuilder();
for (int i = 0; i < 10000000; i++) {
str.append(i);
}
Date date02 = new Date();
long t2 = date02.getTime();
System.out.println("总耗时为:"+(t2-t1)+"毫秒");
}
输出 总耗时为:263毫秒
1000万次的字符串拼接,StringBuilder类型的字符串耗时为263毫秒左右