一、不可变
String 是不可变类,不可变的意思是 String 类型变量初始化后,其引用指向内存内容不能改变,变量引用可以指向其他内存。
String str = "abc";
str = "def";
字符串引用改变地址
定义一个 String 变量 str,引用指向内存字符串 abc。
变量赋值时,新开辟内存 def 字符串,str 引用指向新对象,原内存内容 abc 不变。
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
private final char value[];
...
}
String 类是一个字符串数组的封装类(内部一个 char[] 数组)。数组类型 final private,引用 value 值不可变,外部无法访问。
String 对象内存因此,String 对象本质是指向一个 char 数组内存的引用,设计成 final 不可变类型,一旦 String 对象创建,value 值初始化,指向一块字符数组内存,char[] 引用不可变,String 对象即不可改变。
二、replace() 方法
替换字符串中的某个字符,String 类 replace() 方法,不直接更改 char[] 引用指向内存,而是开辟一块新内存。
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++;
}
return new String(buf, true);
}
}
return this;
}
创建新 char[] 数组,分配内存,长度和老数组一致,复制,替换,new 一个新 String 类对象,在构造方法,新 char[] 数组赋值 String 类内部 value,返回新 String 引用。
三、常量池
class 文件常量池,在文件中,编译器编译生成字面量和符号引用,未载入内存,字面量是文本字符串,(如 String str = "abc" 中的 abc)。
符号引用是类/接口全限定名,(如 java/lang/String ),变量名称( str ),方法名称和描述符(参数和返回值)。
类加载内存后,class 文件常量池(字面量和符号引用),进入方法区运行时常量池,该常量池区全局共享。
字面量(字符串常量池), jdk1.7 后不再方法区,移到堆中,符号引用如方法名、类全限定名仍然在方法区。
public class StrClass {
public String a = "hello2";
public String a1 = "hello2";
public String a2 = "hello"+2;
}
定义一个 String 变量 a,编译后 hello2 是文本字符串,在 class 文件常量池,编译阶段确定 a 的值。
两个字符串字面值,编译时会进行优化(拼接),解析成一个,所以 a2 在编译期由编译器直接解析为 hello2。
反编译 javap -verbose StrClass.class 命令,查看 class 文件常量池。
编译时会检查常量池是否已存在 hello2 字符串,只有一个 #2,String,对应 #22,即 hello2。
class code类加载内存时
(运行时)在对象初始化阶段,初始化 Code,在常量池中 ldc 获取第 #2 项( hello2 字符串对象),putfield 将引用入栈,分别是 a(#3),a1(#4) 和 a2(#5),它们指向常量池中相同的对象 hello2,(a==a1==a2)。
此过程会查找字符串常量池是否存在 hello2,若不存在,在堆创建 char[] 数组,创建 String 对象关联 char[] 数组,保存到字符串常量池,最后将a指向这个对象。
public class StrClass {
public String a = "hello2";
public String b = "hello";
public String a3 = b + 2;
public final String c = "hello";
public String a4 = c + 2;
}
编译阶段,不能确定 a3 的值,定义 final 变量 c,字节码替换掉 a4 中的 c 变量,场景和 a2 一致。
class code(运行时)对象变量初始化,new 一个 StringBuilder 对象,a3 引用指向 toString() 方法在堆内存 new 的 String 对象。a==a4,指向字符串常量池,a3 指向堆内存 new 的 String 对象。
public class StrClass {
public String a = "hello2";
public String a5 = new String("hello2");
}
类加载时,在常量池创建对象 hello2,变量 a5,运行时堆内存 new一个 String 对象,字符串 hello2 已经在常量池,#2项,a 引用指向字符串常量池,a5 引用指向堆内存新对象,(a!=a5)。
new 关键字创建字符串对象, 在堆内存创建。
查找常量池是否存在,若没有,创建一个字符串对象,先放入常量池,然后再堆中创建对象,返回堆中的地址,因此,如果常量池中原来没有,会产生两个对象,否则,产生一个对象(堆中)。
public class StrClass {
public String a6 = new String("hello2");
}
class 文件常量池,hello2 文本字符。
Constant pool类加载内存时,在字符串常量池创建一个 hello2 字符串对象。
初始化Code对象初始化时,new 指令,在堆中再次创建一个对象,变量 a6 引用指向它。
public class StrClass {
public String a7 = new String("hello")+new String("2");
}
class 文件常量池只有 hello 和 2 字符串,没有 hello2 字符串,当类加载时,在字符串常量池不存在 hello2 对象。
初始化时,new 指令在堆创建两个 String 对象( hello和2 ),通过 StringBuffer 类 append() 方法,toString() 方法在堆内存中 new 一个 String 对象 (hello2),a7 引用指向它。
四、StringBuffer 和 StringBuilder
前一节的变量 a3=b+2 赋值时,class 字节码中定义了一个 StringBuilder 类,调用两次 append() 方法,依次添加 b 和 2 ,即 hello 和 2,一次 toString() 方法,堆内存创建对象。
StringBuffer 和 StringBuilder 区别是线程安全。
StringBuffer stringBuffer = new StringBuffer();
stringBuffer.append("hello");
stringBuffer.append(2);
StringBuffer 通过 char[] 数组保存数据,每一个 append() 方法的新增数据在 char[] 数组保存,支持不同类型,boolean 类型保存4或5个字符 (true/false),字符串将每个字符保存,StringBuffer 类可以对字符串进行修改,进行字符串拼接时,不会产生新对象,直接对 char[] 数组进行操作更改。
public synchronized StringBuffer append(String str) {
super.append(str);
return this;
}
几乎所有的字符操作方法都 synchronized 同步,该类线程安全。
@Override
public synchronized String toString() {
if (toStringCache == null) {
toStringCache = Arrays.copyOfRange(value, 0, count);
}
return new String(toStringCache, true);
}
toString() 方法,创建一个 String 对象,关联 char[] 数组。
任重而道远
网友评论