结合代码来看
public static void main(String[]args){
/**
*情景一:字符串池
*JAVA虚拟机(JVM)中存在着一个字符串池,其中保存着很多String对象;
*并且可以被共享使用,因此它提高了效率。
*由于String类是final的,它的值一经创建就不可改变。
*字符串池由String类维护,我们可以调用intern()方法来访问字符串池。
*/
String s1="abc";
//↑在字符串池创建了一个对象
String s2="abc";
//↑字符串pool已经存在对象“abc”(共享),所以创建0个对象,累计创建一个对象
System.out.println("s1==s2:"+(s1==s2));
//↑true指向同一个对象,
System.out.println("s1.equals(s2):"+(s1.equals(s2)));
//↑true值相等
//↑------------------------------------------------------over
/**
/*
*情景二:关于new String("")
*/
String s3 = new String("abc");
//↑创建了两个对象,一个 "abc" 存放在字符串池中,一个 String 对象存在与堆区中;
//↑还有一个对象引用s3存放在栈中
String s4 = new String("abc");
//↑字符串池中已经存在“abc”对象,所以只在堆中创建了一个String对象
//↑还有一个对象引用s4存放在栈中
System.out.println("s3==s4:"+(s3==s4));
//↑false s3和s4指向堆区的不同地址;
System.out.println("s3.equals(s4):"+(s3.equals(s4)));
//↑true s3和s4的值相同
System.out.println("s1==s3:"+(s1==s3));
//↑false存放的地区不同,一个栈区,一个堆区
System.out.println("s1.equals(s3):"+(s1.equals(s3)));
//↑true值相同
//↑------------------------------------------------------over
/**
/*情景三:
*由于常量的值在编译的时候就被确定(优化)了。
*在这里,"ab"和"cd"都是常量,因此变量str3的值在编译时就可以确定。
*这行代码编译后的效果等同于:String str1 ="abcd";
*/
String str1 = "ab" + "cd";//1个对象,在编译时直接创建为"abcd"
String str11="abcd";
System.out.println("str1=str11:"+(str1==str11));
//↑--------true--------同情景一
/**
/*情景四:
*局部变量str2,str3存储的是存储两个拘留字符串对象(intern字符串对象)的地址。
*
*第三行代码原理(str2+str3):
*运行期JVM首先会在堆中创建一个StringBuilder类,
*同时用str2指向的拘留字符串对象完成初始化,
*然后调用append方法完成对str3所指向的拘留字符串的合并,
*接着调用StringBuilder的toString()方法在堆中创建一个String对象,
*最后将刚生成的String对象的堆地址存放在局部变量str3中。
*
*而str5存储的是字符串池中"abcd"所对应的拘留字符串对象的地址。
*str4与str5地址当然不一样了。
*
*内存中实际上有五个字符串对象:
*三个拘留字符串对象、一个String对象和一个StringBuilder对象。
*/
String str2 = "ab";//1个对象
String str3 = "cd";//1个对象
String str4 = str2+str3;
String str5 = "abcd";
System.out.println("str4=str5:"+(str4==str5));//false
//↑------------------------------------------------------over
/**
/*情景五:
*JAVA编译器对string+基本类型/常量是当成常量表达式直接求值来优化的。
*运行期的两个string相加,会产生新的对象的,存储在堆(heap)中
*/
Stringstr6="b";
Stringstr7="a"+str6;
Stringstr67="ab";
System.out.println("str7=str67:"+(str7==str67));
//↑str6为变量,在运行期才会被解析。
finalStringstr8="b";
Stringstr9="a"+str8;
Stringstr89="ab";
System.out.println("str9=str89:"+(str9==str89));
//↑str8为常量变量,编译期会被优化
//↑------------------------------------------------------over
}
总结:
1.String类初始化后是不可变的(immutable)
这一说又要说很多,大家只要知道String的实例一旦生成就不会再改变了,比如说:Stringstr=”kv”+”ill”+”“+”ans”;是有4个字符串常量,首先”kv”和”ill”生成了”kvill”存在内存中,然后”kvill”又和””生成“kvill“存在内存中,最后又和生成了”kvillans”;并把这个字符串的地址赋给了str,就是因为String的”不可变”产生了很多临时变量,这也就是为什么建议用StringBuffer的原因了,因为StringBuffer是可改变的。
下面是一些String相关的常见问题:
String中的final用法和理解
finalStringBuffera=newStringBuffer(“111”);
finalStringBufferb=newStringBuffer(“222”);
a=b;//此句编译不通过finalStringBuffera=newStringBuffer(“111”);
a.append(“222”);//编译通过
可见,final只对引用的”值”(即内存地址)有效,它迫使引用只能指向初始指向的那个对象,改变它的指向会导致编译期错误。至于它所指向的对象的变化,final是不负责的。
2.代码中的字符串常量在编译的过程中收集并放在class文件的常量区中,如”123”、”123”+”456”等,含有变量的表达式不会收录,如”123”+a。
3.JVM在加载类的时候,根据常量区中的字符串生成常量池,每个字符序列如”123”会生成一个实例放在常量池里,这个实例是不在堆里的,也不会被GC,这个实例的value属性从源码的构造函数看应该是用new创建数组置入123的,所以按我的理解此时value存放的字符数组地址是在堆里,如果有误的话欢迎大家指正。
4.使用String不一定创建对象
在执行到双引号包含字符串的语句时,如Stringa=“123”,JVM会先到常量池里查找,如果有的话返回常量池里的这个实例的引用,否则的话创建一个新实例并置入常量池里。如果是Stringa=“123”+b(假设b是”456”),前半部分”123”还是走常量池的路线,但是这个+操作符其实是转换成[SringBuffer].Appad()来实现的,所以最终a得到是一个新的实例引用,而且a的value存放的是一个新申请的字符数组内存空间的地址(存放着”123456”),而此时”123456”在常量池中是未必存在的。
要注意:我们在使用诸如Stringstr=“abc”;的格式定义类时,总是想当然地认为,创建了String类的对象str。担心陷阱!对象可能并没有被创建!而可能只是指向一个先前已经创建的对象。只有通过new()方法才能保证每次都创建一个新的对象
5.使用newString,一定创建对象
在执行Stringa=newString(“123”)的时候,首先走常量池的路线取到一个实例的引用,然后在堆上创建一个新的String实例,走以下构造函数给value属性赋值,然后把实例引用赋值给a:
publicString(Stringoriginal){
intsize=original.count;
char[]originalValue=original.value;
char[]v;
if(originalValue.length>size){
//ThearrayrepresentingtheStringisbiggerthanthenew
//Stringitself.Perhapsthisconstructorisbeingcalled
//inordertotrimthebaggage,somakeacopyofthearray.
intoff=original.offset;
v=Arrays.copyOfRange(originalValue,off,off+size);
}else{
//ThearrayrepresentingtheStringisthesame
//sizeastheString,sonopointinmakingacopy.
v=originalValue;
}
this.offset=0;
this.count=size;
this.value=v;
}
从中我们可以看到,虽然是新创建了一个String的实例,但是value是等于常量池中的实例的value,即是说没有new一个新的字符数组来存放”123”。
如果是Stringa=newString(“123”+b)的情况,首先看回第4点,”123”+b得到一个实例后,再按上面的构造函数执行。
6.String.intern()
String对象的实例调用intern方法后,可以让JVM检查常量池,如果没有实例的value属性对应的字符串序列比如”123”(注意是检查字符串序列而不是检查实例本身),就将本实例放入常量池,如果有当前实例的value属性对应的字符串序列”123”在常量池中存在,则返回常量池中”123”对应的实例的引用而不是当前实例的引用,即使当前实例的value也是”123”。
publicnativeStringintern();
存在于.class文件中的常量池,在运行期被JVM装载,并且可以扩充。String的intern()方法就是扩充常量池的一个方法;当一个String实例str调用intern()方法时,Java查找常量池中是否有相同Unicode的字符串常量,如果有,则返回其的引用,如果没有,则在常量池中增加一个Unicode等于str的字符串并返回它的引用;看示例就清楚了
publicstaticvoidmain(String[]args){
Strings0="kvill";
Strings1=newString("kvill");
Strings2=newString("kvill");
System.out.println(s0==s1);//false
System.out.println("**********");
s1.intern();//虽然执行了s1.intern(),但它的返回值没有赋给s1
s2=s2.intern();//把常量池中"kvill"的引用赋给s2
System.out.println(s0==s1);//flase
System.out.println(s0==s1.intern());//true//说明s1.intern()返回的是常量池中"kvill"的引用
System.out.println(s0==s2);//true
}
最后我再破除一个错误的理解:有人说,“使用String.intern()方法则可以将一个String类的保存到一个全局String表中,如果具有相同值的Unicode字符串已经在这个表中,那么该方法返回表中已有字符串的地址,如果在表中没有相同值的字符串,则将自己的地址注册到表中”如果我把他说的这个全局的String表理解为常量池的话,他的最后一句话,”如果在表中没有相同值的字符串,则将自己的地址注册到表中”是错的:
publicstaticvoidmain(String[]args){
Strings1=newString("kvill");
Strings2=s1.intern();
System.out.println(s1==s1.intern());//false
System.out.println(s1+""+s2);//kvillkvill
System.out.println(s2==s1.intern());//true
}
在这个类中我们没有声名一个”kvill”常量,所以常量池中一开始是没有”kvill”的,当我们调用s1.intern()后就在常量池中新添加了一个”kvill”常量,原来的不在常量池中的”kvill”仍然存在,也就不是“将自己的地址注册到常量池中”了。
s1==s1.intern()为false说明原来的”kvill”仍然存在;s2现在为常量池中”kvill”的地址,所以有s2==s1.intern()为true。
StringBuffer与StringBuilder的区别,它们的应用场景是什么?
jdk的实现中StringBuffer与StringBuilder都继承自AbstractStringBuilder,对于多线程的安全与非安全看到StringBuffer中方法前面的一堆synchronized就大概了解了。
这里随便讲讲AbstractStringBuilder的实现原理:我们知道使用StringBuffer等无非就是为了提高java中字符串连接的效率,因为直接使用+进行字符串连接的话,jvm会创建多个String对象,因此造成一定的开销。AbstractStringBuilder中采用一个char数组来保存需要append的字符串,char数组有一个初始大小,当append的字符串长度超过当前char数组容量时,则对char数组进行动态扩展,也即重新申请一段更大的内存空间,然后将当前char数组拷贝到新的位置,因为重新分配内存并拷贝的开销比较大,所以每次重新申请内存空间都是采用申请大于当前需要的内存空间的方式,这里是2倍
【
StringBuffer始于JDK1.0
StringBuilder始于JDK1.5
从JDK1.5开始,带有字符串变量的连接操作(+),JVM内部采用的是
StringBuilder来实现的,而之前这个操作是采用StringBuffer实现的。
】
我们通过一个简单的程序来看其执行的流程:
publicclassBuffer{
publicstaticvoidmain(String[]args){
Strings1="aaaaa";
Strings2="bbbbb";
Stringr=null;
inti=3694;
r=s1+i+s2;
for(intj=0;i<10;j++){
r+="23124";
}
}
}
使用命令javap-cBuffer查看其字节码实现:
将清单1和清单2对应起来看,清单2的字节码中ldc指令即从常量池中加载“aaaaa”字符串到栈顶,istore_1将“aaaaa”存到变量1中,后面的一样,sipush是将一个短整型常量值(-32768~32767)推送至栈顶,这里是常量“3694”。
让我们直接看到13,1317是new了一个StringBuffer对象并调用其初始化方法,2021则是先通过aload_1将变量1压到栈顶,前面说过变量1放的就是字符串常量“aaaaa”,接着通过指令invokevirtual调用StringBuffer的append方法将“aaaaa”拼接起来,后续的24~30同理。最后在33调用StringBuffer的toString函数获得String结果并通过astore存到变量3中。
看到这里可能有人会说,“既然JVM内部采用了StringBuffer来连接字符串了,那么我们自己就不用用StringBuffer,直接用”+“就行了吧!“。是么?当然不是了。俗话说”存在既有它的理由”,让我们继续看后面的循环对应的字节码。
3742都是进入for循环前的一些准备工作,37,38是将j置为1。44这里通过if_icmpge将j与10进行比较,如果j大于10则直接跳转到73,也即return语句退出函数;否则进入循环,也即4766的字节码。这里我们只需看47到51就知道为什么我们要在代码中自己使用StringBuffer来处理字符串的连接了,因为每次执行“+”操作时jvm都要new一个StringBuffer对象来处理字符串的连接,这在涉及很多的字符串连接操作时开销会很大。