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

1

那么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 协议 ,转载请注明出处!

HashMap源码深入理解 上一篇
使用hexo搭建自己的博客 下一篇