【Java SE】认识String类
文章目录
一、String类的重要性与基本概念
1.1 为什么需要String类
在C语言中,字符串是通过字符数组或字符指针来表示的,需要借助标准库函数(如strlen、strcpy等)进行操作。这种方式存在几个问题:
- 数据和操作分离,不符合面向对象封装的原则
- 需要手动管理内存,容易造成内存泄漏或越界访问
- 功能有限,缺乏现代字符串处理所需的高级功能
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() 可以主动将字符串对象加入到字符串常量池中,并返回常量池中的引用。
原理:
- 检查常量池:调用
intern()时,JVM 会先检查字符串常量池里是否已经存在一个内容相同的字符串。 - 复用或添加:
- 如果存在,直接返回常量池里那个字符串的引用。
- 如果不存在,就把当前这个字符串对象的引用加入常量池,然后返回这个引用。
- 内存优化:这个方法可以让堆中的字符串对象也能被常量池复用,从而减少重复对象,节省内存。
// 双引号创建,直接在常量池
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(都指向常量池)
使用场景:
- 大量重复字符串:当需要处理大量重复的字符串时(比如从数据库读取的大量重复数据),用
intern()可以显著减少内存占用。 - 需要
==比较的场景:如果业务场景要求用==来快速比较字符串是否相等(性能要求极高时),可以先用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);
}
注意事项:
- 字符
|,*,+等都得加上转义字符,前面加上\\ - 如果是
\,那么就得写成\\\\ - 如果一个字符串中有多个分隔符,可以用
|作为连字符
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
注意事项:
- 索引从0开始
- 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是一种不可变对象,即字符串一旦创建,其内容就不能被修改。这种不可变性体现在以下几个方面:
String类被final修饰,不能被继承- 存储字符的数组被
final修饰(不是实现不可变性的主要原因) - 所有修改操作都返回新对象,而不是修改原对象
10.2 为什么String要设计成不可变的
- 安全性:不可变对象是线程安全的,可以在多线程环境中安全共享
- 性能优化:可以缓存hash code,提高作为HashMap键时的性能
- 字符串常量池:不可变性使得字符串常量池成为可能,节省内存
- 安全性:在网络传输和参数传递时,不可变字符串可以防止被意外修改
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
}
上面的代码实际上执行了以下操作:
- 创建一个
StringBuilder的对象 - 将s的内容追加到
StringBuilder中 - 将
world追加到StringBuilder中 - 调用
StringBuilder的toString方法创建新String对象 - 将新
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中提供了StringBuilder和StringBuffer类。这两个类大部分功能是相同的,主要区别在于 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的区别
-
可变性:
String:不可变字符序列StringBuffer和StringBuilder:可变字符序列
-
线程安全性:
String:线程安全(因为不可变)StringBuffer:线程安全(方法使用synchronized修饰)StringBuilder:线程不安全
-
性能:
String:修改操作性能最低(每次修改都创建新对象)StringBuffer:性能中等(有同步开销)StringBuilder:性能最高(无同步开销)
-
使用场景:
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对象
十三、总结
String是不可变对象,所有修改操作都会创建新对象- 字符串常量池是Java优化内存的重要机制
- 比较字符串内容应使用
equals()方法,而非==运算符 - 频繁修改字符串时应使用
StringBuilder或StringBuffer StringBuilder性能高于StringBuffer,但非线程安全- 理解字符串的不可变性有助于编写更安全和高效的代码
更多推荐



所有评论(0)