String类菜鸟级教程(字符串常量池及不可变,StringBuffer 和 StringBuilder)

Posted Ischanged

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了String类菜鸟级教程(字符串常量池及不可变,StringBuffer 和 StringBuilder)相关的知识,希望对你有一定的参考价值。

前言

java是面向对象的一门编程语言,通过实例化对象,对象间的交互实现相应的功能,决解相应的问题。,java中实现了很多的类供我们使用,我们要学习java一定要学好一些常用的类,去学习里面的构造方法,成员方法,接口方法等,所以今天我就简单的向大家介绍String类,和类里面的一些构造方法及常用的方法。首先我打开java使用帮助文档,针对的是JDK1.8版本,看文档对Sting这个类的介绍。这里简单的说明了Sting这个类是在java.lang这个包底下的,它继承了object类,以及这个类实现了哪些接口,有哪些构造方法等许多的描述,我们有时间可以多多阅读这个中文帮助文档。在使用其他的类有什么不明白的也可以查阅该文档。

1.创建字符串

🎇常见的构造 String 的方式:

  public static void main(String[] args) 
     // 方式一
        String str1 = "hellobit";
        System.out.println(str1);
        // 方式二
        String str2 = new String("hellobit");
        System.out.println(str2);
        // 方式三
        char[] chars = 'a','b','c';
        String str3 = new String(chars);
        System.out.println(str3);
    

✏️方式三String里面重写了toString方法,再通过println进行打印,就打印了str3这个引用所指向的对象了,所以这里打印的是字符串的内容,而不是引用里面的地址。对于字符串的创建,我们需要重点理解创建时的内存布局情况,这样可以帮助我们更好地理解字符串的创建,方式一,方式二创建字符串的内存布局比较简单,直接就是栈上的引用变量指向堆上的对象。后面有构造String的内存布局图,接下来我简单地画一下方式三创建字符串的内存布局图。

首先我们在堆区创建一个字符数组,由引用变量chars来指向它,之后我们再new了一个对象String(chars),对象里面的成员变量有一个char类型的数组value,value是一个引用变量,构造这个匿名对象的时候我们要传一个参数,参数是数组或者字符串都行,调用String的其中一个构造方法public String(char value[])
this.value = Arrays.copyOf(value, value.length);
该方法复制产生了一个新的数组,这个数组由value这个引用来维护。

2.字符串比较相等

✏️📃如果现在有两个int型变量,判断其相等可以使用 == 完成。

int x = 10 ;
int y = 10 ;
System.out.println(x == y);
// 执行结果
true

如果说现在在String类对象上使用 == ?
代码1

String str1 = "Hello";
String str2 = "Hello";
System.out.println(str1 == str2);
// 执行结果
true

看起来貌似没啥问题, 再换个代码试试, 发现情况不太妙.
代码2

String str1 = new String("Hello");
String str2 = new String("Hello");
System.out.println(str1 == str2);
// 执行结果
false

代码一的内存布局:

代码二的内存布局:

📝📐我们来分析上面两种创建 String 方式的差异.我们发现, 代码1中str1 和 str2 是指向同一个对象的. 此时如 “Hello” 这样的字符串常量是在 字符串常量池 中,代码2中str1 和 str2 是指向的不是同一个对象,但最终指向的字符串内容都是 “Hello” ,通过 String str1 = new String(“Hello”); 这样的方式创建的 String 对象相当于再堆上另外开辟了空间来存储"Hello" 的内容, 也就是内存中存在两份 “Hello”.
🔍⌨️ String 是引用数据类型,如果使用“==”来比较两个引用的值,比较的还是值相同不相同,但是这个值不是字符串值,是引用值,str1和str2分别指向堆上两个不同地址处,代码2 str1和str2的值不同,所以结果为false。如果要比较字符串的内容可以使用equals方法进行比较。上图中代码2首先第一行代码传入字符串"Hello",发现字符串常量池里面没有该字符串,就将其入池,value是String里面的一个数组引用,是new产生的匿名对象的一个成员,new String("Hello")产生对象时要传入一个参数Hello,传入参数后调用构造方法,这样Srting字符串才能完成构造,这样value这个引用指向了字符串常量池里面的"Hello",str1指向value。同理str2也指向它创建的对象的成员value,创建对象时也要传入参数Hello,但发现常量池里面有Hello了就直接用了,value就直接指向常量池里面唯一的那个Hello了。

String str1 = new String("Hello");
String str2 = new String("Hello");
System.out.println(str1.equals(str2));
// System.out.println(str2.equals(str1)); // 或者这样写也行
// 执行结果
true

equals 使用注意事项
现在需要比较 str 和 “Hello” 两个字符串是否相等, 我们该如何来写呢?

String str = new String("Hello");
// 方式一
System.out.println(str.equals("Hello"));
// 方式二
System.out.println("Hello".equals(str));

在上面的代码中, 哪种方式更好呢?❓❓❓
🎇🎆那推荐使用 “方式二”. 一旦 str 是 null, 方式一的代码会抛出异常, 而方式二不会。

String str = null;
// 方式一
System.out.println(str.equals("Hello")); // 执行结果 抛出 java.lang.NullPointerException 异// 方式二
System.out.println("Hello".equals(str)); // 执行结果 false

2.1字符串传参

Java 中数组, String, 以及自定义的类都是引用类型,由于 String 是引用类型, 因此对于以下代码,我们应该注意传引用不一定改变字符串的值。

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

它的内存布局图如上面代码1,, 这时我们可能会想是不是修改 str1 , str2 也会随之变化呢?

str1 = "world";
System.out.println(str2);
// 执行结果
Hello

我们发现, “修改” str1 之后, str2 也没发生变化, 还是 hello?
事实上, str1 = “world” 这样的代码并不算 “修改” 字符串, 而是让 str1 这个引用指向了一个新的 String 对象.
在如下面的代码:

 public static void func(String str) 
        str = "bit";
    

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

下面的代码在函数传参的时候,传引用不会改变原来的值

这里有两个引用,一个main函数里面的,一个func函数里面的,开始的时候这两个引用指向同一内存空间0x123,之后被调函数里的引用指向一个新的地址,但不影响调用函数里的引用。

3.字符串常量池

在上面的例子中, String类的两种实例化操作, 直接赋值和 new 一个新的 String.
直接赋值

String str1 = "Hello";
String str2 = "Hello";
System.out.println(str1 == str2);
// 执行结果
true

🎇为什么现在并没有开辟新的堆内存空间呢??因为String类的设计使用了共享设计模式
在JVM底层实际上会自动维护一个对象池(字符串常量池)

  • 如果现在采用了直接赋值的模式进行String类的对象实例化操作,那么该实例化对象(字符串内容)将自动保存 到这个对象池之中.
  • 如果下次继续使用直接赋值的模式声明String类对象,此时对象池之中如若有指定内容,将直接进行引用。
  • 如若没有,则开辟新的字符串对象而后将其保存在对象池之中以供下次使用 也就是简单地说该字符串常量池中只会有不同的String类对象,不会出现相同的String类对象,常量池的一个功能帮助我们节省内存空间,同样的东西我们存储一份就行了

采用构造方法

String str1 = new String("Hello");
String str2 = new String("Hello");
System.out.println(str1 == str2);
// 执行结果
false

📝由上面的代码二的内存布局图可知:
这样的做法有两个缺点:

  1. 如果使用String构造方法就会开辟两块堆内存空间,并且其中一块堆内存将成为垃圾空间(字符串常量 “hello” 也是一个匿名对象, 用了一次之后就不再使用了, 就成为垃圾空间, 会被 JVM 自动回收掉).
  2. 字符串共享问题. 同一个字符串可能会被存储多次, 比较浪费空间

4.字符串创建实例分析

下面我将通过一些简单的例子,进一步地带老铁们熟悉直接赋值,采用构造方法构造字符串及字符串常量池在构造字符串时的作用。
代码1:

  String str1 = "helloboy";
        String str2 = new String("helloboy");
        System.out.println(str1 == str2);//false
        //比较的还是值相同不相同,但是这个值不是字符串值,是引用值
        System.out.println(str1.equals(str2));//true

内存布局图:

📝该代码的内存布局图,可以结合最上面上面代码1,代码2的内存布局图来看,首先直接赋值创建了一个字符串 String str1 = “helloboy”;,放在字符串常量池,之后通过构造的方式,str2指向在堆区实例化的对象,这个对象的产生需要传一个字符串参数,刚好字符串常量池有这个参数,,直接用就行了,value就指向了这个参数。
代码2:

String str1 = "hello";
        String str2 = "hello" + "world";
        System.out.println(str1 == str2);//true

像"helloworld",“world”,"hello"这些量被称为字符串字面值常量,是一个常量,代码在进行字符串的拼接的时候,编译期间会进行优化,把它优化为一个常量,由常量池的知识可知在运行期间就只开辟一块空间存储优化过的常量了。因此str1和str2就指向了字符串常量池的同一块空间了。
代码3:

String str1 = "goodboy";
        String str2 = "good";
        String str3 = str2+"boy";
        System.out.println(str1 == str3);

直接赋值构造两个字符串,“goodboy”,"good"放在字符串常量池中,str1和str2分别指向这两个字符串,定义变量str3在堆区开辟空间,它的内容等于str2+“boy”;,str3最终指向拼接好的字符串。
代码4:

 String str1 = "goodboy";
        String str4 = "good"+new String("boy");
        System.out.println(str1 == str4);


✏️📘"goodboy","good","boy"这些字符串之前没有的首先都是放入常量池,new String(“boy”)新产生一个对象,对象里的引用指向传入的参数,之后在堆上开辟一块内存,存拼接好的字符串"goodboy",拼接好的字符串虽然也是"goodboy",但是和常量池里的不一样。
代码5:

String str1 = "goodboy";
        String str5 = new String("good)+new String("boy");
        System.out.println(str1 == str5);//false
        System.out.println(str1.equals(str5));//ture

这代码和上面的差不多,用脚指头想想也明白,哈哈!!(借助上面的代码和图秒懂),首先常量池里放入"goodboy",“good”,“boy”,通过构造的方式在堆区构造两个字符串,之后两个字符串在堆区拼接形成和常量池内容一样的字符串,str5指向该字符串。
代码6:

 String str1 =  "goodboy";
        String str2= new String("goodboy");
        str2.intern();
        System.out.println(str1 == str2);


📝这个代码和代码1的内存布局图一样,只是多了一步对字符串的入池操作 str2.intern();对于我们在堆区构造产生的字符串,str2
拿到对象里面的字符串的值,和常量池里面的字符串比较,如果常量池有该字符串就不如池,如果没有该字符串就把该字符串放入常量池中。因为原来字符串常量池里有我们构造出来的字符串,所以堆区的字符串就不如池了,内存代码布局就像开始一样的、
代码7:

String s3 = new String("1") + new String("1");
        s3.intern();
        String s4 = "11";
        System.out.println(s3 == s4);//true


上面的代码是字符串要入池的时候,池子里有该字符串,现在的这个代码是入池的时候,池子里没有这个字符串,首先在堆上创建两个字符串对象,之后拼接形成字符串“11”,s3这个引用指向指向“11”,之后将s3指向的字符串做入池操作,池子里没有就入进去,入的时候在常量池里面开辟一块内存,存储要入的字符串在栈区的地址,而不是内容,之后又创建一个对象“11”,因为产量池里面有这个对象,所以s4这个引用就直接存储了常量池里那个字符串的地址了,文字和上面的图一起食用效果更佳!!!
代码8:

String s3 = new String("1") + new String("1");
        String s4 = "11";
        s3.intern();
        System.out.println(s3 == s4);


这代码和上面的代码只是有几行代码颠倒了,就产生了不一样的效果了,s3在s4放入常量池后再入池,就入不了池了,就什么操作也不发生。

5.理解字符串不可变

🌈✨字符串是一种不可变对象. 它的内容不可改变.
从源代码可知String 类的内部实现也是基于一个 char value[]的数组来实现的,这个数组被finalprivate修饰 但是 String 类并没有提供 set 方法来修改内部的字符数组,所以简单的认为字符串是一种不可变对象。

感受下形如这样的代码

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

形如 += 这样的操作, 表面上好像是修改了字符串, 其实不是. 内存变化如下:


从上面的内存变化图中,我们可以看到,最终形成了一个新的字符串,但是这个字符串不是在原来字符串的后面添加字符串形成的,没有修改字符串的内容,是通过字符串的拼接形成一个新的对象,第一次产生一个对象hello world并且str指向这个对象,第二次产生一个hello world!!!对象,str1又指向这个新的对象。每次拼接完成后,str这个引用里面的地址都发生了改变。
❌🚫因此我们在开发中要注意不要出现类似如下的代码,会产生大量的临时对象, 效率比较低.

String str = "hello" ;
for(int x = 0; x < 1000; x++) 
str += x ;//拼接字符串

System.out.println(str);

那么如果实在需要修改字符串, 例如, 现有字符串 str = “Hello” , 想改成 str = “hello” , 该怎么办?
💯常见办法: 借助原字符串, 创建新的字符串
substring()方法截取一个字符串的子串,括号里面的参数表示从主串的什么位置开始截取,主串的第一个位置默认为0位置,之后又进行字符串的截取,那么情况就和上面的情况一样了。

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

特殊办法: 使用 “反射” 这样的操作可以破坏封装, 访问一个类内部的 private 成员,但今天的这篇博文我们不详讲,只是提一下,后面我的博文会详细讲到。
为什么 String 要不可变?(不可变对象的好处是什么?) ❓❓

  1. 方便实现字符串常量池. 如果 String 可变, 那么对象池就需要考虑何时深拷贝字符串的问题了.
  2. 不可变对象是线程安全的.
  3. 不可变对象更方便缓存 hash code, 作为 key 时可以更高效的保存到 HashMap 中.

6.字符, 字节与字符串

6.1字符与字符串

字符串内部包含一个字符数组,String 可以和 char[] 相互转换.

字符与字符串常用的一些方法如下表:

No方法名称类型描述
1public String(char value[])构造将字符数组中的所有内容变为字符串
2public String(char value[], int offset, int count)构造将部分字符数组中的内容变为字符串,offset 为偏移量,从0开始 ,count为转换的字符个数
3public char charAt(int index)普通取得指定索引的字符,索引从0开始
4public char[] toCharArray()普通将字符串变为字符数组返回

方法代码简单演示:
方法一:将字符数组中的所有内容变为字符串

char[] value = 'a', 'b', 'c', 'd', 'e';
String str = new String(value);
System.out.println(str);

结果为:abcde
方法二:将部分字符数组中的内容变为字符串

 char[] value = 'a','b','c','d','e';
        String str = new String(value,1,2);
        //从偏移量为1的位置开始取2个字符来构造String对象
        System.out.println(str);

结果为:bc
方法三: 取得指定索引的字符,索引从0开始

String str2 = "hello";
        char ch = str2.charAt(1);//字符串的第一个字符为0位置
        System.out.println(ch);

结果为:e
方法四:将字符串变为字符数组返回

String str3 = "hello";
        char[] chars = str3.toCharArray();//将字符串以字符数组的方式进行存储
        System.out.println(Arrays.toString(chars));

结果为:[h,e,l,l,o]
❗❗❗注意:对于上述方法给定的数字位置和数字范围,一定要合理,不然就会产生异常。

6.2字节与字符串

字节常用于数据传输以及编码转换的处理,字符串 String 也能和字节数组 byte[] 相互转换

No方法名称类型描述
1public String(byte bytes[])构造将字节数组变成字符串
2public String(byte bytes[], int offset, int length)构造将部分字节数组中的内容变为字符串
3public byte[] getBytes()普通将字符串以字节数组的形式返回
4public byte[] getBytes(String charsetName)throws java.io.UnsupportedEncodingException普通编码转换处理

方法代码简单演示:
方法一:将字节数组变成字符串

byte[] bytes = 97, 98 ,99 ,100;
String str = new String(bytes);
System.out.println(str);

结果为:abcd
方法二:将部分字节数组中的内容变为字符串

byte[] bytes = 97, 98 ,99 ,100;
String str = new String(bytes, 1, 2);
System.out.println(str);

结果为:bc
方法三:将字符串以字节数组的形式返回

public static void main(String[] args) 
    String str = "abcde";
    byte[] bytes = str.getBytes();
    System.out.println(Arrays.toString(bytes));


结果为:[97, 98, 99, 100, 101]
方法四:编码转换处理(很少使用,了解即可)

  public static void main(String[] args) throws UnsupportedEncodingException 
        String str = "李敏敏";
        byte[] bytes = str.getBytes("utf-8");//几乎不用
        System.out.println(Arrays.toString(bytes));
    

结果为:[-26, -99, -114, -26, -107, -113, -26, -107, -113]
如果我们将编码方式“utf-8”,改为“gbk”则会有不同的结果

  public static void main(String[] args) throws UnsupportedEncodingException 
        String str = "李敏敏";
        byte[] bytes = str.getBytes("gbk");//几乎不用
        System.out.println(Arrays.toString(bytes));
    

结果为:[-64, -18, -61, -12, -61, -12]
其实这个方法就是把我们给的字符串,以我们指定的编码规则转换为字节数组的,编码规则不同则转换的内容不同,在utf-8中一个汉字占3个字节,gbk中占一个字节。

小结:
那么何时使用 byte[], 何时使用 char[] 呢?

  • byte[] 是把 String 按照一个字节一个字节的方式处理, 这种适合在网络传输, 数据存储这样的场景下使用. 更适合针对二进制数据来操作.
  • char[] 是把 String 按照一个字符一个字符的方式处理, 更适合针对文本数据来操作,尤其是包含中文的时候.
  • 一个简单粗暴的区分方式就是用记事本打开能不能看懂里面的内容. 如果看的懂, 就是文本数据(例如 .java 文件), 如果看不懂, 就是二进制数据(例如 .class 文件)。

7.字符串常见操作

7.1字符串比较

上述介绍到 equals 可以比较字符串是否相等,并且是区分大小写的。而除了它,String 类还有其他比较字符串的方法

No方法名称类型描述
1public boolean equals(Object anObject)普通区分大小写的比较
2public boolean equalsIgnoreCase(String anotherString)普通不区分大小写的比较
3public int compareTo(String anotherString)普通比较两个字符串大小关系

以上是关于String类菜鸟级教程(字符串常量池及不可变,StringBuffer 和 StringBuilder)的主要内容,如果未能解决你的问题,请参考以下文章

String类菜鸟级教程(字符串常量池及不可变,StringBuffer 和 StringBuilder)

String类菜鸟级教程(字符串常量池及不可变,StringBuffer 和 StringBuilder)

JAVA String介绍常量池及StringStringBuilder和StringBuffer得区别. 以及8种基本类型的包装类和常量池得简单介绍

Java String 类

Java String 类

String