【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 bit";System.out.println(s1);// 2. 直接new String对象String s2 = new String("hello bit");System.out.println(s2);// 3. 使用字符数组进行构造char[] array = {'h','e','l','l','o',' ','b','i','t'};String s3 = new String(array);System.out.println(s3);
}
2.2 String的内存模型
String是引用类型,其内部实际存储结构在JDK 1.8及之前版本是char数组:
public final class Stringimplements 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 Stringimplements 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,内容相同
三、String对象的比较
Java提供了多种方式比较字符串,每种方式有其特定的使用场景。
3.1 ==运算符比较
==运算符比较的是两个对象的引用是否相同(即是否指向同一内存地址):
public static void main(String[] args) {int a = 10;int b = 20;int c = 10;// 对于基本类型变量,==比较两个变量中存储的值是否相同System.out.println(a == b); // falseSystem.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); // falseSystem.out.println(s2 == s3); // falseSystem.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引用的是三个不同对象,因此==比较结果全部为falseSystem.out.println(s1 == s2); // falseSystem.out.println(s1 == s3); // false// equals比较:String对象中的逐个字符// 虽然s1与s2引用的不是同一个对象,但是两个对象中放置的内容相同,因此输出true// s1与s3引用的不是同一个对象,而且两个对象中内容也不同,因此输出falseSystem.out.println(s1.equals(s2)); // trueSystem.out.println(s1.equals(s3)); // false
}
String类重写了Object类的equals方法,其实现逻辑如下:
public boolean equals(Object anObject) {// 1. 先检测this和anObject是否为同一个对象比较,如果是返回trueif (this == anObject) {return true;}// 2. 检测anObject是否为String类型的对象,如果是继续比较,否则返回falseif (anObject instanceof String) {// 将anObject向下转型为String类型对象String anotherString = (String)anObject;int n = value.length;// 3. this和anObject两个字符串的长度是否相同,是继续比较,否则返回falseif (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)); // 不同输出字符差值-1System.out.println(s1.compareTo(s3)); // 相同输出0System.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)); // 不同输出字符差值-1System.out.println(s1.compareToIgnoreCase(s3)); // 相同输出0System.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')); // 6System.out.println(s.indexOf('c', 10)); // 15System.out.println(s.indexOf("bbb")); // 3System.out.println(s.indexOf("bbb", 10)); // 12System.out.println(s.lastIndexOf('c')); // 17System.out.println(s.lastIndexOf('c', 10)); // 8System.out.println(s.lastIndexOf("bbb")); // 12System.out.println(s.lastIndexOf("bbb", 10)); // 3
}
五、字符串转换操作
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 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 不可变性的实现原理
以下来自JDK 1.8中String类的部分实现:
/*** The {@code String} class represents character strings. All* string literals in Java programs, such as {@code "abc"}, are* implemented as instances of this class.* <p>* Strings are constant; their values cannot be changed after they* are created. String buffers support mutable strings.* Because String objects are immutable they can be shared.*/
public final class Stringimplements 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
}
10.3 为什么String要设计成不可变的
- 安全性:不可变对象是线程安全的,可以在多线程环境中安全共享
- 性能优化:可以缓存hash code,提高作为HashMap键时的性能
- 字符串常量池:不可变性使得字符串常量池成为可能,节省内存
- 安全性:在网络传输和参数传递时,不可变字符串可以防止被意外修改
10.4 关于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(" "); // hellosb1.append("world"); // hello worldsb1.append(123); // hello world123System.out.println(sb1); // hello world123System.out.println(sb1 == sb2); // trueSystem.out.println(sb1.charAt(0)); // 获取0号位上的字符 hSystem.out.println(sb1.length()); // 获取字符串的有效长度14System.out.println(sb1.capacity()); // 获取底层数组的总大小sb1.setCharAt(0, 'H'); // 设置任意位置的字符 Hello world123sb1.insert(0, "Hello world!!!"); // Hello world!!!!Hello world123System.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类相关算法题目
13.1 第一个只出现一次的字符
class Solution {public int firstUniqChar(String s) {int[] count = new int[256];// 统计每个字符出现的次数for(int i = 0; i < s.length(); ++i) {count[s.charAt(i)]++;}// 找第一个只出现一次的字符for(int i = 0; i < s.length(); ++i) {if(1 == count[s.charAt(i)]) {return i;}}return -1;}
}
13.2 最后一个单词的长度
import java.util.Scanner;public class Main {public static void main(String[] args) {// 循环输入Scanner sc = new Scanner(System.in);while(sc.hasNext()) {// 获取一行单词String s = sc.nextLine();// 找到最后一个空格// 获取最后一个单词:从最后一个空格+1位置开始,一直截取到末尾// 打印最后一个单词长度int len = s.substring(s.lastIndexOf(" ") + 1).length();System.out.println(len);}sc.close();}
}
13.3 检测字符串是否为回文
class Solution {public static boolean isValidChar(char ch) {if((ch >= 'a' && ch <= 'z') || (ch >= '0' && ch <= '9')) {return true;}return false;}public boolean isPalindrome(String s) {// 将大小写统一起来s = s.toLowerCase();int left = 0, right = s.length() - 1;while(left < right) {// 1. 从左侧找到一个有效的字符while(left < right && !isValidChar(s.charAt(left))) {left++;}// 2. 从右侧找一个有效的字符while(left < right && !isValidChar(s.charAt(right))) {right--;}if(s.charAt(left) != s.charAt(right)) {return false;} else {left++;right--;}}return true;}
}
十四、总结
本文全面介绍了Java中String类的各个方面,从基本概念到高级应用,从内存模型到性能优化。String作为Java中最常用的类之一,其重要性不言而喻。理解String类的特性和正确使用方式,对于编写高效、可靠的Java程序至关重要。
关键要点总结:
- String是不可变对象,所有修改操作都会创建新对象
- 字符串常量池是Java优化内存的重要机制
- 比较字符串内容应使用equals()方法,而非==运算符
- 频繁修改字符串时应使用StringBuilder或StringBuffer
- StringBuilder性能高于StringBuffer,但非线程安全
- 理解字符串的不可变性有助于编写更安全和高效的代码
通过掌握这些知识,开发者可以更好地利用String类及其相关类,编写出更加高效、健壮的Java应用程序。