一、String类的重要性与基本概念

1.1 为什么需要String类

在C语言中,字符串是通过字符数组或字符指针来表示的,需要借助标准库函数(如strlenstrcpy等)进行操作。这种方式存在几个问题:

  1. 数据和操作分离,不符合面向对象封装的原则
  2. 需要手动管理内存,容易造成内存泄漏或越界访问
  3. 功能有限,缺乏现代字符串处理所需的高级功能

Java的String类解决了这些问题,提供了丰富而安全的字符串操作功能,包括:

  • 字符串拼接、比较和搜索
  • 大小写转换和格式化
  • 正则表达式匹配和替换
  • 国际化支持

1.2 String类的基本特性

String类具有以下几个重要特性:

  • 不可变性(Immutability):String对象一旦创建,其内容就不能被修改
  • 字符串常量池(String Pool):Java使用字符串常量池来优化内存使用
  • final类:String类被声明为final,不能被继承

二、String对象的创建与内存模型

2.1 创建String对象的三种常用方式

public static void main(String[] args) {
    // 1. 使用常量串构造
    String s1 = "hello";
    System.out.println(s1);
    
    // 2. 直接new String对象
    String s2 = new String("hello");
    System.out.println(s2);
    
    // 3. 使用字符数组进行构造
    char[] array = {'h','e','l','l','o'};
    String s3 = new String(array);
    System.out.println(s3);
}

2.2 String的内存模型

String是引用类型,其内部实际存储结构在JDK 1.8及之前版本是char数组:

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
}

从JDK 9开始,为了节省内存,String内部改用byte数组存储,同时添加了coder标识编码:

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    /** The value is used for character storage. */
    private final byte[] value;
    
    /** The identifier of the encoding used to encode the bytes in {@code value}. */
    private final byte coder;
    
    /** Cache the hash code for the string */
    private int hash; // Default to 0
}

2.3 字符串常量池

字符串常量池是Java为了减少内存开销而设计的一种机制。当使用字面量创建字符串时,JVM会首先检查字符串常量池中是否存在相同内容的字符串:

  • 如果存在,则返回池中对象的引用
  • 如果不存在,则在池中创建一个新的字符串对象,并返回其引用
String s1 = "hello";  // 在常量池中创建
String s2 = "hello";  // 直接使用常量池中的对象
String s3 = new String("hello");  // 在堆中创建新对象

System.out.println(s1 == s2);  // true,引用相同对象
System.out.println(s1 == s3);  // false,引用不同对象
System.out.println(s1.equals(s3));  // true,内容相同

用双引号""引起来的字符串会直接存储在常量池中;而通过new String()创建的字符串会在堆内存中创建新的String对象,s3指向堆中的这个String对象,而这个对象指向常量池中的字符串"hello"

2.4 intern()方法

intern() 可以主动将字符串对象加入到字符串常量池中,并返回常量池中的引用。

原理:

  1. 检查常量池:调用 intern() 时,JVM 会先检查字符串常量池里是否已经存在一个内容相同的字符串。
  2. 复用或添加
    • 如果存在,直接返回常量池里那个字符串的引用。
    • 如果不存在,就把当前这个字符串对象的引用加入常量池,然后返回这个引用。
  3. 内存优化:这个方法可以让堆中的字符串对象也能被常量池复用,从而减少重复对象,节省内存。
// 双引号创建,直接在常量池
String s1 = "hello";
// new String() 在堆中创建新对象
String s2 = new String("hello");
// 调用intern(),返回常量池中的引用
String s3 = s2.intern();

System.out.println(s1 == s2);   // false(常量池 vs 堆)
System.out.println(s1 == s3);   // true(都指向常量池)

使用场景:

  1. 大量重复字符串:当需要处理大量重复的字符串时(比如从数据库读取的大量重复数据),用 intern() 可以显著减少内存占用。
  2. 需要 == 比较的场景:如果业务场景要求用 == 来快速比较字符串是否相等(性能要求极高时),可以先用 intern() 确保它们都指向常量池。

三、String对象的比较

Java提供了多种方式比较字符串,每种方式有其特定的使用场景。

3.1 ==运算符比较

==运算符比较的是两个对象的引用是否相同(即是否指向同一内存地址):

public static void main(String[] args) {
    int a = 10;
    int b = 20;
    int c = 10;
    
    // 对于基本类型变量,==比较两个变量中存储的值是否相同
    System.out.println(a == b); // false
    System.out.println(a == c); // true
    
    // 对于引用类型变量,==比较两个引用变量引用的是否为同一个对象
    String s1 = new String("hello");
    String s2 = new String("hello");
    String s3 = new String("world");
    String s4 = s1;
    
    System.out.println(s1 == s2); // false
    System.out.println(s2 == s3); // false
    System.out.println(s1 == s4); // true
}

3.2 equals方法比较

equals方法比较的是两个字符串的内容是否相同:

public static void main(String[] args) {
    String s1 = new String("hello");
    String s2 = new String("hello");
    String s3 = new String("Hello");
    
    // s1、s2、s3引用的是三个不同对象,因此==比较结果全部为false
    System.out.println(s1 == s2);    // false
    System.out.println(s1 == s3);    // false
    
    // equals比较:String对象中的逐个字符
    // 虽然s1与s2引用的不是同一个对象,但是两个对象中放置的内容相同,因此输出true
    // s1与s3引用的不是同一个对象,而且两个对象中内容也不同,因此输出false
    System.out.println(s1.equals(s2)); // true
    System.out.println(s1.equals(s3)); // false
}

String类重写了Object类的equals方法,其实现逻辑如下:

public boolean equals(Object anObject) {
    // 1. 先检测this和anObject是否为同一个对象比较,如果是返回true
    if (this == anObject) {
        return true;
    }
    
    // 2. 检测anObject是否为String类型的对象,如果是继续比较,否则返回false
    if (anObject instanceof String) {
        // 将anObject向下转型为String类型对象
        String anotherString = (String)anObject;
        int n = value.length;
        
        // 3. this和anObject两个字符串的长度是否相同,是继续比较,否则返回false
        if (n == anotherString.value.length) {
            char v1[] = value;
            char v2[] = anotherString.value;
            int i = 0;
            
            // 4. 按照字典序,从前往后逐个字符进行比较
            while (n-- != 0) {
                if (v1[i] != v2[i])
                    return false;
                i++;
            }
            return true;
        }
    }
    return false;
}

3.3 compareTo方法比较

compareTo方法按字典序比较两个字符串,返回int类型结果:

  • 负数:当前字符串小于参数字符串
  • 零:两个字符串相等
  • 正数:当前字符串大于参数字符串
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)); // 不同输出字符差值-1
    System.out.println(s1.compareTo(s3)); // 相同输出0
    System.out.println(s1.compareTo(s4)); // 前k个字符完全相同,输出长度差值-3
}

3.4 compareToIgnoreCase方法比较

compareToIgnoreCase方法与compareTo类似,但忽略大小写:

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)); // 不同输出字符差值-1
    System.out.println(s1.compareToIgnoreCase(s3)); // 相同输出0
    System.out.println(s1.compareToIgnoreCase(s4)); // 前k个字符完全相同,输出长度差值-3
}

四、字符串查找操作

String类提供了丰富的查找方法,可以方便地定位字符或子串:

方法 功能
char charAt(int index) 返回index位置上字符,如果index为负数或者越界,抛出IndexOutOfBoundsException异常
int indexOf(int ch) 返回ch第一次出现的位置,没有返回-1
int indexOf(int ch, int fromIndex) 从fromIndex位置开始找ch第一次出现的位置,没有返回-1
int indexOf(String str) 返回str第一次出现的位置,没有返回-1
int indexOf(String str, int fromIndex) 从fromIndex位置开始找str第一次出现的位置,没有返回-1
int lastIndexOf(int ch) 从后往前找,返回ch第一次出现的位置,没有返回-1
int lastIndexOf(int ch, int fromIndex) 从fromIndex位置开始找,从后往前找ch第一次出现的位置,没有返回-1
int lastIndexOf(String str) 从后往前找,返回str第一次出现的位置,没有返回-1
int lastIndexOf(String str, int fromIndex) 从fromIndex位置开始找,从后往前找str第一次出现的位置,没有返回-1
public static void main(String[] args) {
    String s = "aaabbbcccaaabbbccc";
    System.out.println(s.charAt(3)); // 'b'
    System.out.println(s.indexOf('c')); // 6
    System.out.println(s.indexOf('c', 10)); // 15
    System.out.println(s.indexOf("bbb")); // 3
    System.out.println(s.indexOf("bbb", 10)); // 12
    System.out.println(s.lastIndexOf('c')); // 17
    System.out.println(s.lastIndexOf('c', 10)); // 8
    System.out.println(s.lastIndexOf("bbb")); // 12
    System.out.println(s.lastIndexOf("bbb", 10)); // 3
}

五、字符串转换操作

  • 序列化:对象转化为字符串
  • 反序列化:字符串转化为对象

注意:与String类相关的转变都是创建新的对象,不会在原字符串做改变

5.1 数值和字符串相互转换

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("Hanmeimei", 18));
    
    System.out.println(s1);
    System.out.println(s2);
    System.out.println(s3);
    System.out.println(s4);
    
    System.out.println("===============================");
    
    // 字符串转数字
    // 注意:Integer、Double等是Java中的包装类型
    int data1 = Integer.parseInt("1234");
    double data2 = Double.parseDouble("12.34");
    
    System.out.println(data1);
    System.out.println(data2);
}

5.2 大小写转换

public static void main(String[] args) {
    String s1 = "hello";
    String s2 = "HELLO";
    
    // 小写转大写
    System.out.println(s1.toUpperCase());
    
    // 大写转小写
    System.out.println(s2.toLowerCase());
}

5.3 字符串与数组转换

public static void main(String[] args) {
    String s = "hello";
    
    // 字符串转数组
    char[] ch = s.toCharArray();
    for (int i = 0; i < ch.length; i++) {
        System.out.print(ch[i]);
    }
    System.out.println();
    
    // 数组转字符串
    String s2 = new String(ch);
    System.out.println(s2);
}

5.4 字符串格式化

public static void main(String[] args) {
    String s = String.format("%d-%d-%d", 2019, 9, 14);
    System.out.println(s);
}

六、字符串替换操作

String类提供了多种替换方法,可以替换字符或子串:

方法 功能
String replace(char oldChar, char newChar) 替换字符
String replace(CharSequence target, CharSequence replacement) 替换所有匹配的字符串
String replaceAll(String regex, String replacement) 替换所有的指定内容
String replaceFirst(String regex, String replacement) 替换首个内容
String str = "helloworld";
System.out.println(str.replaceAll("l", "_")); // he__owor_d
System.out.println(str.replaceFirst("l", "_")); // he_loworld

注意事项

  • 前两个方法不支持正则表达式,只匹配字面量
  • 由于字符串是不可变对象,替换不会修改当前字符串,而是产生一个新的字符串。

七、字符串拆分操作

将一个完整的字符串按照指定的分隔符划分为若干个子字符串,返回值是数组

方法 功能
String[] split(String regex) 将字符串全部拆分
String[] split(String regex, int limit) 将字符串以指定的格式,拆分为limit组

7.1 基本拆分操作

String str = "hello world hello bit";
String[] result = str.split(" "); // 按照空格拆分
for(String s : result) {
    System.out.println(s);
}

7.2 限制拆分次数

String str = "hello world hello bit";
String[] result = str.split(" ", 2); // 最多拆分为2部分
for(String s : result) {
    System.out.println(s);
}
// 输出:
// hello
// world hello bit

7.3 特殊字符的拆分

某些特殊字符作为分隔符时需要转义:

String str = "192.168.1.1";
String[] result = str.split("\\."); // 点号需要转义
for(String s : result) {
    System.out.println(s);
}

注意事项:

  1. 字符|,*, +等都得加上转义字符,前面加上\\
  2. 如果是\,那么就得写成\\\\
  3. 如果一个字符串中有多个分隔符,可以用|作为连字符

7.4 多次拆分

String str = "name=zhangsan&age=18";
String[] result = str.split("&");
for (int i = 0; i < result.length; i++) {
    String[] temp = result[i].split("=");
    System.out.println(temp[0] + " = " + temp[1]);
}

这种代码在Web开发中处理查询参数时会经常使用。

八、字符串截取操作

字符串截取是从一个完整的字符串中获取部分内容:

方法 功能
String substring(int beginIndex) 从指定索引截取到结尾
String substring(int beginIndex, int endIndex) 截取部分内容
String str = "helloworld";
System.out.println(str.substring(5)); // world
System.out.println(str.substring(0, 5)); // hello

注意事项:

  1. 索引从0开始
  2. substring(beginIndex, endIndex)表示包含beginIndex位置的字符,不包含endIndex位置的字符

九、其他常用操作

方法 功能
String trim() 去掉字符串中的左右空格,保留中间空格
String toUpperCase() 字符串转大写
String toLowerCase() 字符串转小写

9.1 trim方法使用

String str = "   hello world   ";
System.out.println("[" + str + "]");
System.out.println("[" + str.trim() + "]");

trim()方法会去掉字符串开头和结尾的空白字符(空格、换行、制表符等)。

9.2 大小写转换

String str = "   hello%$$%@#$%world 哈哈哈   ";
System.out.println(str.toUpperCase());
System.out.println(str.toLowerCase());

这两个方法只转换字母字符,不影响非字母字符。

十、字符串的不可变性

10.1 什么是不可变对象

String是一种不可变对象,即字符串一旦创建,其内容就不能被修改。这种不可变性体现在以下几个方面:

  1. String类被final修饰,不能被继承
  2. 存储字符的数组被final修饰(不是实现不可变性的主要原因)
  3. 所有修改操作都返回新对象,而不是修改原对象

10.2 为什么String要设计成不可变的

  1. 安全性:不可变对象是线程安全的,可以在多线程环境中安全共享
  2. 性能优化:可以缓存hash code,提高作为HashMap键时的性能
  3. 字符串常量池:不可变性使得字符串常量池成为可能,节省内存
  4. 安全性:在网络传输和参数传递时,不可变字符串可以防止被意外修改

10.3 关于final修饰符的误解

有些人认为String不可变是因为其内部保存字符的数组被final修饰了,这种说法是错误的:

public static void main(String[] args) {
    final int array[] = {1, 2, 3, 4, 5};
    array[0] = 100; // 可以修改数组元素
    System.out.println(Arrays.toString(array)); // [100, 2, 3, 4, 5]
    
    // array = new int[]{4, 5, 6}; // 编译错误:无法为最终变量array分配值
}

final修饰引用类型表明该引用变量不能引用其他对象,但是其引用对象中的内容是可以修改的。String的不可变性是通过设计保证的,而不是仅仅依靠final关键字。

十一、字符串修改与性能优化

11.1 直接修改字符串的性能问题

由于String的不可变性,直接对String进行修改操作会产生大量临时对象,效率低下:

public static void main(String[] args) {
    String s = "hello";
    s += "world"; // 创建了新对象
    System.out.println(s); // 输出: hello world
}

上面的代码实际上执行了以下操作:

  1. 创建一个StringBuilder的对象
  2. 将s的内容追加到StringBuilder
  3. world追加到StringBuilder
  4. 调用StringBuildertoString方法创建新String对象
  5. 将新String对象的引用赋值给s

11.2 性能对比测试

public static void main(String[] args) {
    // 1. 使用String直接拼接
    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) + "ms");
    
    // 2. 使用StringBuffer拼接
    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) + "ms");
    
    // 3. 使用StringBuilder拼接
    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) + "ms");
}

测试结果表明,在对String类进行修改时,效率是非常低的,因此应尽量避免对String的直接修改,建议使用StringBuilder或StringBuffer。

十二、StringBuilder和StringBuffer

12.1 StringBuilder介绍

由于String的不可更改特性,为了方便字符串的修改,Java中提供了StringBuilderStringBuffer类。这两个类大部分功能是相同的,主要区别在于 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 minimumCapacity) 扩容
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最后一次出现的位置
StringBuilder insert(int offset, String str) 在offset位置插入:八种基类类型 & String类型 & Object类型数据
StringBuilder deleteCharAt(int index) 删除index位置字符
StringBuilder delete(int start, int end) 删除[start, end]区间内的字符
StringBuilder 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的方式返回
StringBuilder reverse() 反转字符串
String toString() 将所有字符按照String的方式返回

12.2 StringBuilder使用示例

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();  // 将StringBuilder以String的方式返回
    System.out.println(str);
}

12.3 String、StringBuffer和StringBuilder的区别

  1. 可变性

    • String:不可变字符序列
    • StringBufferStringBuilder:可变字符序列
  2. 线程安全性

    • String:线程安全(因为不可变)
    • StringBuffer:线程安全(方法使用synchronized修饰)
    • StringBuilder:线程不安全
  3. 性能

    • String:修改操作性能最低(每次修改都创建新对象)
    • StringBuffer:性能中等(有同步开销)
    • StringBuilder:性能最高(无同步开销)
  4. 使用场景

    • String:适用于字符串不经常变化的场景
    • StringBuffer:适用于多线程环境下字符串频繁修改的场景
    • StringBuilder:适用于单线程环境下字符串频繁修改的场景

12.4 面试题:创建了多少个String对象

String str = new String("ab");  // 会创建多少个对象

答案:2个或1个

  • "ab"作为字面量,在编译期会被放入常量池
  • new String()会在堆中创建一个新对象
  • 如果常量池中已存在"ab",则只创建1个对象(堆中的对象)
  • 如果常量池中不存在"ab",则创建2个对象(常量池中的对象和堆中的对象)
String str = new String("a") + new String("b");  // 会创建多少个对象

答案:最多6个,最少4个

  • 两个"a"和"b"字面量(可能已存在于常量池)
  • 两个new String()对象
  • 一个StringBuilder对象(用于拼接)
  • 一个toString()方法创建的String对象

十三、总结

  1. String是不可变对象,所有修改操作都会创建新对象
  2. 字符串常量池是Java优化内存的重要机制
  3. 比较字符串内容应使用equals()方法,而非==运算符
  4. 频繁修改字符串时应使用StringBuilderStringBuffer
  5. StringBuilder性能高于StringBuffer,但非线程安全
  6. 理解字符串的不可变性有助于编写更安全和高效的代码
Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐