JAVA中的String类和StringBuilder类以及StringBuffer类方法的使用与详解

1.String类的基本概念

String 在 Java 开发中使用非常广泛,可以直接通过字面量的形式声明。但是值得注意的是它并不是基本数据类型,是引用类型。所以 String 对象存储在堆空间,它的引用在栈空间。

当 Java 中对引用类型变量赋值时,可以理解为将这个变量(引用)指向一块内存地址。当执行这段代码时,实际上是栈中一个 str 指向堆中一块内存空间

String str ="Hello";

  1. String 类是 final 类,不能被其他的类继承。
  2. String 类中有属性 private final char value[],这是一个 char 类型的常量数组,用于存放字符串中的每一个字符。
  3. 注意:value 数组 是一个 final 类型, 不可以被修改,即数组 value 不能再指向新的数组地址,但是数组中单个字符内容是可以改变的
  4. 从 jdk1.9开始该类的底层不使用 char[] 来存储数据,而是改成 byte[] 加上编码标记,从而节约了一些空间。

1.1 String不可变

下面是String的源码 

 ​​​​​

代码说明: 

final char[] value = {'a','b','c'};
        char[] v2 = {'t','o','m'};
        value[0] = 'H';// 可以改变数组中的元素的值
        //value = v2; 错误,不可以修改 value地址

 

由于 value 是 final 修饰的,我们都知道 final 修饰的变量只能被初始化一次,也就是常量,所以这个字符数组指向的内存空间就固定了,也就是上图的红色线不会变。当你对 str 重新赋值的时候 。

实际上只是 str 指向的内存空间变了,也就是蓝色指针断开,重新指向了其他地方。原来的红色指针还是没有变化。

说到这里你可能会有疑问,String 不是有 replace()、subString()、toLowerCase() 等方法吗?看下面代码

String str = "Hello";
str.toLowerCase();
str.substring(0,1);
str.replace("H","h");

这个问题非常简单,这些方法的确是返回了一个新的符合预期的 String 对象,但是你查看源码就会发现,它们方法内部都是 new 了一个新的 String 对象,并没有改动原来的 str 指向的对象内容。对于 String 的所有操作,都是新生成了一个对象,原对象没有改变。

下面我们来看一个练习题

public static void main(String[] args){
    String str = "Hello World";
    char[] arr = {"H","e","l","l","o"};
    change(str);
    changeArr(arr);
    System.out.println(str);//Hello World
    System.out.println(arr);//Wello
}
private static void change(String str){
    str = "World";
}
private static void changeArr(char[] arr){
    arr[0] = 'W';
    arr = new char[5];
}

可以尝试做一下这个题目,我相信有大多数初学者可能会做错。为什么 change() 方法没有改变 str ? 为什么 changeArr() 改掉了数组 arr 的内容?

在搞清楚这个问题前,先来科普一个知识。Java 程序是运行在 JVM 上的,JVM 有两个较为重要的内存区:虚拟机栈、堆。 Java 中所有方法的调用,都是一个栈帧在虚拟机栈中入栈操作,所有方法调用完成,都是栈帧在虚拟机栈中出栈的操作。那回过来看这个代码,总共三个方法:main、change、changeArr。它们的在执行过程中的出栈入栈是这样的。

 

最后main方法也出栈,方法执行完毕。

程序开始,执行 main 方法,main 方法栈帧入栈,字符串引用 str 和数组引用 arr 指向堆内存。执行到 change 方法时,change 对应的栈帧入栈,我们都知道栈的数据结构特性,先入后出。此时 main 栈帧在栈底,change 栈帧在栈顶。change 方法把传递进来的形参 str 重新赋值。注意,在 change 方法中的形参 str,运行时是 main 方法中 str 的一个拷贝,你可以这么来理解 Java 中引用类型赋值的操作。

如果是将一个变量赋值给另一个变量

String str2 = str1;

它代表的含义就是让 str2 指向 str1 所指向的那块堆中的内存地址。 那再回到我们的 change 方法,调用时,实际上是让 change 栈帧中的 str 指向原来 main 方法中 str 所指向的那块内存地址。 (也就是将原来的 main 栈帧中的 str 拷贝一份,我怕大家不好理解,这样说比较清晰)

之后 change 方法执行结束,change 栈帧出栈,栈帧内部的变量(引用)也都被销毁。所以 main 输出的 str 内容当然没有变。如果你把第 6 行的输出放到 change 内部,输出的就是 change 栈帧里面的 str 指向的 "World",但是原来的 "HelloWorld" 仍然没有改变,只是新开了一块内存。所以这里也涉及到变量作用域的问题。

再来看下为什么数组就改掉了原来的对象内容。当执行到 changeArr方法时,对应的栈帧在栈顶,main方法对应的栈帧在栈底,我们将 arr 传递给了 changeArr ,在changeArr 的栈帧里面复制了一个引用,通过这个引用,我们将数组的第一个元素改掉。那你都通过这个引用改掉原内容了,那肯定就变了呀!因为数组的内容本身就可变,它又不是新开了内存地址,这就相当于你用一把钥匙打开了仓库门,把西瓜换成了哈密瓜。那你以后开仓库看的时候当然变成了哈密瓜。

然后这行代码

arr = new char[5];

这个和上面 str 是一样的,我把我这个栈帧里面的引用重新指向了一块内存,关原来 main 方法中的 arr 啥事呢,当 changeArr 执行结束,栈帧出栈,栈帧里面引用的生命周期就到头了,被销毁了。

1.2 String的常量池

String 常量池,也就是字符串常量池,这个东西要细说的话,涉及的 JVM 内存区域、类加载等相关知识很多。这里我们简单的理解 JVM 提供了一块内存用于存放 String 字符串对象。这样以后如果用到相同字符串就不用开辟新的空间,直接使用字符串常量池中的对象就可以了。若后续代码中出现了相同字符串内容则直接使用池中已有的字符串对象而无需申请内存及创建对象,从而提高了性能。

前面我们提到过 String 可以通过字面量形式直接声明一个对象,那么 String 作为引用类型,当然是可以通过 new 关键字来创建对象的。

  • 当使用字面量形式声明对象时,会先检查字符串常量池中是否已经存在该对象,如果已存在直接将引用指向已存在的对象,如果不存在,将该对象放入常量池。
  • 当使用 new 关键字声明对象时,会先检查字符串常量池中是否已经存在该对象,如果已存在直接将引用指向已存在的对象,如果不存在,将该对象放入常量池。此外还会在堆空间开辟一块内存地址。并且将该引用指向堆中的地址
  • 由于 new 关键字会在堆中开辟空间,所以开发中一般情况不建议使用,直接用字面量形式声明即可。
    String str1 = "Hello";//产生1个对象放入常量池
    String str2 = new String("World");//创建两个对象,一个在堆,一个在常量池
    String str3 = new String("Hello");//创建一个对象在堆,由于常量池已经有 Hello 不会再创建
    

再来看下面几个示例 

public static void main(String[] args){
  String str1 = "Hello";
  String str2 = "Hello";
  System.out.println(str1 == str2);//true
  String str3 = new String("Hello");
  System.out.println(str1 == str3);//false
  String str4 = new String("Hello");
  System.out.println(str3 == str4);//false
}

String str = new String("Hello");//如果前面已经声明过 Hello 字符串,这行代码只会在堆创建一个对象,否则会创建两个对象,一个在堆,一个在常量池

 2.字符串构造

有以下常用的方法:

public class Test1 {
    public static void main(String[] args) {
      // 使用常量串构造
    String s1 = "hello bit";
    System.out.println(s1);
      // 直接newString对象
    String s2 = new String("hello bit");
    System.out.println(s1);
    // 使用字符数组进行构造
    char[] array = {'h','e','l','l','o','b','i','t'};
    String s3 = new String(array);
    System.out.println(s1);
}
}

两种机制不同
方式1:
先从常量池找是否有“hello bit”的数据空间,有直接指向,没有,创建再指向。最后指向常量池的空间地址
方式2:
先在堆中创建空间,里面维护value属性,指向常量池“hello bit”的空间,如果常量池中没有"hello bit",重新创建,如果有直接通过value指向,最终指向的是堆中的空间地址

 2.1字符串的比较

【==比较是否引用同一对象】

对于java中的内置类型,==比较的是变量的值,而对于引用类型,==比较的是引用中的地址。

public static void main(String[] args) {
    int a = 10;
    int b = 20;
    int c = 10;
    // 对于基本类型变量,==比较两个变量中存储的值是否相同
    System.out.println(a == b); // false
    System.out.println(a == c); // true

    // 对于引用类型变量,==比较两个引用变量引用的是否为同一个对象
    String s1 = new String("hello");
    String s2 = new String("hello");
    String s3 = new String("world");
    String s4 = s1;
    System.out.println(s1 == s2); // false
    System.out.println(s2 == s3); // false
    System.out.println(s1 == s4); // true
}

【boolean equals(Object anObject)方法进行比较】(按照字典序进行比较,即比较字符串中的内容)。

字典序:字符大小的顺序,类似于C语言中的ASCLL码大小进行比较。

String类中重写了父类Object类中的equals方法,Object中的equals默认按照==比较,String类重写equals了方法后,按照如下规则进行比较,比如: s1.equals(s2)

public boolean equals(Object anObject) {
    // 1. 先检测this 和 anObject 是否为同一个对象比较,如果是返回true
    if (this == anObject) {
    return true;
}

    // 2. 检测anObject是否为String类型的对象,如果是继续比较,否则返回false
    if (anObject instanceof String) {
    // 将anObject向下转型为String类型对象
    String anotherString = (String)anObject;
    int n = value.length;
    
    // 3. this和anObject两个字符串的长度是否相同,是继续比较,否则返回false
    if (n == anotherString.value.length) {
    char v1[] = value;
    char v2[] = anotherString.value;
    int i = 0;
    
    // 4. 按照字典序,从前往后逐个字符进行比较
    while (n-- != 0) {
        if (v1[i] != v2[i])
        return false;
        i++;
	}
        return true;
	}
  }
return false;
}
public static void main(String[] args) {
    String s1 = new String("hello");
    String s2 = new String("hello");
    String s3 = new String("Hello");
    // s1、s2、s3引用的是三个不同对象,因此==比较结果全部为false
    System.out.println(s1 == s2); // false
    System.out.println(s1 == s3); // false
	// equals比较:String对象中的逐个字符
    // 虽然s1与s2引用的不是同一个对象,但是两个对象中放置的内容相同,因此输出true
    // s1与s3引用的不是同一个对象,而且两个对象中内容也不同,因此输出false
    System.out.println(s1.equals(s2)); // true
    System.out.println(s1.equals(s3)); // false
public class StringExercise04 {
    public static void main(String[] args) {
        Person p1 = new Person();
        p1.name = "lyedu";
        Person p2 = new Person();
        p2.name = "lyedu";

        System.out.println(p1.name.equals(p2.name)); //比较的内容 true
        System.out.println(p1.name == p2.name); //true
        //"lyedu"返回的地址:就是在常量池中的地址
        //p1.name 指向的也是常量池中的"lyedu" 对应的地址
        System.out.println(p1.name == "lyedu"); //true

        String s1 = new String("bcde");
        String s2 = new String("bcde");
        System.out.println(s1 == s2); //false
    }
}

class Person {
    public String name;
}

2.2【 int compareTo(String s) 方法】( 按照字典序进行比较)

与equals不同的是,equals返回的是boolean类型,而compareTo返回的是int类型。具体比较方式:

  1. 先按照字典次序大小比较,如果出现不等的字符,直接返回这两个字符的大小差值
  2. 如果前k个字符相等(k为两个字符长度最小值),返回值两个字符串长度差值
public static void main(String[] args) {
    String s1 = new String("abc");
    String s2 = new String("ac");
    String s3 = new String("ABc");
    String s4 = new String("abcdef");
    System.out.println(s1.compareToIgnoreCase(s2)); // 不同输出字符差值-1
    System.out.println(s1.compareToIgnoreCase(s3)); // 相同输出 0
    System.out.println(s1.compareToIgnoreCase(s4)); // 前k个字符完全相同,输出长度差值 -3
}

 3.字符串的常用使用方法

1.toCharArray()

toCharArray() 方法:将当前字符串内容转换为 char 数组并返回,返回值为 char[],该方法的返回值可作为 String 构造方法的参数

String str = new String("Hello World");
char[] cArr = str.toCharArray();
for (int i = 0; i < str.length(); i++) {
    System.out.println("下标为" + i + "的元素为:" + cArr[i]);//打印的是每个字母
}

2.substring(…)

substring(int beginIndex, int endIndex) 方法:返回字符串中从下标 beginIndex(包括) 开始到 endIndex(不包括) 结束的子字符串,返回值为 String 类型,参数为 int 类型

substring(int beginIndex) 方法:返回字符串中从下标 beginIndex(包括) 开始到字符串结尾的子字符串,返回值为 String 类型,参数为 int 类型

注意:Java 中涉及到区间的问题,一般都为 左边包括,右边不包括,即左开右闭,“[ x , y )”

public class Text {
    public static void main(String[] args) {
        //单参数版本:只指定字符串的起始位置,返回从该位置到原字符串末尾的所有字符
         String str ="Hello,world!";
         String sub =str.substring(7);//返回world
        
        String str1 ="Hello,World!";
        String sub1=str1.substring(7,12);//返回world
        
    }
    }

 3.charAt(int index)

charAt(int index) 方法:用于返回字符串指定位置的字符,返回值为 char 类型,参数为 int 类型

String str = new String("Hello World");
for (int i = 0; i < str.length(); i++) {
    System.out.println("下标为" + i + "的元素为:" + str.charAt(i));//打印的是每个字母
}

4.indexof() 

返回ch第一次出现的位置,没有返回-1

​
String str = "Good Good Study, Day Day Up!";
System.out.println(str.indexOf('g')); // -1  代表查找失败
System.out.println(str.indexOf('G')); // 0   该字符第一次出现的索引位置

​

4.StringBuilder和StringBuffer

说到 String 类就自然而然的想到了两个与之关系密切的类。由于 String 对象不可变,每一个操作都会产生新的对象,这样似乎不太友好,可能会造成内存占用过大,而且会频繁创建对象。所以官方给我们提供了一个类 StringBuilder 。这个类可以在原字符串的基础上进行增删改,并且不会新开辟内存空间。这就弥补了有些场景下 String 的不友好性。它跟 String 非常类似,还记得之前我们说 String 内部维护了一个 final char[] value 吗?StringBuilder 内部维护了一个 char[] value 没有用 final 修饰,所以它是可以在原字符串基础上做修改的。

StringBuffer 其实和 StringBuilder 是一样的,只是 StringBuffer 对于字符串的增删改方法都加上了 synchronized 关键字,这样一来对于字符串的操作就是线程安全的,由于线程安全所以其性能也次于 StringBuilder。

也许平时你没有或者很少见过后面两个类,但是 StringBuilder 其实与我们息息相关,每一个 String 的 “+” 都离不开它。当你程序中有字符串使用 “+” 连接的时候,底层会给你 new 一个 StringBuilder 对象调用 append 来连接字符串。

例如下面代码:

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

为什么StringBuffer和StringBuilder比String更适合在循环中使用?

第四行在 JVM 底层其实会 new 一个 StringBuilder 调用其 append 方法来连接 str1 和 str2 然后生成一个新的 String 对象。这一点我们可以使用 javap -v 命令去反编译 class 文件来证明。所以代码中我们要善用 “+” 来连接字符串,如果你像下面这样再循环里面使用 “+”

public static void main(String[] args) {
        String str = "hello";
        for(int i = 0;i<1000;i++){
            str = str + i;
        }
    }

 那这个问题就大了,这会在底层创建 1000 个 StringBuilder 对象,浪费堆空间内存。所以这种代码,我们把 StringBuilder 对象提前创建好放在循环外面,然后使用 append 方法来连接,会有更好的效果。

public static void main(String[] args) {
        String str = "hello";
        StringBuilder sb = new StringBuilder(str);
        for(int i = 0;i<1000;i++){
            sb.append(i);
        }
    }

下面是一些常用的使用方法 

// 创建一个空的StringBuffer对象
StringBuffer sb = new StringBuffer();
 
// 向StringBuffer对象中添加字符串
sb.append("Hello");
sb.append(" ");
sb.append("World");
 
// 将StringBuffer对象转换为String对象
String str = sb.toString();
System.out.println(str); // 输出: Hello World
 
// 在指定位置插入字符串
sb.insert(5, " my");
System.out.println(sb.toString()); // 输出: Hello my World
 
// 删除指定位置的字符
sb.deleteCharAt(5);
System.out.println(sb.toString()); // 输出: Hellomy World
 
// 反转字符串
sb.reverse();
System.out.println(sb.toString()); // 输出: dlroW ymolleH

 String类,StringBuffer类,StringBuilder类总比较

 String : 不可变字符序列,效率低,但是复用率高。
 StringBuffer : 可变字符序列,效率较高,且线程安全。
 StringBuilder : 可变字符序列,效率最高,但线程不安全。

① String : 适用于字符串很少被修改,且被多个对象引用的情况,比如定义数据库的IP信息,配置信息等。
② StringBuffer : 适用于存在大量修改字符串的情况,且满足 多线程条件。
③ StringBuilder : 适用于存在大量修改字符串的情况,且满足 单线程条件。

  • 22
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值