String可谓人人皆知,但是Sting不属于java 8大基本类型,它为字符串操作提供了一系列的成员函数,现在我们稍微了解一下,对其中的坑加以区分
Java String类定义
首先我们查看String类的源码(基于jdk8)
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
private final char value[];
private int hash; // Default to 0
private static final long serialVersionUID = -6849794470754667710L;
private static final ObjectStreamField[] serialPersistentFields =
new ObjectStreamField[0];
public String() {
this.value = "".value;
}
//······//
}
String类是final类,也就是说String类无法被继承。
成员变量value是通过char[]进行存储,可以看到这也是final变量,意味着String的值无法被修改?
等等,似乎和我们认知有一些不同,按理来说String的值不仅可以replace,add,同样也可以直接=另外一个字符串。这是为什么呢?
String常量池
字符串的分配和其他的对象分配一样,需要耗费高昂的时间与空间作为代价,为了提高性能和减少内存开销,JVM做了以下操作:
为字符串开辟一个字符串常量池,有点像缓冲区
在创建字符串常量时需要判断常量池里是否有这样的字符串,如果有就返回引用实例,否则就需要先在常量池中创建字符串然后返回引用实例
所以常量池中是不会存在相同的字符串
举个栗子:
String str1 = "abc";
String str2 = new String("abc");
可以看到,str1直接指向字符串常量池中的abc,而str2指向堆中的new String,然后new String的值为字符串常量池的abc
那么String str1 = “abc”;String str2 = new String(“abc”);分别创建了几个新对象?
String str1=”abc”;产生一个对象,”abc”放入常量池中,而str1为引用,因此有一个新对象
String str2 = new String(“abc”);产生一个或者两个对象,堆中new了一个新的String,str2引用了这个新对象(这里我们叫它temp),然后判断常量池中是否存在”abc”,如果不存在,需要创建一个”abc”对象,然后temp引用常量池的”abc”
是否接受挑战?判断创建了几个对象(假设语句之前常量池都是空的)
String str3 = "a"+"b";//3
String str4 = "ab"+"ab";//2
String str5 = new String("ab"+"ab");//3
String str6 = new String("a"+"b");//4
结合以上,我们可以得出结论:
单独使用””引号创建的字符串都是常量,编译期就已经确定存储到String Pool中
使用new String(“”)创建的对象会存储到heap中,是运行期新创建的
equal 和 ==
依旧是查看源码
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof String) {
String anotherString = (String)anObject;
int n = value.length;
if (n == anotherString.value.length) {
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
while (n-- != 0) {
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
return false;
}
我们都知道,Object类中的equals是判断引用地址的,但是在String类中稍有不同,原因在于String重写了equals
String类中==判断地址相同与否,而equals判断值相同与否
通过源码可以看出equal中先判断==,然后判断字符串的char[]是否相同,也就是说比较的是内容
结合上一节我们可以看出
String a = "abc";
String b = "abc";
boolean judge1 = a.equals(b);//true
boolean judge2 = a==b;//false
字符串替换
既然字符串创建完后不可修改,那么字符串的改变操作是怎样完成的呢,如replace,还有+=操作
replace
public String replace(char oldChar, char newChar) {
if (oldChar != newChar) {
int len = value.length;
int i = -1;
char[] val = value; /* avoid getfield opcode */
while (++i < len) {
if (val[i] == oldChar) {
break;
}
}
if (i < len) {
char buf[] = new char[len];
for (int j = 0; j < i; j++) {
buf[j] = val[j];
}
while (i < len) {
char c = val[i];
buf[i] = (c == oldChar) ? newChar : c;
i++;
}
return new String(buf, true);
}
}
return this;
}
通过return new String(buf,true);
可以看出来其实replace后返回的是一个新的String了,也就是说实际上原本的String并没有改变。只不过是引用到了一个新的String上,并且原本常量池的对象不会被GC回收
也因此,调用replace时需要将对象分配给新的引用实例
s.replace('a','b');//不起效,新的对象创建后未分配引用实例
s = s.replace('a','b');//新的对象返回给原来的引用,看起来像是在原本的s上进行replace一样
字符串+
+=其实和+是一个道理,我们不做特殊讨论
Java机制保证了无法重载操作符。但是String不属于8大基本类型的情况下是怎么使用+的呢。只有一种可能:
java自己破例帮我们写了一个重载,在使用加号进行计算的表达式中,只要遇到String字符串,则所有的数据都会转化为String类型进行拼接,如果是原始数据,则直接拼接,如果是对象。则调用toStirng方法的返回值然后拼接。
我们看下面几个问题
String str1 = 'abc';
String str2 = 'def';
String res = str1 + str2;
String str1 = "abc";
String res = str1 + "def";
String str1 = new String("abc");
String str2 = new String("def");
String res = str1 + str2;
String str1 = new String("abc");
String res = str1 + "def";
以上四组代码获取的res.equals(“abcdef”)的值为true,字符串常量池中分别有什么?
利用javap进行反解析
javac test.java
javap -verbose -c test
对四种情况查看constant pool
Constant pool:
#1 = Methodref #9.#18 // java/lang/Object."<init>":()V
#2 = String #19 // abc
#3 = String #20 // def
#4 = Class #21 // java/lang/StringBuilder
#5 = Methodref #4.#18 // java/lang/StringBuilder."<init>":()V
#6 = Methodref #4.#22 // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#7 = Methodref #4.#23 // java/lang/StringBuilder.toString:()Ljava/lang/String;
#8 = Class #24 // com/FJL/test
#9 = Class #25 // java/lang/Object
Constant pool:
#1 = Methodref #9.#18 // java/lang/Object."<init>":()V
#2 = String #19 // abc
#3 = Class #20 // java/lang/StringBuilder
#4 = Methodref #3.#18 // java/lang/StringBuilder."<init>":()V
#5 = Methodref #3.#21 // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#6 = String #22 // def
#7 = Methodref #3.#23 // java/lang/StringBuilder.toString:()Ljava/lang/String;
#8 = Class #24 // com/FJL/test
#9 = Class #25 // java/lang/Object
Constant pool:
#1 = Methodref #11.#20 // java/lang/Object."<init>":()V
#2 = Class #21 // java/lang/String
#3 = String #22 // abc
#4 = Methodref #2.#23 // java/lang/String."<init>":(Ljava/lang/String;)V
#5 = String #24 // def
#6 = Class #25 // java/lang/StringBuilder
#7 = Methodref #6.#20 // java/lang/StringBuilder."<init>":()V
#8 = Methodref #6.#26 // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#9 = Methodref #6.#27 // java/lang/StringBuilder.toString:()Ljava/lang/String;
#10 = Class #28 // com/FJL/test
#11 = Class #29 // java/lang/Object
Constant pool:
#1 = Methodref #11.#20 // java/lang/Object."<init>":()V
#2 = Class #21 // java/lang/String
#3 = String #22 // abc
#4 = Methodref #2.#23 // java/lang/String."<init>":(Ljava/lang/String;)V
#5 = Class #24 // java/lang/StringBuilder
#6 = Methodref #5.#20 // java/lang/StringBuilder."<init>":()V
#7 = Methodref #5.#25 // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#8 = String #26 // def
#9 = Methodref #5.#27 // java/lang/StringBuilder.toString:()Ljava/lang/String;
#10 = Class #28 // com/FJL/test
#11 = Class #29 // java/lang/Object
可以看到constant pool里都没有abcdef的,对比一下如下代码的结果
String str1 = new String("abc");
String str2 = new String("def");
String res = new String("abc"+"def");
Constant pool:
#1 = Methodref #8.#17 // java/lang/Object."<init>":()V
#2 = Class #18 // java/lang/String
#3 = String #19 // abc
#4 = Methodref #2.#20 // java/lang/String."<init>":(Ljava/lang/String;)V
#5 = String #21 // def
#6 = String #22 // abcdef
#7 = Class #23 // com/FJL/test
#8 = Class #24 // java/lang/Object
是有abcdef生成的,这是为什么呢。
通过观察可以发现+针对于上述四种情况都是相当于调用了StringBuilder.append(“___”).toString();
而最后一种情况直接加两个常量则没有这种字节码。
因此针对于String 的+ ,是不会在字符串常量池中创建新对象的。
结论:+前后有一个是String类,就不会把加完的结果创建在字符串常量池中
StringBuffer and StringBuilder
由于String前面介绍的特殊性质,才有了StringBuffer和StringBuilder,二者都可以改变内容的值
即StringBuffer和StringBuilder类的对象可以被多次修改,但是不会产生新的未使用的对象
二者的区别在于
@Override
public synchronized int length() {
return count;
}
@Override
public synchronized int capacity() {
return value.length;
}
@Override
public synchronized void ensureCapacity(int minimumCapacity) {
super.ensureCapacity(minimumCapacity);
}
······
StringBuffer类的override的函数都有synchronized关键字修饰,即可以多线程访问
StringBuffer是线程安全的,StringBuilder不是线程安全的
执行效率一般情况下是:
StringBuilder > StringBuffer > String
而对于String,StringBuffer和StringBuilder三者而言,需要在特定的场合下使用:
String:适用于少量的字符串操作的情况
StringBuilder:适用于单线程下在字符缓冲区进行大量操作的情况
StringBuffer:适用多线程下在字符缓冲区进行大量操作的情况
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!