学习资料:
我电脑环境JDk 1.8
看到一篇很有深度的讲解:How many Objects created with: String str=new String("Hello")?
1. String字符串
String
不是Java
中的基本数据类型
在C
语言中,字符串的处理通常是使用char
数组,但数组本身无法封装字符串操作所需要的方法。在Java
中,String
对象可以看作是char
数组的延伸和进一步封装
String
主要由3部分组成:char数组
,offset偏移
,conut长度
char数组
表示String
的内容,String
对象所表示字符串的超集。String
的真实内容还需要由偏移量和长度在char
数组中进行定位和截取
1.1 3个基本特点
不变性
针对常量池的优化
类的final定义
1.1.1 不变性
当String
对象一旦生成,就不能对再对它进行改变
个人理解:
String
类中的操作字符串的方法,并不是直接改变当前的String
对象,而创建了一个新的String
对象
String
的这个特性可以泛化成不变(immutable)模式
,即一个对象被创建后就不再发生变化。作用在于当一个对象需要被多线程共享,并且访问频繁时,可以省略同步和锁等待的时间
1.1.2 针对常量池的优化
当两个String
对象拥有相同的值时,只引用常量池中的同一个拷贝。当一个字符串反复出现时,可以节省内存空间
String str1 = "abc";
String str2 = "abc";
String str3 = new String("abc");
System.out.println(str1 == str2); // true
System.out.println(str1 == str3); // false
System.out.println(str1 == str3.intern()); // true
str1
与str2
引用了相同的地址,str3
重新开辟了一块内存空间
str3
在常量池中的位置和str1
是一样的,也就是说,虽然str3
单独占用了堆空间,但是它所指向的实体和str1
完全一样
str3.intern()
返回了String
对象在常量池中的引用
1.1.3 类的 final 定义
作为final
类的String
对象在系统中不可能有任何子类,这是对系统安全性的保护
在JDK 1.5
版本之前的环境中,使用final
定义,有助于帮助虚拟机寻找机会,内联所有的final
方法,提高系统效率。但在JDK 1.5
以后,效果并不明显
注意:
在1.7
之后,String
的substring()
方法已经不会再引起内存泄露
可能会引起内存泄露的是String()
中的一个私有构造方法,在1.7
之后已经修复
1.2 字符串的分割和查找
1.2.1 分割
作者用的
JDK
版本不知道多少,但应该不是1.8
,很可能是1.6
,感觉同样的代码测试,时间已经比作者测试时间少了不止一个量级。除了JDK
代码迭代升级的优化,还得考虑电脑的差距
结论:一般情况下直接使用split()
方法足够,在一些要分割的目标长度很长很长并且需要分割的次数很多很多,split()
耗时久时,再考虑使用StringTokenzier
,但感觉一个字符串要分割10000次已经有些丧心病狂
测试代码:
测试次数,为了方便观察,跨度有些大,应该有梯度的测试
public class StringL {
private static final String LINE = "************************************************";
public static void main(String[] args) {
test(1000);
test(10000);
test(100000);
test(1000000);
test(10000000);
}
/**
* 根据字符串的长度完成一轮测试
*/
private static void test(int num) {
String str = getHugeString(num);
System.out.println("字符串长度为 " + str.length() + " ,同一个字符串需要分割 " + num + " 次时:");
// split 方式
splitTest(str);
// StringTokenizer 方式
StringTokenizerTest(str);
// 打印分割线
System.out.println(LINE);
}
/**
* 使用 StringTokenizer 分割字符串
*/
private static void StringTokenizerTest(String str) {
StringTokenizer st = new StringTokenizer(str, ";");
long now = System.currentTimeMillis();
while (st.hasMoreTokens()) {
st.nextToken();
}
System.out.println("StringTokenizer 用时:" + (System.currentTimeMillis() - now));
}
/**
* 原始的 split 方法分割字符串
*/
private static void splitTest(String str) {
long now = System.currentTimeMillis();
str.split(";");
System.out.println("split 用时:" + (System.currentTimeMillis() - now));
}
/**
* 获取字符串
*/
private static String getHugeString(int num) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < num; i++) {
sb.append(i);
sb.append(";");
}
return sb.toString();
}
}
输出结果,时间是毫秒:
字符串长度为 3890 ,同一个字符串需要分割 1000 次时:
split 用时:2
StringTokenizer 用时:1
************************************************
字符串长度为 48890 ,同一个字符串需要分割 10000 次时:
split 用时:4
StringTokenizer 用时:4
************************************************
字符串长度为 588890 ,同一个字符串需要分割 100000 次时:
split 用时:28
StringTokenizer 用时:21
************************************************
字符串长度为 6888890 ,同一个字符串需要分割 1000000 次时:
split 用时:503
StringTokenizer 用时:60
************************************************
字符串长度为 78888890 ,同一个字符串需要分割 10000000 次时:
split 用时:3895
StringTokenizer 用时:770
************************************************
多次测试,当对同一个字符串要分割次数达不到一定期限时,有时StringTokenizer
的效率还不如split()
方法,只有到达一定期限后,StringTokenizer
的优势就很明显
1.2.1 查找
String
的charAt(),indexOf()
方法非常非常高效
测试代码:
分别使用startWith()
和endWith()
比较100w
次
public class CharAtTest {
public static void main(String[] args) {
String str = "abc123456xyz";
int num = 1000000;
String startTag = "abc";
String endTag = "xyz";
// startWith 方法
long now = System.currentTimeMillis();
for (int i = 0; i < num; i++) {
boolean b = str.startsWith(startTag);
}
System.out.println("startWith()方法耗时 : " + (System.currentTimeMillis() - now));
// myStartWith() 方法
now = System.currentTimeMillis();
for (int i = 0; i < num; i++) {
myStartWith(str);
}
System.out.println("myStartWith()方法耗时 : " + (System.currentTimeMillis() - now));
// endWith方法
now = System.currentTimeMillis();
for (int i = 0; i < num; i++) {
boolean b = str.endsWith(endTag);
}
System.out.println("endsWith()方法耗时 : " + (System.currentTimeMillis() - now));
// myEndWith() 方法
int length = str.length();
now = System.currentTimeMillis();
for (int i = 0; i < num; i++) {
myEndWith(str, length);
}
System.out.println("myEndWith()方法耗时 : " + (System.currentTimeMillis() - now));
}
private static void myEndWith(String str, int length) {
boolean b = str.charAt(length - 3) == 'a' && str.charAt(length - 2) == 'b' && str.charAt(length - 1) == 'c';
}
private static void myStartWith(String str) {
boolean b = str.charAt(0) == 'a' && str.charAt(1) == 'b' && str.charAt(2) == 'c';
}
}
输出结果:
startWith()方法耗时 : 15
myStartWith()方法耗时 : 6
endsWith()方法耗时 : 13
myEndWith()方法耗时 : 6
结论:在极为敏感的系统中,必要的时候可以考虑使用charAt()
方法来代替startWith()
或者endWith()
。但几乎大部分情况下,直接startWith()
或者endWith()
足够
2. StringBuilder 和 StringBuffer
由于String
对象是不可变对象,在需要对字符串进行修改操作时,如连接、替换,总是会生成新的对象,导致在某些时候性能比较差,JDK
专门提供了创建和修改字符串的工具类StringBuffer
和StringBuilder
2.1 String 常量的累加操作
关于String s = "a" + "b" + "c"
几个对象: 一个
String s = "a" + "b" + "c"
这种静态字符串的连接操作,Java
在编译时期将多个连接操作的字符串在编译时合成一个单独的长字符串abc
对于常量字符串的累加,Java
在编译时期就做了充分优化,在编译时期便能确定取值的字符串操作,在编译时期进行计算,在运行时,并不会生成大量的String
实例对象
2.2 String 变量的累加操作
测试代码:
public class StringCreate {
public static void main(String[] args) {
commonConcat();
stringBuilderConcat();
}
private static void stringBuilderConcat() {
long now = System.currentTimeMillis();
for (int i = 0 ; i < 50000; i ++){
StringBuilder sb = new StringBuilder();
String str1 = "abc";
String str2 = "897";
String str3 = "xyz";
String str4 = "123";
String result = sb.append(str1).append(str2).append(str3).append(str4).toString();
}
System.out.println("使用 StringBuilder 用时:" + (System.currentTimeMillis() - now));
}
/**
* 直接拼接
*/
private static void commonConcat() {
long now = System.currentTimeMillis();
for (int i = 0; i < 50000; i++) {
String str1 = "abc";
String str2 = "897";
String str3 = "xyz";
String str4 = "123";
String result = str1 + str2 + str3 + str4;
}
System.out.println("直接拼接用时:" + (System.currentTimeMillis() - now));
}
}
结果
直接拼接用时:27
使用 StringBuilder 用时:15
平均耗时差距并不大
原因在于,对于字符串变量的累加,Java
也做了优化,使用了StringBuidler
对象来实现字符串的累加
直接拼接
的代码中,for()
内字符串部分反编译之后:
反编译,我并没有做,而是直接将反编译的代码照着书上的形式,抄了下来
String str1 = "abc";
String str2 = "897";
String str3 = "xyz";
String str4 = "123";
String result = (new StringBuilder(String.valueOf(str1))).append(str2).append(str3).append(str4).toString();
在构建超大的String
对象时,优先考虑:显示使用StringBuilder
或者 StringBuffer
2.3 StringBuilder 和 StringBuffer
两者都继承之AbstractStringBuiler
,拥有几乎相同的对外接口
两者最大的区别: StringBuffer
对几乎所有的方法做了同步
也就是说选择需要根据使用的具体场景来确定,考虑线程安全时,多线程环境就选择StringBuffer
在两者的构造方法中,都有一个方法提供了设置容量参数
,如果预先能确定容量,指定容量大小,也可以再次对性能进行提升
默认情况下,不指定容量时,是16
字节,之后需要的容量超过实际char
数组长度时,就会进行扩容,int newCapacity = (value.length << 1) + 2
,扩容的细节,暂时不考虑
若不指定容量,扩容又基本始终是2
倍大小,就会造成一些空间浪费
3. 最后
有错误,请指出
共勉 : )