JavaSE语法(14)—— 详细解读java中 String的不可变性与字符串常量池

文章详细介绍了Java中String的不可变性,解释了字符串常量池的概念及工作原理,包括如何通过intern方法手动入池。同时对比分析了StringBuffer和StringBuilder的异同,指出StringBuffer的线程安全性及其对性能的影响。最后讨论了字符串拼接的效率问题,建议在单线程环境下使用StringBuilder以提高性能。
摘要由CSDN通过智能技术生成



1.String的不变性

  String是不可变对象,其内容无法被修改。

  我们先看看它的源码:

在这里插入图片描述

  可以看到String是用数组来存储字符串的,怎么验证呢?进行调试就可以看到。(jdk8以及之前的版本value数组类型是char,jdk9开始为byte类型)

在这里插入图片描述

  但这不是重点,重点是这个数组是被fianlprivate修饰。有的人可能会认为fianl是字符串不能被修改的真正原因,这是错误的观点。这里final的作用是限制value的值(value变量装的是地址),就是说value不能引用其它的数组。

  被final修饰的数组,其数组内容是可以修改的。

public class Main {
    public static void main(String[] args) {

        final int[] value = {1,2,3,4};
        arr[0] = 100;
        System.out.println(Arrays.toString(value));
    }
}

结果:

在这里插入图片描述

  String字符串不能被修改的主要原因是被private修饰String的源码中没有提供修改value数组的方法,String中需要修改字符串的方法也只是返回新的字符串。然而value数组private修饰,这就断绝了从外部修改value数组的途径,所以就无法修改字符串。

  官方为什么要把String设计成不可变的?(不全面,了解即可)

  • 不可变 天生线程安全
  • String常被用作HashMap的key,如果可变会有安全问题,如两个key相同。
  • 方便实现字符串常量池。


2.字符串常量池


2.1 什么是字符串常量池?

  我们先看看下面的代码:

public class Main {
    public static void main(String[] args) {

        String str1 = "Hello";
        String str2 = "Hello";

        String str3 = new String("Hello");
        String str4 = new String("Hello");

        System.out.println(str1 == str2);
        System.out.println(str3 == str4);
    }
}

结果:

在这里插入图片描述

  从上可以得出,str1str2引用的是同一个实例,str3str4引用的是不同的实例。我们再进一步思考,上面的不同之处就是前者用字符串常量来创建字符串,后者是通过new关键字来创建字符串。也就是说,通过字符串常量创建字符串的方式来创建多个相同的字符串时,变量会引用同一个实例。

  为什么呢?这是因为“Hello”这一常量被装入字符串常量池中,后面想要再创建一个相同的字符串时,不用再重新创建而是直接引用(前提是用字符串常量来创建字符串),这就省掉了创建对象的时间与空间,使程序的运行速度更快、更节省内存。

  字符串常量池就像一个“池子”一样,用来装程序中的字符串常量,在我们有需求的时候直接使用。


2.2 字符串常量池是怎么工作的?

  在 jdk8 以及之后的版本中,字符串常量池是在堆中。字符串常量池的底层是一个哈希表,叫StringTable。(哈希表是查询效率很高的数据结构)

  可以把这个哈希表看成数组,每个元素是指向一个链表,每个节点连接一个String对象,这些String对象就代表了常量串。(每个节点还有其它的信息,这里进行简化了。)

在这里插入图片描述

  我们有以下代码:

public class Main1 {
    public static void main(String[] args) {
        
        String str1 = "Hello";
        String str2 = "Hello";
    }
}

  在字节码文件加载的时候,“hello” 等常量串已经创建好了,并且保存在字符串常量池中。(链表的长度可以为 1 ,链表的长度是不固定的,是根据常量串的数量来动态变化的)

在这里插入图片描述

  我们可以简化一下,以便理解。(注意:下图不是真正的存储方式,但不影响阅读)

在这里插入图片描述

  当我们用“字符串常量”来赋值创建字符串时,先在字符串常量池找到该字符串对象,然后直接赋值给栈中的变量:

在这里插入图片描述

  这也就解释了为什么“str1 == str2”的值为true


  我们再来看看下面的这种情况:

public class Main1 {
    public static void main(String[] args) {

        String str1 = "Hello";
        String str2 = new String("Hello");

        System.out.println(str1 == str2);//返回的是false
    }
}

  我们已知晓str1的调用情况,那么对于str2呢?只要是new就会调用构造方法,我们看看 String 相应的构造方法:

在这里插入图片描述

  这里的original其实就是字符串常量池中的那个对象,也就是说,str2的这种情况是在堆中重新开辟一个空间,value数组是共享的:

在这里插入图片描述

  其实很好验证,我们可以调试一下:

在这里插入图片描述

  可以看到它们的value值(地址值)是一样的。

  那如果是下面这种情况呢?

public class Main1 {
    public static void main(String[] args) {
        
        String str1 = new String("Hello");
    }
}

  它会先把“Hello”放入字符串常量池(前提是常量池没有“Hello”,如果有就是上面案例的情况),然后再new一个空间。

  在程序中,如果有带了引号的字符串常量出现,它就会被放入字符串常量池中

在这里插入图片描述


2.3 手动入池: intern方法

  下面代码的输出结果是什么?:

public class Main1 {
    public static void main(String[] args) {

        char[] arr = {'H','e','l','l','o'};
        String str1 = new String(arr);

        String str2 = "Hello";
        System.out.println(str1 == str2);
    }
}

  结果:

在这里插入图片描述

  看一看相应的构造方法:(jdk版本不同 源码会有所不同,下面这个是 jdk8 的源码)

在这里插入图片描述

  很显然,它会拷贝一个数组,并不是共用同一个数组。对于 str1 的创建并没有涉及到 “字符串常量”,所以就不会放入池里;而对于str2这里的“Hello”,是肯定会入池的。如图:

在这里插入图片描述

  所以就返回了 false


  如何让结果为 true 呢?我们可以调用intern方法。intern是一个native方法(底层使用C++实现的,看不到其实现的源代码),该方法的作用是手动将创建的String对象添加到常量池中。

public class Main1 {
    public static void main(String[] args) {

        char[] arr = {'H','e','l','l','o'};
        String str1 = new String(arr);
        str1.intern();//将“Hello”放入常量池

        String str2 = "Hello";
        System.out.println(str1 == str2);
    }
}

结果:

在这里插入图片描述

  在调用intern之后,str1字符串被放入常量池中,str2那里就不会再创建String:“Hello”对象了。

在这里插入图片描述



3.StringBuffer和StringBuilder


3.1 是什么?

  你可以把StringBufferStringBuilder这两个类看作String的升级版。StringBufferStringBuilder这两个类也可以代表字符串,它们是可以改变的(具有可变性)。

  StringBufferStringBuilder也都是用数组来存储字符串的,它们都继承了
AbstractStringBuilder抽象类。

在这里插入图片描述

  可以看到value数组没有被private、final修饰,限制没有String中的value严格,并且StringBufferStringBuilder重写了修改字符串的相关方法。


3.2 提供的方法

  StringBufferStringBuilder中的方法大部分都是相同的,你可以认为它们是孪生姐妹。

  以下是常用的方法(包括父类AbstractStringBuilder的方法,下面以StringBuffer为视角)

方法说明
StringBuff append(String str) 在尾部追加,相当于String的+=,可以追加:boolean、char、char[]、 double、float、int、long、Object、String、StringBuff的变量
char charAt(int index) 获取index位置的字符
int length() 获取字符串的长度
int capacity() 获取底层保存字符串空间总的大小
void ensureCapacity(int mininmumCapacity) 扩容
void setCharAt(int index, char ch) 将index位置的字符设置为ch
int indexOf(String str) 返回str第一次出现的位置
int indexOf(String str, int fromIndex) 从fromIndex位置开始查找str第一次出现的位置
int lastIndexOf(String str) 返回最后一次出现str的位置
int lastIndexOf(String str, int fromIndex) 从fromIndex位置开始找str最后一次出现的位置
StringBuff insert(int offset, String str) 在offset位置插入:八种基类类型 & String类型 & Object类型数据
StringBuffer deleteCharAt(int index) 删除index位置字符
StringBuffer delete(int start, int end) 删除[start, end)区间内的字符
StringBuffer replace(int start, int end, String str)将[start, end)位置的字符替换为str
String substring(int start) 从start开始一直到末尾的字符以String的方式返回
String substring(int start,int end) 将[start, end)范围内的字符以String的方式返回
StringBuffer reverse() 反转字符串
String toString() 将所有字符按照String的方式返回

  StringBufferStringBuilder的方法与String的方法大同小异,最主要的区别就是StringBufferStringBuilder提供了setCharAt()deleteCharAt()等能修改字符串的方法(效率相关问题在后文),在刷题的时候使用StringBufferStringBuilder就很方便。

  (1)StringBufferStringBuilder 的构造方法都是一样的,这里就看StringBuilder的:
在这里插入图片描述

public class Main2 {
    public static void main(String[] args) {
        
        //无参数的时候初始容量为16
        StringBuilder stringBuilder1 = new StringBuilder();
        
        //直接
        StringBuilder stringBuilder2 = new StringBuilder("Hello");

        //间接
        String str = "World";
        StringBuilder stringBuilder3 = new StringBuilder(str);
        
        //不能直接赋值
        //StringBuilder stringBuilder4 = "Hello World!";这是错误的!!!
        
        //`StringBuffer`和`StringBuilder`重写了toString()方法,可以直接打印。
        System.out.println(stringBuilder1);
        System.out.println(stringBuilder2);
        System.out.println(stringBuilder3);
    }
}

  (2)StringBufferStringBuilder的方法就不举例了,表里的文字就已经描述得很清楚了。


3.3 StringBuffer和StringBuilder的区别

  StringBuffer:
在这里插入图片描述

  StringBuilder:在这里插入图片描述

  StringBuffer的方法都加了synchronized关键字,这个是多线程的知识,表示对该对象加锁。换言之,就是StringBuffer线程安全,StringBuilder线程不安全。

  除此之外没什么区别,在单线程的情况下用谁都可以。



4.字符串拼接的过程

  String有一个特性,就是可以直接在尾部追加新字符串,就比如:

public class Main {
    public static void main(String[] args) {

        String str = "Hello";
        str += " World";
        System.out.println(str);//结果为 Hello World
    }
}

  前面介绍过String具有不可变性,那它是怎么完成追加操作的呢?我们看看它的汇编代码:

在这里插入图片描述

  在String拼接的时候会借助StringBuilder类来修改字符串,因为StringBuilder类是“可变的”,它等价于下面的代码:

public class Main {
    public static void main(String[] args) {

        String str1 = "Hello";
        StringBuilder stringBuilder = new StringBuilder();
        
        stringBuilder.append(str1);//尾部追加“Hello”
        stringBuilder.append(" World");//尾部追加“ World”
        str1 = stringBuilder.toString();//转换为 String 类型、
        System.out.println(str);//结果为 Hello World
    }
}

  从上面代码可以得出,用String来拼接字符串效率较低,其中间过程需要创建新的对象。我们从下面代码就可以看出问题:

public class Main2 {
    public static void main(String[] args) {

        //String 拼接
        long start = System.currentTimeMillis();//获取当前时间(开始时间)
        String s1 = "";
        for(int i = 0; i < 50000; ++i){
            s1 += i;
        }
        long end = System.currentTimeMillis();//获取当前时间(结束时间)
        System.out.println("String:" + (end - start) + "毫秒");//时间差(用时多少)  下同

        //StringBuffer 拼接(追加)
        start = System.currentTimeMillis();
        StringBuffer s2 = new StringBuffer("");
        for(int i = 0; i < 50000; ++i){
            s2.append(i);
        }
        end = System.currentTimeMillis();
        System.out.println("StringBuffer:" + (end - start) + "毫秒");

        //StringBuilder 拼接(追加)
        start = System.currentTimeMillis();
        StringBuilder s3 = new StringBuilder();
        for(int i = 0; i < 50000; ++i){
            s3.append(i);
        }
        end = System.currentTimeMillis();
        System.out.println("StringBuilder:" + (end - start) + "毫秒");
    }
}

结果:

在这里插入图片描述

  为什么StringBuffer 相对于 StringBuilder 要慢一丢丢?因为 StringBufferappend方法加了 synchronized 关键字,加锁 解锁 也是要消耗时间的。


  如有错误请在评论区指正,码字不易,求点点赞谢谢🌹🌹🌹

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值