String的特殊之处
String是Java编程中很常见的一个类,这个类的实例是不可变的(immutable ).为了提高效率,JVM内部对其操作进行了一些特殊处理,本文就旨在于帮助大家辨析这些特殊的地方.
在进入正文之前,你需要澄清这些概念:
1) 堆与栈
2) 相同与相等,==与equals
3) =的真实意义.
栈与堆
1. 栈(stack)与堆(heap)都是Java用来在内存中存放数据的地方。与C++不同,Java自动管理栈和堆,程序员不能直接地设置栈或堆。每个函数都有自己的栈,而一个程序只有一个堆.
2. 栈的优势是,存取速度比堆要快,仅次于直接位于CPU中的寄存器。但缺点是,存在栈中的数据大小与生存期必须是确定的,缺乏灵活性。另外,栈数据可以共享,详见第3点。堆的优势是可以动态地分配内存大小,生存期也不必事先告诉编译器,Java的垃圾收集器会自动收走这些不再使用的数据。但缺点是,由于要在运行时动态分配内存,存取速度较慢。 3. Java中的数据类型有两种。 一种是基本类型(primitive types), 共有8种,即int, short, long, byte, float, double, boolean, char(注意,并没有string的基本类型)。这种类型的定义是通过诸如int a = 3; long b = 255L;的形式来定义的,称为自动变量。值得注意的是,自动变量存的是字面值,不是类的实例,即不是类的引用,这里并没有类的存在。如int a = 3; 这里的a是一个指向int类型的引用,指向3这个字面值。这些字面值的数据,由于大小可知,生存期可知(这些字面值固定定义在某个程序块里面,程序块退出后,字段值就消失了),出于追求速度的原因,就存在于栈中。 另外,栈有一个很重要的特殊性,就是存在栈中的数据可以共享。假设我们同时定义 int a = 3; int b = 3; 编译器先处理int a = 3;首先它会在栈中创建一个变量为a的引用,然后查找有没有字面值为3的地址,没找到,就开辟一个存放3这个字面值的地址,然后将a指向3的地址。接着处理int b = 3;在创建完b的引用变量后,由于在栈中已经有3这个字面值,便将b直接指向3的地址。这样,就出现了a与b同时均指向3的情况。 特别注意的是,这种字面值的引用与类对象的引用不同。假定两个类对象的引用同时指向一个对象,如果一个对象引用变量修改了这个对象的内部状态,那么另一个对象引用变量也即刻反映出这个变化。相反,通过字面值的引用来修改其值,不会导致另一个指向此字面值的引用的值也跟着改变的情况。如上例,我们定义完a与 b的值后,再令a=4;那么,b不会等于4,还是等于3。在编译器内部,遇到a=4;时,它就会重新搜索栈中是否有4的字面值,如果没有,重新开辟地址存放4的值;如果已经有了,则直接将a指向这个地址。因此a值的改变不会影响到b的值。 另一种是包装类数据,如Integer, String, Double等将相应的基本数据类型包装起来的类。这些类数据全部存在于堆中,Java用new()语句来显示地告诉编译器,在运行时才根据需要动态创建,因此比较灵活,但缺点是要占用更多的时间。
相同与相等,==与equals
在Java中,相同指的是两个变量指向的地址相同,地址相同的变量自然值相同;而相等是指两个变量值相等,地址可以不同.
相同的比较使用==,而相等的比较使用equals.
对于字符串变量的值比较来说,我们一定要使用equals而不是==.
=的真实意义
=即赋值操作,这里没有问题,关键是这个值有时是真正的值,有的是地址,具体来说会根据等号右边的部分而变化.
如果是基本类型(八种),则赋值传递的是确定的值,即把右边变量的值传递给左边的变量.
如果是类类型,则赋值传递的是变量的地址,即把等号左边的变量地址指向等号右边的变量地址.
指出下列代码的输出
String andy="andy";
String bill="andy";
if(andy==bill){
System.out.println("andy和bill地址相同");
}
else{
System.out.println("andy和bill地址不同");
}
String str=“andy”的机制分析
上页代码的输出是andy和bill地址相同.
当通过String str=“andy”;的方式定义一个字符串时,JVM先在栈中寻找是否有值为“andy”的字符串,如果有则将str指向栈中原有字符串的地址;如果没有则创建一个,再将str的地址指向它. String andy=“andy”这句代码走的是第二步,而String bill=“andy”走的是第一步,因此andy和bill指向了同一地址,故而andy==bill,andy和bill地址相等,所以输出是andy和bill地址相同.
这样做能节省空间—少创建一个字符串;也能节省时间—定向总比创建要省时.
指出下列代码的输出
String andy="andy";
String bill="andy";
bill="bill";
if(andy==bill){
System.out.println("andy和bill地址相同");
}
else{
System.out.println("andy和bill地址不同");
}
输出及解释
上页代码的输出是:andy和bill地址不同
当执行bill=“bill”一句时,外界看来好像是给bill变换了一个新值bill,但JVM的内部操作是把栈变量bill的地址重新指向了栈中一块值为bill的新地址,这是因为字符串的值是不可变的,要换值(赋值操作)只有将变量地址重新转向. 这样andy和bill的地址在执行bill=“bill”一句后就不一样了,因此输出是andy和bill地址不同.
指出下列代码的输出
String andy=new String("andy");
String bill=new String("andy");
// 地址比较
if(andy==bill){
System.out.println("andy和bill地址相同");
}
else{
System.out.println("andy和bill地址不同");
}
// 值比较
if(andy.equals(bill)){
System.out.println("andy和bill值相等");
}
else{
System.out.println("andy和bill值不等");
}
输出及机制分析
andy和bill地址不同
andy和bill值相等
我们知道new操作新建出来的变量一定处于堆中,字符串也是一样.
只要是用new()来新建对象的,都会在堆中创建,而且其字符串是单独存值的,即每个字符串都有自己的值,自然地址就不会相同.因此输出了andy和bill地址不同.
equals操作比较的是值而不是地址,地址不同的变量值可能相同,因此输出了andy和bill值相等.
指出下列代码的输出
String andy=new String("andy");
String bill=new String(andy);
// 地址比较
if(andy==bill){
System.out.println("andy和bill地址相同");
}
else{
System.out.println("andy和bill地址不同");
}
// 值比较
if(andy.equals(bill)){
System.out.println("andy和bill值相等");
}
else{
System.out.println("andy和bill值不等");
}
输出
andy和bill地址不同
andy和bill值相等
道理仍和第八页相同.只要是用new()来新建对象的,都会在堆中创建,而且其字符串是单独存值的,即每个字符串都有自己的值,自然地址就不会相同.
指出下列代码的输出
String andy="andy";
String bill=new String(“Bill");
bill=andy;
// 地址比较
if(andy==bill){
System.out.println("andy和bill地址相同");
}
else{
System.out.println("andy和bill地址不同");
}
// 值比较
if(andy.equals(bill)){
System.out.println("andy和bill值相等");
}
else{
System.out.println("andy和bill值不等");
}
输出及解析
andy和bill地址相同
andy和bill值相等
String bill=new String(“Bill”)一句在栈中创建变量bill,指向堆中创建的”Bill”,这时andy和bill地址和值都不相同;而执行bill=andy;一句后,栈中变量bill的地址就指向了andy,这时bill和andy的地址和值都相同了.而堆中的”Bill”则没有指向它的指针,此后这块内存将等待被垃圾收集.
指出下列代码的输出
String andy="andy";
String bill=new String("bill");
andy=bill;
// 地址比较
if(andy==bill){
System.out.println("andy和bill地址相同");
}
else{
System.out.println("andy和bill地址不同");
}
// 值比较
if(andy.equals(bill)){
System.out.println("andy和bill值相等");
}
else{
System.out.println("andy和bill值不等");
}
输出
andy和bill地址相同
andy和bill值相等
道理同第十二页
结论
使用诸如String str = “abc”;的语句在栈中创建字符串时时,str指向的字符串不一定会被创建!唯一可以肯定的是,引用str本身被创建了。至于这个引用到底是否指向了一个新的对象,必须根据上下文来考虑,如果栈中已有这个字符串则str指向它,否则创建一个再指向新创建出来的字符串. 清醒地认识到这一点对排除程序中难以发现的bug是很有帮助的。
使用String str = “abc”;的方式,可以在一定程度上提高程序的运行速度,因为JVM会自动根据栈中数据的实际情况来决定是否有必要创建新对象。而对于String str = new String(“abc”);的代码,则一概在堆中创建新对象,而不管其字符串值是否相等,是否有必要创建新对象,从而加重了程序的负担。
如果使用new()来新建字符串的,都会在堆中创建字符串,而且其字符串是单独存值的,即每个字符串都有自己的值,且其地址绝不会相同
当比较包装类里面的数值是否相等时,用equals()方法;当测试两个包装类的引用是否指向同一个对象时,用==。
由于String类的immutable性质,当String变量需要经常变换其值如SQL语句拼接,HTML文本输出时,应该考虑使用StringBuffer类,以提高程序效率。