Java关于String创建和其不可变的一些理解

一、String是不可变的

 private final byte[] value;//String源码

通过String的原码我们可以知道String的储存本质是一个byte数组,在Java之前的版本中使用过char但是后来进行了一些改变成了现在的byte。其前面有着final修饰说明了其引用是不可变的,这里限定了String是不可变的。
那么为什么要将String设置成不可变的量呢,原因有很多,其中最主要的则是安全和效率。
将String设为不可变让String的常量池成为可能,每一个String对象在创建时都可以在常量池中创建一个String来缓存其内容和hash码,这样以后再有同样的创建时就可以不必重新计算hash码只用在常量池中检索并复制过来就行(String中每一个串的值对应一个hash码)。

String s1="abc";
String s2="abc";
System.out.println(s1==s2);//True

从这个例子我们就可以看出来s1和s2指向的同一段地址,都是指向其常量池中缓存的内容,这样就节约了空间也加快了效率。倘若String是可变的那么上面这段代码这样的就不可能实现,因为万一其中一个改变了另外一个就会受到影响。

String s1=new String("abc");
String s2=new String("abc");
System.out.println(s1==s2);//false
System.out.println(s1.hashCode()==s2.hashCode());//true

上面这段代码中创建值为"abc"的String对象时则是会检索常量池中是否存在一个值为abc的串,如果有则直接复制过来,节约了很多时间,提高了效率。但是这个只是复制并非让引用指向同一个对象,所以他们的引用不相等,但是他们的hash码是相等的。
之后就是安全性,String对象不可改变对与多线程操作时是安全的,不会因为一个线程修改数据而出现问题,同时其参数传递是安全的。

static void change(String s) {
		s=s+"d";
	}
	public static void main(String[] args) {
		// TODO Auto-generated method stub
		String s1="abc";
		change(s1);
		System.out.println(s1);//abc
}

上述代码可以看出在传参后s修改了String的值,在原来的s1中值并没有改变,其本质就是创建了一个全新的对象值为abcd并让引用s指向它。

	static String change(String s) {
		s=s+"d";
		return s;
	}
	public static void main(String[] args) {
		// TODO Auto-generated method stub
		String s1="abc";
		String s=change(s1);
		System.out.println(s1==s);
}

从这段代码中我们可以更明确的发现s1和已经不是同一个对象了,所有的改变都不会作用在原来的对象上,这样也就保护了原来对象的值,比如密码由String来保存,就不用担心会被黑客修改。
当然Java为了方便操作,引入了StringBuffer和StringBuilder来创建可以修改的串,用append方法来延长串。

二、String的创建机制

上面提到String在创建时会在常量池先创建一个串,再复制到对象所在的堆地址中,我们通过一段代码来看一下:

String s1="abc";
String s2=new String("abc");
String s3=new String("abc");
String s4="abc";
String s5="a"+"bc";
String s6="a"+new String("bc");
String s7="a".concat("bc");
System.out.println(s1==s4);//True
System.out.println(s1==s2);//false
System.out.println(s2==s3);//false
System.out.println(s3==s4);//false
System.out.println(s1==s5);//true
System.out.println(s1==s6);//false
System.out.println(s1==s7);//false
System.out.println(s1==s2.intern());//true
System.out.println(s2.intern()==s3.intern());//true

s1创建时在常量池创建一个值为abc的串并指向它,s2和s3分别在堆中创建了两个不同的但是值均为abc的串。s4创建时先检索了常量池存在abc就直接指向常量池中对象。s5这种方式会被JVM优化为与s4相同的方式,在JVM对其操作时会自动把“+”去掉,来在常量池中寻找值为abc的串并指向。而s6则由于其中有一个堆中对象,所以这种操作会在堆中创建一个值为bc的无名对象并且再创建一个值为abc的对象,当然也是在堆中,所以引用和s1也不相同。s7与s6类似,concat方法也算创建一个全新的对象来进行赋值的。intern方法用来返回一个串在常量池中相应对象的引用,如果没有则会新建一个常量池对象。所以只要值相同的串其intern方法返回值均相同。
我们注意到我们每次用new创建一个新的串都会在常量池中创建一个串,所以String s=new String("abc");创建的对象可能是一个也可能是两个,因为这一句代码并不能知道常量池中到底有没有一个值为abc的串。
那么问题又来了,如果是这样的话,那为什么intern方法要去考虑常量池没有这个串的情况呢,只是为了保险起见么?当然并不是。

String s11=new String("a");
String s12=new String("bc");
String s13=s11+s12;
System.out.println(s13);//abc

看这段代码,倘若前面并没有创建值为abc的对象,那么这段代码便没有在常量池中创建值为abc的值。因为s13的abc是由s11和s12连接而来,所以就没有产生值为abc的常量。

String s1="abc";
String s8="abc".concat("");
String s9=s1.concat("");
System.out.println(s1==s8);//true
System.out.println(s1==s9);//true

另外我们从源码中可以看出concat方法并非一定会创建新的对象,当连接对象为""时返回的是this,也就是并没有新对象产生。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章