昨天遇到一道编程题关于字符串中字符内容的替换,题目如下:
请实现一个函数,将一个字符串中的空格替换成“%20”。例如,当字符串为We Are Happy.则经过替换之后的字符串为We%20Are%20Happy。
public class Solution {
public String replaceSpace(StringBuffer str) {
//添加代码
}
}
于是想通过此题对java中字符串的定义与处理方式有所了解。
一、String
java中没有内置的字符串类型,在标准java类库中预定义类字符串类,所有用双引号表示的都是String类的一个实例。在String类中定义了字符型数组value用于存储字符串。源码如下:
private final char value[];
...
public String() {
this.value = new char[0];
}
该value数组被定义成final型,在String类众多构造函数的最简洁创建字符串对象方法中可以看到,即为存储字符串数组value初始化,且长度为0。
定义成final型的字符数组value因此也具有独特性质:final成员变量表示常量,只能被赋值一次,赋值后值不再改变。而数组作为一种特殊的变量类型(引用类型),其在内存中由栈内存中的数组变量名指向存储于堆内存中的数组值。堆内存的特性之一包括默认值初始化操作,int型默认初始化值为0,char型默认初始化值为' '。也就是说,字符串一旦创建对象后,其值即不可改变。
String str = "xuwenping";
str = str + "123";
上面两行代码,表面看上去String str 与变量无意义,其值可以更改。其实际在代码层的操作是:第一行是在栈内存中创建字符串的引用变量str,在堆内存中创建字符串"xuwenping",("xuwenping"字符串实际以字符数组形式存储),且str作为字符串指针指向"xuwenping"的首地址(数组的首地址)。第二行则是在堆内存中创建了新的字符串"xuwenping123",将str指向新的字符串的值。而原有的"xuwenping"字符串则无人引用,将会被JVM垃圾回收。
如上所述,可知字符串的值不可变。若变,则是创建新的字符串。创建过程不可避免的效率低下,当然,java设计者实现了另外一种字符串对象赋值方式以希望消除效率低下的问题:字符串共享池(缓冲池)。缓冲池是java为了节省内存空间,会在内存中创建一个专门为String设计的缓冲池,用来保存已经存在的字符串,若2个字符串是一样的,则使用池中的字符串,不再创建新的对象。
String str1 = "abc";
String str2 = "abc";
System.out.println(str1==str2); //true
String str3 =new String ("abc");
String str4 =new String ("abc");
System.out.println(str3==str4); // false
String str = "hello";
上面这种方式会创建一个"hello"字符串,而且JVM的字符缓存池还会缓存这个字符串。
String str = new String("hello");
此时程序除创建字符串外,str所引用的String对象底层还包含一个char[]数组,这个char[]数组依次存放了h,e,l,l,o
二、StringBuffer
由于字符串的变化会导致创建新的字符串效率低下,java提供了可变的字符串类StringBuffer。StringBuffer继承AbstractStringBuilder类。其默认初始化操作如下:
public StringBuilder() {
super(16);
}
...
AbstractStringBuilder(int capacity) {
value = new char[capacity];
}
char[] value;
StringBuffer初始化调用父类构造方法,父类有一非final成员变量char[ ] value,默认创建长度为16的字符数组。当随着字符数增加,StringBuffer会自动增大,查看源码发现,其调用父类的expandCapacity方法并调用java.util.Arrays数组工具类中的copyOf方法创建新的大小为value.length * 2 + 2的数组,并将原有数组字符复制值新数组中。从而实现了字符串的随意改变。
void expandCapacity(int minimumCapacity) {
int newCapacity = value.length * 2 + 2;
if (newCapacity - minimumCapacity < 0)
newCapacity = minimumCapacity;
if (newCapacity < 0) {
if (minimumCapacity < 0) // overflow
throw new OutOfMemoryError();
newCapacity = Integer.MAX_VALUE;
}
value = Arrays.copyOf(value, newCapacity);
}
三、StringBuilder
从java1.5开始,StringBuilder开始出现,API文档如下描述:”该类被设计用作 StringBuffer
的一个简易替换,用在字符串缓冲区被单个线程使用的时候(这种情况很普遍)“。此句包含两层意思:一是其具有StringBuffer的特性:可随着存储字符串元素的增加而扩容;二是StringBuffer是多线程同步的,而StringBuilder是多线程不同步的。
查看源码,并由StringBuilder的寻找过程:append ->AbstractStringBuilder.append ->ensureCapacityInternal -> expandCapacity ->Arrays.copyOf,发现两者均是调用父类的expandCapacity方法并调用java.util.Arrays数组工具类中的copyOf方法创建新的大小为value.length * 2 + 2的数组,并将原有数组字符复制值新数组中从而实现扩容性质。
而在多线程方面,查看源码可知StringBuffer除了构造器外其他方法均以synchronized实现锁同步,而StringBuilder则始终是裸奔。在《java核心卷一:基础知识》有如下论断:在jdk5.0中引入了StringBuilder类。这个类的前身是StringBuffer,其效率稍有些低,但容许采用多线程的方式执行添加或删除字符的操作。如果所有字符串在一个单线程中编辑(通常都是这样),则应该用StringBuilder替换它。这两个类的API是相同的。
四、解答
最后,针对引出该话题的问题予以解答:
public class Solution {
public String replaceSpace(StringBuffer str) {
String stri = str.toString();
stri = stri.replaceAll(" ", "%20");
return stri;
}
}
StringBuffer、StringBuilder均可以通过toString方法转换成Sting对象。