String对象是不可修改的,因为底层维护的是private final char value[]
所以当对String进行replace、substring、split、toLowerCase
、concat等修改操作时,都是返回新的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;
}
下面举几个例子:
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的引用指向的是常量池空间的内存地址
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”存放在常量池中。
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对象
"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指向的数组也是同一个,肯定也修改。
相对于可变对象,不可变对象有很多优势:
private int hash;//用来缓存HashCode
假若字符串对象允许改变,那么将会导致各种逻辑错误,比如改变一个对象会影响到另一个独立对象
构成内容元素是父类的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;
private void ensureCapacityInternal(int minimumCapacity) {
// overflow-conscious code
if (minimumCapacity - value.length > 0) {
value = Arrays.copyOf(value,
newCapacity(minimumCapacity));
}
}
// 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);
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 a = "aaa";
for(int i=0;i<5;i++){
a+="ccc";
}
优化: String a = "aaa";
StringBuilder sb = new StringBuilder(a);
for(int i=0;i<5;i++){
sb.append("ccc");
}
System.out.println(sb.toString());
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 该篇个人觉得写的真的不错