文章目录
String
String不可修改
String对象是不可修改的,因为底层维护的是private final char value[]
- 首先final修饰了char[],说明char[]的引用地址不可修改(实际是hash值),也就是说一旦初始化完成内部也不能修改它的引用地址,当然具体的元素也不会修改
- 由private修饰,且没有提供对应的set方法,外部不能修改char[]里面的每一个元素
所以当对String进行replace、substring、split、toLowerCase
、concat等修改操作时,都是返回新的String对象,与原来无关,如下
replace修改String误区
String a = "ABCabc";
System.out.println("a = "+ a);
a = a.replace('A', 'a');
System.out.println("a = "+ a);
结果
a = ABCabc
a = aBCabc
误以为能修改其实是底层返回了一个新的string对象
public String replace(char oldChar, char newChar) {
if (oldChar != newChar) {
int len = value.length;
int i = -1;
char[] val = value; /* avoid getfield opcode */
while (++i < len) {
if (val[i] == oldChar) {
break;
}
}
if (i < len) {
char buf[] = new char[len];
for (int j = 0; j < i; j++) {
buf[j] = val[j];
}
while (i < len) {
char c = val[i];
buf[i] = (c == oldChar) ? newChar : c;
i++;
}
// 返回了新的string堆区对象
return new String(buf, true);
}
}
return this;
}
String 内存结构解析
下面举几个例子:
1.
//String的两种创建方式
String s1 = new String("hello");
String s2 = "hello";
System.out.println(s1==s2); // false 比较地址,地址不相同
System.out.println(s1.equals(s2)); // true 因为string重写equals方法,只比较内容
-
如图 第一种会先在堆中创建空间,里面维护了
char[] value
属性,因为数组也是引用类型,会实际指向常量池的"hello"
。如果常量池没有“hello”,重新创建,如果有,直接通过value char数组引用指向。s1的引用 实际指向的是堆中的空间地址0x22
。 -
如图 第二种先从常量池查找是否有“hello” 数据空间。如果有,直接指向;如果没有则重新创建,然后指向。最终s2的引用指向的是常量池空间的内存地址
2.
String a = new String(“abc”);
String b =new String(“abc”);
System.out.println(a.equals(b)); // true [内容]
System.out.println(a==b); //false //跟上图分析一样, a和b new了两个String对象,地址当然不同,但是两个堆的string对象同样会去常量池查找是否有"abc"存在,有则直接使用,没有则创建后使用
String a = “abc”;
String b =new String(“abc”);
System.out.println(a.equals(b)); //true
System.out.println(a==b); //false
System.out.println(a==b.intern()); //true
System.out.println(b==b.intern()); //false
当调用 intern 方法时,如果string常量池已经包含一个等于此 String 对象的字符串(用 equals(Object) 方法确定),则返回池中的字符串。否则,将此 String 对象添加到池中,并返回此 String 对象的引用
即b.intern() 方法最终返回的是常量池的地址.
编译器确定时优化的情况
String a = “hello”+“abc”;//问:创建了几个对象?
分析
当Java能直接使用字符串直接量(包括在编译时就计算出来的字符串值时,如String e = “张” + “三”;),JVM就会使用常量池来管理这些字符串。
只会产生1个对象.
编译器会做优化
String a = “hello”+“abc”; 这种情况编译器会直接在常量池创建 “helloabc”而不必创建 “hello”,“abc”;
=>优化
String a = “helloabc”; 因此只创建了一个对象
final String s2 = "111"; //pool
String s7 = "sss" + s2; //pool
String s3 = "sss111"; //pool
System.out.println(s3 == s7); //true
分析
对于final String s2 = “111”。s2是一个用final修饰的变量,在编译期已知,在运行s2+"sss"时直接用常量“111”来代替s2。所以s2+"sss"等效于“111”+ “sss”。在编译期就可以确定的字符串对象“111sss”存放在常量池中。
编译器不确定时(运行时才能确定变量的值)Stringbuilder帮助拼接优化
String a = “hello”;
String b =“abc”;
String c=a+b; //创建了几个对象? 可以自行debug force step into源码查看
因为编译时不确定,JVM会对String对象重载“+”“+=”进行一些优化
在底层其实是编译器擅自调用了StringBuilder类进行+的操作,主要原因是StringBuilder的append()更加高效
因为Stringbuilder是由 char[] value构成,是非final的,字符串拼接直接在该数组上可以完成,默认构造器会创建16字符大小char[],所以直接够用,不用扩容;就算不够,也可以扩容解决,因为char[] value的引用是可以变的,为了优化,可以将char[]的大小设置大一点,避免反复扩容
如果是String的话,因为不可修改的特性,要完成拼接,必然是需要创建新的String对象
- string 对象char[] value的大小应该是根据后面字符串的字符长度决定,如4个字符,string.length()就会得到4,
- 所以如果两个要拼接,因为a的char[] value的大小只有5,要拼接后面的势必要扩容,而又因为char[] 是final的,无法创建新char数组,重新赋值,只能新建1个string对象,让里面的char[] 构建9个字符的长度大小并将
"helloabc"
的引用赋予
public AbstractStringBuilder append(String str) {
if (str == null)
return appendNull();
int len = str.length();
ensureCapacityInternal(count + len);
//进行数组的copy赋值粘贴
str.getChars(0, len, value, count);
count += len;
return this;
}
1. new StringBuilder();
2. 调用append("hello");
3. 调用append("abc");
```
以上两步都是调用str.getChars进行string维护的char[]属性数组的copy赋值粘贴
value是该builder自己的char[] ,即已经拼接好的char[]
append就是对stirng的char[]进行拼接
str.getChars(0, len, value, count);
```
4. 调用StringBuilder的tostring
1. return new String(value, 0, count); 返回新的堆区String对象
String s1 = "hello";
String s2 = "java";
String s5 = "hellojava"; //常量池
String s6 = (s1 + s2).intern(); //常量池
System.out.println(s5 == s6); //true 【s5 和 s6 的内容相同,并且都指向池】
System.out.println(s5.equals(s6));//true [内容,相等的.]
public class Test1 {
String str = new String("good");
char[] ch = { 't', 'e', 's', 't' };
public void change(String str, char ch[]) {
str = "test";
ch[0] = 'g';
}
public static void main(String[] args) {
Test1 ex = new Test1();
ex.change(ex.str, ex.ch);
System.out.print(ex.str + " and "); // good and
System.out.println(ex.ch);//gest
}
}
如图,因为每个方法都会开辟栈,所以在栈里面的局部变量作用域只在当前方法,
那么change方法将str重新指向 常量池的"test",不会对main 的str引用造成影响
而ch因为是将数组引用传递了过来,如果修改数组引用对象下标为0位置的元素,那么main方法的ch指向的数组也是同一个,肯定也修改。
String不可修改的优点
相对于可变对象,不可变对象有很多优势:
- 不可变对象可以提高String Pool的效率和安全性。如果你知道一个对象是不可变的,那么需要拷贝这个对象的内容时,就不用复制它的本身而只是复制它的地址,复制地址(通常一个指针的大小)需要很小的内存效率也很高。对于同时引用这个“ABC”的其他变量也不会造成影响。
- 不可变对象对于多线程是安全的,因为在多线程同时进行的情况下,一个可变对象的值很可能被其他进程改变,这样会造成不可预期的结果,而使用不可变对象就可以避免这种情况。
- 字符串不变性保证了hash码的唯一性,因此可以放心地进行缓存.这也是一种性能优化手段,意味着不必每次都去计算新的哈希码. 这就使得字符串很适合作为Map中的键,字符串的处理速度要快过其它的键对象。这就是HashMap中的键往往都使用字符串。在String类的定义中有如下代码:
private int hash;//用来缓存HashCode
- 安全性: String被许多的Java类(库)用来当做参数,例如 网络连接地址URL,文件路径path,还有反射机制所需要的String参数等, 假若String不是固定不变的,将会引起各种安全隐患。
- 如类加载器要用到字符串,不可变性提供了安全性,以便正确的类被加载。譬如你想加载java.sql.Connection类,而这个值被改成了myhacked.Connection,那么会对你的数据库造成不可知的破坏。
假若字符串对象允许改变,那么将会导致各种逻辑错误,比如改变一个对象会影响到另一个独立对象
Stringbuffer
- java.lang.StringBuffer代表可变的字符序列,可以对字符串内容进行增删。
- 很多方法与String相同,但StringBuffer是可变长度的。
- StringBuffer是一个容器。
优化
构成内容元素是父类的char数组,构造器可以自己设置char数组的大小,合适的大小设置可以增加效率,避免反复扩容带来的性能损耗。
当然stringbuilder也一样
//构成元素是父类的char数组
abstract class AbstractStringBuilder implements Appendable, CharSequence {
/**
* 该值用于字符存储。
*/
char[] value;
public final class StringBuffer
extends AbstractStringBuilder
implements java.io.Serializable, CharSequence
{
/**
* toString返回的最后一个值的缓存。每当修改StringBuffer时清除。
*/
private transient char[] toStringCache;
与string的区别
- String保存的是字符串常量,里面的值不能更改,每次String类的更新实际上就是更改地址,效率较低
- StringBuffer保存的是字符串变量,里面的值可以更改,每次StringBuffer的更新实际上可以更新内容,不用更新地址,效率较高
为什么拼接效率比String高
- Stringbuffer: char数组可以修改,且数组容量可以自己定义,那么可以String拼接在一个char[]上完成,如果容量不足,也只需要对char数组进行扩容
private void ensureCapacityInternal(int minimumCapacity) {
// overflow-conscious code
if (minimumCapacity - value.length > 0) {
value = Arrays.copyOf(value,
newCapacity(minimumCapacity));
}
}
- String: 而string因为不可修改,所以至少每次需要创建新的String对象才行(实际会调用StringBuilder进行拼接),效率很低
String和StringBuffer相互转换
// String——>StringBuffer
String s = "hello";
// 方式1:
StringBuffer b1 = new StringBuffer(s);
// 方式2:
StringBuffer b2 = new StringBuffer();
b2.append(s);
System.out.println("b2=" + b2); //? hello
// StringBuffer——>String
// 方式1:
String s2 = b1.toString(); //b1 [StringBuffer]
System.out.println("s2=" + s2);
// 方式2:
String s3 = new String(b1);
StringBuilder
- 一个可变的字符序列。此类提供一个与 StringBuffer 兼容的 API,但不保证同步[多并发+多线程]。该类被设计用作 StringBuffer 的一个简易替换,用在字符串缓冲区被单个线程使用的时候(这种情况很普遍)。如果可能,建议优先采用该类,因为在大多数实现中,它比 StringBuffer 要快。
- 在 StringBuilder 上的主要操作是 append 和 insert 方法,可重载这些方法,以接受任意类型的数据。每个方法都能有效地将给定的数据转换成字符串,然后将该字符串的字符追加或插入到字符串生成器中。append 方法始终将这些字符添加到生成器的末端;而 insert 方法则在指定的点添加字符。
- 总结:
(1) StringBuilder 和 StringBuffer 的API(应用程序接口, 方法)是一样.
(2) StringBuilder 没有线程同步和互斥的机制,因此他的速度快,但是有线程安全问题。当单线程或者并发要求不高时,选择使用StringBuilder , 如果多并发要求高,我们使用StringBuffer
与stringbuffer的区别
StringBuffer 的方法基本上都加了synchronized
@Override
public synchronized StringBuffer append(String str) {
toStringCache = null;
super.append(str);
return this;
}
StringBuilder 的方法上就没加synchronized
@Override
public StringBuilder append(String str) {
super.append(str);
return this;
}
三者区别
StringBuilder 和 StringBuffer 非常类似,均代表可变的字符序列,而且方法也一样
- String:不可变字符序列,因为不可变,所以线程安全, 效率低,但是复用率高。
- StringBuffer:可变字符序列、效率较高(增删)、线程安全
- StringBuilder(JDK1.5):可变字符序列、效率最高、线程不安全
循环拼接时的区别
- String如果进行多次的拼接来修改String字符串内容,那么会创建多个StringBuilder对象,然后每次循环都tostring(new String ),创建新的堆对象(对应有堆和常量池),会导致大量副本字符串对象存留在内存中,浪费内存,降低效率,影响性能。如下
优化:String a = "aaa"; for(int i=0;i<5;i++){ a+="ccc"; }
只创建1个StringBuilder,且最后才会toString(new String)创建新的String堆对象String a = "aaa"; StringBuilder sb = new StringBuilder(a); for(int i=0;i<5;i++){ sb.append("ccc"); } System.out.println(sb.toString());
- 那么如果String增和删除操作多,可以选择使用StringBuilder 或者StringBuffer 只会操作修改1个char数组对象
- 如果要求线程安全,则使用 StringBuffer, 否则使用 StringBuilder
- 只是为了读操作,选择使用String
效率测试
package com.test.StringBuilder;
public class PerformanceTest {
public static void main(String[] args) {
// TODO Auto-generated method stub
String text = "";
long startTime = 0L;
long endTime = 0L;
StringBuffer buffer = new StringBuffer("");
StringBuilder builder = new StringBuilder("");
startTime = System.currentTimeMillis();//获取当前的系统的毫秒数
for (int i = 0; i < 2000000; i++) {
buffer.append(String.valueOf(i));
}
endTime = System.currentTimeMillis();//获取当前的系统的毫秒数
System.out.println("StringBuffer的执行时间:" + (endTime - startTime));
startTime = System.currentTimeMillis();
for (int i = 0; i < 2000000; i++) {
builder.append(String.valueOf(i));
}
endTime = System.currentTimeMillis();
System.out.println("StringBuilder的执行时间:" + (endTime - startTime));
startTime = System.currentTimeMillis();
for (int i = 0; i < 2000000; i++) {//javaEE 90%读
text = text + i;
}
endTime = System.currentTimeMillis();
System.out.println("String的执行时间:" + (endTime - startTime));
}
}
参考
https://blog.csdn.net/qq_31615049/article/details/80891142
https://blog.csdn.net/u010887744/article/details/50844525 该篇个人觉得写的真的不错