Java String —— 字符串常量池

要点

String不是基本数据类型。
我们可以看一下jdk中的String.java源码(源码使用的是jdk1.8的版本),我简要的摘录如下:

public final class String
    implements java.io.Serializable, Comparable, CharSequence {
    /** The value is used for character storage. */
    private final char value[];
    ...
}

首先String类是final类,不能被继承,其次String类其实是通过char数组来保存字符串的,它值是不可变的。同时我们发现,String在它对自身的操作的时候,比如replace、subString等,实际在内部实现都是创建了一个全新的String对象(也就是说进行这些操作后,最原始的字符串并没有被改变)。看下面的源码片段:

    public String substring(int beginIndex, int endIndex) {
        if (beginIndex < 0) {
            throw new StringIndexOutOfBoundsException(beginIndex);
        }
        if (endIndex > value.length) {
            throw new StringIndexOutOfBoundsException(endIndex);
        }
        int subLen = endIndex - beginIndex;
        if (subLen < 0) {
            throw new StringIndexOutOfBoundsException(subLen);
        }
        return ((beginIndex == 0) && (endIndex == value.length)) ? this
                : new String(value, beginIndex, subLen);
    }
    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;
    }

谨记

“String对象一旦被创建就是固定不变的了,对String对象的任何改变都不影响到原对象,相关的任何change操作都会生成新的对象”。

字符串常量值

由于字符串我们使用很频繁,同时分配字符串就像分配其他对象一样,如果这样的话,那么会对性能和内存的小号有一定的影响。所以JVM在实例化字符串的时候进行了一些优化:使用字符串常量池

那么,什么是字符串常量值?
每当我们创建一个字符串对象时,首先就会检查字符串常量池中是否存在面值相等的字符串,如果有,则不再创建,直接返回字符串常量池中对该对象的引用;若没有,则创建然后放入到字符串常量池中并且返回新建对象的引用。这个机制是非常有用的,因为可以提高效率,减少了内存空间的占用。所以在使用字符串的过程中,推荐使用直接赋值(即String s=”aa”),除非有必要才会新建一个String对象(即String s = new String(”aa”))。
那么这两个有什么区别呢?

  • String str = “aa”;
    Java String —— 字符串常量池_第1张图片
  • String str = new String(“aa”);
    Java String —— 字符串常量池_第2张图片
    这两种哪一个节约内存资源,我就不用说了吧。。。

一些实例帮助理解

在我之前的博客中曾经说过:“= =“其实是比较两个对象的的内存地址”。那么我们使用”= ="来对上面的描述进行一些拓展。
废话少说上代码:

  • 场景1
    private void test1(){
        String s = "s";
        String s1 = "s";
        System.out.println(s==s1);
    }

输出:true

这种场景不必多说了吧,这就是设置字符串常量池的初衷。
解析:
执行String s=“s”:JVM首先会去字符串常量池中查找是否存在"s"这个对象,不存在,则在字符串常量池中创建"s"这个对象,然后将字符串常量池中"s"这个对象的引用地址返回给栈中的字符串常量s,这样s会指向池中"s"这个字符串对象;
执行String s1 = “s”:查找"s",发现存在,则不创建任何对象,直接将池中"s"这个对象的地址返回,赋给字符串常量。
也就是说str1和str2指向了同一个对象,因此语句System.out.println(s == s1)输出true。具体见下图:
Java String —— 字符串常量池_第3张图片

  • 场景2
    private void test2(){
        String s2 = new String("s");
        String s3 = new String("s");
        System.out.println(s2==s3);
    }

输出:false
解析:
Java String —— 字符串常量池_第4张图片
因为比较的是s2==s3,抛开上面的图不说,既然是使用了new关键字来构造String,那么s2和s3的内存地址肯定是不一样的。图示为了清晰说明。

  • 场景3
    private void test3() {
        String s = "friend";
        String s1 = "friend";
        String s2 = "fri" + "end";
        System.out.println(s == s1);
        System.out.println(s1 == s2);
    }

输出: true true
解析:这里比较让我们迷惑的是s2这个String,它指向的的是一个堆中的一个String对象,还是直接指向字符串常量池呢?答案是直接指向字符串常量池中的“friend”。因为“fri”和“end”两个都是字符串常量,s2是由两个字符串常量连接起来的,自己也是字符串常量,这是在编译期就确定的事。
Java String —— 字符串常量池_第5张图片

  • 场景4
    private void test4(){
        String s = "friend";
        String s1 = new String("friend");
        String s2 = "fri" + new String("end");
        System.out.println(s==s1);
        System.out.println(s==s2);
        System.out.println(s1==s2);
    }

输出: false false false
解析:s直接指向字符串常量池中的“friend”;s1指向堆中的String对象;s2比较让人纠结,其实s2在编译期不能确定它的值,所以它最后会指向堆中的String对象。
Java String —— 字符串常量池_第6张图片

  • 场景5
    private void test5(){
        String s = "fri";
        String s1 = "end";
        String s2 = s1 + s;
        System.out.println(s2 == "friend");
    }

输出: false
解析:经过了上面的场景4的例子,这里我们就清楚了,s2在编译器是不能确定的,因为s1和s的值是字符串常量池中的引用(如果是这样写String s2 = “fri”+“end”;就能在编译期确定)。那么这句String s2 = s1+s;具体做了什么呢?主要是做了以下几件事:1)+运算符将会在堆中建立两个String对象,然后再在堆中建立s2指向的对象,然后在字符串常量池中创建“friend”,将其引用指向堆中的s2的对象。
Java String —— 字符串常量池_第7张图片

  • 场景6
    private void test6(){
        String s = "A1";
        String s1 = "A"+1;
        String s2 = "A3.4";
        String s3 = "A"+ 3.4;
        System.out.println(s==s1);
        System.out.println(s2==s3);
    }

输出:true true
解析:这里s1和s3分别是将字符串分别和int和小数进行“+”操作,可能会对我们造成困扰,我们只要知道在编译期进行相关的优化,可以直接确定s2和s3的值。所以s和s1、s2和s3分别指向字符串常量池中的相同字符串。

  • 场景7
    private void test7(){
        String s = "friend";
        String s1 = "end";
        String s2 = "fri"+s1;
        System.out.println(s==s2);
    }

输出:false
解析:编译期无法确定s2的值,所以s2最后指向的是堆中的String对象。
Java String —— 字符串常量池_第8张图片
以上总结起来如下:形如"s"、“hello"等使用+拼接是在编译期间进行的,拼接后的字符串存放在字符串常量池中;而字符串引用的”+“拼接运算是在运行时进行的,新创建的字符串存放在堆中。
对于直接相加字符串,效率很高,因为在编译器便确定了它的值,也就是说形如"fri”+“end”; 的字符串相加,在编译期间便被优化成了"friend"。对于间接相加(即包含字符串引用),形如s1+s2+s3; 效率要比直接相加低,因为在编译器不会对引用变量进行优化。

小结

String使用private final char value[]来实现字符串的存储,也就是说String对象创建之后,就不能再修改此对象中存储的字符串内容,就是因为如此,才说String类型是不可变的(immutable)。
然而,String类对象确实有编辑字符串的功能,比如replace()。这些编辑功能是通过创建一个新的对象来实现的,而不是对原有对象进行修改。例如:
s = s.replace(“World”, “Universe”);
上面对s.replace()的调用将创建一个新的字符串"Hello Universe!",并返回该对象的引用。通过赋值,引用s将指向该新的字符串。如果没有其他引用指向原有字符串"Hello World!",原字符串对象将被垃圾回收。

创建字符串的方式

创建字符串的方式归纳起来有两类:
(1)使用"“引号创建字符串;
(2)使用new关键字创建字符串。
结合上面例子,总结如下:
(1)单独使用”“引号创建的字符串都是常量,编译期就已经确定存储到String Pool中;
(2)使用new String(”")创建的对象会存储到heap中,是运行期新创建的;
new创建字符串时首先查看池中是否有相同值的字符串,如果有,则拷贝一份到堆中,然后返回堆中的地址;如果池中没有,则在堆中创建一份,然后返回堆中的地址(注意,此时不需要从堆中复制到池中,否则,将使得堆中的字符串永远是池中的子集,导致浪费池的空间)!
(3)使用只包含常量的字符串连接符如"aa" + “aa"创建的也是常量,编译期就能确定,已经确定存储到String Pool中;
(4)使用包含变量的字符串连接符如"aa” + s1创建的对象是运行期才创建的,存储在heap中;

注意点

  1. 使用String不一定创建对象:在执行到双引号包含字符串的语句时,如String a = “123”,JVM会先到常量池里查找,如果有的话返回常量池里的这个实例的引用,否则的话创建一个新实例并置入常量池里。所以,当我们在使用诸如String str = “abc”;的格式定义对象时,总是想当然地认为,创建了String类的对象str。担心陷阱!对象可能并没有被创建!而可能只是指向一个先前已经创建的对象。只有通过new()方法才能保证每次都创建一个新的对象。
  2. 使用new String,一定创建对象。
  3. String中使用“+”:
    1)String中使用 + 字符串连接符进行字符串连接时,连接操作最开始时如果都是字符串常量,编译后将尽可能多的直接将字符串常量连接起来,形成新的字符串常量参与后续连接
    2)接下来的字符串连接是从左向右依次进行,对于不同的字符串,首先以最左边的字符串为参数创建StringBuilder对象,然后依次对右边进行append操作,最后将StringBuilder对象通过toString()方法转换成String对象(注意:中间的多个字符串常量不会自动拼接)。也就是说String c = “xx” + "yy " + a + “zz” + “mm” + b; 实质上的实现过程是:
    String c = new StringBuilder("xxyy ").append(a).append(“zz”).append(“mm”).append(b).toString();
    =》当使用+进行多个字符串连接时,实际上是产生了一个StringBuilder对象和一个String对象

关于String、StringBuffer、StringBuilder

对于这三者使用的场景做如下概括(参考:《编写搞质量代码:改善java程序的151个建议》):
1、String:在字符串不经常变化的场景中可以使用String类,如:常量的声明、少量的变量运算等。
2、StringBuffer:在频繁进行字符串的运算(拼接、替换、删除等),并且运行在多线程的环境中,则可以考虑使用StringBuffer,例如XML解析、HTTP参数解析和封装等。
3、StringBuilder:在频繁进行字符串的运算(拼接、替换、删除等),并且运行在多线程的环境中,则可以考虑使用StringBuffer,如SQL语句的拼装、JSON封装等。

声明

部分转载自 https://www.cnblogs.com/xiaoxi/p/6036701.html ,如有侵权,请联系我。
上述网址中还有许多干货!

你可能感兴趣的:(java杂谈)