别翻了,我敢保证全网我的String、StringBuffer和StringBuilder讲的最透彻,最清晰,最明白

      今天来和大家聊聊字符串那些事,来彻底把字符串类型吃透摸透,让你在面试时,当问到字符串时,你可以和他喷个半小时,来个吊打面试官,哈哈,开玩笑啦,可不能吊打面试官,要是吊打了,那你还想不想拿到录取offer啦呀。好了,废话少说,来开始干吧,准备吊打面试官的工具。

1、String字符串

      java八大基本类型:byte,short,int,long,float,double,char,boolean。在八大基本类型中,我们没有找到String类型,那说明它不属于基本类型,java中除了基本类型,剩下的全是引用类型,毫无以外,String也不例外,它属于引用类型。

      八大基本类型即存放在栈中也存放在堆中,那么它是如何确定是存在堆中还是栈中呢,下面我举个例子,大家心里就一目了然。

void function(){
    //局部变量
    int a = 3;
}

      这个例子中的基本类型是在方法中所定义,也就是局部变量,那自然它是存放在栈中。

class test{
    //全局变量
    int a = 3;
}

      这个例子的基本类型是在类中所定义,它也就是全局变量,那肯定是随对象存放在堆里。

      因此不要相信大多数博客中所说的基本类型全部存放在栈中,,不要一概而论,要视情况而定。

      往更深层次来了解它,为什么全局变量存放在堆中,而局部变量存放在栈中呢?

      如果你熟悉Java的内存结构的话,那么你对这种问题就不感觉到疑惑了,堆是所有线程共享的内存区域,栈是每个线程独享内存区域。如果将全部变量的的基本类型也放到栈中,那么多个线程就不能访问同一个对象资源,这显然是不对的,全局变量线程是不安全的。

      好了,上面和大家探讨了那么多基本类型,也算是个抛砖引玉吧,接下来把玉String给引出来。

1.1、String存放地址

      它属于引用类型,它的内容即可以存放在堆中,也可以存放在常量池中,同样视情况而定,但是它肯定会在栈中开辟一块区域来存放变量名,这点毋庸置疑。

第一种,直接创建String:

//String 直接创建
String str1 = "china";
String str2 = "china";

这个例子是直接创建,变量名会存放在栈中,内容会存放在常量池中。
image-20210806110050596
步骤:

      1、首先将str1变量名存放在栈中,然后将内容"china"存放在常量池中,然后将str1的地址指向常量池"china"。

      2、将str2变量名存放在栈中,然后将"china"去与常量池中所有内容进行对比,查看到常量池中已经存在“china”。

      3、将str2地址直接指向常量池中已经存在"china"。

第二种,对象创建String

//对象创建String
String str3 = new String("china");
String str4 = new String("china");

这个例子是对象创建,变量名存放在栈中,对象存放在堆中。

image-20210806111659598

步骤:

      1、str3创建时,先在栈中开辟一块内存,存放变量名,将对象存放堆中,然后将str3指向堆中对象地址。

      2、str4创建时,先在栈中开辟一块内存,存放str4变量名,然后新创建对象存放在堆中,然后str4指向堆中新创建地址。

      注意: String对象创建时,和直接创建是不一样的,每次对象创建时都会在堆中重新创建个新对象;但是直接创建时会会先在常量池中查看对比是否有这个内容,如果有的话,那么直接将地址指向它即可,否则的话,将在常量池中新建。

1.2、String源码分析

      首先,我给大家贴出String部分源码来分析下,我的是jdk11,和1.8的有些不同,源码如下所示:

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    
    @Stable
    private final byte[] value;
}

      a、从源码中可以看出String类是用final来进行修饰,说明String这个类不能被子类继承重写。

      b、底层采用的是byte[]数组,并且变量是用final关键字来进行修饰,说明引用类型一旦初始化之后,便不会再指向另一个对象;基本类型被赋值之后,其数值就不会发生改变。byte[]类型是引用类型,说明用final修饰之后,其指向对象地址不会改变,但是数据可以发生改变,那么结合private一起使用,这样就保证了String的不可变性,因此String也是线程安全的。

      c、这里我贴出源码是jdk11的,但是在jdk1.8中源码是不是这样的,1.8中底层采用的是char[]数组为底层。我们可以思考下为什么之前采用char数组,现在却采用byte数组。char是占用2个字节,byte占用1个字节 ,这样在每次初始化的时候为内存节省1个字节,节省其内存。

1.3、String的双等号(==)比较其地址

      在双等号比较值时,比较的是其地址,如果为true则说明是同一个值或者同一个对象,反之则为false。我下列出一段代码,然后再详细分析。代码如下:

public class TestString {

    public static void main(String[] args) {

        //将内容存放在常量池中,如果常量池中已经存在,则不需要再进行创建
        String str1 = "china"; //直接创建
        String str2 = "china"; //直接创建
        
	   //创建一个对象存放在堆中
        String str3 = new String("china"); //对象创建
        
        //这里的拼接也是个知识点,具体分析大家可以看下面的str4和str5比较分析
        String str4 = str1 + "add"; // 利用"+"来进行拼接
        
        //这里的code4和下面的code5值是一样的,这就能说明他们指向的是同一个地址么?
        //显然是不可以的,String中将hashcode()方法进行重写,分析源码可以看出:只要内容相等,那么hashCode值就相等,因此可以说明hashcode相等,不代表值相等
        int code4 = str4.hashCode();//code4:1661267946
        String str5 = "chinaadd"; //直接拼接好字符串
        int code5 = str5.hashCode();//code5:1661267946

        String str6 = new String("china"); //对象创建
        
        //两个直接创建的String类型变量名直接双等号比较,返回值为true
        //说明两个变量指向的是同一个常量
        //这也说明了直接创建的内容存放在常量池中,变量名中存放在栈中
        //栈中变量名直接指向常量池,每当直接创建一个String时,会拿它的内容去常量池中对比看是否已经存在
        //如果已经存在,则直接将地址指向已经存在的内容;否则在常量池中重新定义一个内容,变量名地址指向它
        //因此str1地址指向常量池的内容和str2指向的地址是一样的。因此双等号地址比较是相等
        System.out.println(str1 == str2);//true
        
        //str1地址指向的是常量池中"china"内容
        //str3指向的是对堆中创建的地址
        //因此,他们两个完全指向的不是一个地址,因此双等号地址比较为false
        System.out.println(str1 == str3);//false
        
        //str4是用的字符串拼接,str5用的是直接拼接好的字符串,我刚一开始认为这str4拼接的字符串直接存放在常量池中
        //str5正好直接指向常量池中的内容,双等号比较应该是true啊,为什么是false呢?
        //经过分析源码后发现,字符串在拼接时,利用了new StringBuilder(),然后再利用append()方法来拼接
        //最后在转换String的时候,调用了toString()方法,在toString()方法中发现用的时new String()对象
        //因此,拼接的时候就类似于new了个新对象放在堆中,这两个变量名指向的都不是同一个地址
        //所以返回值为false,我分析到这里大家应该都比较一目了然了。
        System.out.println("str4 == str5:" + (str4 == str5));//false
        
        //这两个比较我就不过多详述了吧,大家都比较熟悉
        //这两个创建对象存放在堆中,指向的都不是同一个对象地址,因此为false
        System.out.println("str5 == str6:" + (str5 == str6));//false

      这里为了方便大家去理解,我把分析内容不在这里和大家分析,直接在代码中的注释中给大家分析,这样方便大家理解,省的来回找代码了。

1.4、String中的equals()方法比较

大家对equals()比较都特别熟悉了,今天我这篇文章里也再老生常谈一次,给大家再分析一下equals()的源码,让大家再增深一次印象。我还是写下举例代码,在例子中给大家来详细分析其内容。例子如下所示:

public class TestString {

    public static void main(String[] args) {
        
        String str1 = "china";
        String str2 = "china";
        String str3 = new String("china");
        String str4 = str1 + "add";
        String str5 = "chinaadd";
        
        System.out.println(str1.equals(str2));//true
        System.out.println(ObjectUtils.nullSafeEquals(str1, str2));//true
        System.out.println(str1.equals(str3));//true
        System.out.println("str4.equals(str5):" + (str4.equals(str5)));//true
        
    }

      equals()是值比较,这里所说的值比较是只针对于String来说,如果是对象用equals来比较的话,那么就不是值比较了,还是用的地址比较,我把源码给大家贴在下面:

public boolean equals(Object anObject) {
        if (this == anObject) {
            return true;
        }
        if (anObject instanceof String) {
            String aString = (String)anObject;
            if (coder() == aString.coder()) {
                return isLatin1() ? StringLatin1.equals(value, aString.value)
                                  : StringUTF16.equals(value, aString.value);
            }
        }
        return false;
    }

      在源码中可以看到,首先进行地址比较,如果地址相等,那么直接返回true,如果地址不相等,那么去判断是否是String类型,如果是String类型,那么去比较hashcode,刚刚在上面双等号代码例子中也提到了hashcode值,String重写了hashcode方法,只要值相等,那么hashcode就相等,因此可以通过hashcode来判断是否相等;如果不是String类型,那么就直接返回false。

      从上面的分析中就可以看出当类型为String时,进行的是值比较;不是String类型时,比较的是其地址。

      注意: 在equals比较时,例如str1.equals(str2)时,str1为空,那么在运行时将会报空指针异常,这里我们可以采用先进行判断下是否为null,如果为null,那么不判断;你也可以采用另外一种方法,就是对象工具类中的equasl()方法——>ObjectUtils.nullSafeEquals(str1, str2)这里的源码中就直接对空做了处理,不需要我们额外处理空指针。

      对了,说到空指针,我再给大家提下字符串在赋值的时候。

//这是空字符串,长度为0,不会报空指针异常
String s1 = "";

//这是空,无值,和刚才的空字符串是不一样的,会报空指针异常
String s2 = null;

今天突然想起来StringUtils还有两个方法未和大家分享,自我感觉比较重要些,现在给大家分享下,分别是isNotEmpty()和isNotBlank()方法。
isNotEmpty() 方法举例如下

public static void main(String[] args) {
        // isNotEmpty==判断某字符串是否非空
        System.out.println(StringUtils.isNotEmpty(null)); // = false;
        //空字符串
        System.out.println(StringUtils.isNotEmpty("")); // false;
        //有字符串,字符串为 空格
        System.out.println(StringUtils.isNotEmpty(" "));// true;
        System.out.println(StringUtils.isNotEmpty("bob")); // true;
}

isNotBlank() 举例如下所示:

public static void main(String[] args) {
        // isNotBlank:判断某字符串是否不为空且长度不为0且不由空白符(whitespace)构成,
        System.err.println(StringUtils.isNotBlank(null)); // false
        System.err.println(StringUtils.isNotBlank("")); // false
        System.err.println(StringUtils.isNotBlank(" ")); // false
        System.err.println(StringUtils.isNotBlank("\t \n \f \r")); // false
    }

isNotEmpty()和isNotBlank()方法总结:
    isNotEmpty判断字符是否为null和空字符串,
    isNotBlank判断字符是否为null和空字符串,且字符串不是空白字符串

StringUtils.isNotEmpty(str)  等同于:
str != null && str.length > 0

StringUtils.isNotBlank(str) 等价于:
str != null && str.length > 0 && str.trim().length > 0
即:判断是否==null时,还需要判断length是否>0

2、StringBuffer详细分析

      谈到String不和大家聊聊StringBuffer我都无脸再去和家乡父老相见,哈哈,开玩笑开玩笑,来步入正题。

      当我们频繁的修改String类型字符串时,那么他会创建很多对象或者常量池内容,那么这样势必会效率比较低下且最主要的是占用其内存。这个时候,StringBuffer就登场开始它的表演。

      StringBuffer即能保证线程安全性,因为StringBuffer类上是的操作方法全是用synchronized关键字来修饰的;又能够节省内存开支,因为StringBuffer类型下的字符串支持在本身字符串上进行更改,这样省去了来回创建对象的开支,这一点是String类型的字符串所没有的;StringBuffer的效率相对来说是比较低的,因为在安全性和效率方面只能选其一,不可二者兼得;为了追求效率,那么你的安全性不得不舍弃;追求安全性,那么你的效率不得不丢掉。

2.1、追加方法append()

      这个方法比较长用,它是在原对象基础上添加新字符,不用生成新对象。

      老规矩,我还是写段代码,在代码中给大家进行一步一步的延申和分享,代码如下:

public class TestString {

    public static void main(String[] args) {
        
    String str1 = "china";
        
    //如果是空构造函数,则默认长度是16;调用有参构造时,默认长度为 传值参数长度(lenth)+16
    StringBuffer stringBuffer1 = new StringBuffer("china");
        
    int stringBuffer1HashCode = stringBuffer1.hashCode();
        
    //append添加字符是在原有对象上进行操作,不产生新对象,线程安全,效率较低
    //当追加的字符大于其容量时,扩充按照(旧容量*2+2)长度 采用的是位运算,位运算效率比较高
    //内部结构是new byte[];也就是数组,只能是byte类型数据。
    StringBuffer stringBuffer2 = stringBuffer1.append("add");
        
    int stringBuffer2HashCode = stringBuffer2.hashCode();
        
    //这里equals()比较源码是其地址比较,不像String重写了equals方法
    System.out.println(stringBuffer1.equals(str1)); //false
        
    //这里是两个对象相互比较,地址都不相等,因此肯定为false
    System.out.println(str1.equals(stringBuffer1)); //false
        
    //这里比较的hashcode值,其目的是为了看操作字符串的时候是不是又生成了新对象    
    //结果为false,那说明是在原对象上操作,没有生成新对象
    System.out.println("stringBuffer是否可以在原来字符上操作:" + (stringBuffer1HashCode == stringBuffer2HashCode)); //false

        
    }

      上面例子的分析,我全部写在了代码的注解中,这里就不作详细描述了,大家不懂的一定好好看看代码中的注解,这样能让你了解更透彻。

3、StringBuilder详细分析

      哈哈,这个就特别好和大家分享了,和StringBuffer基本一样,只有一点不一样,那就是,它是线程不安全的,但是效率比较高,好了,这就是不一样的地方,特别容易吧。

      又到了说再见的时候了,哪里有分享不对的地方,希望大家在下面评论区中指出哦

      记得一键三联哦——>点赞、转发、评论

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

岭岭颖颖

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值