JAVA中String的底层解析

JAVA中String 是Final类不能被继承。JAVA 对String的处理和一般Class有所不同。
这文章主要是解释一下String的存储模式和java的字符串常量池的机制,和几个涉及底层的引用问题解析。

首先提出几个问题:
1.String的内容为什么是不可更改的?
2.JAVA中“adc”这种创建的字符串的创建过程是怎样的?
3.String(String string)的构造方法是如何工作的?
4.一个线程中内容为“adc”的String对象,存储的char[]是否是同一个,char[]数组是否一定在字符串常量池中?
5思考java中String 不可更改的好处在哪?
6 intern方法和字符串常量池的关系?
7string的+编译器是如何处理的?

1.String的内容为什么是不可更改的?
我们通过源代码可以看到存储string内容的char[]是这么定义的:

private final char value[];

可能有人会有疑问既然是final引用却没有附初始值。
答案是final变量是可以在构造方法中进行赋值的。
所以value的所有赋值都在String的几个构造方法中。
这样从代码逻辑上控制了String不可变。

2.JAVA中“adc”这种创建的字符串的创建过程是怎样的?

这个问题比较简单,就是”adc“会被放到字符串常量池中,可以称为字面量,所有String s=“adb” 的字符串的引用都是指向字符串常量池中的。

3.String(String string)的构造方法是如何工作的?

 public String(String original) {
        this.value = original.value;
        this.hash = original.hash;
    }

这说明从用String去new String构造新的对象是,确实会产生新的对象,而且新的value和hash值都和原String对象一致。
当然String还是有其他构造方法,都会去new char[]

4一个线程中内容为“adc”的String对象,存储的char[]是否是同一个,char[]数组是否一定在字符串常量池中?
答案其实可以根据3推导出。
由String产生的String里面的char[]是同一个,其他方式产生的都是新的。“”包裹产生的字符串会在常量池中,其他的都是正常的存在堆中。所以堆中可以有n份“adb”的串,常量池中的“adc”永远只有一个,可以被多个引用所指向。后续将会用代码解释上述所有现象。

public static void main(String[] args)
			throws NoSuchFieldException, SecurityException, IllegalArgumentException, IllegalAccessException {
		// TODO Auto-generated method stub

		String string = "abc";
		String string2 = "abc";
	    String string3 = new String(string);
	    char[] charString={'a','b','c'};
		String string4 = new String(charString);
		String string5 = new String(string4);
		
		charString[2]='e';
	
		Class sClass = String.class;
		Field privateStringField = sClass.getDeclaredField("value");
		privateStringField.setAccessible(true);
		char[] chars = (char[]) privateStringField.get(string);
		char[] chars2 = (char[]) privateStringField.get(string2);
		char[] chars3 = (char[]) privateStringField.get(string3);
		char[] chars4 = (char[]) privateStringField.get(string4);
		char[] chars5 = (char[]) privateStringField.get(string5); 
		
		System.out.println("++" + chars + "       " + Arrays.toString(chars));
		System.out.println("++" + chars2 + "       " + Arrays.toString(chars2));
		System.out.println("++" + chars3 + "       " + Arrays.toString(chars3));
		System.out.println("++" + chars4 + "        " + Arrays.toString(chars4));
		System.out.println("++" + chars5 + "       " + Arrays.toString(chars5));
		System.out.println("++" + charString + "       " + Arrays.toString(charString));
		System.out.println(string==string2);
		System.out.println(string3==string);
		System.out.println(string4==string);
		System.out.println(string5==string4);
		
		

	}

控制台输出:

++[C@4e25154f       [a, b, c]
++[C@4e25154f       [a, b, c]
++[C@4e25154f       [a, b, c]
++[C@70dea4e        [a, b, c]
++[C@70dea4e        [a, b, c]
++[C@5c647e05      [a, b, e]
true
false
false
false

我们用反射可以拿到char[] 的引用,并打印出char[] 指向的内存地址。
可见前三个String都是指向同一个地址的(字符串常量池),后三个String都是指向堆中的地址。

5思考java中String 不可更改的好处在哪?
1.如果可变复用将变得不稳定。复用可以节约内存
2.hashcode被缓存,没必要重复结算
3.线程安全(网上的说法,我并不完全赞同,能保证值得安全,并不能保证引用的安全总是)
4.如果定义为final 也就是String的引用和内容都会稳定不可变(当然不包括使用反射的情况)

6intern方法的作用?
intern的作用是 将string放入常量池,并返回该引用,如果已经在常量池中直接返回引用。
在JDK1.6之后 intern方法是将不存在字符串常量池的String去记录到常量池里(存的是引用)实例还是在堆上
在JDK1.6之前intern方法是把String复制到字符串常量池里是新对象了(据说是在永久代中)目前可以确定String实例肯定不是一个了,里面的char[]是否复用,这个还不确定,手里没有JDK16的环境,可以用我上面的反射方法来进行验证。

7string的+编译器是如何处理的?
两种情况:
如果只是“” +“”+“”+… 不涉及变量的,javac会直接合并所有的"" “” ,形成utf8 结构体,保存下来,结构体中有个2byte的length字段记录字面量(”“这种东西官方称为字面量)在类运行中会把结构体的东西加载到内存的字符串常量池中(具体加载细节后续再聊)
第二种情况:

        String j1="a";
	    String j2="b";
	    String j12="ab";
	    String j3="a"+j12;
	    System.out.println(j3 == j12); (输出是false)

遇到这种情况javac并不会合并”“ ”“,尽管j12也是字面量的变量。我们可以通过javap -c查看字节码:

Code:
       0: ldc           #2                  // String a
       2: astore_2
       3: ldc           #3                  // String b
       5: astore_3
       6: new           #4                  // class java/lang/StringBuilder
       9: dup
      10: invokespecial #5                  // Method java/lang/StringBuilder."":()V
      13: ldc           #2                  // String a
      15: invokevirtual #6                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      18: aload_3
      19: invokevirtual #6                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      22: invokevirtual #7                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
      25: astore        4

很清楚的看到,这里会new StringBuilder 调用append方法,最后toString。StringBulider的toString方法我们可以看一下,是根据当前的char[]去 newString的,所以必然不是用一个对象,所以输出的结果是false。

未完待续…

你可能感兴趣的:(JAVA基础)