先来看以下问题,以下代码创建了几个对象?
String s1=new String("abc");
网上比较流行的说法是创建了两个对象:字符串对象abc和引用对象s1。这种说法十分模糊,什么是引用对象?s1究竟是引用还是对象?
正确的说法应该是s1是引用了一个字符串对象的变量,通过s1可以操作其所引用的字符串对象。因此从某种意义上,可以把s1直接视为对象(可以把s1理解为一个指向字符串对象的指针,但建议不要这样理解,因为Java里没有指针的概念)
查看String的帮助文档可以知道,执行以上代码将创建一个字符串对象,此对象的内容将“复制”所传入的参数值,这里为字符串abc。很显然,必须先创建abc然后才能进行复制。那么是谁创建的abc呢?
在Java里,字符串值如“abc”被实现为常量。在使用abc时,首先JVM会查找内存看是否已存在字符串abc,如果不存在则创建字符串abc对象,在随后的使用中,字符串abc对象都不会被改变。因此abc可称为字符串常量。
因此执行以上代码可以肯定的是将创建变量s1指向的字符串对象。而在执行此代码前,如果abc对象不存在,那么将先创建abc字符串常量对象,否则不会创建。
因为字符串实现为常量,如果所使用的字符串在内存里不存在,那么JVM需要先创建他们(即为其分配内存),这可能会比较影响性能,以下代码将会创建4个字符串常量对象,即包含4次分配内存的操作:
String s1="a";
String s2="b";
String s3="c";
String s4=s1+s2+s3;
为了解决以上问题,Java提供了StringBuffer类。在创建StringBuffer对象时,会为其分配一块内存缓冲区,用于存放需要拼接的字符串值。在缓冲区存满前都不会再次进行分配,进而提高性能。
StringBuilder与StringBuffer基本相同,不同点在于StringBuilder是非线程安全的,而StringBuffer是线程安全的。因此StringBuilder性能高于StringBuffer
以下使用TestNG对以上部分观点进行了测试
以下测试说明,如果字符串常量对象已存在,那么后续变量将直接引用此对象:
package com.bingo.practice;
import org.testng.Assert;
import org.testng.annotations.Test;
public class StringEqualTest {
@Test
public void test(){
String s1=new String("abc");
String s2=new String("abc");
//s1,s2引用不同的字符串对象,但是其字符串值相同
Assert.assertEquals(s1, s2);
Assert.assertEquals(s1, "abc");
Assert.assertNotSame(s1, s2);
String s3="abc";
String s4="abc";
//s3, s4都引用字符串常量对象abc
Assert.assertEquals(s3, s4);
Assert.assertSame(s3, s4);
}
}
以下测试拼接字符串的性能比较:StringBuilder>StringBuffer>String
package com.bingo.practice;
import org.testng.annotations.Test;
public class StringPerformanceTest {
private static final int count=100000;
@Test
public void testStringBuilder(){
StringBuilder b=new StringBuilder();
for(int i=0;i<count;i++)
b.append("a");
System.out.println(b.toString());
}
@Test
public void testStringBuffer(){
StringBuffer s=new StringBuffer();
for(int i=0;i<count;i++)
s.append("a");
System.out.println(s.toString());
}
@Test
public void testString(){
String s="";
for(int i=0;i<count;i++)
s+="a";
System.out.println(s);
}
}
测试结果如下:
以下测试线程安全,String,StringBuilder非线程安全(测试失败),StringBuffer线程安全(测试通过)
package com.bingo.practice;
import org.testng.Assert;
import org.testng.annotations.AfterClass;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.Test;
public class StringTest {
private String actualString;
private String expectedString;
@BeforeClass
public void init(){
this.actualString="";
this.expectedString="";
for(int i=0;i<10000;i++)
expectedString+="a";
}
@Test(invocationCount=10000,threadPoolSize=10)
public void test(){
actualString+="a";
}
@AfterClass
public void finish(){
Assert.assertEquals(actualString, expectedString);
}
}
package com.bingo.practice;
import org.testng.Assert;
import org.testng.annotations.AfterClass;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.Test;
public class StringBuilderTest {
private StringBuilder actualBuilder;
private StringBuilder expectedBuilder;
@BeforeClass
public void init(){
this.actualBuilder=new StringBuilder();
this.expectedBuilder=new StringBuilder();
for(int i=0;i<10000;i++)
expectedBuilder.append("a");
}
@Test(invocationCount=10000,threadPoolSize=10)
public void test(){
actualBuilder.append("a");
}
@AfterClass
public void finish(){
Assert.assertEquals(actualBuilder.toString(), expectedBuilder.toString());
}
}
package com.bingo.practice;
import org.testng.Assert;
import org.testng.annotations.AfterClass;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.Test;
public class StringBufferTest {
private StringBuffer actualBuffer;
private StringBuffer expectedBuffer;
@BeforeClass
public void init(){
this.actualBuffer=new StringBuffer();
this.expectedBuffer=new StringBuffer();
for(int i=0;i<10000;i++)
expectedBuffer.append("a");
}
@Test(invocationCount=10000,threadPoolSize=10)
public void test(){
actualBuffer.append("a");
}
@AfterClass
public void finish(){
Assert.assertEquals(actualBuffer.toString(), expectedBuffer.toString());
}
}