在阅读本文之前,建议读者有限阅读本专栏内前面的文章。
目录
前言
本文主要介绍一些和Java中String类相关的一些知识。
一、String类
在C语言中已经涉及到字符串了,但是在C语言中要表示字符串只能使用字符数组或者字符指针,可以使用标准库提供的字符串系列函数完成大部分操作,但是这种将数据和操作数据方法分离开的方式不符合面相对象的思想,而字符串应用又非常广泛,因此Java语言专门提供了String类。比如说我们创建一个新的字符串:
java
public class Main {
public static void main(String[] args) {
String str = "123";
}
}
我们之前说过String是一个类,那么既然它是一个类,就一定有相应的构造方法,那么我们就转到它的声明来看一下:

可以看到它是有很多的构造方法的。我们上面出现的那种形式实际上就是调用如下的构造方法:
java
public String() {
this.value = "".value;
this.coder = "".coder;
}
既然有着这么多的构造方法,我们就有着多种创建字符串的方式,比如说我们也可以这样创建字符串:
java
public class Main {
public static void main(String[] args) {
String str = "123";
String str2 = new String("456");
char[] c = new char[]{'a', 'b', 'c'};
String str3 = new String(c);
System.out.println(str);
System.out.println(str2);
System.out.println(str3);
}
}
这段代码的运行结果如下:

如果说大家需要用到其他的方法,也可以参考下面的在线文档:
Overview (Java Platform SE 8 )
二、String对象的比较
在讲其常用方法之前,我们首先需要提醒大家几点,首先String是引用类型,内部并不存储字符串本身,在String类的实现代码之中,它的实例变量如下:

它下面那个整型变量是一个哈希值,但我们还并没有学习过哈希表相关知识,所以我们仅仅简单说明下。我们首先键入如下的代码:
java
public class Main {
public static void main(String[] args) {
String s1 = "hello";
String s2 = "world";
String s3 = s1;
}
}
然后我们在s1那行代码设置一个断点并且进行调试,我们可以得到如下的结果:

这个value实际是一个byte类型的数组,他其中存放的就是每个字母对应着的一个十进制的数字。那么我们这个整个的过程是什么样子的呢?实际上就相当于我们在虚拟机栈中存放了一个地址,这个地址指向堆区某个空间,空间内有我们上面出现的value等成员变量,而value其实也是存放着一个地址,它指向了我们堆区的另一个空间,这个空间存放了我们的字符数组。


此时我们再添加如下几行代码:
java
public class Main {
public static void main(String[] args) {
String s1 = "hello";
String s2 = "world";
String s3 = s1;
System.out.println(s1 == s2);
System.out.println(s1 == s3);
System.out.println(s2 == s3);
System.out.println("====================");
System.out.println(s1.equals(s2));
System.out.println(s1.equals(s3));
System.out.println(s2.equals(s3));
}
}
其运行结果如下:

可以看到两者结果是完全相同的,但是两者得到结果的原因是完全不一致的,第一部分的结果是因为在比较二者存放的地址,第二部分的结果则是因为在比较二者内存放的内容。我们再键入如下的代码:
java
public class Main {
public static void main(String[] args) {
String s1 = new String("abc");
String s2 = new String("ac");
String s3 = new String("abc");
String s4 = new String("abcdef");
System.out.println(s1.compareTo(s2));
System.out.println(s1.compareTo(s3));
System.out.println(s1.compareTo(s4));
}
}
其运行结果如下:

与equals不同的是,equals返回的是boolean类型,而compareTo返回的是int类型。具体比较方式为先按照字典次序大小比较,如果出现不等的字符,直接返回这两个字符的大小差值;如果前k个字符相等(k为两个字符长度最小值),返回值两个字符串长度差值。然后我们再键入下面这个代码:
java
public class Main {
public static void main(String[] args) {
String s1 = new String("abc");
String s2 = new String("ac");
String s3 = new String("ABc");
String s4 = new String("abcdef");
System.out.println(s1.compareToIgnoreCase(s2));
System.out.println(s1.compareToIgnoreCase(s3));
System.out.println(s1.compareToIgnoreCase(s4));
}
}
其运行结果如下:

这个方法也是按照字典序进行比较,只不过它忽略了字符的大小写问题。那么我们其实可以进行一个总结,在我们上述介绍的这四种比较之中,==比较是否引用同一个对象, boolean equals(Object anObject) 方法是比较二者内容是否相同, int compareTo(String s) 方法:是按照字典序进行比较,int compareToIgnoreCase(String str) 方法与compareTo方式相同,但是忽略大小写比较。
三、字符串的查找
在字符串中,查找也是我们经常需要使用的方法,String类为我们提供了许多的方法:

需要注意的是,上述的方法全部是实例方法,我们接下来一一来进行说明。我们首先看看charAt方法,这个代码其实就是把字符串看成了数组,然后直接访问对应数组下标的字符,我们可以键入如下的代码:
java
public class Main {
public static void main(String[] args) {
String s = "hello world";
for(int i = 0; i < s.length(); i++) {
System.out.println(s.charAt(i));
}
}
}
其运行结果如下:

然后我们再来看看indexOf方法,当我们只输入一个参数的时候,他返回的就是我们传入的字符参数第一次出现的下标;而如果我们再输入一个整型数字,它就会以我们输入数字为下标起点开始寻找第一次出现的字符参数。我们键入如下的代码:
java
public class Main {
public static void main(String[] args) {
String s = "hello world";
int index = s.indexOf('l');
System.out.println(index);
index = s.indexOf('l', 3);
System.out.println(index);
}
}
其运行结果如下:

当然,我们这个方法也可以传入一个字符串,此时我们就会把这个传入的字符串当做子串,在整个原字符串中找到它第一次出现的位置。我们键入如下的代码:
java
public class Main {
public static void main(String[] args) {
String s = "hello world";
int index = s.indexOf("ll");
System.out.println(index);
index = s.indexOf("ll", 5);
System.out.println(index);
}
}
其运行结果如下:

除了从前向后找,我们也可以从后向前找,下面这些方法使用与上面的是类似的,故不再赘述。我们键入如下的方法:
java
public class Main {
public static void main(String[] args) {
String s = "hello world";
int index = s.lastIndexOf('l');
System.out.println(index);
index = s.lastIndexOf('l', 5);
System.out.println(index);
index = s.lastIndexOf("ll");
System.out.println(index);
index = s.lastIndexOf("ll", 1);
System.out.println(index);
}
}
其运行结果如下:

四、字符串的转化
首先我们可以把数值甚至一个对象转化为字符串,我们可以键入如下的代码:
java
class Student{
private String name;
private int age;
public Student(String name,int age){
this.name=name;
this.age=age;
}
}
public class Main {
public static void main(String[] args) {
String s1 = String.valueOf(1234);
String s2 = String.valueOf(12.34);
String s3 = String.valueOf(true);
String s4 = String.valueOf(new Student("suzhe",20));
System.out.println(s1);
System.out.println(s2);
System.out.println(s3);
System.out.println(s4);
}
}
其运行结果如下:

尽管在我们看来打印的结果并无任何差异,但实际上和谐数据都已经被转换为了字符串的形式。我们也可以通过重写toString方法来让s4容易看一些。
java
class Student{
private String name;
private int age;
public Student(String name,int age){
this.name=name;
this.age=age;
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
public class Main {
public static void main(String[] args) {
String s1 = String.valueOf(1234);
String s2 = String.valueOf(12.34);
String s3 = String.valueOf(true);
String s4 = String.valueOf(new Student("suzhe",20));
System.out.println(s1);
System.out.println(s2);
System.out.println(s3);
System.out.println(s4);
}
}
其运行结果如下:

事实上我们刚才这种把对象转换为字符串的方式叫做序列化,而把字符串转化为对象的方法则叫做反序列化。同时同样我们也可以把字符串转化为数值:
java
public class Main {
public static void main(String[] args) {
int data1 = Integer.parseInt("1234");
double data2 = Double.parseDouble("1234.56");
System.out.println(data1 + 1);
System.out.println(data2 + 0.1);
}
}
其运行结果如下:

然后我们再来看看字符串中字符的大小写转化:
java
public class Main {
public static void main(String[] args) {
String s1 = "hello";
String s2 = "WORLD";
System.out.println(s1.toUpperCase());
System.out.println(s2.toLowerCase());
}
}
其运行结果如下:

这里需要注意的是,只要涉及到String类型的转换,都不是在我们原有的字符串上进行的修改,而是我们创建了一个新的对象,我们可以去看看它的声明:
java
public String toUpperCase() {
return toUpperCase(Locale.getDefault());
}
我们再转到它return那个方法的声明:
java
public String toUpperCase(Locale locale) {
return isLatin1() ? StringLatin1.toUpperCase(this, value, locale)
: StringUTF16.toUpperCase(this, value, locale);
}
我们是随便查看一个分支的返回值类型:

可以看到他的返回值是new了一个新的String对象。接下来我们看看将字符串转化为字符数组:
java
public class Main {
public static void main(String[] args) {
String s = "hello";
char[] arr = s.toCharArray();
for(int i = 0; i < arr.length; i++){
System.out.println(arr[i]);
}
}
}
其运行结果如下:

最后让我们看一下字符串的格式化:
java
public class Main {
public static void main(String[] args) {
String s = String.format("%d-%d-%d",2025,12,23);
System.out.println(s);
}
}
其运行结果如下:

五、字符串的替换
我们接触到的字符串替换有如下几种:

首先是最简单的一种,就是把单个的字符进行替换,我们键入如下的代码:
java
public class Main {
public static void main(String[] args) {
String s = "hello world";
String tmp = s.replace('l', 'a');
System.out.println(tmp);
}
}
其运行结果如下:

可以看到我们字符串中所有的l都被替换为了a。接下来让我们看看下一种,它这个参数很奇怪,是个叫做CharSequence的东西,那么这个东西是什么呢?我们可以转到String的声明看看:

我们发现其实这个CharSequence它就是一个接口,那么String作为一个类实现了这个接口,这也就是说其实在这两个参数的位置我们传入字符串就可以了:
java
public class Main {
public static void main(String[] args) {
String s = "hello world";
String tmp = s.replace("ll","aaaaaa");
System.out.println(tmp);
}
}
其运行结果如下:

然后就是最后两种:
java
public class Main {
public static void main(String[] args) {
String s = "aababcabcdabcde";
String tmp = s.replaceFirst("ab", "uuu");
System.out.println(tmp);
tmp = s.replaceAll("ab", "uuu");
System.out.println(tmp);
}
}
其运行结果如下:

六、字符串的拆分
首先我们先来看一下字符串的split方法,如果说我们只传入一个字符串作为参数的话,那么它就会拆分整个字符串的内容:
java
public class Main {
public static void main(String[] args) {
String s = "name=zhangsan&age=10";
String[] strings = s.split("&");
for(int i = 0; i < strings.length; i++) {
String[] ss = strings[i].split("=");
for(int j = 0; j < ss.length; j++) {
System.out.println(ss[j]);
}
System.out.println();
}
}
}
其运行结果如下:

当然,我们也可以把这两个分隔符写在同一个函数之中以避免多重循环,只不过我们需要使用|符号来进行分割,其实也就是说,我们可以用下面的代码替换上面的:
java
public class Main {
public static void main(String[] args) {
String s = "name=zhangsan&age=10";
String[] strings = s.split("&|=");
for(int i = 0; i < strings.length; i++) {
System.out.println(strings[i]);
}
}
}
其运行结果如下:

当然我们也可以实现字符串的部分拆分,也就是将字符串以指定的方式拆分为我们想要数量的组:
java
public class Main {
public static void main(String[] args) {
String s = "hello world hello bit hello suzhe";
String[] ret1 = s.split(" ");
String[] ret2 = s.split(" ", 3);
for(int i = 0; i < ret1.length; i++){
System.out.println(ret1[i]);
}
System.out.println("============");
for(int i = 0; i < ret2.length; i++){
System.out.println(ret2[i]);
}
}
}
其运行结果如下:

拆分是非常常用的操作,所以我们一定要重点掌握,另外有些特殊字符作为分隔符可能无法实现正常的切分,所以需要加上转义。比如说我们在拆分IP地址时想要以.作为分隔符:
java
public class Main {
public static void main(String[] args) {
String ip = "192.168.100.245";
String[] s1 = ip.split(".");
String[] s2 = ip.split("\\.");
for (int i = 0; i < s1.length; i++) {
System.out.println(s1[i]);
}
System.out.println("============");
for (int i = 0; i < s2.length; i++) {
System.out.println(s2[i]);
}
}
}
其运行结果如下:

可以发现如果说我们直接输入.的话就会分割失败,而当我们在前面加上\\来转义才会分割成功。并且除了.之外,|、*、+这些符号前面都要加上转义字符,而如果说是\的话我们就需要\\。
七、字符串的截取
从一个完整的字符串之中截取部分内容主要有两种形式,第一种是substring方法:
java
public class Main {
public static void main(String[] args) {
String s = "abcdefghijklmn";
String ret1 = s.substring(4);
String ret2 = s.substring(4, 8);
System.out.println(ret1);
System.out.println(ret2);
}
}
其运行结果如下:

我们只传入一个参数时,我们就会截去这个参数个的字符剩下后面的;而如果说我们传入两个参数,它就会返回这两个参数范围内的字符。第二种则是trim()方法,它会去掉字符串开头和结尾的空白字符,比如说空格、换行、制表符等等。我们键入如下的代码:
java
public class Main {
public static void main(String[] args) {
String s = " hello world ";
System.out.println(s.trim());
}
}
其运行结果如下:

八、字符串的不可变性
String是一种不可变对象,字符串中的内容是不可改变的。字符串不可被修改则主要是因为首先String类在设计时就是不可改变的,在String类实现的描述中我们就已经有所说明了。

可以看到在String的声明中,就已经明确说明字符串是不可以改变的,它的内容在被创建之后是不能被修改的,但是这是如何实现的呢?我们看看它具体的声明:

他最核心的就是这个value数组,因为我们定义的字符实际就保存在它内部维护的value数组之中。那是不是因为我们定义String类时第一个final让我们的内容无法被修改呢?不是,这第一个final只是修饰整个类,让整个类无法被继承。那是不是这个value前的final的原因呢?也不是,这个final限制的是value自身的值,它禁止我们引用其它的字符数组,但是其引用的内容是可以修改的。其实真正阻止字符串中内容改变的是private这个关键字,它限制了我们在实例化String类的时候无法再更改其中的内容。但是我们为什么要设计成这个样子呢? 它有以下的好处,方便实现字符串对象池,如果String可变,那么对象池就需要考虑写时拷贝的问题了;不可变对象是线程安全的;不可变对象更方便缓存hash code, 作为key时可以更高效的保存到HashMap中。那如果我们一定要去修改字符串的话呢?有一种很简单的方式:
java
public class Main {
public static void main(String[] args) {
String s = "hello";
s += " world";
System.out.println(s);
}
}
其运行结果如下:

但其实这并非是真正的修改,为什么呢,因为它实际上还是创建了很多临时对象,为了方便演示,我使用了IDEA中的一个叫Jclasslib的插件来查看字节码,读者可不必安装,仅供了解:

这个是Java17环境下的,相对来说不好理解一些,我们仅仅来看一下它的解释。首先加载并存储字符串常量,偏移量0的ldc #7 <hello>指令从常量池索引7的位置取出字符串常量"hello",将其引用压入操作数栈;偏移量2的astore_1指令弹出栈顶的"hello"引用,存入局部变量表索引1的位置,此时局部变量表索引1持有"hello"的引用,操作数栈清空。然后进行动态拼接字符串并更新变量,偏移量3的aload_1将局部变量表索引1的"hello"引用重新压入操作数栈;偏移量4的invokedynamic #9是核心,通过引导方法makeConcatWithConstants(BootstrapMethods #0)动态执行字符串拼接逻辑(此处为拼接空字符串,等价于"hello"+""),拼接后新字符串的引用压入栈顶;偏移量9的astore_1弹出该新引用,覆盖局部变量表索引1的原有值(虽内容仍为"hello",但引用可能因拼接操作变化)。接着获取输出流并打印字符串,偏移量10的getstatic #13从java/lang/System类中取出静态字段out(PrintStream类型),将其引用压入操作数栈;偏移量13的aload_1再次加载局部变量表索引1的拼接后字符串引用,此时操作数栈按栈底到栈顶顺序为[System.out引用, 字符串引用];偏移量14的invokevirtual #19调用PrintStream的println(String)方法,弹出栈中字符串参数和System.out调用者,执行打印操作,方法返回void后操作数栈清空。最后方法结束返回,偏移量17的return指令直接结束当前void类型方法的执行,无返回值。我们此时调换一下环境,我们使用Java1.8再来看看:

这个就容易看一些,它就是通过不断调用StringBuilder类中的方法进行实现,经过的过程如下图:

它等价于如下的代码:
java
public class Main {
public static void main(String[] args) {
StringBuilder temp = new StringBuilder();
temp.append("hello");
temp.append(" world");
String s = temp.toString();
System.out.println(s);
}
}
那么这两段代码有什么区别呢?我们键入如下的代码:
java
public class Main {
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(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(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(end - start);
}
}
我们这里面是调用了一个currentTimeMillis方法,它会为我们返回一个毫秒级别的时间戳,我们这里就是在分别计算String、StringBuffer、StringBuilder这三个类在拼接时的效率,其运行结果如下:

可以看到在对String类进行修改时,效率是非常慢的,因此尽量避免对String的直接需要,如果要修改建议尽量使用StringBuffer或者StringBuilder。接下来我们来仔细看一看这两个类。
九、StringBuffer和StringBuilder类
由于String的不可更改特性,为了方便字符串的修改,Java中又提供StringBuilder和StringBuffer类。这两个类大部分功能是相同的,这里先介绍StringBuilder常用的一些方法,其它需要用到了大家可参阅下面的链接:
Overview (Java Platform SE 8 )


我们键入如下的代码:
java
public class Main {
public static void main(String[] args) {
StringBuilder sb1 = new StringBuilder("hello");
StringBuilder sb2 = sb1;
sb1.append(' '); // hello
sb1.append("world"); // hello world
sb1.append(123); // hello world123
System.out.println(sb1); // hello world123
System.out.println(sb1 == sb2); // true
System.out.println(sb1.charAt(0)); // 获取0号位上的字符 h
System.out.println(sb1.length()); // 获取字符串的有效长度14
System.out.println(sb1.capacity()); // 获取底层数组的总大小
sb1.setCharAt(0, 'H'); // 设置任意位置的字符 Hello world123
sb1.insert(0, "Hello world!!!"); // Hello world!!!Hello world123
System.out.println(sb1);
System.out.println(sb1.indexOf("Hello")); // 获取Hello第一次出现的位置
System.out.println(sb1.lastIndexOf("hello")); // 获取hello最后一次出现的位置
sb1.deleteCharAt(0); // 删除首字符
sb1.delete(0,5); // 删除[0, 5)范围内的字符
String str = sb1.substring(0, 5); // 截取[0, 5)区间中的字符以String的方式返回
System.out.println(str);
sb1.reverse(); // 字符串逆转
str = sb1.toString(); // 将StringBuffer以String的方式返回
System.out.println(str);
}
}
其运行结果如下:

从上述例子可以看出String和StringBuilder最大的区别在于String的内容无法修改,而StringBuilder的内容可以修改。频繁修改字符串的情况考虑使用StringBuilder。需要注意的是,String和StringBuilder类不能直接转换。如果要想互相转换,可以采用如下原则,String变为StringBuilder利用StringBuilder的构造方法或append()方法,StringBuilder变为String调用toString()方法。而对于StringBuffer来说,它与StringBuilder是相似的,那么三者有何区别呢?String的内容不可修改,StringBuffer与StringBuilder的内容可以修改;StringBuffer与StringBuilder大部分功能是相似的,StringBuffer采用同步处理,属于线程安全操作,而StringBuilder未采用同步处理,属于线程不安全操作。
总结
本文详细介绍了Java中String类的特性与常用方法,包括字符串创建、比较、查找、转换、替换、拆分、截取等操作。重点分析了String的不可变性原理及其优缺点,并对比了String、StringBuffer和StringBuilder三者的区别:String不可变,线程安全;StringBuffer可变且线程安全;StringBuilder可变但非线程安全。文章通过大量代码示例演示了各类字符串操作方法,并指出在频繁修改字符串时应优先使用StringBuffer或StringBuilder以提高性能。最后总结了字符串比较的四种方式及其适用场景,为Java字符串处理提供了全面的参考指南。