一、String
1、简要介绍
String 是一个用于表示字符串的类,它是Java中最常用的数据类型之一。它用来保存一个字符串,也就是一串字符序列。
同时,String 类是一个 final 类,不能被继承。
2、继承与实现
String 类直接继承 Object 类,同时,String 类实现了 Serializable、Comparable、CharSequence 接口。
实现 Serializable 接口用途:
Serializable 接口是一个标记接口,表示实现该接口的类可以被序列化。序列化是将对象的状态转换为字节流的过程,以便于存储或传输。
对于 String 类来说,实现 Serializable 接口使得字符串对象可以被安全地保存到文件中,或通过网络传输。将字符串对象序列化后存储到文件中或者通过网络发送时,可以方便地恢复原始的字符串对象。
实现 Comparable 接口用途:
Comparable 接口定义了一个比较两个对象的顺序的标准。
实现这个接口允许字符串对象按照字典顺序进行比较,这在排序和搜索时非常有用。
使用 Collections.sort() 方法对字符串列表进行排序时,String 类中的 compareTo(String anotherString) 方法将被调用,以确定字符串的自然顺序。
实现 CharSequence 接口用途:
CharSequence 接口允许 String 类提供对字符序列的访问。
这个接口定义了一组可以用于处理字符序列的方法,如 length()、charAt(int index)、subSequence(int start, int end) 等。
实现这个接口使得String类能够与其他处理字符序列的类(如 StringBuilder、StringBuffer 等)兼容。可以将 String 对象直接传递给需要 CharSequence 类型参数的方法,比如 StringBuilder 的构造函数。
3、构造器
String 类有许多构造器,这里只简要介绍几个常用的构造器:
1)String()
创建一个空字符串对象。
String str = new String();
2)String(String original)
通过复制指定字符串的内容来创建一个新的字符串对象。
String str = new String("Hello");
3)String(char[] value)
通过字符数组创建字符串对象。
char[] chars = {'H', 'e', 'l', 'l', 'o'};
String str = new String(chars);
4)String(byte[] bytes)
通过字节数组创建字符串对象,使用平台的默认字符集。
byte[] bytes = {72, 101, 108, 108, 111}; // 对应 "Hello"
String str = new String(bytes);
5)String(byte[] bytes, int offset, int length)
通过字节数组的一部分创建字符串对象,从指定偏移量开始,指定长度,使用平台的默认字符集。
byte[] bytes = {72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100};
String str = new String(bytes, 0, 5); // "Hello"
4、字符串存储
String 对象的内容是存储在一个名为 value 的字符数组中,而且这个数组是使用 final 修饰的,意味着一旦被初始化,它的引用不能被改变(只是引用不能修改,value 的每一个元素是可以修改的)。这个设计决定了 String 对象的不可变性,以及其在内存中的表现。
private final char value[];
当创建一个 String 对象时,JVM 会为其分配内存,并在 value 数组中存储字符。value 数组会根据字符串的字符数动态大小化。字符串的每个字符会被存储为 char 类型,Java 使用 UTF-16 编码来表示字符,因此每个字符占用两个字节。
5、不可变性
由上面的 String 对象的内容是被存储在一个 final 字符数组 value 中的,我们可知,String 对象是不可变的(immutable)。
这意味着一旦创建了一个 String 对象,它的值就不能被改变。如果你对字符串进行任何修改操作,比如拼接、替换等,实际上会生成一个新的 String 对象。
上面说到 value 被 final 修饰只是 value 的引用不能修改,而 value 的每一个元素值可以修改。如果你想要对 String 对象的某个字母进行修改,可以使用 replace 方法,但是这个方法实际上还是会返回一个新的对象:
String original = "Hello";
String modified = original.replace('H', 'J');
// original 仍为 "Hello"
// modified 为 "Jello"
所以说,String 对象设计理念就是不改动 value 数组的内容,即使 final 修饰只是使 value 的引用不能修改,而 value 的每一个元素可以修改。
其实不直接修改源字符串的 value 的某个元素还有一个原因,因为 value 可能指向字符串常量池的某个字符串常量,字符串常量是没法被修改的。
二、字符串常量
1、字符串常量介绍
字符串常量是指在程序中直接使用的固定字符串值。
String str = "Hello, World!";
这里,"Hello, World!" 就是一个字符串常量。字符串常量一旦创建,它的内容不能被修改。
2、字符串常量池
字符串常量池(String Constant Pool)是用于存放字符串常量的特殊内存区域。这个池的主要作用是避免重复创建相同内容的字符串对象,从而节省内存。
3、字符串常量池测试
public class Example {
public static void main(String[] args) {
String str1 = "Hello";
String str2 = "Hello";
System.out.println(str1 == str2);
}
}
运行结果:
可以证明 str1 和 str2 指向的是同一个对象。
三、字符串创建方式
这里记住一点:
只要使用了字符串常量,JVM 会先查找常量池中是否有相同的字符串常量,如果有,则什么也不做,如果没有的话,这个字符串常量会放到常量池。
1、直接使用字符串常量赋值
如果常量池没有相同的字符串常量的话,这个字符串常量会被放到常量池中,然后 String 引用变量会指向这个字符串常量。如果常量池中有相同的字符串常量,则 String 引用变量直接指向这个相同的字符串常量。
String str = "Hello";
所以这里的 "Hello" 会被放到常量池中。
然后 str 会指向常量池的这个字符串常量,具体到图则是:
2、使用字符串常量加构造器创建
String str = new String("Hello");
JVM 会先在常量池中找是否有 "Hello" 这个字符串常量
- 如果有的话,在堆区创建一个 String 对象,然后这个对象的 value 字段指向常量池的 "Hello" 字符串常量。
- 如果常量池没有 "Hello" 这个字符串常量的话,则会在常量池中创建一个 "Hello" 字符串常量,然后在堆区创建一个 String 对象,然后这个对象的 value 字段指向常量池的 "Hello" 字符串常量。
这里为什么要有找这个字符串常量的动作呢,因为这里使用构造器创建时使用了字符串常量,我们上面提到过使用字符串常量就会导致这一系列动作。
对于上面的例子,我们可以作出以下图:
对于以下例子,
String str1 = "Hello";
String str2 = new String("Hello");
我们可以绘出相应的内存布局图:
3、不使用或间接使用字符串常量创建
char[] chars = {'H', 'e', 'l', 'l', 'o'};
String str = new String(chars);
这种方式会直接在堆区中创建 String 对象,然后 String 对象的 value 数组是从以上代码中的 chars 数组中复制得到的。
对于这个例子,内存分布图是这样的:
String str1 = new String("Hello");
String str2 = new String("World");
String str = str1 + str2;
这种方式创建,对于 str1 和 str2 的创建方式是上面的第二种方式,也就是使用字符串常量加构造器的创建方式。
对于 str 的创建方式,则比较特别:
// StringBuilder s = new StringBuilder;
// s.append(str1);
// s.append(str2);
// str = s.toString();
最后 toString 方法会创建一个新的 String 对象,而且常量池中没有与这个 String 对象内容一样的 字符串常量。
四、intern() 方法
1、intern() 方法机制
在 JDK7 之前:
当调用 intern() 方法时,JVM 会检查字符串常量池中是否有与当前字符串内容一样的字符串常量:
- 如果存在,intern() 方法将返回常量池中那个一样的字符串的引用。
- 如果不存在,intern() 方法会在常量池中创建一个内容与当前字符串相同的字符串对象,并返回这个新创建字符串的引用。
自 JDK7 向后:
当调用 intern() 方法时,JVM 会查看字符串常量池中是否有与当前字符串内容一样的字符串常量:
- 如果存在,则直接返回这个字符串常量的引用。
- 如果不存在,则将当前的 String 对象的引用放到常量池中,然后返回这个引用(这里只是将 当前的 String 对象的引用放到了常量池中,没有创建新的对象)。
这里在 JDK7 之前和 JDK7 及之后有区别的原因是,JDK7 时将字符串常量从方法区移动到了堆区中。
下面举出几个以 JDK7 为环境的案例:
2、案例
1)案例一:常量池中存在一样的字符串常量
public class Example {
public static void main(String[] args) {
String str = new String("Hello");
// 这里使用了字符串常量"Hello",所以它会被放到常量池
System.out.println(str == str.intern()); // false
// 由于字符串常量池中有"Hello"常量,所以这里str.intern()返回的
// 是常量池中的"Hello"常量的引用,而str指向的是堆区的字符串对象,
// 所以这里的结果是false
}
}
运行结果:
2)案例二:常量池中没有一样的字符串常量
public class Example {
public static void main(String[] args) {
String str1 = new String("Hello");
String str2 = new String("World");
String str = str1 + str2;
// 这里是上面字符串创建方式提到的第三种创建方式
// 间接使用构造器创建,所以这里的str指向的是一个堆区的
// 字符串对象,而且常量池中没有与str内容一样的字符串常量
System.out.println(str == str.intern()); // true
// 这里由于str指向的是一个新的在堆区的String对象,内容为"HelloWorld"
// 而在常量池中没有与"HelloWorld"内容一样的字符串常量,所以,str的引用
// 会被放入到常量池中,然后被返回,所以这里str.intern()返回的还是str,所以
// 这里会输出true
}
}
运行结果:
3)案例三:综合案例
public class Example {
public static void main(String[] args) {
String a = "123";
// a指向常量池的"123"
String b = new String("123");
// b指向堆区的String对象,
// String对象的value指向常量池的"123"
System.out.println(a.equals(b)); // true
System.out.println(a == b); // false
System.out.println(a == b.intern()); // true
// 由于常量池中已经有了"123"常量,所以返回的是这个常量的引用,
// 而a的引用就是这个常量的引用,所以这里结果为true
System.out.println(b == b.intern()); // false
// 这里b.intern()返回的是常量池中的"123"常量,对于b指向的则是堆区中的
// 对象,所以这里是结果false
}
}
运行结果:
五、String 常用方法
-
equals(Object obj): 比较两个字符串的内容是否相同,返回布尔值。
-
equalsIgnoreCase(String anotherString): 比较两个字符串的内容是否相同,忽略大小写,返回布尔值。
-
length(): 返回字符串的长度,即字符的数量。
-
indexOf(String str): 返回指定子字符串在字符串中首次出现的位置,如果未找到则返回-1。
-
lastIndexOf(String str): 返回指定子字符串在字符串中最后一次出现的位置,如果未找到则返回-1。
-
substring(int beginIndex): 从指定的开始索引返回一个子字符串。
-
substring(int beginIndex, int endIndex): 返回从指定的开始索引到结束索引之间的子字符串(不包括结束索引)。
-
trim(): 返回去除字符串前后空格的新字符串。
-
charAt(int index): 返回指定索引处的字符。
-
toUpperCase(): 返回一个新的字符串,将原字符串中的所有字符转换为大写。
-
toLowerCase(): 返回一个新的字符串,将原字符串中的所有字符转换为小写。
-
concat(String str): 将指定字符串连接到原字符串的末尾,返回新的字符串。
-
compareTo(String anotherString): 按字典顺序比较两个字符串,返回一个整数,表示它们的相对顺序。
-
toCharArray(): 将字符串转换为一个字符数组。
-
format(String format, Object... args): 使用指定的格式字符串和参数生成格式化的字符串。
六、String 的特性
1、常量折叠
public class Example {
public static void main(String[] args) {
String str = "Hello" + "World";
}
}
由于在编译时已经确定了这个字符串的内容,Java 编译器在编译阶段会对这些字符串进行优化,将其拼接成一个字符串常量 "HelloWorld" 。最终 JVM 只会在字符串常量池中只创建一个字符串对象 "HelloWorld" 。
因此,最终的结果是,str 引用的是常量池中唯一的 "HelloWorld" 对象,而不是创建两个不同的字符串对象,最后再产生一个拼接的新的字符串对象。