• 浅谈java中的String


    Java中的String类型不属于八大基本数据类型,而是一个引用数据类型,所以在定义一个String对象的时候如果不直接赋值给这个对象,它的默认值就是null。我们要怎么理解String类型的不可变,在JDK源码中String这个类的value方法被final关键字修饰,导致String里的值不可以被修改。

    在这里插入图片描述

    也就是说双引号括起来的String对象,从出生到死亡,都无法发生变化。
    对于任意一个String对象,都由value[]和hash组成,例如:String str = “hello”;
    在这里插入图片描述

    当我们直接使用双引号括起来的字符串初始化String对象时,该字符串会被存储在“方法区”的“字符串常量池”当中。而上图中的hash值就是用于查找字符串在常量池中的位置。

    字符串常量池主要用于存储字符串常量,本质是一个哈希表(StringTable)。从JDK1.8开始,这个哈希表就存放在了堆中

    为什么会将字符串放在常量区?
    因为字符串在实际的开发中使用太频繁。为了执行效率,所以把字符串放到了方法区的字符串常量池当中。


    从这里开始,将从内存的角度分析问题

    例1:

    在这里插入图片描述
    这里输出false的原因大家应该都知道,因为str1和str2的引用指向不,那底层是如何实现的呢?
    通过内存图可以清晰明了看起问题本质:

    在这里插入图片描述

    String str1 = “hello”,首先会创建一个字符数组,用来存放"hello",然后再开辟一块空间(假如为ptr1),指向这个数组,最后会在栈上用一个引用去指向ptr1。

    String str2 = new String(“hello”),首先会查看StringTable是否存在"hello",这里已经存在,会开辟一块空间(假如为ptr2),将ptr1中的内容拷贝到ptr2中,那么ptr2就指向了"hello"数组,最后在栈上用一个引用去指向ptr2

    JDK源码:
    在这里插入图片描述
    在这里插入图片描述

    JDK源码的大致含义:当我们用一个字符串去实例化一个String对象时,会将这个字符串通过hash函数得到一个hash值,然后在StringTable中找,如果存在就不需要重新再创建,如果不存在就需要创建这个字符串


    例2:

    public class Test6 {
        public static void main(String[] args) {
            String str1 = "hello";
            String str2 = "hello";
            System.out.println(str1 == str2);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    在这里插入图片描述

    在这里插入图片描述
    因为hello在SringTable中已经存在,所以在栈上引用str2直接指向了0x3344。而例1中str2是new出来的,所以需要在堆上开辟空间,然后这块空间再指向0x3344


    例3:

    public class Test6 {
        public static void main(String[] args) {
            String str1 = "hello";
            String str2 = "he" + "llo";
            System.out.println(str1 == str2);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    在这里插入图片描述
    这里的结果依然是true
    因为“he”和“llo”都是常量,代码在编译的时候就已经确定了最终结果是常量字符串“hello”

    如果“he”或者“llo”有一个是变量,最终结果为false,例如:
    在这里插入图片描述


    例4:

    public class Test6 {
        public static void main(String[] args) {
            String str1 = "11";
            String str2 = new String("1") + new String("1");
            System.out.println(str1 == str2);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    在这里插入图片描述

    还是画图分析:

    在这里插入图片描述

    当new string(“1”)时,产生的匿名对象,也会将"1"放入到字符串常量池中,当两个"1"相加,就会形成一个StringBuilder对象, 里面存放"11",当这个StringBuilder对象要转换成String对象时,需要调用to_string()方法,这个方法并不会判断"11"是否在字符串常量池中,而是直接创建新的字符串。也就是说to_string并不会入池,所以最终的结果为false

    例5:

    public class Test6 {
        public static void main(String[] args) {
            String str2 = new String("1") + new String("1");
            String str1 = "11";
            System.out.println(str1 == str2);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    在这里插入图片描述

    这里的结果也为false,跟例4的原因是一样的。这里就不画图了。

    但是只要对这个代码稍作修改,结果就为true

    在这里插入图片描述

    intern()这个方法叫做手动入池(将String放在字符串常量池中)

    本来str2(“11”)是没有入池,但是调用了intern()方法后,“11"就被放进了常量池中。当定义str1时,发现常量池中有"11”,就不需要再创建字符串"11",直接赋值,就和例2一样,最终str1和str2引用的是同一块空间,所以为true

    注意:使用intern()方法时,如果常量池中没有才会入池,有的话就不会入池


    equals()方法进行比较String

    在这里插入图片描述

    以上是equals的源码,是按照逐一比较字符的方式。

    如果String没有重新equals方法的话,默认是比较地址
    在这里插入图片描述

    任何一个引用去调用方法,都要预防空指针异常,equals方法也不例外,例如:
    在这里插入图片描述

    除此之外,下面两种字符串引用有着本质的区别

    String str1 = null;
    String str2 = "";
    
    • 1
    • 2

    str1这个引用,不指向任何对象
    str2这个引用,指向的字符串是空的

    可以使用length方法验证一下:
    在这里插入图片描述

    String作为参数,在使用之前也需要进行判断:

    public static void func(String str) {
        //正确写法
        if (str == null || str.length() == 0) {
            return;
        }
        //错误写法
        if(str.length() == 0 || str == null) {
            return;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    第二种写法,先判断str的长度可能会存在空指针异常


    尽量少用String去拼接字符串

    public class Test7 {
        public static void main(String[] args) {
            String str = "abcde";
            for(int i = 0; i < 10; ++i) {
                str += i;
            }
            System.out.println(str);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    这段代码,大家可能认为平平无奇,就是一个单纯的字符串拼接。但是每一次拼接都会产生临时对象,可以看一下java的汇编代码:

    在这里插入图片描述
    从汇编代码可以看出,每一次的for循环,都会创建一个StringBuilder对象,最后再调用String方法。这样是非常浪费内存和时间的,虽说循环10次影响比较小,但是循环10w次呢?那开销就很大了,所以,由于String是不可变的,所以应当少用String去拼接字符串。


    StringBuffer和StringBuilder

    首先来回顾下String类的特点:
    任何的字符串常量都是String对象,而且String的常量一旦声明不可改变,如果改变对象内容,改变的是其引用的指向而已。
    通常来讲String的操作比较简单,但是由于String的不可更改特性,为了方便字符串的修改,提供StringBuffer和StringBuilder类。
    StringBuffer 和 StringBuilder 大部分功能是相同的,主要介绍 StringBuffer

    public class Test8 {
        public static void main(String[] args) {
            StringBuilder sb = new StringBuilder();
            sb.append("fff");
            System.out.println(sb);
            sb.append("lll");
            System.out.println(sb);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    在这里插入图片描述

    String是无法更改的,而StringBuilder对象时可以更改的,这里调用了两次append方法,都是在原有对象进行添加操作,并且有重新创建新的对象。

    在这里插入图片描述

    前面也了解了,在循环中用String去拼接字符串,会产生临时对象StringBuilder,而StringBuilder对象又是可以更改的,所以优先使用StringBuilder对象去拼接字符串


    StringBuffer和StringBuilder的区别

    StringBuilder中的部分append:
    在这里插入图片描述

    StringBuffer中的部分append:
    在这里插入图片描述

    通过对比,发现StringBuffer中的append方法是多了synchronized,也就是说StringBuffer是线程安全的,而StringBuilder不是线程安全的。
    因此StringBuilder用于单线程,而StringBuffer用于多线程。

    String、StringBuffer、StringBuilder的区别

    • String的内容不可修改,StringBuffer与StringBuilder的内容可以修改.
    • StringBuffer与StringBuilder大部分功能是相似的
    • StringBuffer采用同步处理,属于线程安全操作;而StringBuilder未采用同步处理,属于线程不安全操作
  • 相关阅读:
    react闪屏问题以及useEffect和useLayoutEffect的对比使用
    springboot:整合其它项目
    Preferences DataStore全解析
    阿里云解决方案架构师张平:云原生数字化安全生产的体系建设
    《uni-app》表单组件-Button按钮
    金仓数据库 KingbaseES 客户端编程接口指南 - PHP PDO (3. PHP配置连接KingbaseES)
    HashMap原理
    JavaEE——网络原理(网络层 IP协议与数据链路层)
    Zookeeper学习笔记(2)—— Zookeeper API简单操作
    unigui点击按钮后弹出悬浮窗,几秒钟后关闭
  • 原文地址:https://blog.csdn.net/qq_56044032/article/details/127693566