Java String 字符串

Java 中的 String 字符串是 Java 中最常用的类之一,String 字符串是一个不可变的字符序列,用于表示文本。

Java String

Java String

String 字符串对象可以通过字面值或构造方法创建。例如:

// 通过字面值创建 String 对象
String str1 = "Hello, world!";
// 通过构造方法创建 String 对象
String str2 = new String("Hello, world!"); 

Java String 不可变

Java 中的 String 有一个很重要的特性,就是 Java String 一旦初始化,就不可改变。虽然 String 类中看起来有很多修改字符串内容的方法,但是其实都是生成新的字符串。比如像下面这样让 str1 拼接上 !!!,会发现输出的还是 Hello world.

String str1 = "Hello, world";
str1.concat("!!!");
System.out.println(str1);
// 输出:Hello, world

想要改变 str1 的值,只能对 str1 直接重新赋值。

String str1 = "Hello, world";
str1 = str1.concat("!!!");
System.out.println(str1);

为什么 String 值不能改变,从实现原因上,可以在源码中看到 final char value[] 的限制。

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    /** The value is used for character storage. */
    private final char value[];
    //.....
}

Java String 方法

String 类提供了许多方法,用于操作字符串。以下是一些常用的方法:

方法 描述
length() 返回字符串的长度。
charAt(int index) 返回字符串中指定位置的字符。
substring(int beginIndex) 返回字符串中从指定位置开始到结尾的子串。
substring(int beginIndex, int endIndex) 返回字符串中从 beginIndex 开始到 endIndex 的子串。
equals(Object obj) 比较字符串是否相等。
equalsIgnoreCase(String anotherString) 比较字符串是否相等,忽略大小写。
compareTo(String anotherString) 比较字符串的大小关系。
compareToIgnoreCase(String str) 比较字符串的大小关系,忽略大小写。
indexOf(int ch) 返回指定字符在字符串中第一次出现的位置。
indexOf(int ch, int fromIndex) 返回指定字符在字符串中从指定位置开始第一次出现的位置。
indexOf(String str) 返回指定字符串在字符串中第一次出现的位置。
indexOf(String str, int fromIndex) 返回指定字符串在字符串中从指定位置开始第一次出现的位置。
lastIndexOf(int ch) 返回指定字符在字符串中最后一次出现的位置。
lastIndexOf(int ch, int fromIndex) 返回指定字符在字符串中从指定位置开始最后一次出现的位置。
lastIndexOf(String str) 返回指定字符串在字符串中最后一次出现的位置。
lastIndexOf(String str, int fromIndex) 返回指定字符串在字符串中从指定位置开始最后出现的位置。
toCharArray() 返回指定字符串的字符数组( "aabb" => {'a','a','b','b'})
trim() 清空字符串收尾的空格( " aabb " => "aabb")

Java String 接口

Java String 接口实现

String 类实现了三个接口,从源码中可以看到。

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    /** The value is used for character storage. */
    private final char value[];
    //.....
}

Comparable

String 类实现了 Comparable 接口,因此可以使用 compareTo() 方法比较两个字符串的大小关系。如果字符串相等,则返回 0;如果当前字符串大于另一个字符串,则返回正整数;如果当前字符串小于另一个字符串,则返回负整数。

String str1 = "abc";
String str2 = "abz";
System.out.println(str1.compareTo(str2));
System.out.println(str2.compareTo(str1));
// 输出:-23
// 输出:23

字符串的比较是字符的自然排序比较,并不会比较总体的数值,比如字符串 199999 是小于字符串 2 的。

System.out.println("19999".compareTo("2"));
// 输出:-1

CharSequence

CharSequence 用于表示一个字符序列的抽象。CharSequence 接口位于 java.lang 包中,是 Java 中的一个接口,CharSequence 接口定义了以下方法:

  • charAt(int index):返回指定位置的字符。
  • length():返回字符序列的长度。
  • subSequence(int start, int end):返回从 start 到 end 的子序列。
  • chars() : 返回字符串的 IntStream 流。
  • codePoints(): 和 chars 类似,返回字符串的 IntStream 流。

codePoints() 方法返回的是 Unicode 代码点,而 chars() 方法返回的是字符的 UTF-16 编码。在大多数情况下,这两种方法的结果是相同的,但是对于一些特殊字符(如 Emoji 表情符号),它们的结果可能不同。因此,在处理字符序列时,应该根据具体情况选择使用哪种方法。

简单演示一下 chars() 方法,它用于返回一个 IntStream,其中包含此字符序列中的字符。例如:

String str = "Hello, world!";
str.chars().forEach(System.out::println);

输出:

72
101
108
108
111

这个示例代码使用 chars() 方法返回一个 IntStream,然后使用 forEach() 方法遍历 IntStream 中的每个元素,并将其打印到控制台上。但是这里的 IntStream 中的每个元素都是字符的 Unicode 编码。如果需要将其转换为字符,可以使用 (char) 进行强制类型转换。例如:

String str = "Hello, world!";
str.chars().mapToObj(c -> (char) c).forEach(System.out::print);

输出:

Hello, world!

这个示例代码使用 mapToObj() 方法将 IntStream 中的每个元素转换为字符,并将其打印到控制台上。

CharSequence 接口的一个重要特点是它的方法都是读取操作。这意味着,一旦创建了一个 CharSequence 对象,就不能修改它的值。如果需要修改字符序列,可以使用 CharSequence 的其他实现类,如 StringBuffer 、 StringBuilder 类。

Java String 内存分配

Java 中的 String 类是一个不可变的类,它的值在创建后不能被修改。String 对象的内存分配逻辑与其他对象相同,即在堆上分配一块内存空间来存储对象的实例变量。但是,String 对象的值是存储在常量池中的,而不是存储在对象的实例变量中的。

字符串对象存储在被称为字符串常量池的特殊内存区域中。

Java 中的常量池是一块特殊的内存区域,用于存储常量。在编译 Java 代码时,编译器会将所有的字符串常量都放入常量池中。在运行 Java 程序时,如果需要创建一个字符串对象,Java 虚拟机会先在常量池中查找是否已经存在一个值相同的字符串对象。如果存在,则直接返回该对象的引用;否则,创建一个新的字符串对象,并将其存储在常量池中。

由于 String 对象的值是存储在常量池中的,因此多个 String 对象可以共享同一个值。例如,下面的代码创建了两个 String 对象,但是它们的值是相同的,因此都会引用常量池中的同一个 Hello! 字符串:

String str1 = "Hello!";
String str2 = "Hello!";
System.out.println(str1 == str2);
// 输出:true

在这个例子中,Java 虚拟机只会在常量池中创建一个字符串对象,然后让 str1 和 str2 都指向这个对象。这种共享字符串对象的机制可以提高程序的性能和节省内存空间。

但是,如果使用 new 关键字创建字符串对象,则不会共享对象。例如:

String str3 = new String("Hello!");
String str4 = new String("Hello!");
System.out.println(str3 == str4);
// 输出:false

在这个例子中,Java 虚拟机会在堆上分配两个不同的字符串对象,它们的值相同但是地址不同。因此,输出结果为 false。

通过下面的图可以很好的展示原因。

可见,如果使用 new String 创建大量的对象,可能会占用较多内存

注意,两个字符串直接拼接,会引用字符串常量池。但是两个 String 变量直接拼接,等于 new String

String str1 = "Hello";
String str2 = "world";
String str3 = str1 + str2;
String str4 = str1 + str2;
String str5 = "Hello" + "world";
String str6 = "Hello" + "world";
System.out.println(str3 == str4); // false
System.out.println(str5 == str6); // true

String 常见问题

由于 String 对象是不可变的,因此每次对字符串进行修改都会创建一个新的 String 对象。如果需要频繁修改字符串,可以使用 StringBuilder 或 StringBuffer 类。StringBuilder 和 StringBuffer 类提供了许多方法,用于修改字符串,例如 append() 方法用于在字符串末尾添加字符或字符串,insert() 方法用于在字符串中插入字符或字符串,等等。StringBuilder 和 StringBuffer 类的区别在于,StringBuilder 类是线程不安全的,而 StringBuffer 类是线程安全的。

一如既往,文章中代码存放在 Github.com/niumoo/javaNotes.