String操作相关类详解
一.String、StringBuffer、StringBuilder基本介绍
五.StringBuffer/StringBuilder的容量和扩容机制
一.String、StringBuffer、StringBuilder基本介绍
- String 类——String字符串常量
在 Java 中字符串属于对象,Java 提供了 String 类来创建和操作字符串。
String是典型的Immutable类,其值是不可变的。也由于它的不可变性,导致每次对String的操作(拼接、裁剪)都会生成新的String对象,这样不仅效率低下,而且大量浪费有限的内存空间。
- StringBuffer
StringBuffer是为了解决String的拼接操作产生太多的中间对象。使用append或add方法。
StringBuffer本身是线程安全的,也会带来额外的性能开销,自然会带来效率问题。
- StringBuilder
StringBuilder是Java1.5中新增的,在功能上与与StringBuffer 没什么区别,但是去掉了线程安全部分,有效减少了开销。也是绝大多数情况下的字符串拼接的首选。
- 小结
String:不可变字符序列
StringBuffer:可变字符序列、效率低、线程安全
StringBuilder:可变字符序列、效率高、线程不安全
二.String类的不可变性
-
String类通过以下方法确保其不可变性。
1.1. 类添加final修饰符,保证类不被继承。
如果类可以被继承会破坏类的不可变性机制,只要继承类覆盖父类的方法并且继承类可以改变成员变量值,那么一旦子类以父类的形式出现时,不能保证当前类是否可变。1.2. 保证所有成员变量必须私有,并且加上final修饰
通过这种方式保证成员变量不可改变。但只做到这一步还不够,因为如果是对象成员变量有可能再外部改变其值。1.3. 不提供改变成员变量的方法,包括setter
避免通过其他接口改变成员变量的值,破坏不可变特性。1.4.通过构造器初始化所有成员,进行深拷贝(deep copy)
如果构造器传入的对象直接赋值给成员变量,还是可以通过对传入对象的修改进而导致改变内部变量的值。1.5.在getter方法中,不要直接返回对象本身,而是克隆对象,并返回对象的拷贝
这种做法也是防止对象外泄,防止通过getter获得内部可变成员对象后对成员变量直接操作,导致成员变量发生改变。
见源码:
public final class String implements java.io.Serializable, Comparable<String>, CharSequence
{
/** The value is used for character storage. */
private final char value[];
/** The offset is the first index of the storage that is used. */
private final int offset;
/** The count is the number of characters in the String. */
private final int count;
/** Cache the hash code for the string */
private int hash; // Default to 0
....
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;
}
...
}
设计细节:
1.String类被final修饰,不可继承。String内部所有成员都设置为私有变量。
2.不存在value的setter,并将value设置为final。
3.当传入可变数组value[]时,进行copy而不是直接将value[]复制给内部变量。
4.获取value时不是直接返回对象引用,而是返回对象的copy。
- String真的不可变吗?
虽然String对象将value设置为final,并且还通过各种机制保证其成员变量不可改变。但是还是可以通过反射机制的手段改变其值。
@Test
public void testStringChange() throws Exception{
//创建字符串"Hello World"
String s = "Hello World";
//获取String类中的value字段
Field valueFieldOfString = String.class.getDeclaredField("value");
//改变value属性的访问权限
valueFieldOfString.setAccessible(true);
//获取s对象上的value属性的值
char[] value = (char[]) valueFieldOfString.get(s);
//改变value所引用的数组中的第5个字符
value[5] = '!';
System.out.println(s);
}
三.字符串设计和考量
- 对于以下两种方式,我们选择哪种?
String append = new StringBuilder().append(“aa”).append(“bb”).append(“cc”);
String append = “aa”+ “bb”+ “cc”;
java还是非常智能的,通过对String append = “aa”+ “bb”+ “cc”反编译;
结果:直接是单个字符串;同时会被自动缓存。
如果换成:
String a= ”aa”;
String b=”bb”;
String append = a + b;
反编译结果是StringBuilder的append方法;
如果是final常量呢,如下:
final String a= ”aa”;
final String b=”bb”;
String append = a + b;
反编译结果是单个字符串;同时会被自动缓存。
所以:
从代码简洁性和可读性上,后者(+号形式)优于前者;再加上java编译会对字符串进行优化,所以少量字符串拼接时可直接使用String自身+号拼接。
同时,硬编码或者字符串常量在代码中拼接会被编译器自动优化和缓存。
java为避免创建相同的字符串,2个String对象拥有相同的值时,会只引用常量池的同一个拷贝。
在java6以后还提供了intern()方法显示排重。调用该方法时,如果缓存有相同值的字符串,直接返回该实例;否则将该对象缓存起来并返回实例。
1.intern 原理:
intern()方法需要传入一个字符串对象,然后检查StringTable里是不是已经有一个相同的拷贝。StringTable可以看作是一个HashSet。StringTable存在的唯一目的就是维护所有存活的字符串的一个对象。如果在StringTable里找到了能够找到所传入的字符串对象,那就直接返回它,否则,把它加入StringTable。
2.intern 用法:
intern适合用在需要读取数据并将这些对象或者字符串纳入一个更大范围作用域的情况。需要注意的是,硬编码在代码中的字符串(例如常量等等)都会被编译器自动的执行intern操作。
3.intern 利弊:
在java6版本,缓存的字符串被存放在永久代中,这个空间有限,如果使用不当,会引起OOM。
在后续的版本被放置到堆中,极大避免了上述情况。在jdk8中,永久代被元数据替代。
intern的显示调用也难以保证效率。所以,并不常用。
扩容规则是:新容量扩为大小变成原来的2倍+2,如果容量不够,直接扩充到需要的容量大小。
所以,我们在创建StringBuffer的时候,可以指定其大小,这样就避免了在容量不够的时候自动增长,以提高性能。
StringBuffer buffer = new StringBuffer(1000);
## 六.通过练习深入理解String * 习题一
final String a = "aa";
String b = "aabb";
String c = a + "bb";
String d = a.intern() + "bb";
System.out.println(b == c); //第一个输出
System.out.println(b == d); //第二个输出
输出结果?
大致解题方式:理解编译结果->理解运行时内存
由于编译器对于硬编码和final常量字符串会有编译期间优化,同时对+号也会转换,
所以,编译优化后的结果是:
String b = "aabb";
String c = "aabb";
String d = new StringBuilder().append("aa".intern()).append("bb").toString();
System.out.println(b == c); //第一个输出
System.out.println(b == d); //第二个输出
这时候其实结果就比较显然了,b和c都是从常量池(StringTable)取,所以相等;而d是新创建的对象,所以和b的地址不等。
- 习题二
String s1 = "Hello";
String s2 = "Hel" + new String("lo");
s2.intern();
System.out.println(s1 == s2); //第一个输出
new Thread(() -> {
final String s3 = "Hel";
final String s4 = "lo";
System.out.println( (s1== s3 + s4)); //第二个输出
}).start();
同理,按以上方式,先理解编译结果:
String s1 = "Hello";
String s2 = new StringBuilder().append("Hel").append(new String("lo")).toString();
s2.intern();
System.out.println(s1 == s2); //第一个输出
new Thread(() -> {
System.out.println( (s1== "Hello")); //第二个输出
}).start();
这里有个陷阱,就是s2.intern()时,并没有将结果赋值给s2,所以s2本身的地址没有变。
这时,看以上结果就比较显然了,s1是常量池的地址,s2是新创建对象的堆地址,肯定不相等。
"Hello"常量也是常量池地址,所以第二个输出是相等的。