String对象创建个数
String类是Java中很重要的一个类,在此总结一下这个类的特别之处。下面的相关资料翻译自《java语言规范》(第三版)和《java虚拟机规范》(第二版),有的直接摘引了原文。下面的代码都是用SUN jdk1.6 javac来编译。
1.String literal,这里将它翻译为字面常量,它由双引号包围的0个或多个字符组成,比如"abc","Hello World"等等。一个String字面常量总是引用相同的String实例,比如"abc","abc"两个常量引用的是同一个对象。
程序测试:
package testPackage;
class Test {
public static void main(String[] args) {
String hello = "Hello", lo = "lo";
System.out.print((hello == "Hello") + " ");
System.out.print((Other.hello == hello) + " ");
System.out.print((other.Other.hello == hello) + " ");
System.out.print((hello == ("Hel"+"lo")) + " ");
System.out.print((hello == ("Hel"+lo)) + " ");
System.out.println(hello == ("Hel"+lo).intern());
}
}
class Other { static String hello = "Hello"; }
另一个包:
package other;
public class Other { static String hello = "Hello"; }
输出:
true true true true false true
结论有六点:
1) 同一个包下,同一个类中的相同的String字面常量表示对同一个String对象的引用。
2) 同一个包下,不同的类中的相同的String字面常量表示对同一个String对象的引用。
3) 不同包下,不同类中的相同String字面常量同样表示对同一个String对象的引用。
4) 通过常量表达式计算的String,计算在编译时进行,并将它作为String字面常量对待。
5) 通过连接操作得到的String(非常量表达式),连接操作是运行时进行的,会新创建对象,所以它们是不同的。
6) 显式的对一个计算得到的String调用intern操作,得到的结果是已经存在的相同内容的String字面常量。
补充说明:
1)像这样的问题,String str = "a"+"b"+"c"+"d";
运行这条语句会产生几个String对象?1个。参考上面第5条,通过常量表达式得到的String 是编译时计算的,因此执行这句话时只有"abcd"着一个String对象存在。
常量表达是的定义可以参考java语言规范。另例:
final String str1 = "a";
String str2 = str1+"b";
执行第二句话会有几个String对象产生?1个。因为str1是常量,所以str1+"b"也是常量表达式,在编译时计算。
遇到这种问题时,不要说它依赖于具体的编译器或者虚拟机实现,因为这就是规范里有的。一般的说,java的编译器实现应该遵守《java语言规范》,而java虚拟机实现应该遵守《java虚拟机规范》。
2)不要这样使用字符串:
String str = new String("abc");
参考文档中的说明:
String
public String(String original)
初始化一个新创建的 String 对象,使其表示一个与参数相同的字符序列;换句话说,新创建的字符串是该参数字符串的副本。由于 String 是不可变的,所以无需使用此构造方法,除非需要 original 的显式副本。
参数:
original - 一个 String。
注意:无需使用此构造方法!!!
3)单独的说明第6点:
String str = new String("abc");
str = str.intern();
当调用 intern 方法时,如果池已经包含一个等于此 String 对象的字符串(用 equals(Object) 方法确定),则返回池中的字符串引用。否则,将此 String 对象添加到池中,并返回此 String 对象的引用。
很明显,在这个例子中"abc"引用的对象已经在字符串池中了,再调用intern返回的是已经存在池中内容为"abc"的字符换对象的引用。在上面的例子中也说明了这个问题。
2. String类的实例表示表示Unicode字符序列。String字面常量是指向String实例的引用。(字面常量是“引用”!)
3.String转换
对于基本类型先转换为引用类型;引用类型调用toString()方法得到String,如果该引用类型为null,转换得到的字符串为"null"。
4. String链接操作“+”
如果“+”操作的结果不是编译期常量,将会隐式创建一个新的对象。为了提高性能,具体的实现可以采用StringBuffer,StringBuilder类对多个部分进行连接,最后再转换为String,从而避免生成再丢弃中间的String对象。为了达到共享实例的目的,编译期常量总是“interned”的。
例子:
String a = "hello ";
String b = a+1+2+"world!";
反汇编结果:
0: ldc #2; //String hello
2: astore_1
3: new #3; //class java/lang/StringBuilder
6: dup
7: invokespecial #4; //Method java/lang/StringBuilder."<init>":()V
10: aload_1
11: invokevirtual #5; //Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
14: iconst_1
15: invokevirtual #6; //Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
18: iconst_2
19: invokevirtual #6; //Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
22: ldc #7; //String world!
24: invokevirtual #5; //Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
27: invokevirtual #8; //Method java/lang/StringBuilder.toString:()Ljava/lang/String;
30: astore_2
实际就是
String b = new StringBuilder().append(a).append(1).append(2).append("world").toString();
这里就使用StringBuilder来避免中间临时String对象的产生而导致性能下降。
补充例子,下面的两个例子主要是对编译时常量做一个说明:
1)
String c = "c";
String str = "a"+"b"+c;
和
2)
String c = "c";
String str = c+"a"+"b";
1)中,str="a"+"b"+c;编译器分析是会把"a"+"b"作为编译时常量,生成字面常量"ab",所以实际执行这句话时,链接的是"ab"和c。实际相当于执行了
String str = new StringBuilder().append("ab").append(c).toString();
2)中,String str = c+"a"+"b";
编译器分析到c为变量,后面的"a"+"b"就不会作为编译时常量来运算了。
实际运行时相当于执行
String str = new StringBuilder().append(c).append("a").append("b").toString();
5.String对象的创建:
1) 包含String字面常量的类或者接口在加载时创建表示该字面常量的String对象。以下两种情况下不会创建新String对象。
a) 一个相同的字面常量已经出现过。
b) 一个相同内容的字符串已经调用了intern操作(比如经过运算产生的字符串调用intern的情形)。
2) 非常量表达式的字符串连接操作有时会产生表示结果的String对象。
3) String字面常量来自类或接口的二进制表示中(也就是class文件中)的CONSTANT_String_info 结构。CONSTANT_String_info结构给出了构成字符串字面常量的Unicode字符序列。
4) 为了生成字符串字面常量,java虚拟机检查 CONSTANT_String_info结构给出的字符序列:
a) 如果与CONSTANT_String_info结构中给出的字符换内容相同的串实例已经调用过String.intern,得到的字符串字面常量就来自该串的同一实例。
b) 否则,根据CONSTANT_String_info 中的字符序列创建一个新的字符串实例,然后调用intern方法。
例子:一个SCJP题目
11. public String makinStrings() {
12. String s = “Fred”;
13. s = s + “47”;
14. s = s.substring(2, 5);
15. s = s.toUpperCase();
16. return s.toString();
17. }
How many String objects will be created when this method is invoked?
答案是3个。上面已经说明,"Fred","47"是字符串字面常量,它们在在类加载时创建的。这里题目问,方法调用时(!)有多少个String对象被创建,两个字面常量自然不包括在内。3个是:"Fred47","ed4","ED4"。
6.String与基本类型的包装类比较
相同点,它们都是不变类,使用"=="判断时可能会有类似的性质。
在java 5之后,java增加了自动装箱和拆箱功能。因此,就有了这样的性质:
Integer i = 5;
Integer j = 5;
System.out.println(i == j);
结果:true.
这表面上看来是和String相同点,但其实现是极为不同的。这里作为一个不同点来介绍。
众所周知,自动装箱是这样实现的:
Integer i = 5;
相当于
Integer i = Integer.valueOf(5);//注意不是new Integer(5),这就无法满足java语言规范中的约定了,约定见本文最后
而在Integer中,静态的创建了表示从-128~+127之间数据的Integer对象,这个范围之内的数进行装箱操作,只要返回相应的对象即可。因此
Integer i = 5;
Integer j = 5;
我们得到的是同一个对象。这是通过类库的设计来实现的。而String的共享是通过java虚拟机的直接支持来实现的,这是它们本质的不同。
这是Integer类中的部分代码:
private static class IntegerCache {
private IntegerCache(){}
static final Integer cache[] = new Integer[-(-128) + 127 + 1];
static {
for(int i = 0; i < cache.length; i++)
cache[i] = new Integer(i - 128);
}
}
public static Integer valueOf(int i) {
final int offset = 128;
if (i >= -128 && i <= 127) { // must cache
return IntegerCache.cache[i + offset];
}
return new Integer(i);
}
关于基本类型的装箱,Java语言规范中有如下说明:
如果被装箱的变量p为true,false,一个处于/u0000~/u007f之间的byte/char,或一个处于-128~+127之间的int/short,令r1和r2为对p的任何两个装箱操作的结果,则r1==r2总是成立的。理想的情况下,对一个基本类型变量执行装箱操作,应该总是得到一个相同的引用。但在实践中,在现存的技术条件下,这是不现实的。上面的规则是一个注重实效的折衷。
最后一点,要理解java的方法调用时的传参模型:java中只有pass by value。(不明确这一点,就有乱七八糟的解释,比如典型的Java既有传值,又有传引用,String很特殊……)
//改变参数的值?
public void test(String str){
str = "Hello";
}
//改变参数的值?
public void test(StringBuffer buffer){
buffer = new StringBuffer("Hello");
}
//交换两个Integer?
public void swap(Integer a,Integer b){
Integer temp = a;
a = b;
b = temp;
}
这三个方法全是没有意义的方法。
本文来自CSDN博客,转载请标明出处:http://blog.csdn.net/ZangXT/archive/2009/05/19/4201979.aspx
另一位高手的总结:
s = new String("xyz");创建了几个String Object?两个对象,一个是“xyx”,一个是指向“xyx”的引用对象s。
String s="你好";int i=3; s=i+s; 这个表达式对吗?在java中会提示数据类型不匹配。因为string是类!正确做法: s+="3" 或者 s+='3'或者 s+=(char)i;
我们要引入另外一种创建String对象的方式的讨论——引号内包含文本。这种方式是String特有的,并且它与new的方式存在很大区别。
在JAVA虚拟机(JVM)中存在着一个字符串池,其中保存着很多String对象,并且可以被共享使用,因此它提高了效率。String a="abc";,这行代码被执行的时候,JAVA虚拟机首先在字符串池中查找是否已经存在了值为"abc"的这么一个对象,判断依据是String类equals(Object obj)方法的返回值。如果有,则不再创建新的对象,直接返回已存在对象的引用;如果没有,则先创建这个对象,然后把它加入到字符串池中,再将它的引用返回。
字符串对象的创建:由于字符串对象的大量使用[它是一个对象,一般而言对象总是在heap分配内存],Java中为了节省内存空间和运行时间[如比较字符串时,==比equals()快],在编译阶段就把所有的字符串文字放到一个文字池中,而运行时文字池成为常量池的一部分。文字池的好处,就是该池中所有相同的字符串常量被合并,只占用一个空间。我们知道,对两个引用变量,使用==判断它们的值[引用]是否相等,即指向同一个对象:
现在看String s = new String("abc");语句,这里"abc"本身就是pool中的一个对象,而在运行时执行new String()时,将pool中的对象复制一份放到heap中,并且把heap中的这个对象的引用交给s持有。ok,这条语句就创建了2个String对象。
String s1 = new String("abc") ;String s2 = new String("abc") ;if( s1 == s2 ){ //不会执行的语句}
//创建了几个String Object? [三个,pool中一个,heap中2个。]
只有使用引号包含文本的方式创建的String对象之间使用“+”连接产生的新对象才会被加入字符串池中。对于所有包含new方式新建对象(包括null)的“+”连接表达式,它所产生的新对象都不会被加入字符串池中。
1.==表示引用自同一对象,equals()表示值相等。
String str1 = "abc";引用的对象在栈(或者叫String池)中。
String str1 =new String ("abc"); 引用的对象在内存/堆中。
2.String str1 = "string";在栈中
String str3 = "str";在栈中
String str4 = "ing";在栈中
String str2 = str3+str4; 在堆中,因为+号的作用是返回另外一个新建的String对象,而不是在栈中找string这个值。如果是String str2 = "str"+"ing";那最后的结果就在栈中。str1==str2为true。
但是有一种情况需要引起我们的注意。请看下面的代码:
public class StringStaticTest {
public static final String A = "ab"; // 常量A
public static final String B = "cd"; // 常量B
public static void main(String[] args) {
String s = A + B; // 将两个常量用+连接对s进行初始化
String t = "abcd";
if (s == t) {
System.out.println("s等于t,它们是同一个对象");
} else {
System.out.println("s不等于t,它们不是同一个对象");
}
}
}
这段代码的运行结果如下:
s等于t,它们是同一个对象
原因是在上面的例子中,A和B都是常量,值是固定的,因此s的值也是固定的,它在类被编译时就已经确定了。也就是说:String s=A+B; 等同于:String s="ab"+"cd";
我对上面的例子稍加改变看看会出现什么情况:
public class StringStaticTest {
public static final String A; // 常量A
public static final String B; // 常量B
static {
A = "ab";
B = "cd";
}
public static void main(String[] args) {
// 将两个常量用+连接对s进行初始化
String s = A + B;
String t = "abcd";
if (s == t) {
System.out.println("s等于t,它们是同一个对象");
} else {
System.out.println("s不等于t,它们不是同一个对象");
}
}
}
它的运行结果是这样:
s不等于t,它们不是同一个对象
只是做了一点改动,结果就和刚刚的例子恰好相反。我们再来分析一下。A和B虽然被定义为常量(只能被赋值一次),但是它们都没有马上被赋值。在运算出s的值之前,他们何时被赋值,以及被赋予什么样的值,都是个变数。因此A和B在被赋值之前,性质类似于一个变量。那么s就不能在编译期被确定,而只能在运行时被创建了。
最后我们再来说说String对象在JAVA虚拟机(JVM)中的存储,以及字符串池与堆(heap)和栈(stack)的关系。我们首先回顾一下堆和栈的区别:
栈(stack):主要保存基本类型(或者叫内置类型)(char、byte、short、int、long、float、double、boolean)和对象的引用,数据可以共享,速度仅次于寄存器(register),快于堆。
堆(heap):用于存储对象。
我们查看String类的源码就会发现,它有一个value属性,保存着String对象的值,类型是char[],这也正说明了字符串就是字符的序列。当执行String a="abc";时,JAVA虚拟机会在栈中创建三个char型的值'a'、'b'和'c',然后在堆中创建一个String对象,它的值(value)是刚才在栈中创建的三个char型值组成的数组{'a','b','c'},最后这个新创建的String对象会被添加到字符串池中。
如果我们接着执行String b=new String("abc");代码,由于"abc"已经被创建并保存于字符串池中,因此JAVA虚拟机只会在堆中新创建一个String对象,但是它的值(value)是共享前一行代码执行时在栈中创建的三个char型值值'a'、'b'和'c'。
说到这里,我们对于篇首提出的String str=new String("abc")为什么是创建了两个对象这个问题就已经相当明了了。
本文来自CSDN博客,转载请标明出处:http://blog.csdn.net/yakihappy/archive/2009/03/10/3977169.aspx