String类的实现方式
在Java 9之前,String类是由char数组实现的,每个char占用两个字节的内存空间。而在Java 9中,String类引入了一种称为"Compact Strings"的新实现方式,将字符串的表示方式从char数组改为byte数组,并使用一种编码方式将Unicode字符映射到一个或两个字节的表示方式。这种实现方式可以大大减少内存使用,尤其是对于包含大量ASCII字符的字符串。
为什么这么说呢?JDK9中,引入了一个coder标识,用来区分是普通的拉丁字母还是UTF16字符。
static final byte LATIN1 = 0;
static final byte UTF16 = 1;
我们在日常使用中可能很多情况下大量使用英文字母,较少使用一些中文或者其他复杂的字符,这时候JDK9中的这种优化就能有很大的用处,因为按照原本JDK8的方案,无论是什么内容,就统一按照char来存储,这样对于那些普通的英文字母根本不需要用两个字节的空间,一个字节就够了,如果涉及到大量的这种纯英文字母的字符串,此时JDK9的存储上的优化就大大体现了出来。
String类中的方法
- 构造方法
String()
: 创建一个空字符串。String(char[] value)
: 创建一个包含指定字符序列的字符串。String(byte[] bytes)
: 使用默认字符集解码指定的字节数组,创建一个新的字符串。String(String original)
: 创建一个与指定字符串内容相同的新字符串。
- 字符串操作方法
charAt(int index)
: 返回指定位置上的字符。concat(String str)
: 将指定字符串连接到此字符串的末尾。substring(int beginIndex)
: 返回一个新的字符串,它是此字符串的子字符串。substring(int beginIndex, int endIndex)
: 返回一个新的字符串,它是此字符串的子字符串。replace(char oldChar, char newChar)
: 返回一个新字符串,它是通过用新字符替换此字符串中出现的所有旧字符得到的。replaceAll(String regex, String replacement)
: 用指定的字符串替换所有匹配给定的正则表达式的子字符串。trim()
: 返回字符串的副本,忽略前导空白和尾部空白。toLowerCase()
: 使用默认语言环境的规则将此字符串转换为小写。toUpperCase()
: 使用默认语言环境的规则将此字符串转换为大写。getBytes()
: 使用平台默认字符集将此字符串编码为字节数组。
- 字符串比较方法
equals(Object anObject)
: 将此字符串与指定对象进行比较。equalsIgnoreCase(String anotherString)
: 将此字符串与指定字符串进行比较,忽略大小写差异。compareTo(String anotherString)
: 按字典顺序比较两个字符串。compareToIgnoreCase(String str)
: 按字典顺序比较两个字符串,忽略大小写差异。
- 其他方法
length()
: 返回此字符串的长度。isEmpty()
: 当且仅当字符串长度为 0 时返回 true。valueOf(int i)
: 返回 int 参数的字符串表示形式。join(CharSequence delimiter, CharSequence... elements)
: 将给定的字符串序列以指定的分隔符拼接起来,并返回结果字符串。
新增的repeat以及strip方法
- repeat()方法
repeat(int count)
方法可以将原字符串重复指定次数,并返回一个新字符串。例如:
javaCopy code
String str = "hello";
String repeatedStr = str.repeat(3);
System.out.println(repeatedStr); // "hellohellohello"
在这个例子中,我们通过调用repeat()
方法将字符串"hello"重复了三次,并返回了一个新字符串"hellohellohello"。
- strip()方法
strip()
方法用于去除字符串两端的空白字符,包括空格、制表符和换行符等,返回一个新字符串。例如:
javaCopy code
String str = " hello \n";
String strippedStr = str.strip();
System.out.println(strippedStr); // "hello"
在这个例子中,原字符串为" hello \n",包含两个前导空格和一个换行符,调用strip()
方法后返回的新字符串为"hello",两端的空格和换行符都被去掉了。
除了strip()
方法,Java 9还新增了stripLeading()
和stripTrailing()
方法,分别用于去除字符串的前导空格和尾部空格。这些方法对于处理输入数据和进行字符串比较时非常有用。
字符串常量池的优化
在Java 9之前,字符串常量池是在永久代(PermGen)中实现的。这意味着在运行时,所有的字符串常量都被存储在一块固定的内存区域中。这种实现方式存在一些问题,比如常量池容易被填满,导致OutOfMemoryError异常;并且,永久代是Java虚拟机中一个相对较小的区域,因此可能会导致内存不足的问题。
在Java 8中,永久代被移除,字符串常量池被转移到了堆(Heap)中。这种实现方式解决了一些问题,但仍然存在一些性能和内存使用方面的问题。在Java 9中,字符串常量池进行了优化,主要包括以下几个方面:
字符串常量池被移到了元空间(Metaspace)中,这是一个更大的内存区域,可以动态调整大小,从而避免了OutOfMemoryError异常。
在元空间中,字符串常量池使用了一种新的数据结构,称为“G1特殊化常量池”。这种数据结构在性能和内存使用方面都有所优化,能够更快地查找和添加常量。
对于使用字符串常量的程序,编译器现在会生成更高效的字节码,以利用这些优化。例如,编译器可以使用ldc2指令来加载常量池中的双字节字符串,而不是使用两个ldc指令。
总之,Java 9中对字符串常量池的优化使得它更加高效和可靠,能够更好地满足大规模应用程序的需要。
性能分析
前面说了这么多,都是关于JDK9性能上的一些优化介绍,但是具体提升了多少呢?下面使用一些直观的例子来感受一下到底提升了多少:
字符串拼接
首先来看看字符串拼接,一直以来我们编程中有一个原则:对于频繁拼接字符串的操作,不要直接使用String,而是考虑使用StringBuilder或者StringBuffer,这是因为String的不可变特性,导致在拼接过程中会产生大量的String对象从而导致内存浪费:
运行下面这段代码:
long startTime = System.nanoTime();
String s = "";
for (int i = 0; i < 100000; i++) {
s += "a";
}
long endTime = System.nanoTime();
System.out.println("cost time: " + (endTime - startTime) + "ns");
将这段代码分别放到JDK1.8和JDK1.9版本对比一下执行时间,这里推荐一个在线的Java编译运行网站,支持动态选择JDK版本:https://www.jdoodle.com/online-java-compiler
为了稍微准确一点,排除一定的偶然性,我JDK1.8和JDK1.9各自都运行了三遍:
// JDK1.8
cost time: 8043104665ns
cost time: 8029676317ns
cost time: 8084159060ns
//JDK1.9
cost time: 1596969483ns
cost time: 2130420129ns
cost time: 2150930645ns
这里可以看到,在十万级别的字符串拼接操作中,JDK1.9的性能提升属于倍数级别的提升了,基本都在四倍左右的提升。
同时每次执行时,页面上结果输出框的上方也有对应的cpu time和memory数据:
//JDK1.8
1394148 kilobyte(s)
1394176 kilobyte(s)
1394388 kilobyte(s)
//JDK1.9
660312 kilobyte(s)
660324 kilobyte(s)
660500 kilobyte(s)
可以看到,这内存占用上直接下降了一个量级,由此可见JDK1.9在字符串的拼接这块,性能上得到了巨大的提升。
字符串替换
long startTime = System.nanoTime();
for (int i = 0; i < 100000; i++) {
String str = "abcdefg";
str.replace("c", "x");
}
long endTime = System.nanoTime();
System.out.println("cost time: " + (endTime - startTime) + "ns");
同样的操作,各自执行3次看看情况:
//JDK1.8
cost time: 130509721ns
cost time: 226153962ns
cost time: 152726076ns
//JDK1.9
cost time: 53052617ns
cost time: 52959038ns
cost time: 66009582ns
/////////////memory////////
//JDK1.8
97120 kilobyte(s)
97096 kilobyte(s)
96936 kilobyte(s)
//JDK1.9
48488 kilobyte(s)
49688 kilobyte(s)
49732 kilobyte(s)
这里可以看到在效率上,直接下降了一个量级,同时在哪存上也有接近50%的节约。
至于其它一些常规的方法,比如:查找(indexOf)、分割(split)这里JDK1.8和JDK1.9并没有多少提升,甚至可能出现JDK1.9的执行效率上反而更低,这并不是说9版本中出现了倒退,而是内部实现的算法不一样,可能在某些比较特殊的场景下,JDK1.9更有优势,就像一些排序算法一样,虽然时间复杂度可以衡量,但是在数据量不大的时候,可能一些复杂度高的算法反而效果更好。
总体来说,JDK1.9中String得split方法相比于JDK1.8,一般认为有如下改进:
使用新的UTF-16字符串匹配器,代替了JDK 8中使用的正则表达式引擎。
改进了分隔符的识别,当分隔符为单个字符时,使用位运算进行匹配,避免了使用正则表达式引擎的开销。
引入了一种优化,当字符串不包含分隔符时,不进行任何操作直接返回原字符串。这里的好处就是避免一些极端情况下产生大量冗余重复的字符串对象
一些内部的实现细节上做了微调。