深入理解String类:揭秘Java字符串常量池的优化机制
1. 概述
字符串String,是程序中使用最多的一种数据,JVM在内存中专门设置了一 块区域(字符串常量池),来提高字字符串对象的使用效率。
在Java中, String 是一个类,用于表示字符串,它是Java中最常用的类之一, 用于处理文本数据。
String基础内容:
- String 类在 java.lang 包下,所以使用的时候不需要导包
- Java 程序中所有字符串字面值(如"abc")都是 String类对象
- 字符串值不可变, String 对象是不可变的,一旦创建,它们的值就不能被 修改
常用构造方法:
案例展示:
package com.briup.chap07.test;public class Test061_Basic {public static void main(String[] args) {// public String() : 创建一个空白字符串对象,不含有任何内容String s1 = new String();System.out.println(s1);// public String(char[] chs) : 根据字符数组的内容,来创建字符串对象char[] chs = {'a', 'b', 'c'};String s2 = new String(chs);System.out.println(s2);// public String(String original) : 根据传入的字符串内容,来创建字符串对象String s3 = new String("123");System.out.println(s3);String s4 = "hello";System.out.println(s4);}
}
注意,字符串对象创建以后,堆空间中字符串值不可以被修改,具体如下图:
2.常量池
问题引入:
创建字符串对象,和其他普通对象一样,会占用计算机的资源(时间和空 间),作为最常用的数据类型,大量频繁的创建字符串对象,会极大程度地影响 程序的性能。
JVM为了提高性能和减少内存开销,在实例化字符串常量的时候进行了一些优化
- 为字符串开辟一个字符串常量池,类似于缓存区
- 创建字符串常量时,首先会检查字符串常量池中是否存在该字符串,如果存 在该字符串,则返回该实例的引用,如果不存在,则实例化创建该字符串, 并放入池中
String常量池:
在Java中,String常量池是一块特殊的内存区域,用于存储字符串常量。String 常量池的设计目的是为了节省内存和提高性能
当我们创建字符串常量时,如果字符串常量池中已经存在相同内容的字符串, 那么新创建的字符串常量会直接引用已存在的字符串对象,而不会创建新的对 象。这样可以避免重复创建相同内容的字符串,节省内存空间。
在JDK8及之后的版本中,字符串常量池的位置与其他对象的存储位置,都位于 堆内存中。这样做的好处是,字符串常量池的大小可以根据需要进行调整,并且 可以享受到垃圾回收器对堆内存的优化。
Java将字符串放入String常量池的方式:
1. 直接赋值:通过直接赋值方式创建的字符串常量会被放入常量池中。
例如: String str = "Hello";
2. 调用String类提供intern()方法:可以将字符串对象放入常量池中,并返回 常量池中的引用。
例如: String str = new String("World").intern();
注意:
通过new关键字创建的字符串对象不会放入常量池中,而是在堆内存中创 建一个新的对象。只有通过直接赋值或调用intern()方法才能将字符串放入常量 池中。
案例1:
package com.briup.chap07.test;public class Test062_String {public static void main(String[] args) {String s1 = "Hello"; // 字符串常量,放入常量池String s2 = "Hello"; // 直接引用常量池中的字符串对象System.out.println(s1 == s2); // true,引用相同// 直接new String对象,不会将'World'放入常量池String s3 = new String("World");// 调用intern()方法,将'World'放入常量池,并返回常量池中的引用String s4 = new String("World").intern();String s5 = "World";System.out.println(s3 == s4); // false,引用不同System.out.println(s4 == s5); // true,引用相同}
}
对应内存图为:
案例2:
package com.briup.chap07.test;public class Test062_String2 {public static void main(String[] args) {String s1 = "a";String s2 = "b";// 常量优化机制:"a" 和 "b"都是字面值常量,借助 + 连接,其结果 "ab" 也被当作常量String s3 = "a" + "b";String s4 = "ab";System.out.println(s3.equals(s4)); // trueSystem.out.println(s3 == s4); // trueSystem.out.println("-------------");String s5 = s1 + s2;System.out.println(s4.equals(s5)); // trueSystem.out.println(s4 == s5); // falseSystem.out.println("-------------");String s6 = (s1 + s2).intern();System.out.println(s4.equals(s6)); // trueSystem.out.println(s4 == s6); // true}
}
解析:
s3
和s4
都是通过编译期的字符串字面量定义的,由于Java的字符串常量池机制,相同的字符串字面量会指向常量池中的同一个实例。因此,s3 == s4
为true
。然而,
s5
是通过变量s1
和s2
的值在运行时进行字符串连接得到的。尽管s1
和s2
分别是"a"
和"b"
,并且它们各自也是指向常量池中的实例,但当执行s1 + s2
时,这个操作是在运行时完成的,并且会产生一个新的String
对象,而不是重用常量池中的"ab"
。因此,s5
并不是指向常量池中的"ab"
,而是指向了一个新的String
实例。所以s4 == s5
为false
。String s6 = (s1 + s2).intern(); // 将运行时生成的字符串放入常量池并返回常量池中的引用 System.out.println(s4 == s6); // true,因为s6现在引用了常量池中的"ab"
如果常量池中已经存在与
(s1 + s2)
生成的字符串内容相同的字符串,那么intern()
方法的行为如下:intern()
方法会直接返回常量池中已存在字符串对象的引用。也就是说,s6
将会指向常量池中已有的那个字符串对象,而不是创建一个新的对象放入常量池。
final修饰的字符串在与常量拼接时也会放入常量池
// final常量测试
public static void main(String[] args) {String str = "ab";// final修饰的String常量 final String str1 = "a";String str2 = str1 + "b";System.out.println(str == str2); // 结果为 true
}
3. 常见方法
String源码:
package java.lang;public final class Stringimplements java.io.Serializable, Comparable<String>, CharSequence {/** The value is used for character storage. */private final char value[];// 省略...// 获取字符串字符个数public int length();// 比较字符串的内容,严格区分大小写public boolean equals(Object anObject);// 返回指定索引处的 char 值public char charAt(int index);// 将字符串拆分为字符数组后返回public char[] toCharArray();// 根据传入的规则切割字符串,得到字符串数组public String[] split(String regex);// 根据开始和结束索引进行截取,得到新的字符串 [begin, end)public String substring(int begin, int end);// 从传入的索引处截取,截取到末尾,得到新的字符串 [begin, str.length())public String substring(int begin);// 使用新值,将字符串中的旧值替换,得到新的字符串public String replace(CharSequence target, CharSequence replacement);
}
获取字符串信息
int length()
:返回字符串的长度。
比较字符串
boolean equals(Object anObject)
:将此字符串与指定对象进行比较。区分大小写。boolean equalsIgnoreCase(String anotherString)
:忽略大小写地比较两个字符串是否相等。int compareTo(String anotherString)
:按字典顺序比较两个字符串。int compareToIgnoreCase(String str)
:忽略大小写地按字典顺序比较两个字符串。
查找子串或字符
int indexOf(int ch)
:返回指定字符在此字符串中第一次出现处的索引。int indexOf(String str)
:返回指定子串在此字符串中第一次出现处的索引。int lastIndexOf(int ch)
:返回指定字符在此字符串中最后一次出现处的索引。int lastIndexOf(String str)
:返回指定子串在此字符串中最右边出现处的索引。
提取子串
String substring(int beginIndex)
:从指定的beginIndex
开始截取到字符串末尾。String substring(int beginIndex, int endIndex)
:从指定的beginIndex
(包含)开始到endIndex
(不包含)结束之间的子串。
转换操作
char[] toCharArray()
:将此字符串转换为一个新的字符数组。String[] split(String regex)
:根据给定正则表达式作为分隔符,将字符串分割为子字符串。String toLowerCase()
:使用默认语言环境的规则将所有字符转换为小写。String toUpperCase()
:使用默认语言环境的规则将所有字符转换为大写。
替换操作
String replace(char oldChar, char newChar)
:返回一个新的字符串,它是通过用newChar
替换此字符串中出现的所有oldChar
得到的。String replace(CharSequence target, CharSequence replacement)
:使用指定的替换序列替换与此字符串中的指定子字符串匹配的子字符串。
其他常用方法
boolean startsWith(String prefix)
:测试此字符串是否以指定的前缀开始。boolean endsWith(String suffix)
:测试此字符串是否以指定的后缀结束。String trim()
:返回一个新字符串,它是通过删除此字符串的开头和结尾的空白字符得到的。String concat(String str)
:将指定字符串连接到此字符串的末尾。