万字解析:Java字符串

目录

[一、 String类](#一、 String类)

[1. String类的初始化](#1. String类的初始化)

[1.1 常用的三种构造String类的方式](#1.1 常用的三种构造String类的方式)

[1.2 String类如何存储字符串?](#1.2 String类如何存储字符串?)

[2. String类的常用功能方法](#2. String类的常用功能方法)

[2.0 字符串长度的获取](#2.0 字符串长度的获取)

[2.1 String对象的比较](#2.1 String对象的比较)

[2.2 字符/字符串的查找](#2.2 字符/字符串的查找)

[2.3 字符串的转化](#2.3 字符串的转化)

[2.4 字符 / 字符串的替换](#2.4 字符 / 字符串的替换)

[2.5 截取一段字符串](#2.5 截取一段字符串)

[2.6 拆分整个字符串](#2.6 拆分整个字符串)

[2.7 其他特殊方法](#2.7 其他特殊方法)

[3. String类的不可变性](#3. String类的不可变性)

[3.1 String内部的成员 ------ 什么是不可变的?](#3.1 String内部的成员 —— 什么是不可变的?)

[3.2 String类为什么是不可变的?](#3.2 String类为什么是不可变的?)

[3.3 String对象的拼接](#3.3 String对象的拼接)

[3.4 补充](#3.4 补充)

[0. 字符集标准与编码方式](#0. 字符集标准与编码方式)

[1. 字符串长度与字符数组长度](#1. 字符串长度与字符数组长度)

[2. 字符串常量池 与 intern()方法](#2. 字符串常量池 与 intern()方法)

[3. "+"操作与concat操作的区别](#3. “+”操作与concat操作的区别)

二、StringBuilder和StringBuffer

[0. 可变性的由来](#0. 可变性的由来)

[1. 两者的异同](#1. 两者的异同)

[2. 常用功能方法(以StringBuilder为例)](#2. 常用功能方法(以StringBuilder为例))

[2.0 长度 与 容量](#2.0 长度 与 容量)

[2.1 可变字符串与不可变字符串的转化](#2.1 可变字符串与不可变字符串的转化)

[2.2 字符串的追加(拼接、插入)](#2.2 字符串的追加(拼接、插入))

[2.3 字符/字符串的查找](#2.3 字符/字符串的查找)

[2.4 字符/字符串的替换](#2.4 字符/字符串的替换)

[2.4 字符/字符串的删除](#2.4 字符/字符串的删除)

[2.5 字符串的截取](#2.5 字符串的截取)

[2.6 字符串的反转](#2.6 字符串的反转)

三、总结

[3.1 两类字符串的成员差异](#3.1 两类字符串的成员差异)

[3.2 两类字符串的方法异同](#3.2 两类字符串的方法异同)

[3.3 字符串 与 方法传参【易错】](#3.3 字符串 与 方法传参【易错】)


在C语言中已经涉及到字符串了,但是在C语言中要表示字符串只能使用字符数组或者字符指针,可以使用标准库提 供的字符串系列函数完成大部分操作,但是这种将数据和操作数据方法分离开的方式不符合面相对象的思想,而字 符串应用又非常广泛,因此Java语言专门提供了String类。

一、 String类

1. String类的初始化

1.1 常用的三种构造String类的方式

String类提供的构造方式非常多,常用的就以下三种:

  1. 字面量赋值(推荐):直接引用常量字符串池中已经存在的字符串,不生成新对象。

  2. 使用参数为字符串****类型的构造方法:生成新对象。【也可以是StringBuilder字符串、StringBuffer字符串】

  1. 使用参数为字符数组类型的构造方法:生成新对象。
cpp 复制代码
public class Test {
    public static void main(String[] args) {
        // 使用常量字符串
        String s0 = "hello";
        String s1 = "hello";
        System.out.println(s1);
        // 直接newString对象
        String s2 = new String("hello");
        System.out.println(s2);
        // 使用字符数组进行构造
        char[] array = {'h','e','l','l','o'};
        String s3 = new String(array);
        System.out.println(s3);
        // 比较是否是同一个对象
        System.out.println("s0与s1相同吗:" + (s0 == s1));
        System.out.println("s1与s2相同吗:" + (s1 == s2));
        System.out.println("s2与s3相同吗:" + (s2 == s3));
    }
}

可以看到 s0 == s1 != s2 != s3,说明字面量赋值引用的是同一个字符串对象;而使用new String(参数)的方式初始化引用变量,都会生成新的对象。

补充 (了解即可): JVM中的内存空间主要划分为4块:栈区(虚拟机栈+native方法栈)、堆区、方法区、程序计数器。

  • Java 6 及之前 :字符串常量池位于 方法区 ,容易引发 OutOfMemoryError

  • Java 7 及之后 :字符串常量池移至 堆区,支持垃圾回收,减少内存泄漏风险。

1.2 String类如何存储字符串?

Java 8 及之前 :使用 final char[] 存储字符,每个字符占 2 字节(UTF-16编码)。

cpp 复制代码
// Java 8 源码
public final class String implements Serializable, Comparable<String>, CharSequence {
    private final char value[]; // 存储字符的数组
    // ...
}

Java 9 及之后 :改用 byte[] 存储,并新增 coder 字段标识编码方式(LATIN-1UTF-16)。

cpp 复制代码
// Java 9+ 源码
public final class String implements Serializable, Comparable<String>, CharSequence {
    private final byte[] value;  // 存储字节的数组
    private final byte coder;    // 0: LATIN1(单字节),1: UTF-16(双字节)
    // ...
}

优化目的 :节省内存。对纯拉丁字符(如英文)使用 LATIN1 编码(1字节/字符),其他字符使用 UTF-16(2字节/字符)。

与 C/C++ 字符串不同的是,Java字符串的末尾不再存放\0 。

无需 \0 终止符的原因

  • 已知长度String 类直接记录字符串的长度(private final int count,Java 9 后由数组长度和编码方式推导),不需要依赖 \0 标识结束。

  • 显式长度控制 :所有操作(如 substringconcat)都基于明确的长度计算,而非遍历到 \0

2. String类的常用功能方法

2.0 字符串长度的获取

方法原型: int length()

作用:通过this调用该函数,返回字符串this的长度。

【无参的实例方法】

cpp 复制代码
    public static void main(String[] args) {
        // 使用常量字符串
        String s0 = "hello";
        int len = s0.length();
        System.out.println(len);
    }

注意区分 字符串长度 和 数组长度:

  • 字符串长度的获取使用的是方法length()
  • 数组长度的获取使用的是成员变量length

2.1 String对象的比较

一. 使用" == "操作符: 比较是否引用同一个对象。
二、equals方法

原型: boolean equals (Object anObject) 【String类已重写原方法】

作用:

  • 按照字符串++从左到右++的顺序比较。
  • 完全相同返回true (如果引用的是同一个对象,返回值也是true)。
  • 有不同都返回false。
cpp 复制代码
    public static void main(String[] args) {
        // 使用常量字符串
        String s1 = "hello";
        String s2 = new String("hello");
        System.out.println("s1与s2是同一个字符串的引用吗:" + (s1 == s2));
        System.out.println("s1与s2是两个相等的字符串吗:" + s1.equals(s2));
        String s3 = s2;
        System.out.println("s2与s3是两个相等的字符串吗:" + s2.equals(s3));
    }

三、compareTo方法

原型: int compareTo (String s) 【String类已重写原方法】

作用:

  • 按照字符串++从左到右++的顺序比较。
  • 如果两个字符串相等,返回值是0。
  • 两个字符串未遍历完时 ,如果出现不等的字符,直接返回这两个字符的大小差值(左this - 右s)。
  • 最小的字符串遍历完时 ,返回值是两个字符串长度的差值(左this - 右s)。
cpp 复制代码
    public static void main(String[] args) {
        String s1 = "hello";
        String s2 = "Hello";
        String s3 = "hello world";
        System.out.println(s1.compareTo(s2));
        System.out.println(s1.compareTo(s3));
    }

s1与s2比较:第一个字符就已经不一样了,返回 'h' - 'H' 的ascii码差值,即32;

s1与s3比较:s1比s3少了一个空格和一个"world",总共6个字符,所以返回-6。

四、compareToIgnoreCase方法

原型: int compareToIgnoreCase(String str)

作用:与compareTo方式相同,但是忽略大小写比较

cpp 复制代码
   public static void main(String[] args) {
        String s1 = "hello";
        String s2 = "Hello";
        String s3 = "Hello world";
        System.out.println(s1.compareToIgnoreCase(s2));
        System.out.println(s1.compareToIgnoreCase(s3));
   }

忽略大小写再次比较s1和s2,此时可以认为s1与s2相等,返回0。

2.2 字符/字符串的查找

一、charAt方法

char charAt(int index)

作用:

  • ++返回下标为index位置上字符。++ (注意:下标是从0开始的)
  • 如果index为负数或者越界,抛出 IndexOutOfBoundsException异常。
cpp 复制代码
    public static void main(String[] args) {
        String s1 = "hello";
        System.out.println(s1.charAt(4)); //输出字符o
        System.out.println(s1.charAt(5)); //越界,抛出异常
    }

二、indexOf方法

1. int indexOf (int ch)

作用:

  • 返回字符ch第一次出现的位置。
  • 如果没有则返回-1。

2. int indexOf (int ch, int fromIndex)

作用:

  • 从下标fromIndex位置开始找字符ch第一次出现的位置。(查找范围包括下标fromIndex)
  • 如果没有则返回-1。

3. int indexOf (String str)

作用:

  • 返回字符串str第一次出现的位置。
  • 如果没有则返回-1。
  1. int indexOf (String str, int fromIndex)

作用:

  • 从下标fromIndex位置开始找字符串str第一次出现的位置。(查找范围包括下标fromIndex)
  • 如果没有则返回-1。

【ch为int类型 是因为 填入的是对应该字符的字符编码值。】【如果formIndex的位置越界了,结果也只会返回-1,而不会抛出异常

cpp 复制代码
    public static void main(String[] args) {
        String s1 = "hello world ll";
        System.out.println(s1.indexOf('o'));        //找到的是"hello"的'o'
        System.out.println(s1.indexOf('o', 5)); //找到的是"world"的'o'
        System.out.println(s1.indexOf("ll"));       //找到的是"hello"的"ll"
        System.out.println(s1.indexOf("ll",5)); //找到的是最后面的"ll"
    }

三、lastIndexOf方法

1. int lastIndexOf (int ch)

作用:从后往前找,返回ch第一次出现的位置,没有返回-1。

2. int lastIndexOf (int ch, int fromIndex)

作用:从fromIndex位置开始找(查找范围包括下标fromIndex),从后往前找ch第一次出现的位置,没有返回-1。

3. int lastIndexOf (String str)

作用:从后往前找,返回str第一次出现的位置,没有返回-1。【返回的位置对应str中左边第一个字符】

  1. int lastIndexOf (String str, int fromIndex)

作用:从fromIndex位置开始找(查找范围包括下标fromIndex),从后往前找str第一次出现的位置,没有返回-1。【返回的位置对应str中左边第一个字符】

【如果formIndex的位置越界了,结果也只会返回-1,而不会抛出异常

cpp 复制代码
    public static void main(String[] args) {
        String s1 = "hello world ll";
        System.out.println(s1.lastIndexOf('o'));     //找到的是world的'o'
        System.out.println(s1.lastIndexOf('o', 5));  //找到的是hello的'o'
        System.out.println(s1.lastIndexOf("ll"));    //找到的是最后面的"ll"
        System.out.println(s1.lastIndexOf("ll",5));  //找到的是hello的"ll"
    }

结果值与使用indexOf方法时的结果刚好两两对换。

2.3 字符串的转化

一、大小写的转化

1. String toLowerCase ( )

作用:创造一个新的字符串并返回,新字符串中的字符都是原字符的小写形式

2. String toUpperCase ( )

作用:创造一个新的字符串并返回,新字符串中的字符都是原字符的大写形式

【都是无参的实例方法】

cpp 复制代码
    public static void main(String[] args) {
        String str = "Hello World!";
        String s1 = str.toLowerCase(); //全小写
        System.out.println(s1);
        String s2 = str.toUpperCase(); //全大写
        System.out.println(s2);
    }

二、数字/布尔值 ------> 字符串

方案1**【推荐】:使用++String类++ 的静态方法valueOf** 。【Stinrg类】
方案2:使用各++包装类++ 中的静态方法toString 。【其他包装类】

方案1的举例:

cpp 复制代码
    public static void main(String[] args) {
        //整型值转字符串
        int a = 123456789;
        String sa = String.valueOf(a);
        System.out.println(sa);
        //浮点值转字符串
        double b = 9876.54321;
        String sb = String.valueOf(b);
        System.out.println(sb);
        //布尔值转字符串
        boolean c = true;
        String sc = String.valueOf(c);
        System.out.println(c);
    }

【】

方案2的举例:

cpp 复制代码
    public static void main(String[] args) {
        System.out.println(Integer.toString(12345));    //Integer类:参数int
        System.out.println(Character.toString('a'));   //Character类:参数char
        System.out.println(Double.toString(543.21));   //Double类:参数double
        System.out.println(Boolean.toString(false));   //Boolean类:参数boolean
    }

【还有Short类、Long类 和 Float类】

三、字符串 ------> 数字/布尔值【其他包装类】

其他包装类可以用自己的toString方法把各自的类型转换为字符串类型,也可以通过自己的valueOf方法把字符串转换为自己的类型。

cpp 复制代码
    public static void main(String[] args) {
        int a = Integer.valueOf("123");
        System.out.println(a);
        double d = Double.valueOf("123.789");
        System.out.println(d);
        boolean b = Boolean.valueOf("true");
        System.out.println(b);
    }

【注意:只有Character类不能通过自己的valueOf方法把字符串类型转换为字符类型】


三、字符串 ------> 字符数组

char[ ] toCharArray ( )方法

作用:通过this调用toCharAarry方法,把字符串按顺序存入一个字符数组中,并返回该字符数组的引用。

【无参的实例方法】

cpp 复制代码
    public static void main(String[] args) {
        String s = "hello";
        char[] chars = s.toCharArray();
        for(char x: chars){ //输出数组chars的每个元素
            System.out.print(x+" ");
        }
    }

四、格式化

static String format(String format, Object... args)方法

作用:用法类似C语言中的printf函数(支持字符串、整数、浮点数、日期等的格式化),但并不会把格式化的结果输出到终端,而是直接返回格式化后的字符串。

例如:

cpp 复制代码
    public static void main(String[] args) {
        String s = String.format("%d__\n__%.2f__%s",98,12.347,"hello");
        System.out.println(s);
    }

2.4 字符 / 字符串的替换

一、replace方法

原型:String replace (char oldChar, char newChar)

作用:

  • 把字符串this中的字符oldChar全部替换 为字符newChar。
  • 原this字符串不变,返回一个新的字符串对象。
    二、 replaceAll方法

原型:String replaceAll(String regex, String replacement)

作用:

  • 把字符串this中出现过的regex小字符串,都用replacement小字符串替换。
  • 原this字符串不变,返回一个新的字符串对象。
    三、replaceFirst方法

原型:String replaceFirst(String regex, String replacement)

作用:

  • 把在字符串this中第一次出现的regex小字符串,用replacement小字符串替换。
  • 原this字符串不变,返回一个新的字符串对象。
cpp 复制代码
    public static void main(String[] args) {
        String str = "hello world lll";
        System.out.println(str.replace('l', 'L'));
        System.out.println(str.replaceAll("ll", " "));      //所有的ll替换为空格
        System.out.println(str.replaceFirst("ll", "999"));   //把第一次出现的ll替换成999
    }

2.5 截取一段字符串

substring方法

原型1:String substring (int beginIndex)

作用:

  • 从下标为beginIndex的位置开始截取字符串this,一直截取到结尾。
  • 原this字符串不变,返回一个新的字符串对象。

原型2:String substring (int beginIndex, int endIndex)

作用:

  • 从下标为beginIndex的位置开始截取字符串this,截取到下标为endIndex的位置。
  • 原this字符串不变,返回一个新的字符串对象。
cpp 复制代码
    public static void main(String[] args) {
        String str = "hello world";
        System.out.println(str.substring(3));
        System.out.println(str.substring(3,9));
    }

2.6 拆分整个字符串

split方法

原型1:String[ ] split (String regex)

原型2:String[ ] split (String regex, int limit)

作用:

  • 两者都是按照正则表达式regex将字符串拆分成若干个字符串,并返回一个字符串数组用来接收拆分后的字符串。

  • 原字符串this使用split方法后,自身并不会被改变。

  • 前者是把整个字符串全部分割;后者按照 limit 最多分割成 limit-1 组。
    补充1:正则表达式

  • :表示"正向的"、"标准的" 或 "符合规则的"。 :表示 "规则" 或 "规律"。合起来 就是 ------ "符合特定规则的文本模式" 。

  • 正则表达式是一种用于匹配、搜索、替换或提取文本中特定模式 的工具。它最初由数学家 Stephen Kleene 在1956年提出,用于描述一种被称为**"正则集合"** 的字符串集合。这类集合遵循特定规则,因此"正则"一词源于数学中的"规则性"。

  • 没有负则表达式 ,负则一词是俗语,通常被理解为正则表达式的否定操作 ,就类似于我们的取反操作。比如某个正则表达式是用来查找++符合A规则的字符串++ 的,那么它的否定形式(负则)就是查找++不符合A规则的字符串++。
    补充2:regex的使用

split是根据正则表达式拆分的,不过键盘上并不是所有字符都是特殊字符,下面列举了几个常见的字符(只是简单举例,并不完全)。

  • 仅代表字符本身的普通字符:'@' '#' '%' '=' ':' ';' '('英文单引号) ' '("英文双引号) ' ' / ' '~' '!' '&'
  • 具有匹配、搜索、替换或提取文本功能的特殊字符:

易错点:

  • 字符 & 仅表示字符,无需转义;而字符 | 表示逻辑或,若要仅表示字符需转义成 \\| 。
  • 正斜杠 / 是普通字符,无需转义; 而反斜杠 \ 是特殊字符,需转义。在Java字符串中,一个 \\ 才表示一个字符 \ ,所以在正则表达式中一个 \\\\ 才表示一个字符 \ 。

例1:根据单个字符分割。

cpp 复制代码
public static void main(String[] args) {
        String str1 = "apple,banana,orange";
        String str2 = "255.127.43.21";

        String[] ret1 = str1.split(",");   //逗号是普通字符
        String[] ret2 = str2.split("\\."); //点句号是特殊字符,需转义成 //.

        for (String x: ret1) System.out.println(x);
        System.out.println("================");
        for (String x: ret2) System.out.println(x);
    }

例2 :根据多个字符分割。用逻辑或 | 来表示多个字符,注意两边的字符不能用空格与 逻辑或| 隔开,否则split会根据空格分割。

cpp 复制代码
    public static void main(String[] args) {
        String str = "123\\456\\789/2025/05/01";
        String[] ret = str.split("/|\\\\"); //根据正斜杠/和反斜杠\来分割
        for (String x: ret)
            System.out.println(x);
        System.out.println("原字符串是:" + str);  //原字符串str不会被改变
    }

补充3:limit的取值

limit 参数可以是任意整数,包括负数。

  • 当 limit > 0 时:split方法会将字符串最多分割 limit-1 次,结果数组长度不超过 limit
  • 当 limit == 0 时:分割所有匹配项,并丢弃末尾的空字符串。
  • 当 limit < 0 时:分割所有匹配项,保留所有空字符串(包括末尾的空字符串)。

例如:

cpp 复制代码
    public static void main(String[] args) {
        String str = "1,,2,3,,4,5,,";

        String[] ret1 = str.split(",", 5); //分成5组,分割完第4个','就结束
        for(String x: ret1)
            System.out.println(x);

        String[] ret2 = str.split(",",0);    //丢弃后面的2个空字符串
        System.out.println(Arrays.toString(ret2));

        String[] ret3 = str.split(",",-1);   //保留所有空字符串
        System.out.println(Arrays.toString(ret3));
    }

2.7 其他特殊方法

1. String trim()方法

作用:

  • 用于去除字符串**两端的空白字符,**中间的空白字符会被保留。返回的是新对象。

  • 若字符串全为空白字符,返回空字符串""。

  • 空白字符包括空格、制表符\t、换行符\n、回车符 \r等 ASCII 空白字符。

  • 若对 null 调用trim方法,则会触发 NullPointerException异常。

例如:

cpp 复制代码
    public static void main(String[] args) {
        String str = "     hello world\t\n";
        System.out.print(str.trim());
    }

2. String strip()方法

作用:strip方法的功能与trim方法完全一致,但是strip方法支持更广泛的 Unicode 空白字符。
3. String repeat(int n)方法

作用:

  • 将原字符串重复n次后拼接成新字符串。
  • 原字符串不变。

例如:

cpp 复制代码
    public static void main(String[] args) {
        String str = "     hello world\t\n";
        System.out.print(str.strip().repeat(3));
    }

3. String类的不可变性

我们常说Java中String类字符串是不可变的。那么是什么不可变?还有为什么String类是不可变的?下面将为大家一一介绍:

3.1 String内部的成员 ------ 什么是不可变的?

简单来说,String 类不可变是指一旦一个 String 对象被创建,它的值(字符序列)就不可被修改。

上图是String类中最重要的3个成员。

数组value:它是字符串的底层存储载体,字符串中的字符按顺序存放在byte类型的数组value中。

coder :coder的值表示字节编码,它是在 String 对象创建时根据字符串内容自动确定

例如,当字符串中只有英文时,字符串采用 Latin-1 编码,此时coder = 0;当字符串中存在中文时,字符串采用 UTF-16 编码,此时coder = 1。

hash :当String类对象创建时,会首次计算对象的哈希值并把其存放到hash成员变量当中,后续要使用对象的哈希值时避免了重复计算

【其实还有一个布尔类型 的成员变量hashIsZero。因为hash在初始时默认是0 ,但是计算得出的哈希码也有可能为0 ,所以为了区分出这两种情况,于是使用了hashIsZero。在成员hash==0的前提下,当它等于false时,表示没计算过字符串的哈希码,触发上层hashCode()方法计算哈希值;当它等于true时,表示字符串哈希值的计算结果就是0】

所以具体来说,String类的不可变可以包括:value中存放的字符值和字符顺序不变、成员coder的值不变、成员hash的值不变......

《小总结》

String类不可变 是指,当一个String对象被创建(new)出来后,其内部的一切成员变量都不会再有改变。


3.2 String类为什么是不可变的?

这个问题的答案既有主观意愿,也有客观上的结构设计。

因(设计者的主观意图):

Java 创造者主动选择将 String 设计为不可变,原因包括:

  1. 安全性:防止恶意修改(如数据库连接字符串、文件路径被篡改)。

  2. 线程安全:不可变对象天然线程安全,无需同步。

  3. 哈希码缓存StringhashCode() 被频繁使用(如 HashMap 键),不可变性允许缓存哈希值。

  4. 字符串常量池优化:不可变性使得字符串可被复用。

【注意:String类对象 一定是线程安全的,但是String类型的引用变量不一定是线程安全的,两者要注意区分】

果(技术实现的结构):

String中有多个成员变量,设计思路都是差不多的,这里以value成员为例++【共性设计】++:

  1. final修饰value数组: final修饰value使得value的值不可变,意味着value不能指向另一个字符数组(或byte[ ]数组)。【对于coder和hash来说则是不能改变它们的值】
  2. private修饰value数组: value数组私有化后,我们不能在其他类中通过访问数组下标的方式直接修改value数组里面的内容
  3. 无setter方法和getter方法: 没有getter方法,我们就无法拿到value所指向的数组内存空间并修改;没有setter方法,我们也就不能修改这个数组内存空间的值。总的来说,没有这两个方法我们无法间接访问被保护的数组对象

除了针对成员变量的共性设计,还有其他的++特殊设计++:

  • 所有修改操作(具有修改功能的方法)的返回值都是新对象。(注意:旧的String类对象始终不变,但可以被抛弃,由JVM自动回收)
  • 禁止通过反射机制 破坏不可变性,否则会触发 InaccessibleObjectException 异常

***String类中所有方法的返回值都是新对象如何体现?***下面以字符串的拼接操作举例论证:

3.3 String对象的拼接

首先让我们回忆一下C语言中的strcat函数C语言字符串函数的讲解

char* strcat(char * dest, const char * src );

在C语言中,strcat的作用是把字符串src追加到字符串dest中,原本的dest字符串会被改变。

而我们java中String对象可以通过concat方法把两个字符串拼接过来,且不会改变原本的字符串。

String concat (String str)

cpp 复制代码
    public static void main(String[] args) {
        String str1 = "123";
        String str2 = str1.concat("abc");
        System.out.println(str2);
        System.out.println(str1); //str1依然是"123"
    }

**为什么concat方法为什么不会改变原本的字符串this呢?**让我们来看看它的源代码:

可以发现,concat方法最终会调用String类的构造方法,也就是说创建了新的String对象。我们接收到的是新的String对象,原本旧的字符串并没有改变。

在java中,我们还可以通过"+"操作来实现字符串的拼接,那么"+"操作也会创建新的String对象吗?

答案是肯定的。字符串的 + 操作实际上是语法糖,底层通过 StringBuilder(或 StringBuffer)实现。Java编译器 会将单行内的多个"+"操作自动转换为StringBuilderappend()调用,减少中间对象的生成。例如:

cpp 复制代码
String s = "A" + "B" + "C";
等效于:
String s = new StringBuilder().append("A").append("B").append("C").toString();

对于这里"A+B+C",编译器会自动转化为StringBuilder类的append操作。先是创建出StringBuilder对象,使用append方法对同一个字符串进行拼接操作;最后通过toString方法,根据StringBuilder对象来生成一个字符串一样的String类对象。

【tips:String方法返回新对象且不改变原对象,这一现象还在字符串拆分方法中有体现。比如C语言的字符串拆分函数是strtok ,会对原字符串进行修改;但java中的split方法并不会改变原字符串。这里不详细展开介绍】


3.4 补充

0. 字符集标准与编码方式

常见的编码方式归纳:

ASCII 编码 : ascii 是++首个广泛标准化的单语言编码方案++ 。它既是传统的字符集标准,也是一种编码方式。字符总数是128个,包括33个控制字符、部分基本符号、数字字符 和 全部英文字符

ISO 标准(系列): ISO 是一种字符集标准旨在统一多语言编码 ,但ISO本身不是具体的编码方式 。目前最新版本的ISO 10646 通用字符集 (根据其定义的编码方式有UCS-2和UCS-4) 已与Unicode同步扩展。ISO 字符集标准系列下的常见编码方式有:

  • ISO 646 :与ASCII基本一致,但允许部分符号(如$@)根据国家需求替换为本地字符(如£§)。ASCII可视为ISO 646的美国本地化版本
  • Latin-1 [属于ISO 8859系列] :扩展了ASCII以支持西欧语言 。字符总数是256个前面128个字符与 ASCII 完全一样 ,后面是西欧语言扩展字符(如ç, ñ, , ©等)。

Unicode 标准: Unicode是目前全球范围内最常用的 字符集标准。ISO 和 Unicode 联盟在1991年达成协议,将ISO 10646Unicode码点定义统一 。Unicode 是实践中的主流标准,而 ISO 10646 是其国际标准化版本,两者共同覆盖全球字符需求。Unicode 字符集标准下的常见编码方式有:

  • UTF-8:每个编码单元是8位比特位(1个字节),完全兼容ASCII。UTF-8 将 Unicode 码点转换为1~4 个字节,例如:英文占1个字节 ,拉丁文占2个字节,中文占3个字节,辅助平面字符( emoji表情包 )占4个字节。对英文高效,中文略冗余。
  • UTF-16:每个编码单元是16位比特位(2个字节),不兼容 ASCII,但适合内存操作。UTF-16 将 Unicode 码点转换为 2 或 4 字节,例如:英文和中文都是2个字节,辅助平面字符( emoji表情包 )占4个字节。中文处理高效,英文浪费空间。

GB 系列: GB系列是指中国国家标准中围绕汉字编码逐步扩展的字符集规范,(GB是"国标"拼音缩写) 主要包括以下三个核心标准:

  • GB2312:首个简体中文编码标准,发布时间是1980年,基本简体中文支持。编码方式是双字节编码。

  • GBK:发布时间是1995年。扩展了生僻字、繁体字。编码方式仍然是双字节编码,但范围有很大扩展。

  • GB18030:定位是GBK的终极扩展,发布于2000年,更新于2005年。支持多语言,支持Unicode的所有字符 。编码方式是 1 / 2 / 4 字节变长编码。

1. 字符串长度与字符数组长度

提问:使用 String对象的length()方法 取得的字符串长度与通过 getBytes().length的方式取得的数组长度,这两者的长度有什么区别?

回答这个问题之前,要补充几个知识。

++补充1:coder的诞生++

在 Java8 以及更低的版本,数组value是char[ ]类型的而不是byte[ ]类型,而且当时并没有成员coder。现在使用coder的原因是:

  • 旧版本使用的是char[ ]数组,所以无论存储哪种字符都占用2个字节,如果字符串中全是英文的话(英文字符只占1个字节),那么将会浪费50%的内存。

  • 现版本采用byte[ ]数组和coder的组合,大大减少了内存空间的浪费。
    ++补充2:coder的取值、内部编码和外部编码++
    内部编码:

  • 定义:程序在内存中表示字符时使用的编码方式,直接决定字符串在内存中的存储格式。

  • Java中只使用Unicode码中的 Latin-1UTF-16这两种编码方式作为内部编码。

  • coder的取值只有 01,因为Java采用的内部编码只有 Latin-1 和 UTF-16。

外部编码:

  • 定义:数据在**硬盘存储(如文件、数据库)传输(如网络、API)**时的字节序列表示方式。
  • 外部编码方式的使用由开发者指定。

补充了上述2个知识点,让我们正式解答刚才的问题 ------ String对象的length()方法取得的大小与getBytes().length取得的大小有什么区别?

String对象的length()方法的内部逻辑,即我们用length()方法取得的长度是 value数组的长度右移coder位的结果(value.length >> coder)

java中的源码:

  • 当字符串中只有英文字母时,采用 Latin-1编码,每个字符占一个字节,此时coder等于0。所以length()得到的大小就是value.length
  • 当字符串中含有中文时,采用 UFT-16编码,每个字符占两个字节,此时coder等于1。所以length()得到的值是value.length >> 1,也就是value数组大小的二分之一

getBytes()方法的作用

  • 无参方法:把String字符串按照默认的编码方式转换为字节序列(byte[ ]),Java中的默认编码方式是UTF-8
  • 有参方法getBytes(Charset charset):把String字符串按照给定的编码方式charset转换为字节序列(byte[ ])。

例如:

cpp 复制代码
    public static void main(String[] args) {
        String str = "ab你好cd";        //有中文,采用UTF-16编码
        System.out.println(str.length());       //结果是value.length >> 1
        System.out.println(str.getBytes().length);      //把str转换为UTF-8编码
        System.out.println(str.getBytes(StandardCharsets.UTF_16BE).length); //把str转换为UTF-16编码
    }

由于字符串str中含有中文,所以内部编码采用了UTF-16,coder等于1。UTF-16中每个字符都是2个字节,"ab你好cd" 总共6个字符,所以字节数组value的大小是12str.length()的返回值 是value.length >> 1,也就是6

getBytes()将字符串转化成以UTF-8为编码方式的字节序列 。在UTF-8中,英文是1个字节,中文是3个字节。"ab你好cd" 中有4个英文字符,2个中文字符,加起来就是 1 * 4 + 3 * 2 = 10个字节

getBytes(StandardCharsets.UTF_16BE)将字符串转化成以UTF-16为编码方式的字节序列。所以字节数列的大小是6 * 2 = 12个字节。

总的来说,str.length() 与 str.getBytes().length 的区别,本质上是不同编码方式的区别

  • 前者length()涉及到的编码方式只有 Latin-1 和 UTF-16(内部编码)。且由于" >> coder"操作,使得length()最终表达的是字符串一共有多少字符【不分英文和中文】。
  • 后者getBytes()涉及的编码方式有非常多种(内部编码 + 外部编码)。所以getBytes().length真正表达的是字节序列一共有多少字节
2. 字符串常量池 与 intern()方法

字符串常量池的作用:

字符串常量池是 Java 设计的一种内存优化机制 ,旨在通过共享不可变字符串对象 ,减少内存开销和提升性能。其核心意义是避免重复创建相同字符串,即相同内容的字符串在内存中仅保留一份

注意:字符串常量池只存储String对象,其他字符串类对象(如StringBuilder、StringBuffer)不能入池。

字符串常量池所在内存空间:

Java6 及之前:存在于方法区 ,常量池的字符串对象并不会被回收,可能导致内存溢出,触发 OutOfMemoryError: PermGen

Java7 及之后:现存在于堆内存中,可回收未引用的字符串。

字符串常量池的初始化与内存回收:

  • JVM启动时会加载一些核心类库,这些核心类中所使用到的字符串常量会被自动放入字符串常量池中。也就是说,执行到main方法的时候,字符串常量池里面并不是空的
  • 即使程序运行中,若字符串常量池中的字符串无引用 (如无静态引用或全局变量持有),JVM的垃圾回收机制会回收这些字符串。
  • 程序结束时,JVM进程退出,操作系统会回收其所有内存(包括堆、栈、元空间等),字符串常量池作为堆内存的一部分自然被释放
    ++补充:字面量(字面常量) 和 final常量++

字面量(字面常量):字面量是代码中的定值,表示数据的具体值。它们是源代码中对数据值的直接表示,无需通过变量或计算来定义

字面常量就是字面量 ,它只不过是字面量的另一种表述称谓,强调其不可变性 。在 Java 中,所有字面量本身就是常量值,无法被修改。】

final常量:是指通过关键字 final 定义的变量,其值只能赋值一次,赋值后不能被改变。

【字面量就是常量 ,而final常量是不可变的变量。】

问题:字符串常量池的入池机制是怎样的?

入池机制大致可以分为2种,一种是自动入池,一种是手动入池。

1、自动入池

字符串字面量: 直接使用双引号("...")定义的字符串会自动存入常量池(若池中不存在相同内容)。

cpp 复制代码
    public static void main(String[] args) {
        String s1 = "java";     //第一次出现,自动加入常量池中
        String s2 = "java";     //第二次出现,直接引用常量池中的字符串对象
        System.out.println(s1 == s2);   //两者相同
    }

编译时的"+"操作优化: 编译期可确定的常量表达式(如 "a" + "b")会被折叠为单一字面量,直接存入常量池。

cpp 复制代码
    public static void main(String[] args) {
        String s1 = "ja" + "va";     //编译时拼接的结果是"java",加入常量池中
        String s2 = "java";         //直接引用常量池中的字符串对象
        System.out.println(s1 == s2);   //两者相同
    }


注意:只有 常量表达式final字符串变量才会被自动入池,如果 "+"号其中一边是变量,那么会生成一个新的String对象且不在常量池中。

情况一:操作数是字面量或 final 常量------ 编译器会直接拼接并优化为常量,结果存入常量池。(包括字符串字面量 + 非字符串字面量的情况)

情况二:操作数包含非 final 变量------ 编译器将 + 转换为 StringBuilder 操作,在运行时动态生成新对象。

使用new String("...")的方式创建String对象,它只存在于堆内存中,但不在字符串常量池中

cpp 复制代码
    public static void main(String[] args) {
        String s1 = new String("java");     //在堆内存中开辟一个String对象,内容是"java"
        String s2 = new String("java");     //在堆内存中再开辟一个对象,内容也是"java"
        System.out.println(s1 == s2);   //两者不同
    }

如果想让new出来的对象入池,就要使用intern()方法。

  1. 使用intern()方法手动入池

intern()方法的逻辑:

  • 若常量池中已存在相同内容的字符串,直接返回池中对象的引用

  • 若不存在,将当前字符串的引用添加到常量池(Java 7+ 后常量池位于堆中,存储引用而非对象拷贝)。

cpp 复制代码
    public static void main(String[] args) {
        String s1 = new String("java").intern();     //检查内容,池中无相同字符串内容,保存该内容的引用
        String s2 = new String("java").intern();     //检查内容,返回相同内容的对象引用(s1的引用)
        String s3 = "java";                                  //s1的内容已入池,共享该引用
        //三者都相同
        System.out.println(s1 == s2);
        System.out.println(s1 == s3);
    }

补充: 对于非String类字符串的入池 ,要先通过toString()方法转变为String类字符串,然后再使用intern()方法入池。例如:String str = new StringBuilder("java").toString().intern()

intern方法让new出来的String对象保留1个并转移到字符串常量池中,剩下大String对象全部回收,其引用变量全部都指向常量池中的唯一引用。不过有String类的不可变性在,不用担心该唯一引用会被改变。

3. "+"操作与concat操作的区别

"+"操作是Java的语法糖,该操作会被编译器优化。这使得"+" 与 concat()方法之间有4个微小的区别。

1. 对null的处理

+ 操作符: 如果操作数是 null,会将其转换为字符串 "null"

cpp 复制代码
    public static void main(String[] args) {
        String s1 = "hello" + null;
        System.out.println(s1);
    }

concat( )方法: 如果参数是 null,会抛出 NullPointerException

cpp 复制代码
    public static void main(String[] args) {
        String s2 = "hello".concat(null);
        System.out.println(s2);
    }


2. 对空字符串("")的处理

+ 操作符: 在常量表达式下,不会生成新对象,仍然使用字符串常量池中的唯一引用;在非常量表达式下,在编译期间转化为StringBuilder操作,最终会生成新对象。(两种情况)

cpp 复制代码
    public static void main(String[] args) {
        String s0 = "hello";
        String s1 = "hello" + "";   //仍是常量池引用
        String s2 = s0 + "";        //转化为StringBuilder操作,生成新对象
        System.out.println(s1 + "||" + s2);
        System.out.println(s0 == s1);   //相同
        System.out.println(s0 == s2);   //不同
    }

concat( )方法: 参数是空字符串 ""时,直接返回原字符串对象,无新对象生成(一种情况)

cpp 复制代码
    public static void main(String[] args) {
        String s0 = "hello";
        String s1 = s0.concat("");
        String s2 = "hello".concat("");
        System.out.println(s1 + "||" + s2);
        //无新对象的产生,均为s0的引用
        System.out.println(s0 == s1);
        System.out.println(s0 == s2);
    }


3. 对非String类的处理

+ 操作符: 支持任意类型的操作数,非 String 类型会隐式调用 toString() 转换为字符串

cpp 复制代码
    public static void main(String[] args) {
        String str = "hello" + false;
        System.out.println(str);
    }

concat( )方法: 只能接受 String 类型的参数,String 类型会导致编译错误


4. 能否进入常量池

+ 操作符: 如果操作数是编译时常量(如字面量或 final 常量),结果会被编译器优化为常量,并进入常量池

concat( )方法: 无论参数是否是常量,结果都是运行时生成的新字符串对象,不会进入常量池

二、StringBuilder和StringBuffer

刚刚说了这么久的String类字符串是不可变字符串,那有没有可变的字符串?

答案是肯定的,我们最常用的可变字符串有StringBuilder和StringBuffer。

0. 可变性的由来

StringBuilderStringBuffer ,两者均继承自AbstractStringBuilder 父类,该类维护的有字节数组value是可变的。

要回答 StringBuilderStringBuffer 为什么是可变的,那就要解答 AbstractStringBuilder为什么是可变的。

AbstractStringBuilder 中比较重要的三个成员:字节数组value、coder 和 count。

其中AbstractStringBuilder的数组value和coder的作用 与 在String中的value和coder 一样,都是value存储字符串序列,coder记录编码方式。但AbstractStringBuilder类中多了count,而count的作用是用来记录字符串长度的。

AbstractStringBuilder 字符串可变性的4种体现(由来):

  1. 字节数组value非私有,且非final,允许动态扩容。这为value数组的修改提供了前提条件。
  2. 初始coder默认为0,采用 Latin-1 编码;当有中文加入时,corder会自动升级为1 ,改为 UTF-16 编码。【注意:coder只能自动升级,不会主动降级。即使删除了字符串中的所有非Latin-1 字符,字符串仍然采用UTF-16编码,coder不会从1变回0。】
  3. 通过方法(如 append()insert())直接修改原数组内容,不生成新对象
  4. count 记录字符串长度,当遇到修改字符串的操作时(如 append()、deletetCharAt()),API方法会动态更新count的值

源码示例:

1. 两者的异同

相同点:

  • 均继承自 AbstractStringBuilder ,底层使用动态扩容的字节数组(byte[] value)实现可变性。
  • 方法数量几乎完全相同,对应的方法功能一致
  • 可变字符串的对象都是在堆内存上,而不在字符串常量池中 。而且通过toString()方法转化成String对象后,也不会加入到字符串常量池中
    不同点:

StringBuilder :无同步机制,非线程安全 ,适用于单线程场景。无锁设计,单线程下性能显著优于StringBuffer。

StringBuffer :所有公共方法使用 synchronized 修饰,保证多线程环境下的线程安全 。因同步锁机制,操作效率较低。

2. 常用功能方法(以StringBuilder为例)

2.0 长度 与 容量

int length( ) 方法

作用:通过this.length()的方式获取字符串的长度。

源码:

我们注意到可变字符串的length()返回的是count,而不是value.length >> coder。这是为什么?

String类中value.length严格等于字符串序列的长度,这是由

由于**AbstractStringBuilder会有**"2倍"扩容操作,value.length的长度有可能大于字符串序列的长度,所以不能再使用value.length() >> coder来求字符串的长度。

int capacity( )方法

作用:获取底层保存字符串空间总的大小。

源码:

capacity()方法内部就是我们熟悉的value.length >> coder,反映的是value数组可以装多少个字符。例如:

cpp 复制代码
    public static void main(String[] args) {
        StringBuilder ss = new StringBuilder("hello");
        System.out.println(ss.length());
        System.out.println(ss.capacity());
    }

可以看到,这里字符串"hello"只有5个字符,而当前的value数组中可以存放21个字符。

可变字符串中的value数组遇到追加操作时 (如append()、insert()方法),如果内存不够会自动触发扩容。当然,我们也可以手动触发扩容操作。

void ensureCapacity(int mininmumCapacity)方法

作用:确保 value 的容量至少为 minimumCapacity。如果当前容量小于 minimumCapacity,则触发扩容。mininmumCapacity的单位是字符,而不是字节,与内部编码和coder有关。

  • 扩容规则

    • 默认扩容策略:旧容量 * 2 + 2。

    • 如果 minimumCapacity 远大于默认策略,则直接扩容到 minimumCapacity

cpp 复制代码
    public static void main(String[] args) {
        StringBuilder ss = new StringBuilder();
        System.out.println(ss.capacity());      //当前容量为16
        ss.ensureCapacity(20);   //16 < 20,触发扩容
        System.out.println(ss.capacity());      //2*16+2,扩容成34

        ss.ensureCapacity(100);  //100远大于34,直接扩容成100
        System.out.println(ss.capacity());      
    }

2.1 可变字符串与不可变字符串的转化

如果String字符串要转成可变字符串,可以使用带Sting类型参数的StringBuilder(或StringBuffer)构造方法。

而StringBuilder(或StringBuffer) 转化成 String对象时,也可以调用含参的String构造方法。不过我推荐使用toString()方法。

String toString() 方法:把StringBuilder(或StringBuffer) 对象 转化成 String对象。

cpp 复制代码
    public static void main(String[] args) {
        String str1 = "hello";
        StringBuilder ss = new StringBuilder(str1); //String转成StringBuilder

        String str2 = ss.toString();   //toString方法转成String
        System.out.println(str2);

        String str3 = new String(ss);   //使用String构造方法转换
        System.out.println(str3);
    }

2.2 字符串的追加(拼接、插入)

StringBuilder append(参数1) 方法

作用:

  • 在尾部追加内容,相当于String的+=。
  • 可以追加的 内容(参数1的)类型 如下:
    • 基本类型:所有基本类型都可以被拼接(如:boolean、double、int )。
    • 引用类型:拼接时会调用对象的toString()方法,拼接前建议重写toString()方法。
    • 数组类型:只有 char[ ] 数组可以被拼接,其他数组都不可以。如需拼接其他类型的数组,则要通过手动遍历的方式拼接。
    • 其他字符序列:实现了CharSequance接口的类,如String、StringBuilder、StringBuffer。
  • StringBuilder拼接StringBuffer的内容后,拼接结果不会有同步锁保护;StringBuffer拼接StringBuilder的内容后,拼接结果将会受到同步锁保护。
  • append方法不能拼接空引用null。
cpp 复制代码
    public static void main(String[] args) {
        StringBuilder ss = new StringBuilder("hello ");
        ss.append(100);     //整型
        ss.append(5.56);    //浮点型
        ss.append(true);    //布尔类型

        ss.append(new Dog("小狗",7));  //引用类型
        char[] arr = {'a','b','c',' '};         //char[]数组
        ss.append(arr);
        StringBuffer sbf = new StringBuffer("hello");  //StringBuffer对象
        ss.append(sbf);
        System.out.println(ss);
    }

StringBuilder insert(int offset, 参数2) 方法

作用:

  • 在offset位置的前面插入字符串str。
  • 插入位置位于 下标offset-1 与 下标offset 之间。
  • offset 越界(如负数或超过当前长度),抛出 StringIndexOutOfBoundsException
  • 能插入的 (参数2)内容为:基本类型、引用类型、char[ ]数组 和 其他字符序列。【这里的要求与append()方法一致】
cpp 复制代码
    public static void main(String[] args) {
        StringBuilder ss = new StringBuilder("HelloWorld");
        ss.insert(5, new Dog("小狗",7));  //5位置是'W',所以插入的位置位于'o'和'W'之间
        System.out.println(ss);
    }

2.3 字符/字符串的查找

字符的查找

char charAt(int index)方法

作用:

  • 获取 下标为index位置的字符。(对原字符串无影响)
  • 下标index越界会触发 StringIndexOutOfBoundsException 异常。
cpp 复制代码
public static void main(String[] args) {
        StringBuilder stringBuilder = new StringBuilder("hello");
        char ch = stringBuilder.charAt(4);  //获取下标为4的字符
        System.out.println(ch);
        ch = stringBuilder.charAt(0);   //获取下标为0的字符
        System.out.println(ch);
    }

字符串的查找

int indexOf(String str)方法

作用:

  • 从最左边开始,向右查找字符串str第一次出现的位置。

int indexOf(String str, int fromIndex)方法

作用:

  • 从fromIndex位置开始,向右查找字符串str第一次出现的位置。
    int lastIndexOf(String str)方法

作用:

  • 从最右边开始,向左查找字符串str第一次出现的位置(返回最后一次出现str的位置)
  • 返回值是str中的第一个字符对应的位置。

int lastIndexOf(String str, int fromIndex)方法

作用:

  • 从fromIndex位置开始,向左查找字符串str第一次出现的位置。(从fromIndex位置开始找str最后一次出现的位置)
  • 返回值是str中的第一个字符对应的位置,该首字母所在的查找范围是[0,fromIndex],该str所在的查找范围是[0,fromIndex + str.length() - 1]。
  • 如果fromIndex大于字符串的长度,那么查找效果与lastIndexOf(String str)方法一样。

注意:对于上述查找方法,如果找不到对应的字符串str,都是返回-1。

cpp 复制代码
    public static void main(String[] args) {
        StringBuilder stringBuilder = new StringBuilder("hello world"); //空格位置下标为5
        System.out.println(stringBuilder.indexOf("ll"));                      //结果是左边第一个l的位置
        System.out.println(stringBuilder.indexOf("ll", 5));     //找不到
        System.out.println(stringBuilder.lastIndexOf("ll"));             //结果是左边第一个l的位置
        System.out.println(stringBuilder.lastIndexOf("ll",2)); //结果是左边第一个l的位置
    }

2.4 字符/字符串的替换

字符的替换

void setCharAt(int index, char ch)方法

作用:

  • 将index位置的字符设置为字符ch。
  • 会改变原字符串。
cpp 复制代码
    public static void main(String[] args) {
        StringBuilder stringBuilder = new StringBuilder("hello");
        stringBuilder.setCharAt(2,'p'); //把第一个l改成p
        System.out.println(stringBuilder);
    }

字符串的替换

StringBuffer replace(int start, int end, String str)方法

作用:

  • 将[start, end)位置的字符替换为str。(注意是开区间)
  • 原字符串中 被替换部分的长度,是在 [start, end)区间长度 和 字符串str的长度 之间取最大值,即max{ (end - 1) - start,str.length() }。
  • 如果end超出右边界,则会自动优化为length();但start不能超出两边边界,否则会触发**StringIndexOutOfBoundsException异常。**
  • 返回this,支持链式调用。
cpp 复制代码
public static void main(String[] args) {
        StringBuilder ss = new StringBuilder("aabbccdd");
        System.out.println(ss.replace(1,7, "java"));
    }

2.4 字符/字符串的删除

字符的删除

删:StringBuffer deleteCharAt(int index)方法

作用:

  • 删除index位置的字符。
  • 会改变原字符串,++返回值就是原字符串++。(支持链式调用)
cpp 复制代码
    public static void main(String[] args) {
        StringBuilder s1 = new StringBuilder("hello");
        StringBuilder s2 = s1.deleteCharAt(2);  //删掉第一个l
        System.out.println(s1);
        System.out.println(s2);
        System.out.println(s1 == s2);   //s2的引用与s1相同
    }

字符串的删除

删:StringBuffer delete(int start, int end)方法

作用:

  • 删除[start, end)区间内的字符。(注意是开区间)
  • 原字符串被修改,返回值就是原字符串,支持链式调用。
  • start和end使用时最好不要超出边界,容易触发**StringIndexOutOfBoundsException异常。**
cpp 复制代码
    public static void main(String[] args) {
        StringBuilder ss = new StringBuilder("aabbcc");
        System.out.println(ss.delete(3,6)); //被删字符对应的下标有3、4、5
    }

2.5 字符串的截取

String substring(int start)方法

作用:

  • 截取的子字符串从start下标开始(包括start),一直到原字符串末尾,范围是[start, this.length() )。
  • start不能越界,当start == this.length()时,返回空字符串"";当start > this.length()时,触发**StringIndexOutOfBoundsException异常。**

String substring(int start,int end)方法

作用:

  • 截取的子字符串从start下标开始(包括start),一直到end-1位置,范围是[start, end)。
  • start和end都不能越界,end不能小于start。当end == start时,返回值是空字符串""。
cpp 复制代码
    public static void main(String[] args) {
        StringBuilder ss = new StringBuilder("HelloWorld");
        int len = ss.length();
        System.out.println(ss.substring(len));          //空字符串
        System.out.println(ss.substring(len-1));  //最后一个字符"d"
        System.out.println(ss.substring(len+1));  //抛出异常
    }
cpp 复制代码
    public static void main(String[] args) {
        StringBuilder ss = new StringBuilder("HelloWorld");
        System.out.println(ss.substring(5));//范围[5,ss.length())
        System.out.println(ss.substring(0, 5));  //范围[0,5)
        System.out.println(ss.substring(5, 5));  //截取的是空字符串""
        System.out.println(ss.substring(5, 6));  //截取的是下标为5的字符'W'
    }

2.6 字符串的反转

StringBuffer reverse()方法

作用:将整个字符串反转。(支持链式调用)

reverse()方法常用于判断回文、数字逆序、字符串对称操作......

cpp 复制代码
    public static void main(String[] args) {
        //判断回文
        String s1 = "一二一";
        String s2 = new StringBuilder(s1).reverse().toString();
        System.out.println(s1.equals(s2));
        //数字逆序
        StringBuilder ss = new StringBuilder("1234");
        System.out.println(ss.reverse());
        //字符串对称操作
        String str = "abc";
        str += new StringBuilder(str).reverse();
        System.out.println(str);
    }

三、总结

3.1 两类字符串的成员差异

  1. 存储字符的数组value
  • String类:由 final 和 private修饰,没有提供数组value的修改方法。
  • 可变字符串:包级私有(default权限),有可以修改数组value的方法 (如append、insert)。
  1. 确定编码方式的coder
  • String类:0表示 Latin-1 编码,1表示 UTF-16 编码,且永久不变
  • 可变字符串:取值同上,但可以自动升级转码。
  1. 字符串对象的哈希码hash
  • String类:有hash成员 ,且由于String的不可变性,使得hash可以长期保存 方便后续复用
  • 可变字符串:没有hash成员 ,且StringBuilder和StringBuffer均没有重写Object类中的hashCode()方法。如果需要基于字符串内容的哈希码,可以通过**" toString().hashCode() "** 的方式获得,每次进行操作都会经过完整的哈希码计算过程。
  • 补充:可变字符串没有成员hash,自然也没有布尔类型的成员hashIsZero。
  1. 字符串的长度count(单位是字符)
  • String类:没有成员count ,数组value的大小严格等于字符串序列的大小。
  • 可变字符串:有成员count ,因为数组value的大小在大部分情况下都大于字符串序列的大小。

3.2 两类字符串的方法异同

0. 返回值的引用

String类:一切涉及修改字符串的方法,返回值都是new出来的新对象

可变字符串:一切涉及修改字符串的方法,返回值都是this字符串本身
1. 长度

String类:

  • length():返回固定长度,与数组value的长度强相关。

可变字符串:

  • length():返回当前内容的长度(可动态变化),与数组value的长度相关较弱。
  • capacity()方法 和 ensureCapacity()方法与数组value的容量强相关。
    2. 比较

String类:

  • equals方法:判断两字符串内容是否相等。
  • compareTo方法:比较两字符串的大小。
  • compareToIgnoreCase方法:忽略字符串大小后,比较两字符串的大小。

可变字符串:无比较类方法。
3. 查找

相同点:

  • 都有3个方法------charAt:按字符内容查下标;indexOf:按下标查内容(从左往右查);lastIndexOf:按下标查内容(从右往左查)。

不同点:

  • String类:index和lastIndexOf都能查找字符和字符串
  • 可变字符串:index和lastIndexOf只能查字符串,不支持直接查找字符。(需将字符转为字符串调用)
    4. 转化

String类:

  • 大小写的转化: toLowerCase方法 和 toUpperCase方法。
  • 向String转化:
    • 引用类型:可以重写toString方法,常用于打印对象。
    • 基础类型:可以使用对应包装类的toString静态方法,也可以使用String类中的valueOf静态方法。
  • 向字符数组char[ ]转化:可通过toCharArray()方法直接转换。(char类型是2个字节的,只使用UTF-16)
  • 字符串格式化:通过format方法可以返回格式化后的字符串。

可变字符串:

  • 大小写转化的方法。
  • StringBuffer / StringBuilder 向String类型转化:可以使用new String(参数)的方式转化,也可以使用toString实例方法 (this调用)。
  • 不能直接向char[ ]数组转化,可以先通过toString方法转变成String类型再调用toCharArray()方法。
  • 不可格式化。
    5. 替换

【两类字符串的替换方法,除了++都有一个叫"replace"的方法++ ,其他全是差异 (包括两种replace方法的功能)

String类:

  • 字符的替换 ------ replace方法:共2个参数,参数1是旧字符,参数2是新字符。功能是把字符串中所有的旧字符都替换成新字符。生成新字符串。
  • 字符串的替换:
    • replaceAll:参数共2个,参数1是正则表达式,参数2是新字符串。功能是把原字符串中满足正则表达式的子字符串,全都替换成新字符串。
    • replaceFirst:参数共2个,参数1是正则表达式,参数2是新字符串。功能是把原字符串中满足正则表达式的第一个子字符串,替换成新字符串。

可变字符串:

  • 字符的替换 ------ setCharAt方法:共2个参数,参数1是目标位置,参数2是新字符。功能是把目标位置的字符替换成新字符。很精确,只替换一个位置 的字符。修改原字符串。
  • 字符串的替换 ------ replace方法:共3个参数,参数1与参数2构成替换区间,参数3是新字符串。功能是用新字符串替换整个替换区间 的内容。不支持正则,需手动处理。
    6. 截取

相同点: 两类字符串的截取方法都叫subString,都有 单参数版本 和 双参数版本,而且返回值都是新String对象。

【没有不同点】
7. 拆分

不同点:只有String类内置了split方法把字符串拆分成多个子字符串,返回一个String[ ]数组。可变字符串没有拆分方法,需要先转成String类再使用split方法。
8. 追加

String类:

  • 拼接:
    • 通过" + "操作进行拼接:支持多种类型的拼接,但不支持对char[ ]数组内容的直接拼接,而是默认调用数组的toString()方法 ,其他数组也是如此。当操作数是空引用null时,会自动转成字符串"null"
    • 通过concat方法进行拼接,仅支持String类型的参数。
  • 插入 ------ 无插入方法。

可变字符串:

  • 拼接:
    • 通过append方法进行拼接:支持多种类型的拼接,支持对char[ ]数组内容的直接拼接 ,但其他数组的拼接也是默认调用toString()方法 ;参数不能为空引用null。
  • 插入:
    • 通过insert方法,可以在指定位置进行多种类型的插入。同样不支持空引用null参数。
      9. 删除

不同点:只有可变字符串 内置了 deleteCharAt方法detele方法, 可以对字符或子字符串 进行删除操作。String类中无删除方法。
10. 反转

不同点:只有可变字符串 内置了 reverse 方法, 可以整个字符串进行反转操作。String类中无反转方法。

3.3 字符串 与 方法传参【易错】

在 Java 中,String 类型与 StringBuilder/StringBuffer 类型在方法传参时有显著的行为差异。

若当前方法的参数为String类型:虽然刚开始时方法外部的引用 与 方法内部参数的引用是同一个对象,但要是该方法中涉及字符串的修改由参数接收,那么该过程中产生了新String对象。此时方法外部引用与方法内部参数的引用 不再是同一个对象 了。

若当前方法的参数为 StringBuilder/StringBuffer 类型:如果当前方法涉及字符串的修改,返回值都是this,所以方法外部的引用 与 方法内部参数的引用 始终是同一个对象

总结:

在方法传参时,String类型可以当做局部变量处理, StringBuilder/StringBuffer 类型可以当做全局变量处理。

我们设计一个func方法,它可以递归调用使得可以在字符串后面拼接n次的"&"。让我们看看String类型与可变字符串类型在传参时有什么不同:

cpp 复制代码
public class Test {
    public static String func(String str, int n){
        if(n == 0)  return str;

        str += "&";
        func(str, n-1);
        return str;
    }

    public static StringBuilder func(StringBuilder ss, int n){
        if(n == 0) return ss;

        ss.append("&");
        func(ss, n-1);
        return ss;
    }

    public static void main(String[] args) {
        String str = "a";
        StringBuilder ss = new StringBuilder("b");
        func(str, 3);
        func(ss,3);
        System.out.println(str);
        System.out.println(ss);
    }
}

可以看到,String类型str在调用函数func后没有任何变化;而StringBuilder类型ss在调用函数func后成功在其后面添加了3个&。

把两类字符串在传参时看作不同类型的变量,这种思想在算法题中尤为常见,可以重点掌握。


本期分享完毕,感谢大家的支持Thanks♪(・ω・)ノ。。

相关推荐
Java程序员-小白21 分钟前
使用java -jar命令指定VM参数-D运行jar包报错问题
java·开发语言·jar
ClearViper31 小时前
Java的多线程笔记
java·开发语言·笔记
全栈凯哥2 小时前
Java详解LeetCode 热题 100(17):LeetCode 41. 缺失的第一个正数(First Missing Positive)详解
java·算法·leetcode
神经毒素2 小时前
WEB安全--Java安全--LazyMap_CC1利用链
java·开发语言·网络·安全·web安全
逸夕2 小时前
httpclient请求出现403
java
呆呆洁ᵔ·͈༝·͈ᵔ3 小时前
配置集群-日志聚集操作
java·ide·eclipse
lyrhhhhhhhh3 小时前
Spring 模拟转账开发实战
java·后端·spring
banzhenfei3 小时前
xp_cmdshell bcp 导出文件
java·数据库·sql
带刺的坐椅3 小时前
SpringBoot3 使用 SolonMCP 开发 MCP
java·ai·springboot·solon·mcp
胡斌附体3 小时前
微服务调试问题总结
java·微服务·架构·调试·本地·夸微服务联调