[java篇]一次性帮你搞懂String,StringBuffer,StringBuilder类

Posted 小周同学ヾ(❀╹◡╹)ノ゙❀~

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了[java篇]一次性帮你搞懂String,StringBuffer,StringBuilder类相关的知识,希望对你有一定的参考价值。

前言:在我们学习String这个专题之前,让我们回忆一下,在我们之前学习的C语言中有没有String类型,答案是没有的,在C++和java中都引进了字符串类型,这样让我们在日常处理字符串时,增添了诸多便利。
前期文章:
[java篇]包,继承,组合
[java篇]多态,抽象类,接口
[java篇]图书管理系统,是你的期末大作业吗?

废话不多说,开干!!!

1.认识String类

让我们先看看String类到底是个啥?
在我们学习java语法的时候,我们不可能把所有的java语法都记得非常清楚,所以我们就提供了一个专门查阅有关java学习的API文档。

1.String类型,在java.lang包下,在我们使用String类型的时候,不用手动导包,系统会自行导包。
2.String类型被final修饰,表示这个类型不可更改,这个类不能被继承,String类属于一个密封类。
3.String类型继承了Object类型
4.String类型实现了Comparator接口(在前期文章介绍过),CharSxequence接口,Serializable接口。

当我们要学习一个类型的时候,首先要了解它的构造方法,这样我们在之后的学习中才会如鱼得水。

我了个去,这这这,怎么这么多呢?这要学到什么时候。
其实我们常用的一共就是用两种构造方法,上面的构造方法我们知道即可。

1.String str = "hello world";
2.String str1 = new String("hello world");

创建字符串

我们在创建字符串的时候一般有两种方法:

直接赋值,即String str = “hello world”;
另一种是利用构造方法:String str = new String(“abc”);在括号里面的字符串,被存储到new出的对象里面的value数组 。

字符串常量池

字符串常量池是利用哈希表实现的,所以在常量池中不允许出现相同的字符串,String类型,采用共享模式如果两个变量所指向的字符串相等,那么在字符串常量池中只会保留一份字符串,让两个变量同时指向这个字符串
其实在jvm中没有划分这个区域的。
来分析一下代码:

     public static void main(String[] args) {
        String str = "123";
        String str1 = "123";
        System.out.println(str == str1);
    }

这个应该很好判断吧,但是你是不是按这个思路想的呢?
请看内存图:

他们比较的是两个引用是否相等str和str1的字符串内容都是hello 所以只能在字符串常量池中存储一份"hello",而String类型又具有共享模式,所以两个引用同时都指向一块内存空间。

构造方法下字符串在堆中的存储

String str = new String(abc);
str作为一个引用,它引用了一个对象,这个对象里面有一个value数组,这个数组指向的就是添加进来的字符串

利用构造方法赋值的缺点:
在堆中必须要开辟两块内存空间,并且在有一份会成为垃圾空间。
同一个字符串可能被存放多次,浪费空间。

请简述String类型两种赋值的区别:
第一种:直接赋值:在堆中开辟了一块内存空间,并且传入的字符串会自动的保存在字符串常量池中。
第二种:利用构造方法进行赋值,在堆中会开辟两块内存空间,并且传入的字符串不会入池,必须通过intern()方法,手动入池。

案例分析:

请大家阅读以下代码,并回答true or false

案例一:

    public static void main1(String[] args) {
        String str1 = "hello world";
        String str2 = new String("hello world");
        System.out.println(str1 == str2);//false
    }

大家先根据题目做出解答哦,以下会具体分析

单眼一看,这有什么难得,不就是比较吧,这不两个字符串相等,不就是true嘛。
哈哈哈,上当了吧,答案是false,其实它们比较的是是两个字符串的引用,这个引用就相当于与我们在C语言阶段学习的指针一样,当时java里面没有指针哦,这个引用也没有C/C++里面的指针那么的灵活。
str1作为一个引用,在栈帧里面开辟空间,而赋予str的值是在堆上存放
str2作为一个引用,它引用了一个String类型的对象,而这个引用在栈上开辟空间,对象被存放在堆上。

看图说话:
分析:

  1. str1直接赋值,把"hello world"这个字符串传给了str1,在堆中,凡是被双引号引起来的常量,都会存放在字符串常量池中。
  2. str2是一个引用,他指向了一个对象,在对象里边存在一个value数组,这个数组又引用了对象里边存放的值,即对象里面的value数组引用了"hello world"这个字符串,又因为这个字符串要存放到字符串常量池中去,而在此之前池中已经有了"hello world"这个字符串并且在前文中说道字符串常量池是由哈希表构成的,所以这个对象就指向字符串常量池中已有的字符串常量。

案例二:

请看下一段代码:

 public static void main(String[] args) {
        String str1 = "hello world";
        String str2 = "hello " + "world";
        System.out.println(str1 == str2);
    }

答案是 true
为什么呢?
因为字符串常量在编译阶段就已经实现了连接,即"hello " + “world” 变成了"hello world",又因为在执行str2这段代码时,在堆中的字符常量池中已经有了"hello world"这个字符串,当str2指向的这个已经连接好的字符串将要存放到池中的时候,先要检查池中是否已经有了将要存放的字符串,如果有的话,str2就指向这个已有的字符串。
不信的话,那么我们就反编译一下,看是否在编译的时候已经实现了拼接。

看图说话:
常量在编译的过程中,就已经被运算了。

案例三:

请阅读一下代码:

 public static void main(String[] args) {
        String str1 = "hello world";
        String str2 = "hello " + new String("world");
        System.out.println(str1 == str2);
    }

答案是false
为什么呢?

因为当str2这段代码执行的过程中引用了一个String对象这个对象指向存放在字符串常量池中放入字符串"world",而这个被String对象所引用的字符串"world"要和在池中的"hello"实现拼接形成了一个新的对象至于拼接的过程这就牵连到了StringBuffer/StringBuilder知识点,我们在文章后半段介绍,而这个str3指向了这个新的对象,自然和str1所指向的"hello world"所在的地址不相同。

看图说话:

案例四:

请阅读以下代码:

 public static void main(String[] args) {
        String str1 = "hello world";
        String str2 = "hello ";
        String str3 = str2 + "world";
        System.out.println(str1 == str3);
    }

答案是:false
为什么呢?

因为当我们执行到了str3这句代码时,str2变成了变量,那么让我们回忆一下,变量的概念是什么,所谓变量就是在编译的时候不知道变量里边是什么值,只有在运行的时候才知道里面是什么值。在字符串拼接的过程中,会形成一个新的对象,str3就指向这个对象,str1指向的是一个字符串,显然他们指向的不是同一块地址,所以是false.

看图说话:

案例五:

请阅读一下代码:

 public static void main(String[] args) {
        String str1 = "hello world";
        String str2 = new String("hello ") + new String("world");
        System.out.println(str1 == str2);
    }

当大家做到这个题的时候,就很容以判断出,答案肯定是false,因为str1和str2所指向的不是同一块地址。

看图说话:

案例六:

请阅读一下代码:

  public static void main(String[] args) {
        String str1 = "hello world";
        String str2 = new String("hello world");
        str2.intern();
        System.out.println(str1 == str2);
    }

答案是:false

在这里就不得不介绍 intern()方法 了,这个方法的作用是把一个字符串手动填入字符串常量池。
我们已知str2作为一个引用,它引用了一个String对象,在这个对象里面有一个value数组,这个value数组又引用了一个字符串,这个字符串"hello world"已经在字符串常量池中已经有了,即使手动的填入这个已有的字符串也是不行的,就是也为字符串常量池是由哈希表实现的在哈希表中,不允许有重复的值出现。所以str2.intern(),这段代码,存不存在代码的结果都是一样的,等于它啥也没干。

看图说话:
它的内存图和案例一一样,在这里就不过多赘述。

案例七:

请阅读一下代码:

     public static void main(String[] args) {
        String str1 = new String("1") + new String("1");
        str1.intern();
        String str2 = "11";
        System.out.println(str1 == str2);
    }

答案是:true
为什么呢?

分析:
两个对象所指向的字符串都是"1",当两个字符串拼接之后形成了新的字符串"11",然后手动的添加到字符串常量池中当代码执行到str2这句代码时,它所指向的字符串"11",在池中已经有了,所以无需添加这时候str2指向被拼接好的字符串"11",假设这时候字符串"11"的地址为0x123,那么指向它的对象这时候的地址也是0x123,所以str1和 str2的的地址相同

看图说话:

案例八:

请阅读一下代码:

     public static void main(String[] args) {
        String str1 = new String("1") + new String("1");
        String str2 = "11";
        str1.intern();
        System.out.println(str1 == str2);
    }

答案是:false
为什么呢?

因为两个对象拼接好的字符串"11",然后str2指向字符串"11",而现在的字符串常量池中里的字符串"11"是str2这个引用指向的,现在手动的把拼接好的字符串手动添加到字符串常量池中,但是已经有了这个字符,所以手动添加不了
str1指向的是一个字符串,str2指向了一个被引用的对象,他们的引用不相同,所以是false.

案例九:

请看一下代码:

  public static void func(String str) {
        str = "bit";
    }
    public static void main(String[] args) {
        String str = "gaobo";
        func(str);
        System.out.println(str);
    }

答案是: gaobo
为什么呢?

因为我们这里的main方法中的str指向的是一个被存放到堆区中的字符串常量池中的字符串,而我们向func方法中传去的是一个str引用,起初func方法中的str引用和main方法中的str引用同时指向同一块地址,但是在func方法中str这个引用指向了一个新的地址(即在堆区的字符串常量池中的"bit"的地址),所以两个不是指向的是一块地址,所以他们不相同。依据上述的代码我们可以知道最后打印的结果为"gaobo".

看图说话:

案例十:

请看一下代码:

public static void main(String[] args) {
        String str1 = "Hello";
        String str2 = str1;
        str1 = "world";
        System.out.println(str1 == str2);
    }

答案是:false
为什么呢?

这个思路和上一个题的思路基本一致,str1是一个引用,这个引用指向堆区中的字符串常量池中的"Hello"str2引用了str1所引用的对象(即字符串"Hello")所以起初两个引用指向同一块地址但是str1这个引用又指向了一块新的地址(即"world"所在的地址空间),所以在比较两个引用的时候肯定不相等。

看图说话:

正确理解==和equal的区别

  1. 双等于比较的是两个字符串引用是否相等,即他们所指向的地址是否相等
  2. equal是比较两个字符串中的内容是否相同。

equal方法的使用:

     public static void main(String[] args) {
        String str1 = "hello world";
        String str2 = "hello world";
        System.out.println(str1.equals(str2));
    }

两个字符串内容相同,返回true,否则返回false.但是如果有一个字符串为空,那也只能这样写了。

   public static void main(String[] args) {
        String str1 = null;
        String str2 = "hello world";
        System.out.println(str2.equals(str1));
        //System.out.println("hello world".equal(str1));最好写成这样
    }

因为如果把str1放到了equal()方法之前比较,就会发生空指针异常

理解字符串不可变:

我们在文章前面也看到可String.java的源码,知道了String这个类(被final修饰)是一个密封类,不能被继承,同时String所引用的对象也不能被修改。
字符串是一种不可变的对象,它的内容不可修改
让我们看一下下面的代码:

 String str = "hello" ; 
 str = str + " world" ; 
 str += "!!!" ; 
 System.out.println(str); 
 // 执行结果
 hello world!!!

表面上看去,就直接在把要加的字符串添加到原来的字符串后面就好了,但是不是那样的。

请详细看他的内存图:

在字符串常量池中的字符串,在拼接时,每拼接一次就会产生一个新的对象,当代码执行到str = str + "world"时,str相当于一个变量,变量和字符串拼接需要在堆中重新开辟一块新的内存空间,而这个内存空间中的value数组就会指向拼接好的字符串"hello world"在拼接"!!!“的时候也是一样的,还需要开辟一个新的内存空间,在新的内存空间中的value数组就会指向拼接好的"hello world!!!”,在这是str这个引用指向拼接好的字符串
大家可以想一想,如果不断地这样两个是不是就会造成空间浪费。只为得到最后一个拼接好的字符串,那么为了拼接好的这个字符串,那么在这个拼接好的字符串后面是不是有着许许多多的字符串为了拼接所开辟的内空间,使堆区的闲余空间减少,当我们介绍到了StringBuffer/StringBuilder的时候就不用这样拼接一次开辟一次空间了,就直接在原字符串的末尾拼接。
这样也体现出了字符串的不可变,不能在同一个字符串上修改。

那么如果我们实在要对字符串进行修改呢?
详细请看一下代码:

 String str = "Hello";
 str = "h" + str.substring(1);
 System.out.println(str);
 // 执行结果
 hello

在这里就先介绍一下substring()方法它的作用是对字符串进行截取,根据下标,截取这个下标,这个下标之后所对应字符串中的所用元素
在上述代码中就是截取了"hello",1下标之后的所有字符串(“ello”),然后与字符串"h"进行拼接,但是这样也不是在字符串"h"之后直接拼接的,还是需要在堆中重新开辟一块空间,这个空间中的value数组又指向了拼接好的"hello"字符串。

那么我们真的想在原字符串中,做出修改呢,那就必须简单的介绍一下反射了(这里是简单介绍)

反射是面向对象编程的一种重要特性,在有些语言中被称为"自省"
反射的特性:反射是在程序运行的过程中,获取修改某个对象的具体信息(成员属性,成员方法),相当于让对象更好的认识自己。

那我们现在就利用反射的特性对字符串做出修改。

我们可以从String类的源码中看到String所引用的字段,是不可被修改的,并且这个字段在类中是私有属性。

      String str = "hello world";
     // 获取字节码对象
        Class c = String.class;
        //获取String类中的value字段
        Field field = c.getDeclaredField("value");
       //修改字符串的权限,使其权限变为true
        field.setAccessible(true);
       //获取str中的val
        char[] vals = (char[]) field.get(str);
       //对字符串进行修改
        vals[0] = 'H';
        System.out.println(vals);

运行结果:Hello world
并且是在原有的字符串上进行修改,没有重新开辟新的空间。

2.字符,字节,字符串

字符和字符串

一.字符与字符串
字符串内部包含一个和字符串一样的char[]数组,字符串可以和字符进行相互转化。

No方法名称类型描述
1public String(char[]chars)构造方法将字符转换成为字符串
2public String(char[] chars,int offest,int count)构造方法offest表示偏移位置,count表示偏移量,把字符数组中从offest之后偏移count个字符转变为字符串
3public charAt(int index)普通方法index表示下标,在字符串中找到对应下标的字符
4public toCharArray(String str)普通方法将字符串转变为字符数组

方法一:
将字符转换成为字符串

     public static void main(String[] args) {
        char[] array = {'a', 'b', 'c', 'd'};
        String str = new String(array);
        System.out.println(str);
    }
    //运行结果:abcd

方法二:
从偏移点出发,把偏移量个字符转变成为字符串

 public static void main(String[] args) {
        char[] chars = {'a','b','c','d','e','f'};
        String str = new String(chars,2,3);
        System.out.println(str);
    }
    //运行结果:cd

方法三:
在字符串中找到下标为i的字符

 public static void main(String[] args) {
        String str = "abcdefg";
        char ch = str.charAt(5);
        System.out.println(ch);
    }
    //找到下标为5的字符
    //运行结果为:f

方法四:
将字符串转换成为字符数组

  public static void main(String[] args) {
        String str = "abcdef";
        char []chars = str.toCharArray();
        System.out.println(Arrays.toString(chars));
    }
    //运行结果:[a,b,c,d,e,f]

方法小练:
判断一个字符串中是不是全是数字?
已知字符串为"158946a9";

   public static boolean isNUmber(String str){
        if(str.length() == 0){ //判断传来的字符串是不是长度为0
            return false;
        }
        if(str == null){ //判断传来的字符串是不是为null
         return false;
        }
        for(int i = 0;i<str.length();i++){
            if(str.charAt(i)>'9' || str.charAt(i) < '0'){
                return false;
            }
        }
        return true;
    }
    public static void main(String[] args) {
        String str = "158946a9";
        System.out.println(isNUmber(str));
    }
    //运行结果false

字节与字符串:

字节常用于数据传输和编码转化,String也可以和字节之间进行相互转化

NO方法名称类型描述
1public String(Byte[] byte)构造方法将字节数组转换成为字符串
2pubic String(Byte[] byte,int offest,int count)构造方法从字节数组的offest的偏移点到count个字节元素转换成为字符串
3public byte[] getBytes(String str)普通方法将字符串中的所有字符转换成为字节数组
4public byte[] getBytes(String CharsetName)trows unspportedEncodingException普通方法编码处理

方法一:
将字节数组转换成为字符串

    public static void main(String[] args) {
        byte []bytes = {97,98,99,100};
        String str = new String(bytes);
        System.out.println(str);
    }
    //运行结果:abcd

方法二:
从字节数组的offest的偏移点到count个字节元素转换成为字符串

  public static void main(String[以上是关于[java篇]一次性帮你搞懂String,StringBuffer,StringBuilder类的主要内容,如果未能解决你的问题,请参考以下文章

⭐一篇文章帮你搞懂Java之内部类⭐

3天拿到网易Java岗offer,彻底帮你搞懂

java时间差计算,彻底帮你搞懂

如何使用Java操作数据库?一文帮你搞懂Java的JDBC编程

双非本科字节跳动Java面试题分享,彻底帮你搞懂

程序员进阶!超过500人面试阿里,彻底帮你搞懂