String 与 StringBuffer 与 StringBuilder 各自的妙用
每博一文案
我从未见过,一个早起,勤奋,谨慎,诚实的人,抱怨命运不好的。
最完美的状态,不是你从不失误,而是你从没放弃成长。没人能把你变的越来越好,时间和经历
只是陪衬,支撑你变的越来越好的,是你坚强的意志,修养,品行,以及不断的反思和修正。
很喜欢的一段话: "人生最好的贵人,就是努力向上的自己。" 生活不会辜负一个一直在努力的人。
愿我们都能在各自坚持的道路上,遇见更好的自己。
—————— 人民日报(RENMIN RIBAO)
文章目录
- String 与 StringBuffer 与 StringBuilder 各自的妙用
- 每博一文案
- 1. String 的不可变性
- 1.1 字符串常量池
- 1.2 String 相关的面试题陷阱
- 1.2.1 题目一:
- 1.2.2 题目二:
- 1.2.3 题目三:
- 1.2.4 题目四:
- 1.2.5 题目五:
- 1.2.6 题目六:
- 1.2.7 题目七:
- 2. StringBuffer 类
- 2.1 StringBuffer 类的常用方法
- 2.1.1 append( )
- 2.1.2 setCharAt( )
- 2.1.3 replace( )
- 2.1.4 delete( )
- 2.1.5 insert ( )
- 2.1.6 reverse()
- 2.1.7 substring( )
- 2.1.8 charAt( )
- 2.1.9 setCharAt( )
- 2.1.10 StringBuffer 常用方法的总结:
- 3. StringBuilder 类
- 3.1 StringBuilder 类的常用方法
- 3.2 String 与 StringBuffer 与 StringBuilder 三者的区别
- 4. 有关String的几道算法题
- 5. 总结:
- 6. 最后:
1. String 的不可变性
在 Java 编程中广泛使用的字符串是一系列字符。在 Java 编程语言中,字符串是对象。
Java 平台提供了创建和操作字符串的 String 类。
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[];
/** Cache the hash code for the string */
private int hash; // Default to 0
- String 类:代表字符串。java 程序中的所有字符串字面值 (如 “abc” ) 都作为此类的实例实现。
- String 是一个 final 类,不可被继承,扩展子类。字符串是常量,用双引号
""
括起来 ,它们的值在创建之后不能更改。字符串缓冲区支持可变的字符串。因为 String 对象是不可变的,所以可以共享。 - String 对象的字符内容是存储在一个字符数组 value[] 中的。
- 这个 String 类是不可变的,所以一旦创建了一个 String 对象就不能改变。 🌟🌟🌟🌟🌟
创建字符串最直接的方法是编写:String str = "Hello World!";
在这种情况下,“Hello World!” 是一个字符串字面值,在你的代码中 用双引号括起来 的一系列字符。只要遇到代码中的字符串文字,编译器就会用它的值创建一个 String 对象。
与任何其他对象一样,您可以使用 new 关键字和构造器函数创建对象。String 类有 13 个构造函数,允许你使用不同的来源,诸如字符数组来提供串的初始值;
String str = "abc";
// 等同于
String data[] = {'a','b','c'};
String str = new String(data);
如下是一些 String 常用的构造器:
String str1 = "abc";
byte[] bytes = {'a', 'b', 'c'};
String str2 = new String(bytes);
// 指定 bytes[] 数组中的“起始位置”,和“长度” 转换为字符串
// String str3 = new String(bytes,定义对应数组的开始位置,以及需要转换为字符串的长度);
String str3 = new String(bytes, 1, 1);
char[] chars = {'a', 'b', 'c'};
String str4 = new String(chars);
// 同样可以指定char[] 数组中的“起始位置”,和“长度” 转换为字符串
// String str5 = new String(chars,定义对应数组的开始位置,以及需要转换为字符串的长度);
String str5 = new String(chars, 0, 3);
String str6 = new String("abc");
其余的:
1.1 字符串常量池
理解String 类是不可变的,所以一旦创建了一个 String 对象就不能改变。
从源码上看。String 类中有一个 private final char value[];
的字符数组用于存储字符串 的内容,其中我们可以看到,该数组是被 final
修饰的,意味着该数组一经赋值操作,该引用一旦指向某个对象的地址值,后就无法修改了。以及数组一旦创建其的长度就无法修改了。这两方面就限定了,字符串 String 一经创建就无法被修改了。
JDK 当中双引号括起来的字符串,例如“abc”,“def” 都是直接存储在方法区的 字符串常量池
当中的。
为什么 SUN 公司 把字符串存储在一个 “字符串常量池” 当中呢 ? ??
因为字符串在实际的开发中的使用太频繁了,所以把字符串放到方法区的字符串常量池当中,注意:字符串常量池中不会存储重复内容的字符串 。例如,字符串"abc" 只会在字符串常量池中,存储一份,所有对象共用。通过共享,减少内存的消耗。
public class StringTest {
public static void main(String[] args) {
String str = "abc";
System.out.println(str);
}
}
上述代码的内存图示:
使用 new 对象的方式创建 String 的内存图
public class StringTest {
public static void main(String[] args) {
String str = new String("abc");
System.out.println(str);
}
}
String str = “abc” 与 String str = new Stiring(“abc”) 创建的字符串,在内存上有什么区别 ???
String str = “abc” 是只创建了一个对象,“abc” 在字符串常量池中,str 的引用直接指向方法区中的字符串常量池中的 “abc” 的地址。
而 String str = new String(“abc”),则是创建了两个对象一个是在堆区中的 new String 对象,该对象的引用指向另一个在方法区字符串常量池中的 "abc"的地址,str 是指向 堆区的 new String 对象的地址,该 new String 的引用再指向字符串常量池中的 "abc"的地址。str 并不是直接指向 方法区中的字符串常量池中的 “abc”的,而是通过 new String 对象间接的指向的。
- 当对字符串重新赋值时,需要重新指定内存区域进行赋值,不能在原有的 values[] 进行赋值操作,因为 values[] 数组已经被 final 修饰了,并且数组一旦创建其长度是无法修改的。
- 当对现有的字符串进行连续的
+ “拼接”
操作时,需要重新指定内存区域进行赋]值,不能使用原有的 values[] 数组进行赋值操作。 - 当调用 String 中的
replace()
的对象方法修改指定字符或字符串时,也时需要重新指定内存区域进行赋值的,同样不能在原有的 values[] 数组中进行赋值操作。 - 基本上String 类中所有对String 增,删,改基本上都是返回一个修改完的 String 字符串类型,而不是在原有的基础上修改的。对于 String 的一些常用方法,大家可以移步至 : 🔜🔜🔜 你必须要知道的,字符串有关的方法类_ChinaRainbowSea的博客-CSDN博客
- 这些都是 String 不可变性的体现 。
- 如下代码:
public class StringTest {
public static void main(String[] args) {
String str = "hello";
str = "world";
System.out.println(str);
String str2 = "abc";
str2 = str2 + "def";
System.out.println(str2);
String str3 = "Google";
String str4 = str3.replace('o', 'e'); // replace 替换
System.out.println(str3);
System.out.println(str4);
}
}
其上述代码的内存图示如下:
1.2 String 相关的面试题陷阱
1.2.1 题目一:
如下Java程序创建了多少个对象
public class StringTest {
public static void main(String[] args) {
String str = new String("hello");
String str2 = new String("hello");
}
}
答:创建了三个对象,分别是 堆区中的两个:new String , new String ,和方法区中的字符串常量池中的 “hello” 字符串。
1.2.2 题目二:
阅读如下代码,观察其运行结果为:
public class StringTest {
public static void main(String[] args) {
String str = new String("hello");
String str2 = new String("hello");
System.out.println("str == str2 : " + (str == str2));
String str3 = "world";
String str4 = "world";
System.out.println("str3 == str4 : " + (str3 == str4));
String str5 = "hello";
System.out.println("str5 == str : " + (str5 == str));
}
}
答:
==
运算符,对于引用类型来说,比较的是地址值,不是字符串的内容。这一点需要注意。- str == str2 是通过 new 对象创建的字符串,其引用不是直接指向方法区中的字符串常量池的,而是通过在堆区中创建一个 new String 对象的引用指向 字符串常量池中的 ”hello" 的,而 str ,str2 是指向堆区中的 new String对象的地址值,其new String 对象的地址值,是不同的,所以是 false
- str3 == str4 是,直接通过字符串字面量值 双引号括起来的字符串直接赋值的 “world” 。其 str3,str4 引用都是指向方法区中的字符串常量池中的 “world" 。字符串常量池中不会存储相同内容的字符串,只会存储一份,所有对象共用,所以 str3 和 str4 的地址值是一样的。返回 true 。
- str5 == str : 根据上述的几点分析:同理可得, str5 是直接指向方法区中的字符串常量池的 ”hello“的,str 是间接的指向方法区中的字符串常量池的 ”hello“的, str5 , str 两者之间直接引用的地址值是不同的,所以返回 false 。
1.2.3 题目三:
观察如下 Java代码,其运行结果为:
class Person {
int age;
String name;
public Person() {
// 无参构造器
}
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
public class StringTest {
public static void main(String[] args) {
Person p1 = new Person("Tom", 20);
Person p2 = new Person("Tom", 19);
System.out.println(p1.getName() == p2.getName());
}
}
答:
p1.getName() == p2.getName() ,虽然这里的 p1,p2 是两个不同的对象,但是其中的 String 成员变量的值是都是 字符串 ”Tom“ ,都是直接指向方法区中的字符串常量池的 “Tom"的,字符串常量池中不会存储相同重复的字符串,对于重复的字符串仅仅只会存储一份,所有对象共用,共享,减少内存的消耗。所以 p1 和 p2 对象中的 String name 成员变量都是 ”Tom” ,都是指向字符串常量池中的同一个 “Tom" 的,其地址值相同,所以返回 true 。
1.2.4 题目四:
观察如下Java程序,其运行结果是:
public class StringTest {
public static void main(String[] args) {
String s1 = "javaEE";
String s2 = "hadoop";
String s3 = "javaEEhadoop";
String s4 = "javaEE" + "hadoop";
String s5 = s1 + "haddoop";
String s6 = "javaEE" + s2;
String s7 = s1 + s2;
// == 运算符 比较的是地址值,对于引用类型来说
System.out.println("s3 == s4: " + (s3 == s4));
System.out.println("s3 == s5: " + (s3 == s5));
System.out.println("s3 == s6: " + (s3 == s6));
System.out.println("s3 == s7: " + (s3 == s7));
System.out.println("s5 == s6: " + (s5 == s6));
System.out.println("s5 == s7: " + (s5 == s7));
System.out.println("s6 == s7: " + (s6 == s7));
String s8 = s7.intern();
// 堆空间的 s7对象调用intern()对象方法,会将字符串常量池中已经存在 s7 字符串内容的值的地址
// 直接赋值给 s8 , 等同于 s8 = "abc";
System.out.println("s8 == s4: " + (s8 == s4));
}
}
答:
作这道题目记住如下:知识点就可以了:
- 常量(字符串字面常量)
+
(拼接) + 常量(字符串字面常量(是被“”
双引号括起来的字符串)) 后的字符串结果是直接指向字符串常量池当中的。没有堆区的一个String 对象间接指向的,且字符串常量池中是不会存储相同内容的常量(字符串)的。- 使用
+
字符串拼接字符串时,只要存在一个变量(不是字符串字面常量(就是双引号“”
括起来的) ),结果就在堆中。- 如果使用
intern()
方法,返回值就在常量池中String s2 = "abc"; String s1 = s2.intern();
s2 调用 intern() 对象方法,会将字符串常量池中已经存储到的 s2 中的 “abc” 直接赋值到 s1 当中等同于 String s1 = “abc” 。
- 特殊的对于被 final 修饰的变量字符串,本身就是字符串常量 等同于 s = “abc” 字面常量。
1.2.5 题目五:
观察如下,Java代码,其运行结果为
public class StringTest {
public static void main(String[] args) {
String str = "good";
char[] arrs = {'a','b','c'};
change(str,arrs);
System.out.println(str);
for (int i = 0; i < arrs.length; i++) {
System.out.println(arrs[i]);
}
}
public static void change(String str,char[] arrs) {
str = "Google";
arrs[0] = 'A';
}
}
答:
注意了对于引用类型来说,方法
change(String str,char[] arrs)
所传的是地址值。其中参数 String str = “Google” 是修改该变量中的字符串,但是注意了,字符串是被 final 修饰的是不可变的,所以main 方法中的 String str = “good" 是不会被修改的。而其中的 char[ ] 字符数组,是可以被修改的,所以其中的 arrs[0] = ‘A’ 就被修改了。
总的来说就是记住一点:字符串的是被 final 修饰的以及其中存储字符串的 values[] 数组(一旦创建)其数组长度是不可改变的,标志着字符串不可变性 。
1.2.6 题目六:
阅读如下Java程序,观察其运行结果
public class StringTest {
public static void main(String[] args) {
final String str = "hello";
final String str1 = new String("hello");
final String str2 = "world";
String str3 = str + str2;
String str4 = str1 + str2;
String str5 = "helloworld";
System.out.println(str4 == str5);
System.out.println(str3 == str5);
}
}
答:
与第 四道是相同的:其原理也是一样的。
对于被 final 修饰的变量字符串,本身就是字符串常量 等同于 s = “abc” 字面常量。
所以 str3 = str + str2, str3 == str5 :其中的 str,str2 都是通过字面常量创建的,并且被 final 修饰,其 str3 = str + str2,就等同于是 str3 = “hello” + “wolrd” ,字符串字面常量的
+
拼接是,直接指向字符串常量池中的字符串的地址的,没有经过 堆区创建 new 对象的。所以str3 与 str5 的地址是一样的都是来自字符串常量池中的 ”helloworld“字符串。所以返回 true。而其中的 str1 是通过 new String 对象的方式,经过堆中创建字符串对象,再引用的指向 字符串常量池,其地址值不是直接指向字符串常量池的,而是在堆区中的new String 对象中。自然与 str5 直接指向字符串常量池中的地址值是不同的,所以返回 false 。
1.2.7 题目七:
观察如下 Java 程序,其运行结果为:
public class StringExam {
public static void main(String[] args) {
String str = null;
StringBuffer stringBuffer = new StringBuffer();
stringBuffer.append(str); // 4, 添加的是 "null" 这个字符串
System.out.println(stringBuffer.length()); // 4
System.out.println(stringBuffer); // "null:
StringBuffer stringBuffer2 = new StringBuffer(str); // 抛出 null 异常 java.lang.NullPointerException
System.out.println(stringBuffer2);
}
}
答: 注意了,虽然 str 定义了是 null ,但是当调用 StringBuffer .append() 添加字符的时候,是会将其参数的内容转换为 “null” 字符串的,所以 append(str) 实际上添加的是 null 字符串。null 四个字符,stringBuffer.length() 返回的便是 4 了。
当通过 new StringBuffer (str) 传个 null 就会报
NullPointerException
异常了。因为是需要在堆区中创建空间的,一个 null 引用,无法指向对应方法区中的字符串常量池中的字符串地址的。
2. StringBuffer 类
public class StringBufferTest {
public static void main(String[] args) {
String str = "";
for (int i = 0; i < 1000; i++) {
str = str + String.valueOf(i);
}
}
}
像上述代码:str + String.valueOf(i) 不断的拼接字符串,实际上原来上面拼接好的 “0”,“01”,“012”…都是虽然被创建了,但是又被丢弃了,因为要的是最后一次 for()循环的结果。因为字符串的不可变性。 就会在内存中创建 “0”,“01”,“012”…一次性的字符串,创建了又丢弃了,导致大量的这样一次性,不使用就丢弃的字符串对象存留在内存中,降低效率。极大的影响了程序的性能。
对于上述这样频繁对字符串拼接的操作的,为了减少内存的消耗,提高程序的性能。Java提供了两个类分别为 StringBuffer 类 和 StringBuilder 类 用于专门的字符串频繁的拼接问题。
这里我们首先认识一下:StringBuffer 类
StringBuffer 对象就像 String 对象,除了它们可以被修改。在内部,这些对象被视为包含一系列字符的变长数组。 在任何时候,序列的长度和内容都可以通过方法调用来改变。
-
java.lang.StringBuffer
代表线程安全的可变字符序列 。一个类似于String
的字符串缓冲区,但不能修改。虽然在任意时间点上它都包含某种特定的字符序列,但通过某些方法调用可以改变该序列的长度和内容。说白了就是:可以对字符串内容进行增删改查,此时不会产生新的对象。 -
其中 StringBuffer 的方法很多 都与 String 相同。
-
可以作为参数传递时,方法内部可以改变值。不像 String 不可被修改。
-
StringBuffer 继承(extend) 了 AbstractStringBuilder 抽象类,其中的源码分析如下:
- StringBuffer类不同于 String,其对象必须使用构造器生成。有四个构造器:
public StringBuffer(); // 构造一个其中不带字符的字符串缓冲区,其初始容量为 16 个字符。
public StringBuffer(int capacity); // 构造一个不带字符,但具有指定初始容量的字符串缓冲区
public StringBuffer(String str); // 构造一个字符串缓冲区,并将其内容初始化为指定的字符串内容。该字符串的初始容量为 16 加上字符串参数的长度.
public StringBuffer(CharSequence seq); //
public class StringBufferTest {
public static void main(String[] args) {
StringBuffer str1 = new StringBuffer(); // 初始容量为16的字符串缓冲区
StringBuffer str2 = new StringBuffer(16); // 构造指定容量的字符串缓冲区
StringBuffer str3 = new StringBuffer("hello"); // 将内容初始化为指定字符串内容
}
}
2.1 StringBuffer 类的常用方法
2.1.1 append( )
append() 对象方法,向SringBuffer 字符串添加器字符串,就像 +
拼接字符串的效果是一样的。
参数将被转换成字符串,就好象使用了 String.valueOf 方法一样。然后,将所得字符串中的字符追加到此序列
StringBuilder append(boolean b)
StringBuilder append(char c)
StringBuilder append(char[] str)
StringBuilder append(char[] str, int offset, int len)
StringBuilder append(double d)
StringBuilder append(float f)
StringBuilder append(int i)
StringBuilder append(long lng)
StringBuilder append(Object obj)
StringBuilder append(String s)
扩容问题:如果要添加的数据底层数组装不下了,那么就需要扩容底层的数组 ???
扩容的原理是,如下阅读源码,得到答案:
其中的StringBuffer 中的 append()方法调用的所继承 AbstractStringBuilder 父类的中的 append()方法
@Override
public synchronized StringBuffer append(String str) {
toStringCache = null;
super.append(str);
return this;
}
如下就是 AbstractStringBuilder 父类中的 append()方法如下。
public AbstractStringBuilder append(String str) {
if (str == null)
return appendNull();
int len = str.length();
ensureCapacityInternal(count + len);
str.getChars(0, len, value, count);
count += len;
return this;
}
先是通过 int len = str.length() 获取到所传 String 类型的长度,作为参数和 count + len ,其中的 count 是 StringBuffer 中字符串的实际长度。
对于使用 StringBuffer 的无参构造器,其默认是 16 大小的字符数组。
public StringBuffer() {
super(16);
}
当数组容量不够存储时,使用Arrays.copyOf()方法将其原本所不够的数组内容拷贝,复制到一个 newCapacity(int minCapacity)
方法中的一个 <<1
在原本容量不足的基础上扩大 2 倍的一个新的数组的存储。再将其扩大容量后的数组的地址,复制给原理的 value,从而达到一个扩容的效果。
如何优化 StringBuffer的性能 ???
答:在创建StringBuffer 的时候尽可能给定一个初始化容量的构造器,最好减少StringBuffer 底层数组的扩容次数,预估计一下,
给一个大一些的初始化容量。
关键点: 给一个合适的初始化容量,减少扩容的次数,可以提高程序的执行效率。
public class StringBufferTest {
public static void main(String[] args) {
StringBuffer str1 = new StringBuffer();
str1.append(1); // 添加 int 类型的值
str1.append(3.14); // 添加 double 类型的值
str1.append("hello"); // 添加 String 类型的值
str1.append(true); // 添加 boolean 类型的值
str1.append('A'); // 添加 char 类型的值
System.out.println(str1);
}
}
2.1.2 setCharAt( )
修改字符串中指定的下标位置的字符内容,这里修改的是原本的字符串的值,并不会返回一个新的字符串。
注意: 下标位置 index 参数 必须 >= 0(起始下标从 0 开始) ,下标位置不可以超过字符串的索引下标,不然报越界异常。
举例:
public class StringBufferTest {
public static void main(String[] args) {
StringBuffer str = new StringBuffer("hello");
str.setCharAt(0,'A');
str.setCharAt(4,'B'); // 是从原本的字符串基础上,修改的。
System.out.println(str);
}
}
2.1.3 replace( )
replace() 替换字符串中指定的范围中的内容。
注意了,其中的参数 repalce(int start ,int end ,String str) 中的起始位置,和结束位置。[起始位置,结束位置) 是左闭右开的,左边的值可以取到,右边的值是无法取到的。
举例:
public class StringBufferTest {
public static void main(String[] args) {
StringBuffer str = new StringBuffer("helloworld");
str.replace(0,3,"999");
System.out.println(str);
}
}
2.1.4 delete( )
delete() 删除指定位置的字符串的内容。
注意: 不要超出了字符串的索引范围,起始位置是从 0 开始的,其中对应参数 delete(int start , int end) [起始下标位置,结束下标位置)是左闭有开的,左边的可以取到,右边的取不到。
举例:
public class StringBufferTest {
public static void main(String[] args) {
StringBuffer str = new StringBuffer("helloworld");
str.delete(0,5);
System.out.println(str);
}
}
2.1.5 insert ( )
insert() 在指定的下标位置插入内容。将第二个参数插入到字符串构建器中。第一个整数参数表示数据要插入之前的索引。数据在插入操作发生之前转换为字符串。
StringBuilder insert(int offset, boolean b)
StringBuilder insert(int offset, char c)
StringBuilder insert(int offset, char[] str)
StringBuilder insert(int index, char[] str, int offset, int len)
StringBuilder insert(int offset, double d)
StringBuilder insert(int offset, float f)
StringBuilder insert(int offset, int i)
StringBuilder insert(int offset, long lng)
StringBuilder insert(int offset, Object obj)
StringBuilder insert(int offset, String s)
举例:
public class StringBufferTest {
public static void main(String[] args) {
StringBuffer str = new StringBuffer("上海伦敦东京");
System.out.println(str);
str.insert(0, 99); // 插入 int
str.insert(str.length(), 3.14); // 插入 false 类型
str.insert(2, "纽约");
str.insert(5, true);
System.out.println(str);
}
}
2.1.6 reverse()
reverse() 把当前字符序列逆转.
举例:
public class StringBufferTest {
public static void main(String[] args) {
StringBuffer stringBuffer = new StringBuffer("helloWorld");
System.out.println(stringBuffer);
stringBuffer.reverse(); // 当前字符串反转
System.out.println(stringBuffer);
}
}
2.1.7 substring( )
substring() 字符串的截断。同样注意是:参数的截取,是左闭右开的。同时不要超过了字符串的索引位置。
举例:
public class StringBufferTest {
public static void main(String[] args) {
StringBuffer stringBuffer = new StringBuffer("helloWorld");
String str = stringBuffer.substring(5, stringBuffer.length());
System.out.println(str);
}
}
2.1.8 charAt( )
charAt() 返回字符串对应索引下标的字符,注意不要超过了字符串的索引范围。导致报异常。
举例:
public class StringBufferTest {
public static void main(String[] args) {
StringBuffer stringBuffer = new StringBuffer("helloWorld");
char c1 = stringBuffer.charAt(0);
System.out.println(c1);
char c2 = stringBuffer.charAt(5);
System.out.println(c2);
}
}
2.1.9 setCharAt( )
setCharAt() : 修改对应字符串索引下标的 字符 。注意不要超过了字符串的索引下标位置。
举例:
public class StringBufferTest {
public static void main(String[] args) {
StringBuffer stringBuffer = new StringBuffer("helloWorld");
stringBuffer.setCharAt(0,'H');
stringBuffer.setCharAt(1,'E');
System.out.println(stringBuffer);
}
}
2.1.10 StringBuffer 常用方法的总结:
* // 总结:
* 增加: append(xxx)
* 删: delete(int start,int end)
* 改: setCharAt(int n,char ch) / replace(int start,int end, String str)
* 查: charAt(int n)
* 插: insert(int offset, xxx)
* 长度: length()
* 遍历: for + charAt()/toString()
- 字符串增加操作的方法: append(xxx)
- 字符串删除操作的方法:delete(int start, int end)
- 字符串修改操作的方法:setCharAt(int n ,char ch) ,replace(int start , int end , String str)
- 字符串查询操作的方法:charAt(int n )
- 字符串插入操作的方法:insert(int offset , xxx)
- 字符串长度查询的方法:length()
- 字符串遍历操作的方法:for() 循环 + charAt() , toString()
3. StringBuilder 类
StringBuilder 和 StringBuffer 非常类似,均代表可变的字符序列,而且 提供相关功能的方法也一样。只是由于其方法是同步的,所以它是线程安全的。
如下:
3.1 StringBuilder 类的常用方法
StringBuilder 类的方法和 StringBuffer 类是一样的,这里就不多说明了,大家可以回看 StringBuffer 类的常用方法。
为什么 StringBuilder / StringBuffer 是可以变的 ???
我们从源码上分析,可以看到 StringBuffer / StringBuilder 内部实际上是一个
char[] value
字符数组存储的这个 value[] 数组并没有被 final 修饰,StringBuffer / StringBuilder 的初始化容量我们记得应该是 16 当
存储满了,会自动进行扩容,好像是 2 倍的扩容,底层调用了数组拷贝的方法 Arrays.copy() 是这样扩容的。
所以StringBuffer / StringBuilder 适合于使用字符串的频繁拼接操作。
3.2 String 与 StringBuffer 与 StringBuilder 三者的区别
String(JDK1.0): 不可变字符序列。
StringBuffer(JDK1.0): 可变序列,效率低,线程安全。
StringBuilder(JDK5.0): 可变序列,效率高,线程不安全。
注意:作为参数传递的话,方法内部 String 不会改变其值,StringBuffer 和 StringBuilder 会改变其值。
因为 StringBuffer 是线程安全的,使用了 synchronized 同步线程安全,会由多线程变成单线程处理,效率降低了,但是线程安全了,而StringBuilder(JDK5.0) 没有被 synchronized 线程是不安全的,不会出现多线程变成单线程处理。所以 StringBuffer 效率上要比 StringBuilder 差一点,但是更安全。
关于线程安全问题,大家可以移步至:Java多线程:多线程同步安全问题的 “三“ 种处理方式 ||多线程 ”死锁“ 的避免 || 单例模式”懒汉式“的线程同步安全问题_ChinaRainbowSea的博客-CSDN博客
举例: String / StringBuffer / StringBuilder 三者的执行效率的比较
public class StringBufferTest {
public static void main(String[] args) {
// 设置初始比较时间
long startTime = 0L; // 默认 整数是 int 类型
long endTime = 0L;
String stringTest = "";
StringBuffer stringBufferTest = new StringBuffer("");
StringBuilder stringBuilderTest = new StringBuilder("");
// String 执行
startTime = System.currentTimeMillis(); // 获取到当前的毫秒值(时间戳)
for (int i = 0; i < 200000; i++) {
stringTest += i;
}
endTime = System.currentTimeMillis();
System.out.println("String: " + (endTime - startTime));
// StringBuffer 执行
startTime = System.currentTimeMillis();
for (int i = 0; i < 200000; i++) {
stringBufferTest.append(String.valueOf(i)); //将int 类型转换为 String
}
endTime = System.currentTimeMillis();
System.out.println("StringBuffer: " + (endTime - startTime));
// StringBuilder
startTime = System.currentTimeMillis(); // 获取到当前系统的毫秒数(时间戳)
for (int i = 0; i < 200000; i++) {
stringBuilderTest.append(String.valueOf(i));
}
endTime = System.currentTimeMillis();
System.out.println("StringBuilder: " + (endTime - startTime));
}
}
从结果上看:可以比较出处理效率上 : StringBuilder > StringBuffer > String .
4. 有关String的几道算法题
由于涉及到篇幅的问题,感兴趣的可以移步至:🔜🔜🔜 String 有趣简单的编程题_ChinaRainbowSea的博客-CSDN博客
5. 总结:
-
String 是不可变的,无法被修改的,其中存储字符串的是一个被 final 修饰的 char[] values的字符数组,并且数组一旦创建其长度是无法被修改的。
-
String 存储的方法区的常量池的存在。
-
String 作为参数传递的话,方法内部 String 不会改变其值,StringBuffer 和 StringBuilder 会改变其值。
-
常量(字符串字面常量)
+
(拼接) + 常量(字符串字面常量(是被“”
双引号括起来的字符串)) 后的字符串结果是直接指向字符串常量池当中的。没有堆区的一个String 对象间接指向的,且字符串常量池中是不会存储相同内容的常量(字符串)的。 -
使用
+
字符串拼接字符串时,只要存在一个变量(不是字符串字面常量(就是双引号“”
括起来的) ),结果就在堆中 -
对于String 字符串的频繁拼接操作,建议使用 StringBuilder / StringBuffer 的类
-
StringBuilder / StringBuffer 是可变的字符序列,其中存储的字符串数组 并没有被 final 修饰。
-
StringBuffer(JDK1.0): 可变序列,效率低,线程安全。StringBuilder(JDK5.0): 可变序列,效率高,线程不安全。
-
StringBuffer 扩容性问题:默认存储容量是 16 ,不足时,再原来的基础上 扩容 2 倍。
-
在创建StringBuffer 的时候尽可能给定一个初始化容量的构造器,最好减少StringBuffer 底层数组的扩容次数,提高程序的效率。预估计一下,给一个大一些的初始化容量。
6. 最后:
限于自身水平,其中存在的错误,希望大家给予指教,韩信点兵——多多益善,谢谢大家,江湖再见,后会有期 !!!