文章目录
1.String的不变性
String
是不可变对象,其内容无法被修改。
我们先看看它的源码:
可以看到String
是用数组来存储字符串的,怎么验证呢?进行调试就可以看到。(jdk8以及之前的版本value数组
类型是char
,jdk9开始为byte
类型)
但这不是重点,重点是这个数组是被fianl
和private
修饰。有的人可能会认为fianl
是字符串不能被修改的真正原因,这是错误的观点。这里final
的作用是限制value
的值(value
变量装的是地址),就是说value
不能引用其它的数组。
被final
修饰的数组,其数组内容是可以修改的。
public class Main {
public static void main(String[] args) {
final int[] value = {1,2,3,4};
arr[0] = 100;
System.out.println(Arrays.toString(value));
}
}
结果:
String字符串不能被修改的主要原因是被private修饰:String
的源码中没有提供修改value数组
的方法,String
中需要修改字符串的方法也只是返回新的字符串。然而value数组
被private
修饰,这就断绝了从外部修改value
数组的途径,所以就无法修改字符串。
官方为什么要把String
设计成不可变的?(不全面,了解即可)
- 不可变 天生线程安全
- String常被用作HashMap的key,如果可变会有安全问题,如两个key相同。
- 方便实现字符串常量池。
2.字符串常量池
2.1 什么是字符串常量池?
我们先看看下面的代码:
public class Main {
public static void main(String[] args) {
String str1 = "Hello";
String str2 = "Hello";
String str3 = new String("Hello");
String str4 = new String("Hello");
System.out.println(str1 == str2);
System.out.println(str3 == str4);
}
}
结果:
从上可以得出,str1
与str2
引用的是同一个实例,str3
与str4
引用的是不同的实例。我们再进一步思考,上面的不同之处就是前者用字符串常量
来创建字符串,后者是通过new
关键字来创建字符串。也就是说,通过字符串常量创建字符串的方式来创建多个相同的字符串时,变量会引用同一个实例。
为什么呢?这是因为“Hello”
这一常量被装入字符串常量池
中,后面想要再创建一个相同的字符串时,不用再重新创建而是直接引用(前提是用字符串常量
来创建字符串),这就省掉了创建对象的时间与空间,使程序的运行速度更快、更节省内存。
字符串常量池
就像一个“池子”一样,用来装程序中的字符串常量
,在我们有需求的时候直接使用。
2.2 字符串常量池是怎么工作的?
在 jdk8 以及之后的版本中,字符串常量池是在堆中。字符串常量池的底层是一个哈希表,叫StringTable
。(哈希表是查询效率很高的数据结构)
可以把这个哈希表看成数组,每个元素是指向一个链表,每个节点连接一个String
对象,这些String
对象就代表了常量串。(每个节点还有其它的信息,这里进行简化了。)
我们有以下代码:
public class Main1 {
public static void main(String[] args) {
String str1 = "Hello";
String str2 = "Hello";
}
}
在字节码文件加载的时候,“hello”
等常量串已经创建好了,并且保存在字符串常量池中。(链表的长度可以为 1 ,链表的长度是不固定的,是根据常量串的数量来动态变化的)
我们可以简化一下,以便理解。(注意:下图不是真正的存储方式,但不影响阅读)
当我们用“字符串常量”来赋值创建字符串时,先在字符串常量池找到该字符串对象,然后直接赋值给栈中的变量:
这也就解释了为什么“str1 == str2”
的值为true
。
我们再来看看下面的这种情况:
public class Main1 {
public static void main(String[] args) {
String str1 = "Hello";
String str2 = new String("Hello");
System.out.println(str1 == str2);//返回的是false
}
}
我们已知晓str1
的调用情况,那么对于str2
呢?只要是new
就会调用构造方法,我们看看 String
相应的构造方法:
这里的original
其实就是字符串常量池中的那个对象,也就是说,str2
的这种情况是在堆中重新开辟一个空间,value数组
是共享的:
其实很好验证,我们可以调试一下:
可以看到它们的value
值(地址值)是一样的。
那如果是下面这种情况呢?
public class Main1 {
public static void main(String[] args) {
String str1 = new String("Hello");
}
}
它会先把“Hello”
放入字符串常量池(前提是常量池没有“Hello”,如果有就是上面案例的情况),然后再new
一个空间。
在程序中,如果有带了引号的字符串常量出现,它就会被放入字符串常量池中
2.3 手动入池: intern方法
下面代码的输出结果是什么?:
public class Main1 {
public static void main(String[] args) {
char[] arr = {'H','e','l','l','o'};
String str1 = new String(arr);
String str2 = "Hello";
System.out.println(str1 == str2);
}
}
结果:
看一看相应的构造方法:(jdk版本不同 源码会有所不同,下面这个是 jdk8 的源码)
很显然,它会拷贝一个数组,并不是共用同一个数组。对于 str1 的创建并没有涉及到 “字符串常量”,所以就不会放入池里;而对于str2这里的“Hello”,是肯定会入池的。如图:
所以就返回了 false
。
如何让结果为 true
呢?我们可以调用intern
方法。intern
是一个native
方法(底层使用C++实现的,看不到其实现的源代码),该方法的作用是手动将创建的String对象添加到常量池中。
public class Main1 {
public static void main(String[] args) {
char[] arr = {'H','e','l','l','o'};
String str1 = new String(arr);
str1.intern();//将“Hello”放入常量池
String str2 = "Hello";
System.out.println(str1 == str2);
}
}
结果:
在调用intern
之后,str1
字符串被放入常量池中,str2
那里就不会再创建String:“Hello”
对象了。
3.StringBuffer和StringBuilder
3.1 是什么?
你可以把StringBuffer
和StringBuilder
这两个类看作String
的升级版。StringBuffer
和StringBuilder
这两个类也可以代表字符串,它们是可以改变的(具有可变性)。
StringBuffer
和StringBuilder
也都是用数组来存储字符串的,它们都继承了
AbstractStringBuilder
抽象类。
可以看到value
数组没有被private、final
修饰,限制没有String
中的value
严格,并且StringBuffer
和StringBuilder
重写了修改字符串的相关方法。
3.2 提供的方法
StringBuffer
和StringBuilder
中的方法大部分都是相同的,你可以认为它们是孪生姐妹。
以下是常用的方法(包括父类AbstractStringBuilder
的方法,下面以StringBuffer
为视角)
方法 | 说明 |
---|---|
StringBuff append(String str) | 在尾部追加,相当于String的+=,可以追加:boolean、char、char[]、 double、float、int、long、Object、String、StringBuff的变量 |
char charAt(int index) | 获取index位置的字符 |
int length() | 获取字符串的长度 |
int capacity() | 获取底层保存字符串空间总的大小 |
void ensureCapacity(int mininmumCapacity) | 扩容 |
void setCharAt(int index, char ch) | 将index位置的字符设置为ch |
int indexOf(String str) | 返回str第一次出现的位置 |
int indexOf(String str, int fromIndex) | 从fromIndex位置开始查找str第一次出现的位置 |
int lastIndexOf(String str) | 返回最后一次出现str的位置 |
int lastIndexOf(String str, int fromIndex) | 从fromIndex位置开始找str最后一次出现的位置 |
StringBuff insert(int offset, String str) | 在offset位置插入:八种基类类型 & String类型 & Object类型数据 |
StringBuffer deleteCharAt(int index) | 删除index位置字符 |
StringBuffer delete(int start, int end) | 删除[start, end)区间内的字符 |
StringBuffer replace(int start, int end, String str) | 将[start, end)位置的字符替换为str |
String substring(int start) | 从start开始一直到末尾的字符以String的方式返回 |
String substring(int start,int end) | 将[start, end)范围内的字符以String的方式返回 |
StringBuffer reverse() | 反转字符串 |
String toString() | 将所有字符按照String的方式返回 |
StringBuffer
和StringBuilder
的方法与String
的方法大同小异,最主要的区别就是StringBuffer
和StringBuilder
提供了setCharAt()
、deleteCharAt()
等能修改字符串的方法(效率相关问题在后文),在刷题的时候使用StringBuffer
或StringBuilder
就很方便。
(1)StringBuffer
和StringBuilder
的构造方法都是一样的,这里就看StringBuilder
的:
public class Main2 {
public static void main(String[] args) {
//无参数的时候初始容量为16
StringBuilder stringBuilder1 = new StringBuilder();
//直接
StringBuilder stringBuilder2 = new StringBuilder("Hello");
//间接
String str = "World";
StringBuilder stringBuilder3 = new StringBuilder(str);
//不能直接赋值
//StringBuilder stringBuilder4 = "Hello World!";这是错误的!!!
//`StringBuffer`和`StringBuilder`重写了toString()方法,可以直接打印。
System.out.println(stringBuilder1);
System.out.println(stringBuilder2);
System.out.println(stringBuilder3);
}
}
(2)StringBuffer
和StringBuilder
的方法就不举例了,表里的文字就已经描述得很清楚了。
3.3 StringBuffer和StringBuilder的区别
StringBuffer:
StringBuilder:
StringBuffer
的方法都加了synchronized
关键字,这个是多线程的知识,表示对该对象加锁。换言之,就是StringBuffer
线程安全,StringBuilder
线程不安全。
除此之外没什么区别,在单线程的情况下用谁都可以。
4.字符串拼接的过程
String
有一个特性,就是可以直接在尾部追加新字符串,就比如:
public class Main {
public static void main(String[] args) {
String str = "Hello";
str += " World";
System.out.println(str);//结果为 Hello World
}
}
前面介绍过String
是具有不可变性
,那它是怎么完成追加操作的呢?我们看看它的汇编代码:
在String
拼接的时候会借助StringBuilder
类来修改字符串,因为StringBuilder
类是“可变的”,它等价于下面的代码:
public class Main {
public static void main(String[] args) {
String str1 = "Hello";
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append(str1);//尾部追加“Hello”
stringBuilder.append(" World");//尾部追加“ World”
str1 = stringBuilder.toString();//转换为 String 类型、
System.out.println(str);//结果为 Hello World
}
}
从上面代码可以得出,用String
来拼接字符串效率较低,其中间过程需要创建新的对象。我们从下面代码就可以看出问题:
public class Main2 {
public static void main(String[] args) {
//String 拼接
long start = System.currentTimeMillis();//获取当前时间(开始时间)
String s1 = "";
for(int i = 0; i < 50000; ++i){
s1 += i;
}
long end = System.currentTimeMillis();//获取当前时间(结束时间)
System.out.println("String:" + (end - start) + "毫秒");//时间差(用时多少) 下同
//StringBuffer 拼接(追加)
start = System.currentTimeMillis();
StringBuffer s2 = new StringBuffer("");
for(int i = 0; i < 50000; ++i){
s2.append(i);
}
end = System.currentTimeMillis();
System.out.println("StringBuffer:" + (end - start) + "毫秒");
//StringBuilder 拼接(追加)
start = System.currentTimeMillis();
StringBuilder s3 = new StringBuilder();
for(int i = 0; i < 50000; ++i){
s3.append(i);
}
end = System.currentTimeMillis();
System.out.println("StringBuilder:" + (end - start) + "毫秒");
}
}
结果:
为什么StringBuffer
相对于 StringBuilder
要慢一丢丢?因为 StringBuffer
的append
方法加了 synchronized
关键字,加锁 解锁
也是要消耗时间的。
如有错误请在评论区指正,码字不易,求点点赞谢谢🌹🌹🌹