文章目录
- 一、String类的特性
- 二、String字符串的实现原理
- 三、StringBuffer、StringBuilder、StringJoiner
- 四、常用方法
- 五、String一些常用方法
- 六、一些练习
- 七、扩展
一、String类的特性
Java 中的 String 类具有以下特性:
不可变性:字符串在创建后其内容无法更改。这意味着所有对字符串的修改操作都会生成一个新的字符串对象。不可变性有助于提高程序的安全性和稳定性。
存储在常量池:字符串常量(如 “Hello, world!”)会存储在常量池(字符串池)中。当创建具有相同值的字符串时,它们会共享同一个字符串对象,从而节省内存。这种行为仅适用于字符串字面量和使用 intern() 方法创建的字符串。
实例化:String 类既可以使用 new 关键字创建对象,也可以直接使用字符串字面量。当使用字面量创建字符串时,会自动将其放入字符串常量池(如果池中不存在相同内容的字符串)。
可以用作键:String 类重写了 hashCode() 和 equals() 方法,因此字符串对象可以用作哈希表(例如 HashMap)中的键。由于字符串是不可变的,它们的哈希值在创建后不会更改,这有助于提高查找性能。
字符串拼接:Java 支持使用 + 和 += 运算符进行字符串拼接。但是,由于字符串不可变,频繁的拼接操作可能导致性能下降。在这种情况下,可以使用 StringBuilder 或 StringBuffer 类优化性能。
支持 Unicode 字符:String 类支持 Unicode 字符,因此可以在字符串中使用各种语言和符号。
序列化:String 类实现了 Serializable 接口,因此字符串对象可以在网络上传输或写入文件。
这些特性使得 String 类适用于处理各种文本数据,并具有良好的性能和安全性。在进行大量字符串操作时,可以使用 StringBuilder 或 StringBuffer 类优化性能。
二、String字符串的实现原理
Java 中的 String 类是用来表示字符串的,其内部通过字符数组(char[])实现。在 Java 9 之后,String 类使用字节数组(byte[])和一个编码标记(COMPACT_STRINGS)实现,以节省内存。在这种情况下,字符串可以使用一个字节(即 Latin-1 编码)或两个字节(即 UTF-16 编码)表示每个字符。
Java 的 String 是不可变的,这意味着一旦创建了一个字符串,就无法更改其内容。这样做有几个好处:
- 安全性:不可变字符串在多线程环境中是线程安全的,无需担心同步问题。
- 缓存:由于字符串不可变,可以缓存它们的哈希值。这在使用字符串作为哈希表键时特别有用,因为只需计算一次哈希值。
- 字符串常量池:不可变字符串可以在字符串常量池中共享,减少了内存消耗。例如,当创建两个相同的字符串时,它们可以指向同一个内部字符数组。
- 可靠性:不可变对象通常更易于维护和调试,因为它们的状态不会在程序运行过程中发生变化。
然而,字符串的不可变性也带来了一些缺点,特别是在大量字符串操作时,可能导致性能下降。为解决这个问题,Java 提供了 StringBuilder 和 StringBuffer 类,它们允许对字符串进行可变操作。
总之,Java 中的 String 类基于字符数组(Java 9 之后是字节数组和编码标记)实现,具有不可变性。这种设计旨在提高安全性、缓存性能、内存效率和可靠性。当需要对字符串执行大量操作时,可以使用 StringBuilder 和 StringBuffer 类以提高性能。
三、StringBuffer、StringBuilder、StringJoiner
StringBuffer、StringBuilder和StringJoiner都是用于处理字符串的Java类,它们之间的区别和使用场景如下:
- StringBuffer:
- 区别:StringBuffer是一个线程安全的可变字符序列。它在执行字符串拼接、插入、删除等操作时性能优于String,但略低于StringBuilder,因为它需要同步机制来确保线程安全。
- 使用场景:适用于在多线程环境下共享和修改字符串。当需要在多线程程序中处理字符串时,可以选择StringBuffer来确保线程安全。
- StringBuilder:
- 区别:StringBuilder是一个非线程安全的可变字符序列,性能优于StringBuffer和String。它在执行字符串拼接、插入、删除等操作时性能更高。
- 使用场景:适用于单线程环境下处理字符串。当需要在单线程程序中频繁地执行字符串操作(如拼接、插入、删除等)时,推荐使用StringBuilder。
- StringJoiner:
- 区别:StringJoiner是Java 8中引入的一个实用类,用于方便地将多个字符串连接在一起,同时可以添加定制的分隔符、前缀和后缀。它内部使用了StringBuilder来处理字符串操作,因此性能优于普通的字符串拼接。
- 使用场景:适用于需要将多个字符串连接在一起,且需要添加定制的分隔符、前缀和后缀的场景。当需要执行此类操作时,StringJoiner是一个简便、高效的选择。
总结:
当需要在多线程环境下处理字符串时,选择StringBuffer以确保线程安全。
当需要在单线程环境下频繁地执行字符串操作时,选择StringBuilder以获得更高的性能。
当需要将多个字符串连接在一起,并添加定制的分隔符、前缀和后缀时,选择StringJoiner以获得简便、高效的操作。
四、常用方法
这里将分别列举StringBuffer、StringBuilder和StringJoiner的一些常用方法:
StringBuffer
StringBuffer和StringBuilder的方法非常相似,因为它们都继承自AbstractStringBuilder。以下是StringBuffer的一些常用方法:
append():将指定的字符串、字符、数字或其他数据类型的值添加到StringBuffer的末尾。
StringBuffer sb = new StringBuffer();
sb.append("Hello, ");
sb.append("world!");
insert():将指定的字符串、字符、数字或其他数据类型的值插入到StringBuffer的指定位置。
StringBuffer sb = new StringBuffer("Hello world!");
sb.insert(5, ","); // 结果为 "Hello, world!"
delete():删除StringBuffer中的指定范围内的字符。
StringBuffer sb = new StringBuffer("Hello, world!");
sb.delete(5, 7); // 结果为 "Hello world!"
replace():将StringBuffer中的指定范围内的字符替换为给定字符串。
StringBuffer sb = new StringBuffer("Hello, world!");
sb.replace(0, 5, "Hi"); // 结果为 "Hi, world!"
reverse():反转StringBuffer中的字符顺序。
StringBuffer sb = new StringBuffer("Hello, world!");
sb.reverse(); // 结果为 "!dlrow ,olleH"
StringBuilder
StringBuilder的方法与StringBuffer相同,因为它们都继承自AbstractStringBuilder。这里不再重复列举方法,可以直接参考StringBuffer的方法。
StringJoiner
StringJoiner的常用方法如下:
add():将字符串添加到StringJoiner中,并用分隔符分隔。
StringJoiner joiner = new StringJoiner(", ");
joiner.add("apple");
joiner.add("banana");
joiner.add("cherry");
merge():将另一个StringJoiner的内容合并到当前StringJoiner中。合并后,两个StringJoiner之间将使用当前StringJoiner的分隔符。
StringJoiner joiner1 = new StringJoiner(", ");
joiner1.add("apple");
joiner1.add("banana");
StringJoiner joiner2 = new StringJoiner(", ");
joiner2.add("cherry");
joiner2.add("orange");
joiner1.merge(joiner2); // 结果为 "apple, banana, cherry, orange"
setEmptyValue():设置当StringJoiner为空时返回的字符串。默认情况下,空的StringJoiner返回一个空字符串。
StringJoiner joiner = new StringJoiner(", ");
joiner.setEmptyValue("No elements"); // 当joiner为空时,toString()将返回"No elements"
toString():将StringJoiner的内容转成一个字符串。这个方法将StringJoiner中添加的所有字符串连接在一起,并使用分隔符、前缀和后缀(如果有的话)进行格式化。
StringJoiner joiner = new StringJoiner(", ", "[", "]");
joiner.add("apple");
joiner.add("banana");
joiner.add("cherry");
String result = joiner.toString(); // 结果为 "[apple, banana, cherry]"
在这个例子中,我们创建了一个StringJoiner,其中分隔符为逗号和空格(“, “),前缀为左方括号(”[”),后缀为右方括号(“]”)。我们添加了三个字符串(“apple”、“banana"和"cherry”),然后使用toString()方法将StringJoiner的内容转换为一个格式化的字符串。最后,结果为:“[apple, banana, cherry]”。
五、String一些常用方法
Java 中的 String 类提供了许多有用的方法来处理字符串。以下是一些常用方法:
- length():返回字符串的长度。
- charAt(int index):返回字符串中指定索引处的字符。
- substring(int beginIndex):返回从 beginIndex 到字符串末尾的子字符串。
- substring(int beginIndex,int endIndex):返回从 beginIndex(包含)到 endIndex(不包含)的子字符串。
- concat(String str):将指定的字符串连接到此字符串的末尾。
- indexOf(String str):返回指定子字符串在此字符串中第一次出现的索引,如果没有找到,则返回 -1。
- lastIndexOf(String str):返回指定子字符串在此字符串中最后一次出现的索引,如果没有找到,则返回 -1。
- startsWith(String prefix):检查字符串是否以指定的前缀开头。
- endsWith(String suffix):检查字符串是否以指定的后缀结尾。
- replace(char oldChar, char newChar):将字符串中所有的 oldChar 替换为 newChar。
- replace(CharSequence target, CharSequence replacement):将字符串中所有的target 子序列替换为 replacement。
- toLowerCase():将字符串中的所有字符转换为小写。
- toUpperCase():将字符串中的所有字符转换为大写。
- trim():删除字符串开头和结尾的空白字符。
- equals(Object obj):比较字符串和指定对象是否相等。
- equalsIgnoreCase(Stringanothe rString):比较字符串和指定字符串是否相等,忽略大小写。
- compareTo(String anotherString):按照字典顺序比较两个字符串。
六、一些练习
键盘录入一个字符串,打乱字符串的内容。
import java.util.Scanner;
import java.util.Random;
public class ShuffleString {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
System.out.println("请输入一个字符串:");
String input = scanner.nextLine();
scanner.close();
String shuffledString = shuffle(input);
System.out.println("打乱后的字符串为:");
System.out.println(shuffledString);
}
private static String shuffle(String input) {
char[] chars = input.toCharArray();
Random random = new Random();
for (int i = chars.length - 1; i > 0; i--) {
int randomIndex = random.nextInt(i + 1);
char temp = chars[i];
chars[i] = chars[randomIndex];
chars[randomIndex] = temp;
}
return new String(chars);
}
}
程序接受键盘输入的字符串,然后使用 shuffle 方法打乱字符串的内容。shuffle 方法首先将字符串转换为字符数组,然后使用 Fisher-Yates(也称为 Knuth)洗牌算法随机交换字符。最后,通过将字符数组转换回字符串得到打乱的字符串。
洗牌算法(Shuffling Algorithm)是一种用于将有限序列(如数组、列表等)中的元素随机重新排列的算法。在计算机科学中,洗牌算法通常用于模拟随机事件,如模拟纸牌游戏中的洗牌过程。最著名的洗牌算法是 Fisher-Yates 洗牌算法,也称为 Knuth 洗牌算法。
Fisher-Yates 洗牌算法的基本思路是从后向前遍历数组,对于每个元素,随机选择一个之前的元素(包括当前元素)与之交换。以下是 Fisher-Yates 洗牌算法的简化步骤:
- 初始化一个索引变量 i 为数组的最后一个元素的索引。
- 当 i 大于 0 时,执行以下操作:
a. 生成一个介于 0(包括)和 i(包括)之间的随机整数 randomIndex。
b. 交换数组中索引为 i 和 randomIndex 的元素。
c. 将 i 减 1。 - 遍历结束后,数组中的元素被随机打乱。
这种洗牌算法的优点是其随机性能保证,因为每个元素都有相同的概率被放置在任何位置。此外,Fisher-Yates 算法的时间复杂度为 O(n),空间复杂度为 O(1),因此效率很高。
请定义一个方法用于判断一个字符串是否是对称的字符串,并在主方法中测试方法。例如:“abcba”、"上海自来水来自海上"均为对称字符串。
public class Test {
public static void main(String[] args) {
String str = "上海自来水来自海上";
System.out.println(isSym(str));
}
public static boolean isSym(String str) {
// 为了程序的健壮,如果传递的是空值,返回false
if (str == null) {
return false;
}
// 转换为StringBuilder
StringBuilder sb = new StringBuilder(str);
// 反转,再转成String
String reStr = sb.reverse().toString();
// 比较与原字符串是否相等
// 相等返回true,不相等返回false,正好与equals的返回值一致,直接返回即可。
return reStr.equals(str);
}
}
定义方法,返回值类型为boolean,参数列表为String类型的一个参数,将字符串转换为StringBuilder类型,调用StringBuilder的reverse()方法将字符串反转,将反转后的字符串再转回String类型,并与原字符串比较,如果相等,返回true,否则返回false,在主方法中,定义一个字符串,调用方法测试结果。
现有如下文本:“Java语言是面向对象的,Java语言是健壮的,Java语言是安全的,Java是高性能的,Java语言是跨平台的”。请编写程序,统计该文本中"Java"一词出现的次数。
public class Test {
public static void main(String[] args) {
String str = "Java语言是面向对象的,Java语言是健壮的,Java语言是安全的,Java是高性能的,Java语言是跨平台的";
String tar = "Java";
// 调用方法并输出
System.out.println(search(str, tar));
}
// 返回值int表示次数,参数列表str表示在哪个字符串中查找,tar表示要查找的目标子串
public static int search(String str, String tar) {
// 定义统计变量表示次数
int count = 0;
// 定义索引变量,表示每次找到子串出现的索引
int index = -1;
// 定义循环,判断条件为在字符串中找到了目标子串
while ((index = str.indexOf(tar)) != -1) { // 将找到的索引赋值给变量并判断
// 次数累加
count++;
// 把查找过的部分剪切掉,从找到的索引+子串长度的位置开始截取。
str = str.substring(index + tar.length());
}
return count;
}
}
public class Test {
public static void main(String[] args) {
String str = "Java语言是面向对象的,Java语言是健壮的,Java语言是安全的,Java是高性能的,Java语言是跨平台的";
String tar = "Java";
// 调用方法并输出
System.out.println(search(str, tar));
}
// 替换之后求长度差
public static int search(String str, String tar) {
String newStr = str.replace(tar, "");
int count = (str.length() - newStr.length()) / tar.length();
return count;
}
}
定义方法,返回值int表示次数,参数列表两个字符串,第一个表示在哪个字符串中查找,第二个表示要查找的目标子串,定义统计变量表示次数。定义索引变量,表示每次找到子串出现的索引。定义循环,判断条件为在字符串中找到了目标子串,使用indexOf实现。如果找到的索引不是-1,在循环中,统计变量累加。把查找过的部分剪切掉,从找到的索引+子串长度的位置开始截取,使用substring实现。将统计变量返回,在主方法中,定义字符串表示题目中的文本,定义字符串表示要查找的子串,调用方法,获取结果。
七、扩展
可以用作键的特性意味着 String 类的对象可以用作数据结构(如哈希表、哈希集等)的键。这得益于 String 类重写了 hashCode() 和 equals() 方法,以便于根据字符串的内容生成唯一的哈希值和判断字符串是否相等。
举个例子,假设您要创建一个 HashMap,用于存储每个国家的首都。在这种情况下,可以将国家名称(字符串)作为键,首都(字符串)作为值。示例代码如下:
import java.util.HashMap;
public class Main {
public static void main(String[] args) {
HashMap<String, String> capitals = new HashMap<>();
capitals.put("China", "Beijing");
capitals.put("Japan", "Tokyo");
capitals.put("USA", "Washington D.C.");
System.out.println("China's capital: " + capitals.get("China"));
System.out.println("Japan's capital: " + capitals.get("Japan"));
System.out.println("USA's capital: " + capitals.get("USA"));
}
}
在上述代码中,capitals 是一个 HashMap,将字符串键(国家名)映射到字符串值(首都)。当添加或检索元素时,HashMap 会使用键的 hashCode() 方法生成哈希值,并使用 equals() 方法来确定两个键是否相等。因为 String 类已经实现了这些方法,所以能够正确地在哈希表中存储和检索数据。
需要注意的是,字符串的不可变性有助于其作为键的特性。因为字符串内容不会更改,所以它们的哈希值在创建后也不会更改。这有助于提高查找性能,避免因为哈希值变化导致的数据丢失。
JAVA9:
在 Java 9 之后,String 类的内部实现发生了变化。为了提高空间和时间效率,Java 9 引入了 COMPACT_STRINGS 优化。在此优化下,String 类使用字节数组(byte[])来存储字符数据,同时使用一个编码标记(coder)来指示字符串的编码格式。
coder 是一个字节变量,用于表示字符串中字符的编码。Java 9 中的 String 类支持两种编码:ISO-8859-1(单字节编码)和UTF-16(双字节编码)。如果字符串仅包含 ISO-8859-1 编码的字符,它将使用单字节编码存储,从而节省空间。否则,将使用 UTF-16 编码存储。
具体地说,coder 的值可以是:
StringLatin1:表示字符串使用 ISO-8859-1(Latin-1)编码。在这种情况下,每个字符占用一个字节。这种编码包含了大多数西欧语言字符,如英语、法语、德语等。
StringUTF16:表示字符串使用 UTF-16 编码。在这种情况下,每个字符通常占用两个字节(对于某些辅助字符,可能需要四个字节)。UTF-16 编码可以表示大多数现代语言的字符,包括亚洲语言和其他非拉丁语言。
通过使用 coder 和字节数组,Java 9 的 String 类可以在保持字符串处理速度的同时,减少内存占用。这对于那些使用大量字符串的应用程序(例如文本处理或网络应用)来说是一个很大的优势。
让我们来看一个简单的例子来说明 Java 9 之后的 String 类如何使用字节数组和编码标记来存储字符串。
假设我们有以下两个字符串:
String englishText = "Hello, World!";
String chineseText = "你好,世界!";
对于 englishText,因为它仅包含 ISO-8859-1(Latin-1)编码的字符,所以它将使用单字节编码(StringLatin1)进行存储。在这种情况下,englishText 的字节数组将如下所示(十六进制表示):
48 65 6C 6C 6F 2C 20 57 6F 72 6C 64 21
同时,englishText 的 coder 将被设置为 StringLatin1,表示使用 ISO-8859-1 编码。
对于 chineseText,因为它包含中文字符,无法使用 ISO-8859-1 编码表示,所以它将使用双字节编码(StringUTF16)进行存储。在这种情况下,chineseText 的字节数组将如下所示(十六进制表示):
4F 60 59 7D FF 0C 4E 16 51 6B
同时,chineseText 的 coder 将被设置为 StringUTF16,表示使用 UTF-16 编码。
这个例子展示了如何在 Java 9 之后的 String 类中使用字节数组和编码标记来存储字符串。这种实现在减少内存占用的同时,保持了字符串处理的速度。