当前位置:首页 » 《随便一记》 » 正文

「Java」- String 字符串详解_「zero」的博客

9 人参与  2022年05月10日 13:08  分类 : 《随便一记》  评论

点击全文阅读


目录

认识字符串

1.创建字符串

2.字符串比较相等

3.字符串常量池

intern( ) 方法

4.字符串不可变特性

字符串常用操作

1.字符串比较

2.字符串查找

3.字符串替换

4.字符串拆分

5.字符串截取

6.字符串其它方法

​ StringBuilder & StringBuffer 类

构造方法

常用方法

String & StringBuilder & StringBuffer 的区别

认识字符串

Java 中的 String 类代表字符串 , 在程序中使用 " " 定义的内容都是字符串 , 字符串是常量 , 它们的值在创建之后不能更改.

1.创建字符串

String类的构造方法

public String()
// 初始化新创建的 String 对象,使其表示空字符序列。
public String(byte[] bytes)
// 通过使用平台的默认字符集解码指定的字节数组来构造新的 String 。
public String(byte[] bytes,Charset charset)
// 构造一个新的 String 由指定用指定的字节的数组解码 charset
public String(byte[] bytes,  int offset, int length)
// 通过使用平台的默认字符集解码指定的字节子阵列来构造新的 `String` 。
public String(byte[] bytes,  int offset, int length, Charset charset)
// 构造一个新的 String 通过使用指定的指定字节子阵列解码charset
public String(byte[] bytes,  int offset, int length, String charsetName)
// 构造一个新的 String 通过使用指定的字符集解码指定的字节子阵列。
public String(byte[] bytes,  String charsetName)
// 构造一个新的 String 由指定用指定的字节的数组解码charset
public String(char[] value)
// 分配一个新的 String ,以便它表示当前包含在字符数组参数中的字符序列。
public String(char[] value,  int offset, int count)
// 分配一个新的 String ,其中包含字符数组参数的子阵列中的字符。
public String(int[] codePoints,  int offset, int count)
// 分配一个新的 String ,其中包含 Unicode code point数组参数的子阵列中的字符
public String(String original)
// 初始化新创建的 String 对象,使其表示与参数相同的字符序列
public String(StringBuffer buffer)
// 分配一个新的字符串,其中包含当前包含在字符串缓冲区参数中的字符序列。
public String(StringBuilder builder)
// 分配一个新的字符串,其中包含当前包含在字符串构建器参数中的字符序列。

常见的构造方式

String 类提供的构造方法太多太多了 , 挑几个常见或者常用的创建方式给大家演示一下

  • 方式一 , 最常用也最简单的方式 , 直接赋值

    String str = "Hello";
  • 方式二 , 给定字符串创建一个实例

    String str = new String("Hello");
  • 方式三 , 给定一个字符数组构造字符串

    char[] array = {'H','e','l','l','o'};
    String str = new String(array);

2.字符串比较相等

双等号==比较字符串

以上每种创建方式达到的效果都是一样 , 唯一的区别就是内存布局不一样了 , 先看一段代码

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

    String str3 = "He" + "llo";
    System.out.println(str1 == str3);

    String str4 = "He";
    String str5 = "llo";
    String str6 = str4 + str5;
    System.out.println(str1 == str6);
}
// 运行结果
false
true
false

为什么明明看着差不多的代码 , 得到的答案就不是一样的呢 ? 这是因为 String 类是个引用类型 , 如果引用类型之间使用 == 比较 , 那么比较的就是引用类型的指向的对象是否相等 , 既然是比较对象为什么又会有相同的呢 ? 那么接下来我们分段来解析以上代码

代码一

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

// 运行结果
false

 首先我们要明确一个概念 , " " 引起的内容都是一个常量 , 而这个常量会放在内存中的堆区 ( 在JDK1.7开始,被移到堆区中 ) 的字符串常量池中 , 常量池中的字符串也只能存在一份 , 接下来看一下 代码 1 的内存布局

在代码1中 , str1 变量是直接指向的是在常量池中 "Hello" 的地址 , 而 str2 变量指向的是堆区另外一块地址 , 而那一块地址指向的是常量池中的 "Hello" 的地址 , 这是为什么 ? 这里就要先看看 String 类的底层实现了 , 如果你使用的开发工具是 IDEA 的话 , 只需要按住 ctrl + 鼠标左键 点击 代码中的 String 类 , 就能转到实现文件

点进来可以看到 , 在 String 类底层是用一个 value 字符数组用来存储字符串的 , 还有另一个变量用来存储字符串的 哈希 地址

回到代码中 , 我们查看一下 str2 的构造方法是如果实现的

点进来可以看到 , 构造方法是用本类的 value 数组引用传入的字符串参数的 value 数组 , 本类的 hash 变量保存 传入的字符串参数的 hash 变量 , 况且用new这个关键字的话,是调用new指令创建一个对象,然后调用构造方法来初始化这个对象 , 而 new 出来的对象都是放在堆区的 , 那么此时在内存的堆区就新创建了一个 String 类的对象 , 而这个对象指向的就是 字符串常量池 中的 "Hello" , 而 str2 变量引用的是堆区的 String 对象

现在可以知道 str1 变量是直接指向 字符串常量池"Hello" 的地址 , 而 str2 变量是指向堆区中的 String 对象的地址 , 两个指向的地址不同 , 所以结果是 false

代码二

String str1 = "Hello";
String str3 = "He" + "llo";
System.out.println(str1 == str3);

// 运行结果
true

代码 2 的结果为 true 的原因也很简单 , 因为 "He""llo" 两个都是字符串常量 , 代码在编译期间就会自动拼接成 "Hello"

所以 , str3 的代码就相当于 "String str3 = "Hello"; , 由于前面 str1 指向的 Hello 已经放入到在 字符串常量池 中了 , 所以 str3 变量产生的 "Hello" 字符串准备放入 字符串常量池 时 , 发现 "Hello" 已经存在 , JVM 此时就会将 字符串常量池 中的 "Hello"地址返回给 str3 ,这时 str1str3 指向的都是同一块地址 , 所以两个变量比较的结果等于 true

代码三

String str1 = "Hello";
String str4 = "He";
String str5 = "llo";
String str6 = str4 + str5;
System.out.println(str1 == str6);

这个代码看过去跟代码2差不多 , 也是两个字符串相加, 为什么这个比较结果是 false 呢 ? 这是因为 str4str5 是变量 , 在编译期间 , 并不知道 变量中放的是什么内容 , 只有在运行的时候才能知道 , 来看一下内存图

为什么 str6 会在堆中产生一个 String 对象呢 ? 我们可以反编译代码查看一下执行过程 , 先在 IDEA 中找到终端 , 然后将目录切换至要反编译的字节码文件目录下 , 输入 Javap -c <字节码文件> >> <指定文件> 指令 , 生成一个文本文件 , 文本文件中包含了反编译的信息

打开生成的文本文件 , 在编译文件中可以看到 , 代码在执行的过程中 , 创建了一个 StringBuilder 对象进行拼接字符串 , 主要是在后面还调用了一个 toString() 方法

我们转到 StringBuilder 类实现文件下查看 , 发现 StringBuilder 类实现的 toString() 方法是返回一个 String 对象

而这个 String 对象正是 str6 变量引用的 , 在这个 String 对象中传入的参数正是 "He""llo" 拼接得到的 "Hello" , 所以 str6 并不是直接指向 字符串常量池 中的 Hello , 而是指向堆区中的 String 对象 , 再由这个 String 去指向 "Hello" , 所以和 str1 进行比较 , 比较的结果是 false

equals( )方法比较字符串

String 使用 == 比较并不是在比较字符串内容, 而是比较两个引用是否是指向同一个对象 , Java中要想比较字符串内容的话 , 必须采用 String 类 提供的 equals 方法

public static void main(String[] args) {
    String str1 = "Hello";
    String str2 = new String("Hello");
    System.out.println(str1.equals(str2));
    
    String str3 = "He" + "llo";
    System.out.println(str1.equals(str3));

    String str4 = "He";
    String str5 = "llo";
    String str6 = str4 + str5;
    System.out.println(str1.equals(str6));
}

// 运行结果
true
true
true

equals 使用注意事项

如果一个 String变量 和 字符串常量 比较的话有两种方式

String str = new String("Hello");

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

"Hello" 这样的字面值常量, 本质上也是一个 String 对象, 完全可以使用 equals 等 String 对象的方法 , 两种方式更推荐写成方式二 , 因为一旦 str 值为 null , 方式一 会抛出异常 , 而方式二能正常运行

3.字符串常量池

"池" 是编程中的一种常见的, 重要的提升效率的方式, 我们会在未来的学习中遇到各种 "内存池", "线程池", "数据库连接池" .... 字符串常量池是 Java 为String 在堆区开辟的一块内存缓冲区,为了提高性能同时减少内存开销。

String类的设计使用了共享设计模式

在JVM底层会自动维护一个对象池(字符串常量池)

  • 如果现在采用了直接赋值的模式进行String类的对象实例化操作,那么该实例化对象(字符串内容)将自动保存到这个对象池之中.

  • 如果下次继续使用直接赋值的模式声明String类对象,此时对象池之中如若有指定内容,将直接进行引用

  • 如若没有,则开辟新的字符串对象而后将其保存在对象池之中以供下次使用

在上面的例子提到 String 类中两种对象实例化 , 直接赋值和构造方法.

1.直接赋值

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

分析 String 对象创建过程

  1. 在栈中分配一块内存给 str1 变量

  2. JVM 在字符串常量池中查看是否存在 "Hello" 字符串

  3. 如果存在则不创建 , 将 字符串常量池 中的 Hello 地址返回给 str1 变量引用 , 如果不存在 , 则把 Hello 字符串放入 字符串常量池 中再将地址返回给 str1 变量引用

  4. 在栈中分配一块内存给 str1 变量

  5. JVM 在字符串常量池中查看是否存在 "Hello" 字符串

  6. 发现存在 "Hello" 字符串 , 直接将 字符串常量池 的地址返回给 str2 变量

内存布局图示

由于有字符串常量池的存在 , 在两条代码执行中 , 在堆区只开辟了一块内存 , 做到了减少内存开销的效果 , 当然一两条代码是看不出来内存开销的效果的 , 但是假设在一个工程代码中存在了多数的重复字符串 , 此时如果没有字符串常量池的话 , 也就代表每个字符串都需要在堆中开辟一块内存。

2.构造方法

String str1 = new String("Hello");
String str2 = new String("Hello");

分析 String 对象创建过程

  1. 在栈中分配一块内存给 str1 变量

  2. JVM 在字符串常量池中查看是否存在 "Hello" 字符串

  3. 如果存在则不创建字符串 Hello , 如果不存在 , 则把 Hello 字符串放入 字符串常量池

  4. new 操作 在堆区创建一个 String 对象 , 这个 String 对象保存着 字符串常量池Hello 的地址

  5. str1 变量指向堆区的 String .

  6. 在栈中分配一块内存给 str2 变量

  7. JVM 在字符串常量池中查看是否存在 "Hello" 字符串

  8. 发现存在不进行创建字符串 Hello

  9. new 操作 在堆区创建一个 String 对象 , 这个 String 对象保存着 字符串常量池Hello 的地址

  10. str2 变量指向堆区的 String .

内存布局图示

使用构造方法创建字符串时 , 会在堆区中开辟三块内存 , 每次 new 一个 String 对象都会在堆区开辟一块内存用来存储 String 对象.同一个字符串可能会被存储多次, 比较浪费空间 , 而开辟的 String 对象堆区内存用了一次之后就不再使用了。

intern( ) 方法

可以使用 String 的 intern 方法来手动把 String 对象加入到字符串常量池中

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

// 运行结果
false

上列代码 , 在上面就解释过为什么结果会是 false , 原因是 str1 变量是直接指向 字符串常量池 中的 "Hello" , 而 str2 是指向堆区中创建的 String 对象 , 在由 String 保存了 字符串常量池 中的 "Hello" 的地址 , 但是在 String 类中有一个intern( ) 方法可以手动入池 , 也就是说可以手动将 str2 变量指向 字符串常量池 中 , 使用如下:

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

// 运行结果
true

intern( ) 方法的功能是手动入池 , 手动入池的意思就是 , 如果在 字符串常量池 中有 "Hello" 字符串则不入池 , 则将 "Hello" 的地址返回给 str2 , 如果 不存在 "Hello" 字符串 , 则在 字符串常量池 中创建一个 "Hello" 对象再把地址返回给 str2 , 同时 , 堆区格外开辟的一个 String 对象空间会被 JVM 回收.

因此 , 在 str2 变量手动入池后 , str1str2 都指向 字符串常量池 中的同一份字符串地址 , 所以最后得到的结果是 true .

4.字符串不可变特性

字符串是一种不可变对象. 它的内容不可改变.String 类的内部实现也是基于 char[] 来实现的, 但是 String 类并没有提供 set 方法之类的来修改内部的字符数组.

String 类的实现文件中 , 有这么一段话

译文:

  • 字符串常量 , 它们的值在创建后不能更改。字符串缓冲区支持可变字符串 , 因为String对象是不可变的,所以它们可以共享。

前面说过 , String 类是采用共享设计模式 , 在堆区维护一个对象池(字符串常量池) , 当多个变量采用直接赋值方法去定义同一个字符串 , 此时的多个变量都是指向 字符串常量池 中的同一块区域 , 那怎么去理解不可变呢 ? 可以知道的是 , 在String 底层实现的是使用一个 字符数组 来保存字符串的内容的 , 但这个字符数组是被 final 关键字修饰的 , final 关键字表示对象是最终形态的,对象是不可改变的意思。

来看以下一段代码

String str = "hello" ;
str = str + " world" ;
str += "!!!" ;
System.out.println(str);

// 执行结果
hello world!!!

对于以上代码 , 输出结果最后看起来像是修改了字符串, 其实不是. 内存变化如下

因为 String 类不可变特性 , 每个用 " " 引起的字符串 , 都会在 字符串常量池 中存一份 , 虽然 += 之后 str 输出的结果确实变了 , 但是并不是 String 对象本身发生了改变 , 而是 str 引用了其它的字符串。而我们可以发现 , 使用 += 这种去拼接字符串 , 会在内存中开辟多块内存 , 还是会浪费空间 , 有没有什么办法解决呢 ? 答案是有的 : 就是实现文件中说的 , 字符串缓冲区支持可变字符串 , 也是我们下文会讲到的 StringBulider 和 StringBuffer 类

字符串常用操作

String 类中 , 也提供了许多方法API , 这里作者整理了字符串比较常用的方法来介绍 , 具体更多的方法可以查看String官方文档

1.字符串比较

1.equals( ) 方法

public boolean equals(Object anObject)

equals( ) 方法字符串比较中最常用的方法 , 它比较的是字符串中的内容 , 如果两个字符串相同 , 则返回 true , 如果不相同则返回 false , anObject 参数为待比较字符串

String str = "hello";
System.out.println(str.equals("hello"));
System.out.println(str.equals("Hello"));

// 运行结果
true
false

2.equalsIgnoreCase( ) 方法

public boolean equalsIgnoreCase(String anotherString)  

equalsIgnoreCase( ) 方法与 equals( ) 方法都是比较字符串的内容 , 不同的是 , equalsIgnoreCase( ) 方法会忽略字符串中的字母大小写来进行比较 , , 而 equals( ) 方法并不会忽略字母的大小写 , anotherString 参数为为待比较字符串

代码示例 

String str1 = "hello" ;
String str2 = "Hello" ;
System.out.println(str1.equals(str2)); 
System.out.println(str1.equalsIgnoreCase(str2)); 

// 运行结果
false
true

3.compareTo( ) 方法

public int compareTo(String anotherString)  

compareTo 方法比较的是字符串的大小 , 方法 anotherString 为待比较字符串 , 方法返回一个整型,该值会根据大小关系返回三种情况

  1. 相等 : 返回 0

  2. 小于 : 返回小于 0 的值

  3. 大于 : 返回大于 0 的值

compareTo( ) 方法是一个区分字符串中字母 大小关系的方法 , 字符串的比较大小规则总结成三个字 "字典序" , 相当于判定两个字符串在一本词典的前面还是后面 , 先比较第一个字符的大小(根据 unicode 的值来判定), 如果不分胜负, 就依次比较后面的内容

代码示例

System.out.println("A".compareTo("a")); 
System.out.println("a".compareTo("A")); 
System.out.println("A".compareTo("A")); 
System.out.println("AB".compareTo("AC"));

// 运行结果
-32
32
0
-1

2.字符串查找

字符串查找方法 , 也会经常用到 , 一般可以分为两种

  • 第一种 : 查找该字符串中是否包含另一个字符串

  • 第二种 : 查找该字符串中指定字符索引的位置

1.contains( )方法

public boolean contains(CharSequence s)  

在字符串中查找是否包含给定字符串 s , 存在返回 true 不存在返回 false

代码示例

String str = "hello world";
System.out.println(str.contains("abc"));
System.out.println(str.contains("world"));

// 运行结果
false
true

2.indexOf( )方法

public int indexOf(String str)  

从头开始查找指定字符串/字符 str 的位置 , 查到返回位置的开始索引 , 查不到返回 -1 , 如果指定字符串/字符 str 出现多次 , 只返回第一次出现的位置索引

代码示例

String str = "hello world";
System.out.println(str.indexOf('l')); // 指定字符查找
System.out.println(str.indexOf("world"));// 指定字符串查找
System.out.println(str.indexOf("hi"));

// 运行结果
2
6
-1

3.字符串替换

由于字符串是不可变对象, 替换不修改当前字符串, 而是产生一个新的字符串,需要注意的是 , 由于替换字符串是创建一个新的字符串对象并返回 , 所以替换时一般拿要替换的字符串变量来接收返回的String对象 ,

1.replace( ) 方法 

public String replace(CharSequence target, CharSequence replacement)  

在字符串中 , 使用一个字符/字符串替换另一个字符/字符串 , target 参数为待替换的字符串 , replacement 参数为要替换的字符串 , 支持参数为字符

代码示例

String str = "hello world";
str = str.replace("l","-" );
System.out.println(str);

// 运行结果
he--o wor-d

2.replaceAll( ) 方法

public String replaceAll(String regex, String replacement)  

replaceAll( ) 方法 和 replace( ) 方法一样的用法 , 也是替换字符串中的内容 , 不过 regex 参数支持 正则表达式 , 参数只支持字符串

代码示例

String str = "hello world";
str = str.replaceAll("[l*]","-" );
System.out.println(str);

// 运行结果
he--o wor-d

3.replaceFirst( ) 方法

public String replaceFirst(String regex , String replacement)

前两种替换方法都是将字符串中所以出现的字符串都给替换了 , 而 replaceFirst( ) 方法是将字符串中遇到的第一个 regex 值给替换成 replacement , 支持正则表达式

代码示例

String str = "hello world";
str = str.replaceFirst("[l*]","-" );
System.out.println(str);

// 运行结果
he-lo world

4.字符串拆分

split( ) 方法 

public String[] split(String regex, int limit)  

可以将一个完整的字符串按照指定的分隔符划分为若干个子字符串 , regex 参数为分割字符串支持 正则表达式 , 字符串将会以遇到 regex 作为分割位置 , limit 参数为指定分割的长度( 可选 ).

代码示例

一 . 以空格切分字符串

String str = "hello world hello java";
String[] ss = str.split(" ");
for (String s : ss){
	System.out.println(s);
}

// 运行结果
hello
world
hello
java

二 . 切割部分字符串

String str = "hello world hello java";
String[] ss = str.split(" ", 2);
for (String s : ss){
	System.out.println(s);
}

// 运行结果
hello
world hello java

三 . 特殊字符切割字符串 , 在以 特殊字符作为分割符时 , 可能会出现无法正确切分的情况 , 需要加上转义字符

String str = "192.168.1.1" ;
String[] result = str.split("\\.") ;
for(String s: result) {
	System.out.println(s);
}

// 运行结果
192
168
1
1

注意事项

  1. 字符"|","*","+"都得加上转义字符,前面加上"\"

  2. 而如果是"\",那么就得写成"\\".

  3. 如果一个字符串中有多个分隔符,可以用"|"作为连字符.

5.字符串截取

subString( ) 方法

public String substring(int beginIndex, int endIndex)  

从一个完整的字符串之中截取出部分区间内容并返回一个新的字符串对象 , beginIndex 为区间开始索引 , endIndex 为区间结束索引 , 区间采用 左闭右开法 : beginIndex < endIndex , 索引下标从 0 开始

代码示例

String str1 = "hello world";
String str2 = str1.substring(0, 2);
System.out.println(str2);

// 运行结果
he

6.字符串其它方法

1.format( ) 方法

public static String format(String format, Object... args) 

format( ) 是一个类方法 , 直接使用 String 类名调用 , 该方法的功能是能够指定格式生成一个字符串对象并返回

代码示例

String str = String.format("%d %s %s", 1 , "hello" , "world");
System.out.println(str);

// 运行结果
1 hello world

2.toUpperCase( ) 方法

public String toUpperCase() 

toUpperCase( ) 方法 , 可以将字符串中的所有英文字符都转换成大写字符串返回转换后的字符串

代码示例

String str = "hello world";
str = str.toUpperCase();
System.out.println(str);

// 运行结果
HELLO WORLD

3.toLowerCase( ) 方法

toLowerCase( ) 方法 , 可以将字符串中的所有英文字符都转换成小写字符串返回转换后的字符串

代码示例

String str = "HELLO WORLD";
str = str.toUpperCase();
System.out.println(str);

// 运行结果
hello world

4.length( ) 方法

public int length()

返回字符串的长度

代码示例

String str = "hello world";
int len = str.length();
System.out.println(len);

// 运行结果
11

5.charAt( ) 方法

public char charAt(int index)

返回字符串指定索引的字符 , 索引取值范围为 0 - length( ) - 1

代码示例

String str = "hello world";
char ch1 = str.charAt(0);
char ch2 = str.charAt(1);
char ch3 = str.charAt(2);

System.out.println(ch1);
System.out.println(ch2);
System.out.println(ch3);

// 运行结果
h
e
l

 StringBuilder & StringBuffer 类

对于前面 字符串常量池 中讲到 字符串缓冲区可支持可变字符串 , 讲的就是StringBuilder 和 StringBuffer 类 , 这两个类也是属于操作字符串的类 。

为什么有 String 类了 , 还需要创建这两个类呢 ? 

  • 这是因为 Java编译器对 String 类做了特殊处理,使得我们可以直接用 + 拼接字符串 , 会在内存中产生大量的临时对象 , 况且 String 类属于不可变对象, 如果想要去修改一个字符串的内容 , 就得重新创建一个新的字符串 , 每次修改要重新引用新字符串的地址 , 对于复修改字符串可能会显著降低性能 , 为了更高效的进行拼接字符串 , 于是就有了 StringBuilder 和StringBuffer 类 , 它们是一个可变字符串类 , 可变性是指在创建类的实例后,可以通过追加、移除、替换或插入字符来修改它 , 与其说 StringBuilder 是一个字符串缓冲区 , 其实更像是一个容器 , 容器中存的是字符。

由于这两个类的使用方法 , 以及提供的方法api的都是一样的 , 这里就挑 StringBuilder 类来给大家讲解 ~

构造方法

来看一下 StringBuilder 类提供的构造方法

StringBuilder() 
// 构造一个没有字符的字符串构建器,初始容量为16个字符。  
StringBuilder(CharSequence seq) 
// 构造一个包含与指定的相同字符的字符串构建器 CharSequence 。  
StringBuilder(int capacity) 
// 构造一个没有字符的字符串构建器,由 capacity参数指定的初始容量。 
StringBuilder(String str) 
// 构造一个初始化为指定字符串内容的字符串构建器。

构造方法不是很多 , 这里就不介绍使用了 , 接下为了更好的了解 StringBuilder 类 , 我们去查看一下 StringBuilder 实现文件 , 在 IDEA 里使用 ctrl + 鼠标左键 点击 StringBuilder 转到实现文件

StringBuilder sb = new StringBuilder();

进来后可以看到 , StringBuilder 类的直接父类是一个 AbstractStringBuilder 类 , 这个类是一个抽象类 , StringBuilder 类实现具体方法 , 除了继承了 AbstractStringBuilder 之外 , 还看到实现了一个CharSequence 接口 。这个接口描述的是一系列的字符集 , 所以字符串是字符集的子类,如果以后看见 CharSequence,最简单的联想就是字符串。

再来看看 StringBuilder 的构造方法 , 构造方法是调用 AbstractStringBuilder 类的构造方法去创建一个初始容量大小为 16 的字符数组 , 在父类的底层中的 value 数组 跟 String 底层的 value 数组一样 , 并不是一个 final 修饰的字符数组 , 这也决定了可以通过这个 value 数组去修改字符串 , 以此达到可变字符串的效果

常用方法

1.append( ) 方法

public StringBuilder append(String str)

字符串追加方法 , 也是 StringBuilder 中最常用的方法 ,用于字符串拼接 , 类似于 String 使用 + 号拼接一样 , 不过 append( ) 方法不会产生临时对象

代码示例

StringBuilder sb = new StringBuilder();
sb.append("hello");
sb.append(" world");
System.out.println(sb);

// 运行结果
hello world

运行过程

给一段代码 , 我们查看实现文件 , 来看看 append( ) 方法是怎么工作的

StringBuilder sb = new StringBuilder();
sb.append("hello");

转到 StringBuilder 实现文件下 , 可以看到该方法是调用了父类 AbstractStringBuilderappend 方法 并将给定的参数字符串也一同传入了 , 且最后返回的是 this 对象 , this 关键字在单独作为返回值时表示 当前对象的实例 , 也就是返回了当前的 StringBuilder 对象

然后转到 AbstractStringBuilder 的实现方法下查看 , 在代码中调用了一个 ensureCapacityInternal 用来判断是否需要扩容 , 这个就不讲解了 , 主要是看参数调用了字符串中的一个 getChars( ) 的方法。  

getChars( ) 方法 , 传了四个参数 (0 , len , value , count ) , 其中 len 是待拼接字符串长度 , value 是底层的数组 , 那 count 是什么呢 ? 我们可以往上找 , 找到 count 的定义位置 , 文档说 , count 是记录使用的字符数 , 也就是说记录了当前 value 数组中有多少个字符

回到前面 , 我们去查看 getChars( ) 方法做了什么 , 转到实现文件中 , 可以看到 , getChars( ) 方法将那些传进来的参数作为了 System.arraycopy方法的参数.

查阅 JDK 文档 , 可以知道 System.arraycopy 方法是用来拷贝数组的 , 一共有五个参数

也就是说 , getChars( ) 方法 , 将待拼接字符串的底层中保存 "hello" 字符串的 value 数组作为源数组 , "0" 为源数组初始位置索引 , 而将 StringBuilder 底层下的 value 数组作为目标数组 , 且是以 StringBuilder 类底层下的用来记录当前有多少字符的 count 变量作为目标数组的其实位置 , 将长度为 length 的数据进行拷贝至 StringBuildervalue 数组中 , 这里看不到 System.arraycopy 具体实现 , 但数组拷贝应该是类似于如下图示

拷贝完成后 , 以上就是一个 append( ) 方法的执行过程 , 在此过程中 , 并没有产生新的对象 , 每次 append 都在 value 数组后追加字符 , 而当前 sb 引用的还是当初那个 StringBuilder 对象。

2.insert( ) 方法

public StringBuilder insert(int offset , String str)

使用 insert( ) 方法可以在 StringBuilder 对象中指定位置插入字符串 , offset 参数为偏移量 , 也就是索引 , offset 参数必须大于或等于0 ,小于或等于该序列的长度 , str 为待插入的字符串

代码示例

StringBuilder sb = new StringBuilder();
sb.append(" world");
sb.insert(0,"hello");
System.out.println(sb);

// 运行结果
hello world

insert( ) 方法执行过程跟 append( ) 方法差不多 , 也是找到底层的 value 数组 , 然后控制边界再从指定位置开始拷贝 , 这里作者就不详细介绍了, 有兴趣的读者可以去看看源码 !

3.delete( ) 方法

public StringBuilder delete(int start , int end)

删除 StringBuilder 中的一段子区间 , start 为区间开始索引 , end 为区间结束索引 , 采用左闭右开区间法 start < end

代码示例

StringBuilder sb = new StringBuilder();
sb.append("hello world");
sb.delete(0, 6);
System.out.println(sb);

// 运行结果
world

4.toString( ) 方法

public String toString()

StringBuilder 对象中保存的所有字符构造一个 String 对象并返回

代码示例

StringBuilder sb = new StringBuilder();
sb.append("hello world");
String str = sb.toString();
System.out.println(str);

// 运行结果
hello world

扩展

其实在我们上面直接使用 System.out.println(sb) 输出的字符串也是调用的 toString( ) 方法 , 不过这个是输出函数自动调用的 , toString( ) 方法是属于 Object 的方法 , 我们都知道 Object 是所有类的父类 , 只要所有类重写了 toString 都可以在输出函数输出自己重写 toString( ) 方法的内容 , 列如 :

class A{
    @Override
    public String toString() {
        return "你好 世界";
    }
}

public static void main(String[] args) {
    A a = new A();
    System.out.println(a);
}

// 运行结果
你好 世界

以上就是本章介绍的 StringBuilder 类常用方法 , 具体更多方法的了解 , 可以去查看StringBuilder官方文档

如何证明 StringBuilder 类的引用地址没变 ? 

我们可以通过代码观察 String 类 和 StringBuilder 在拼接字符串后的哈希地址查看

String str = "hello";
StringBuilder sb = new StringBuilder("hello");

System.out.println("---------- 第一次查看哈希地址 ----------");
System.out.println("StringBuilder : " + sb.hashCode());
System.out.println("String : " + str.hashCode());

// 拼接字符串
str += " world";
sb.append(" world");

System.out.println("---------- 第二次查看哈希地址 ----------");
System.out.println("StringBuilder : " + sb.hashCode());
System.out.println("String : " + str.hashCode());

// 运行结果
---------- 第一次查看哈希地址 ----------
StringBuilder : 460141958
String : 99162322
---------- 第二次查看哈希地址 ----------
StringBuilder : 460141958
String : 1794106052

根据运行结果我们可以看到 , 经过拼接字符串后 , String 对象的 哈希值 已经发生了变化 , 而 StringBuilder 对象的 哈希值 是没有变的 , 这就可以证明 String 对象每次修改要重新引用新字符串的地址 , 而 StringBuilder 对象的引用地址并不会发生变化

String & StringBuilder & StringBuffer 的区别

StringBuffer 类

在上面直介绍了 String 类和 StringBuilder类 , 在这里就简单提一下 StringBuffer 类吧 , 关于 StringBuffer 类跟 StringBuilder 的使用方法一样 , 也是属于可变字符串类 , 两个类的构造方法 和 方法API 都是一样的 , 唯一的区别就是 StringBuffer 是线程安全的 , 我们可以对比一下 StringBuffer 类和 StringBuilder 类方法实现

append( ) 方法

insert( ) 方法

delete( ) 方法

我们可以看到 , 在 StringBuffer 底层实现的方法中整体跟 StringBuilder 是相同的 , 但是 StringBuffer 方法名前面加了一个 synchronized 关键字 , synchronized 是 Java 中的关键字 ,是一种同步锁 , 修饰一个方法,被修饰的方法称为同步方法,其作用的范围是整个方法 , 简单来说就是给方法加锁 , 让该方法在多线程中调用时能保证线程安全 ,由于加锁机制会让线程进行阻塞等待 , 所以只能保证线程安全 , 但不能保证效率 , 加锁会带来性能上的损耗 , 所以在单线程情况下推荐使用 StringBuilder 效率会更高.

三者的区别

  • String 的内容不可修改,StringBufferStringBuilder 的内容可以修改.

  • StringBufferStringBuilder 大部分功能是相似的 , StringBuffer 采用同步处理,属于线程安全操作 , 而 StringBuilder 未采用同步处理,属于线程不安全操作

  • 单线程情况下推荐使用 StringBuilder 来对字符串进行操作 , 效率更高

  • String 类使用 + 拼接字符串时会被优化成 StringBuilderappend( )方法

  • String 类每次拼接字符串都会引用新字符串的地址 , 而 StringBuilder / StringBuffer 对象自始至终都是引用同一个对象

 

 本章到此结束,如果文中有写的不对或不懂的地方,欢迎评论区讨论,谢谢!


点击全文阅读


本文链接:http://zhangshiyu.com/post/39846.html

字符串  方法  常量  
<< 上一篇 下一篇 >>

  • 评论(0)
  • 赞助本站

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

关于我们 | 我要投稿 | 免责申明

Copyright © 2020-2022 ZhangShiYu.com Rights Reserved.豫ICP备2022013469号-1