系列文章目录
能看懂文字就能明白系列
C语言笔记传送门
java笔记传送门
🌟 个人主页
:古德猫宁-
🌈 信念如阳光,照亮前行的每一步
前言
String 类是 Java 中用于表示字符串的核心类之一。它提供了丰富的方法来操作字符串,包括连接、拆分、替换、查找、截取等,使得字符串处理变得简单和高效。
本节重点:
- 理解String为什么具有不可变性
- StringBuffer、StringBuilder和String的区别
- 为什么StringBuilder比StringBuffer总是快了一毫秒
- 模拟简单String类方法的实现
一、字符串构造
常用的字符串构造有以下三种:
public static void main(String[] args) {
//new一个String对象
String s1 = new String("hello");
//使用常量串进行构造
String s2 = "hello";
//使用字符数组进行构造
char[] arr = {'h','e','l','l','o'};
String s3 = new String(arr);
}
在这里特别注意的是:
-
s1,s2,s3存储的是一个地址,由地址指向所引用的对象,可是当我们打印s1,s2,s3的时候,却发现输出的结果是“hello”
原因是String调用了toString的构造方法,我们打开String的源码看看
最后是print输出“hello”。 -
String类是引用类型,内部并不存储字符串本身,在String类的实现源码中,String类实例变量如下:
我们可以看到,String类中有两个成员,一个是数组value[],一个是整型hash(默认为0),当我们调试的时候,我们可以看到是value[]存储的“hello”
如下图所示,我们可以这样理解: -
字符串常量后面没有以\0标记结尾,我们可以调用length看下字符串的长度。
二、String类的特性
String 对象具有不可变性的,即一旦创建就不能被修改。这种不可变性使得字符串在多线程环境下更安全,也可以被用作常量,有助于提高代码的可读性和可维护性。
我们先来看以下代码:
public static void main(String[] args) {
String s = "hello";
s = s.concat(" java");
System.out.println(s);
}
输出结果:
这里很多人可能就有疑问了,不是说String对象具有不可变性吗,为什么这里还可以被改变呢?
虽然字符串的内容看起来变了,从“hello”变成了“hello java”,但实际上,原来的字符串还是没有改变,而是得到了一个新的字符串了,它的内容是“hello java”。
具体如图所示:
为什么字符串具有不可变性呢?
一方面,String类中的字符实际保存在内部维护的value字符数组中,另一方面,String类被final修饰,表明该类不能被继承,而且value也被final修饰,表明value自身的值不能改变,既不能引用其他字符数组,但是其引用空间中的内容可以修改。
特别注意的是:字符串不可变是因为其内部保存字符的数组被final修饰了,因此不能改变,这句话是错误的,不是因为String类自身,或者其内部value被final修饰而不能被修改。
而是因为value[]被private修饰,我们无法拿到value,自然不能通过value去修改值。
为什么String要设计成不可变的?
- 方便实现字符串对象池,如果String可变,那么对象池就需要考虑写深拷贝的问题了
- 不可变对象是线程安全的
- 不可变对象更方便缓存hash code,作为key时可以更高效的保存到HashMap中
三、StringBuilder和StringBuffer
注意:尽量避免直接对String类型对象进行修改,因为String类是不能修改的,所有的修改都会创建新对象,效率非常低。
所以为了方便字符串的修改,java中又提供了StringBuilder和StringBuffer类。
StringBuffer和StringBuilder虽然也封装了一个字符数组,但与String不同,其定义如下:
char[] value;
与String不同的是,它并不是被final所修饰,所以是可以修改的。
我们可以做个简单的测试:
public static void main(String[] args) {
long start = System.currentTimeMillis();
String s = "";
for(int i = 0; i < 10000; ++i){
s += i;
}
long end = System.currentTimeMillis();
System.out.println("String:"+(end - start));
start = System.currentTimeMillis();
StringBuffer sbf = new StringBuffer("");
for(int i = 0; i < 10000; ++i){
sbf.append(i);
}
end = System.currentTimeMillis();
System.out.println("StringBuffer:"+(end - start));
start = System.currentTimeMillis();
StringBuilder sbd = new StringBuilder();
for(int i = 0; i < 10000; ++i){
sbd.append(i);
}
end = System.currentTimeMillis();
System.out.println("StringBuilder:"+(end - start));
}
从结果可以看出,在对String类进行修改时,效率是非常慢的,因此:尽量避免对String的直接修改,如果要修改尽量使用StringBuffer或者StringBuilder。
注意:String和StringBuilder类不能直接转换。如果要想互相转换,可以采用如下原则:
- String变为StringBuilder:利用StringBuilder的构造方法或者append()方法。
- StringBuilder变为String:利用toString()方法。
总结
为什么StringBuilder和StringBuffer比String快那么多呢?
简单来说String 是不可变的,每次对 String 进行操作(如连接、拼接、替换等),都会生成一个新的 String 对象,原始的 String 对象不变。这样会频繁地创建新的对象,产生大量的临时对象,导致内存开销和垃圾回收压力增大。
而 StringBuffer 和 StringBuilder 是可变的,它们提供了修改字符串内容的方法,而不是创建新的对象。这样可以减少对象的创建和销毁,提高了性能。
StringBuffer 和 StringBuilder 内部都是使用可变长度的字符数组(char[])来存储字符串内容。它们的方法都是基于字符数组的操作,如扩容、复制、移动等。这种直接操作字符数组的方式可以提高性能,避免了额外的对象创建和拷贝操作。
为什么StringBuilder比StringBuffer总是快了一毫秒呢?
原因是StringBuilder 是非线程安全的,而StringBuffer 是线程安全的,StringBuffer 中的方法是同步的,即它们被设计为可以安全地在多线程环境中使用。这是通过在每个方法上使用 synchronized 关键字来实现的,以确保同一时刻只有一个线程可以访问 StringBuffer 的方法。
当方法被 synchronized 修饰时,就意味着该方法在同一时间只能被一个线程执行,其他线程需要等待。
三、StringBuilder一些常见的方法
由于StringBuilder和StringBuffer两大类大部分功能是相同的,这里简单介绍StringBuilder一些常用的方法,其他方法可自行浏览:StringBuilder在线文档
方法 | 说明 |
---|---|
StringBuilder append(String str) | 在尾部追加,相当于String的+=,可以追加:boolean,char,char[],double,float,int,long,Object,String,StringBuilder的变量 |
char charAt(int index) | 获取index位置的字符 |
int length() | 获取字符串的长度 |
int capacity() | 获取底层保存字符串空间总的大小 |
void ensureCapacity(int mininmunCapacity) | 扩容 |
void setChar(int index,char ch) | 将index位置的字符设置为ch |
int indexOf(String str) | 返回str第一次出现的位置 |
int indexOf(String str,int fromIndex) | 从fromIndex位置开始查找str第一次出现的位置 |
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) | 从star开始一直到末尾的字符以String的形式返回 |
StringBuffer reverse() | 反转字符串 |