03|字符串性能优化不容小觑,百M内存轻松存储几十G数据
在文中作者抛出了这样一个问题
String str1 = "abc";
String str2 = new String("abc");
String str3 = str2.intern();
String str4 = "ab";
String str5 = str4+"c";
System.out.println("str1==str2:"+str1 == str2); //1
System.out.println(str1.equals(str2)); //2
System.out.println("str2==str3:"+str2 == str3); //3
System.out.println("str1==str3:"+str1==str3); //4
System.out.println("abc" == str1.substring(0)); //5
System.out.println("bc" == str1.substring(1)); //6
System.out.println(str4 == str5); //7
运行结果:
false
true
false
true
true
false
第一种(String str1 = "abc";)创建String字符串对象时,JVM首先会检查对象是否在字符串常量池中,如果在,就返回该对象的引用,否则新的字符串在常量池中被创建,这种方式可以减少同一个值的字符串对象的重复创建,节约内存.
第二种(String str2 = new String("abc");) 首先在编译类文件时,“abc”常量字符串将会放入到常量池结构中,在类加载时,“abc”将会在常量池中被创建;其次,在调用new时,JVM命令将会调用String 的构造函数,同时引用常量池中的“abc”字符串,在堆中创建一个String对象;最后,str将引用String对象
在Java中比较两个对象是否相等,往往用== ,判断两个对象的值是否相等,用equals()方法来判断
分析运行结果:
-
1 、第一种方法仅仅是一个赋值语句,在创建的时候,JVM 会检查在字符串池中,是否已经存在该字符串,如果已经存在了,那么会返回这个字符串的引用给变量 s。如果不存在,那么会创建一个 abc 字符串对象,再赋值给 str1。因此,这句话可能只创建 1 个或者 0 个对象。 第二种方法会在内存中创建 1 个或者 2 个对象。把 new String(“abc”) 这句话拆成两个部分来看,一个是”abc”, 另一个是 new String()。如果 abc 字符串已经在字符串池中存在了,那么就不需要在创建 abc 字符串的对象了,但是 new String 这行代码会再构造出一个和 abc 一样的字符串,并且是放在堆上。 因此str1==str2 为false
-
2、String重写了equals方法,str1和str2的值相等,因此str1.equals(str2)为true
-
3、str2是使用堆中对象,str3由于是调用的intern()方法,会将str3的引用指向字符串常量池,所用引用的是字符串常量池中的对象.
-
str3intern()返回的引用指向常量池中的"abc",str1已经在常量池中建立了"abc",这个时候str3是从常量池中取出来的和str1指向的是同一个对象,所以为true
-
5和6 sbuString()方法的实现中有个index == 0 的判断
public String substring(int beginIndex) {
if (beginIndex < 0) {
throw new StringIndexOutOfBoundsException(beginIndex);
}
int subLen = value.length - beginIndex;
if (subLen < 0) {
throw new StringIndexOutOfBoundsException(subLen);
}
return (beginIndex == 0) ? this : new String(value, beginIndex, subLen);
}
当inde等于0时直接返回当前对象,否则新创建一个对象返回,而==又是地址比较,所以5为true,6为false
- 7、当字符串常量与String类型变量连接时得到的新的字符串不再保存在常量池中,而是在堆中新建一个String对象存放,很明显常量池中要求的存放的是常量,有 String 类型变量当然不能存在常量池中了。
如何使用String.intern节省内存
String a =new String("abc").intern();
String b = new String("abc").intern();
if(a==b) {
System.out.print("a==b");
}
输出结果:
a==b
在字符串常量中,默认会将对象放入常量池中;在字符串变量中,对象是会创建在堆内存中,同时也会在常量池中创建一个字符串对象,引用赋值到堆内存对象中,并返回堆内存对象引用.
如果调用intern()方法,回去查看字符串常量池中是否有等于该对象到字符串,如果没有,就在常量池中新增该对象,并返回该对象到引用.如果有,就返回常量池中字符串到引用.堆内存中由于原有的对象由于没有引用指向它,将会通过垃圾回收器回收.
了解了上面的原理,我们可以得知:
在一开始创建a变量时,会在堆内存中创建一个对象,同时会在加载类时,在常量池中创建一个字符串对象,调用intern方法之后,会去常量池中查找是否有等于该字符串的对象,有就返回引用.
在创建b字符串变量时,也会在堆中创建一个对象,此时常量池中有该字符串对象,就不再创建.调用intern方法则会去常量池中判断是否有等于该字符串的对象.发现有等于"abc"字符串的对象,就直接返回引用.而在堆内存中的对象,由于没有引用指向他,将会被垃圾回收器回收掉.所以a和b引用的是同一个对象.
下图为String字符串的创建分配内存地址的情况:
常量池是类似于一个HashTable的实现方式,HastTable存储的数据越大, 遍历的时间复杂度就会增加,如果数据过大,会增加整个字符串常量池的负担.
如何使用字符串的分割
spit()方法使用了正则表达式实现了其强大的分割功能,而正则表达式的性能是十分不稳定的,使用不恰当会引起回溯问题,导致CPU居高不下.
可以使用String.indexOf()来替代.
05|ArrayList还是LinkedList?使用不当性能差千倍
ArrayList属性
// 默认初始化容量
private static final int DEFAULT_CAPACITY = 10;
// 对象数组
transient Object[] elementData;
// 数组长度
private int size;
ArrayList的属性elementData被关键字transient修饰,transient关键字修饰该字段则表示该属性不会吧被序列化,但ArrayList其实是实现来序列化接口.由于ArrayList的数组是基于动态扩增的,所以并不是所有被分配的内存空间都存储了数据.如果采用外部序列化的方式实现数组的序列化,会序列化整个数组.ArrayList为避免这些没有存储数据的空间被序列化,内部提供了两个私有化的方法writeObect以及readObject来自我完成序列化与发序列化,从而在序列化与发序列化数组时节省来空间和时间.
因此使用transient修饰数组,是防止对象数组被其他外部方法序列化.
ArrayList新增元素
如果我们在初始化时就比较清楚存储数据的大小,就可以在ArrayList初始化时指定数组容量的大小,并且在添加元素时,只在数组末尾添加元素,那么ArrayList在大量新增元素的情况下,性能并不会变差,反而比其他List集合的性能更好
ArrayList删除元素
ArrayList没删除一个元素,都要进行数组重组,并且删除的元素越靠前,数组重组的开销越大.