目录
5、String和StringBuilder和StringBuffer的效率比较?
1、不可变类简介
-
不可变类:所谓的不可变类是指这个类的实例一旦创建完成后,就不能改变其成员变量值。如JDK内部自带的很多不可变类:Interger、Long和String等。
-
可变类:相对于不可变类,可变类创建实例后可以改变其成员变量值,开发中创建的大部分类都属于可变类。
2、不可变类的设计方法
对于设计不可变类,有以下原则和方法:
(1). 类添加final修饰符,保证类不被继承。
-
如果类可以被继承会破坏类的不可变性机制,只要继承类覆盖父类的方法并且继承类可以改变成员变量值,那么一旦子类以父类的形式出现时,不能保证当前类是否可变。
(2). 保证所有成员变量必须私有private,并且加上final修饰
-
通过这种方式保证成员变量不可改变。但只做到这一步还不够,因为如果是对象成员变量有可能再外部改变其值。所以第4点弥补这个不足。
(3). 不提供改变成员变量的方法,包括setter
-
避免通过其他接口改变成员变量的值,破坏不可变特性。
(4). 通过构造器初始化所有成员,进行深拷贝(deep copy)
-
如果构造器传入的对象直接赋值给成员变量,还是可以通过对传入对象的修改进而导致改变内部变量的值。
(5). 在getter方法中,不要直接返回对象本身,而是克隆对象,并返回对象的拷贝
-
这种做法也是防止对象外泄,防止通过getter获得内部可变成员对象后对成员变量直接操作,导致成员变量发生改变。
3、String对象的不可变性
查看String类的设计,可以观察到以下设计细节:
-
(1). String类被final修饰,不可继承;
-
(2). String内部所有成员都设置为不可变final和变为私有变量private修饰;
-
(3). 不存在value的setter;
-
(4). 当传入可变数组value[]时,进行深拷贝copy而不是直接将value[]复制给内部变量.
-
(5). 获取value时不是直接返回对象引用,而是返回对象的copy.
//final修饰类,不可继承
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
//final修饰变量value,不可改变,属性为private;
private final char value[];
/** Cache the hash code for the string */
private int hash; // Default to 0
/** use serialVersionUID from JDK 1.0.2 for interoperability */
private static final long serialVersionUID = -6849794470754667710L;
....
public String(char value[]) {
this.value = Arrays.copyOf(value, value.length); // deep copy操作
}
...
//值的改变转换为深拷贝;
public char[] toCharArray() {
// Cannot use Arrays.copyOf because of class initialization order issues
char result[] = new char[value.length];
System.arraycopy(value, 0, result, 0, value.length); //深拷贝
return result;
}
...
}
这都符合上面总结的不变类型的特性,也保证了String类型是不可变的类。
4、String对象的不可变性的优缺点
优点:
(1).字符串常量池的需要.
-
字符串常量池可以将一些字符常量放在常量池中重复使用,避免每次都重新创建相同的对象、节省存储空间。但如果字符串是可变的,此时相同内容的String还指向常量池的同一个内存空间,当某个变量改变了该内存的值时,其他遍历的值也会发生改变。所以不符合常量池设计的初衷。
(2).线程安全考虑。
-
同一个字符串实例可以被多个线程共享。这样便不用因为线程安全问题而使用同步。字符串自己便是线程安全的。
(3). 类加载器要用到字符串,不可变性提供了安全性,以便正确的类被加载。
-
譬如你想加载java.sql.Connection类,而这个值被改成了myhacked.Connection,那么会对你的数据库造成不可知的破坏。
(4). 支持hash映射和缓存。
-
因为字符串是不可变的,所以在它创建的时候hashcode就被缓存了,不需要重新计算。这就使得字符串很适合作为Map中的键,字符串的处理速度要快过其它的键对象。这就是HashMap中的键往往都使用字符串。
缺点:
如果有对String对象值改变的需求,那么会创建大量的String对象,产生大量的jvm垃圾碎片,占用内存,触发gc。
5、String和StringBuilder和StringBuffer的效率比较?
实例验证:
public static void main(String[] args) {
String str = "";
long start1 = new Date().getTime();
for (int i = 0; i < 10000; i++) {
str += i;
}
System.out.println("使用String生成耗时: " + (new Date().getTime() - start1));
StringBuilder builder = new StringBuilder();
long start2 = new Date().getTime();
for (int i = 0; i < 10000; i++) {
builder.append(i);
}
System.out.println("使用StringBuilder生成耗时: " + (new Date().getTime() - start2));
result:
使用String生成耗时: 542
使用StringBuilder生成耗时: 1
使用StringBuffer生成耗时: 1
分析:从结果可以看出,StringBuilder和StringBuilder比String的效率要高很多。
这是因为在String在java中是不可变长的,一旦初始化就不能修改长度,简单的字符串拼接就是新建的String对象,再把拼接后的内容赋值给新的对象,在频繁修改的前提下会频繁创建对象,而SpringBuiler则不会,从头到尾都是一个对象,那StringBuilder是怎么实现的呢?
5.1、StringBuilder的设计
从StringBuilder的源码中可以看出,使用StringBuilder时并不是用String存储,而是使用了一个value的char[] 数组来存储,字符串是固定长度的,而数组是可以扩容的,这样就不需要每次都创建一个新的对象了。
abstract class AbstractStringBuilder implements Appendable, CharSequence {
/**
* The value is used for character storage.
*/
char[] value; //存储数据的数组对象;
/**
* The count is the number of characters used.
*/
int count; //数据中使用的字符的大小;
/**
* This no-arg constructor is necessary for serialization of subclasses.
*/
AbstractStringBuilder() {
}
/**
* Creates an AbstractStringBuilder of the specified capacity.
*/
AbstractStringBuilder(int capacity) { //创建初始化大小;
value = new char[capacity];
}
可以看到StringBuilder数组元素的原始大小为16.
public final class StringBuilder
extends AbstractStringBuilder
implements java.io.Serializable, CharSequence
{
/** use serialVersionUID for interoperability */
static final long serialVersionUID = 4383685877147921099L;
/**
* Constructs a string builder with no characters in it and an
* initial capacity of 16 characters.
*/
public StringBuilder() {
super(16);
}
扩容的条件:minimumCapacity = (append的数据长度+value.count) > value.length数组的长度的时候进行扩容。
private void ensureCapacityInternal(int minimumCapacity) {
// overflow-conscious code 扩容条件:minimumCapacity = (append的数据长度+value.count)> value.length
if (minimumCapacity - value.length > 0) {
value = Arrays.copyOf(value,
newCapacity(minimumCapacity));
}
}
扩容的大小:value.length*2 + 2;
private int newCapacity(int minCapacity) {
// overflow-conscious code 扩容的大小为:value.length*2 + 2;
int newCapacity = (value.length << 1) + 2;
if (newCapacity - minCapacity < 0) {
newCapacity = minCapacity;
}
return (newCapacity <= 0 || MAX_ARRAY_SIZE - newCapacity < 0)
? hugeCapacity(minCapacity)
: newCapacity;
}
6、小结
String、StringBuilder、StringBuffer的区别:
-
String 是不可变的,任何改变都会产生新的对象,如果改变频繁将产生很多String垃圾碎片,增加JVM的回收负担,占用大量的内存;
-
StringBuilder是可变类,通过可变数组来存储,但不是线程安全的;
-
StringBuffer也是可变类,是线程安全的,使用了Synchronized对操作进行同步控制;
StringBuilder和StringBuffer的关系类似于Hashmap和Hashtable的关系。
使用场景:
-
字符串相加操作和改动较少的时候用引号的形式;
-
多线程:StringBuffer;append是一个同步方法;
-
单线程但是改动较多:StringBuilder;
7、问题思考
问题一:String str = new String(“abc”) 创建了几个对象?
分情况:
(1) 如果常量池中有”abc”存在,则创建一个堆上的返回;
(2) 如果常量池中没有“abc”存在,则创建两个如下:
-
(1) 使用引号""在编译器就确定了,放在常量池区;
-
(2) 使用new String(“”) 创建的对象存储在堆上,是运行期间创建的,返回的是堆上的数据的引用地址;
解析:可以看成是四步:
-
String str :就是定义一个string的变量str,没有创建对象;
-
= : 赋值操作;将某个对象的引用(或者叫句柄)赋值给它,没有创建对象;
-
"abc" :创建一个"acc"对象,常量池里有就不创建,否则创建放入常量池;
-
new String() : 在堆上创建一个abc对象;
问题二:String str =“king” + “zz” 创建了几个对象?
反编译查看编译器会优化 [常量折叠] 对象的产生,创建1个(kingzz)字符串对象,放在java堆的常量池中
问题三:String str = new String(“king”) + new String(“zz”) 创建了几个对象?
5个字符串对象,3个("king","zz","kingzz")在java堆中,2个("king","zz")在java堆的常量池中。
问题四:什么是常量折叠?
常量折叠:是Java在编译期做的一个优化,简单的来说,在编译期就把一些表达式计算好,不需要在运行时进行计算。
比如: int a = 1 + 2,经过常量折叠后就变成了int a = 3。
“常量折叠”并不是所有的常量都会进行折叠,必须是编译期常量之间进行运算才会进行常量折叠,编译器常量就是编译时就能确定其值的常量,这个定义很严格,需要满足以下条件:
-
1. 字面量是编译期常量(数字字面量,字符串字面量等)。
-
2. 编译期常量进行简单运算的结果也是编译期常量,如1+2,”hello”+”world”。
-
3. 被编译器常量赋值的 final 的基本类型和字符串变量也是编译期常量。因为final修饰的变量是不能被修改的,所以可以在编译期间确定。
问题五:String.intern()的使用?
-
(1) 直接使用双引号申明出来的String对像会直接存储在常量池;
-
(2) 如果不是使用双引号,则有String.intern()函数,将该字符串放入到常量池中,如果常量池中存在该字符串则返回该字符串的引用,如果不存在则在常量池中创建一个字符串,然后返回该字符串的引用。
-
(3) new会在堆上创建对象;
//todo string.intern()的应用
String s1 = new String("kinglovezz");
String s2 = s1.intern(); //将"kinglovezz放入常量池"
String s3 = "kinglovezz";
System.out.println(s2); //kinglovezz
System.out.println(s1 == s2);//false,因为一个是堆内存中的String对象一个是常量池中的String对象,
System.out.println(s3 == s2);//true,因为两个都是常量池中的String对象
注意:string是不可变类型,创建后不可改变,改变的只是变量的引用?
String s1 = "king"; //常量池
s1="zz"; //只是改变了s1的引用,“king”并没有改变,原封不动的坐在常量池里
String s2 = "love"; //常量池
String s3 = new String("love"); //堆
实例验证:
public static void main(String[] args) {
String s1 = new String("kinglovezz");//创建2个对象,一个Class和一个堆里面
String s2 = "kinglovezz"; //创建1个对象,s2指向pool里面的"kinglovezz"对象
String s3 = "kinglovezz"; //创建0个对象,指向s2指想pool里面的那个对象
String s4 = s2; //创建0个对象,指向s2,s3指想pool里面的那个对象
String s5 = new String("kinglovezz");//创建1个对象在堆里面,注意,与s1没关系
String s6 = s1.intern();
System.out.println(s2 == "kinglovezz");//true s2=="kinglovezz"很明显true
System.out.println(s2 == s3); //true,因为指向的都是pool里面的那个"kinglovezz"
System.out.println(s2 == s4); //true,同上,那么s3和s4...:)
System.out.println(s1 == s5); //false,很明显,false
System.out.println(s1 == s2); //false,指向的对象不一样,下面再说
System.out.println(s1 == "kinglovezz");//false,难道s1!="tset"?下面再说
System.out.println(s6 == s2); //true 都是指向常量池
System.out.println("---------------");
s1 = s2;
System.out.println(s1 == "kinglovezz");//true, s2 == s3 == s4 == s6
}
水滴石穿,积少成多。学习笔记,内容简单,用于复习,梳理巩固。