Bootstrap

【Java】字符串细节补充

String 类

String 概述

Java.lang.String 类代表字符串,Java 程序中的所有字符串文字(例如 “abc”)都为此类的对象。

String str1 = "你好";
String str2 = "java";

String 注意点

字符串的内容是不会发生改变的,它的对象在创建后不能被更改,如果更改则会产生新的字符串。

String str1 = "你好";
str1 = "hello ";
String str2 = "java";
System.out.println(str1 + str2);// 字符串拼接产生一个新的字符串 "hello java"
  • String 是引用数据类型,所以当创建了一个 String 类对象后,用变量 str1 指向对象值 “你好”。

在这里插入图片描述

  • 将 "hello " 赋给 str1,并不是修改了原对象的值,而是创建了一个新的对象,并将 str1 指向新对象的值 "hello ",原对象的值依然存在于内存之中。

在这里插入图片描述

字符串相关类(如 StringStringBuilderStringBuffer重写了 toString(),直接返回字符串内容(而非内存地址或其他属性)。

创建 String 对象

直接赋值

String str = "Hello Java";

new 关键字使用构造方法

  • public String() 创建空白字符串,不含任何内容
  • public String(String original) 根据传入的字符串,创建字符串对象
  • public String(char[] chs) 根据字符数组,创建字符串对象
  • public String(byte[] bys) 根据字节数组,创建字符串对象
String s1 = new String();// 输出 ""
String s2 = new String("abc");// 输出 abc
char[] chs = {'a','b','c'};
String s3 = new String(chs);// 输出 abc
byte[] bys = {97,98,99};
String s4 = new String(bys);//输出 abc

内存原理

直接赋值

原理

首先要了解一个概念:字符串常量池(串池)是 Java 虚拟机(JVM)为了节省内存和提高性能而设计的一块特殊内存区域,用于存储字符串字面量和使用 intern() 方法手动添加的字符串对象。在 Java 7 及之后的版本中,串池位于堆内存中。

以下是通过直接赋值创建 String 对象的代码示例:

public class StringDemo {
    public static main(String[] args) {
        String s1 = "abc";
        String s2 = "abc";
    }
}
  • 在这段代码中,main 方法执行入栈,然后创建变量 s1,接着看串池中是否存在 s1 指向的值 “abc”,如果没有,就在串池中创建 “abc” 并将地址值赋值给 s1。

在这里插入图片描述

  • 然后创建变量 s2,因为串池中已经存在 “abc”,就直接将其地址值赋值给 s2。

在这里插入图片描述

总结

当使用双引号直接赋值时,系统会检查该字符串在串池中是否存在:不存在则创建新的,存在则复用。

new 构造方法

原理

当使用 new String() 构造方法创建字符串对象时,JVM 会在堆内存中开辟一块新的空间来存储这个字符串对象,而不管串池中是否已经存在相同内容的字符串。

以下是通过 new 关键字使用构造方法创建 String 对象的代码示例:

public class StringDemo {
	public static main(String[] args) {
        char[] chs = {'a','b','c'};
        String s1 = new String(chs);
        String s2 = new String(chs);
    }
}
  • main 方法执行入栈,先执行char[] chs = {'a','b','c'};,在堆内存中开辟一个空间存放 ‘a’,‘b’,‘c’,然后将地址值赋值给变量 chs。

在这里插入图片描述

  • 然后执行String s1 = new String(chs);,在堆内存中开辟一个空间存放 “abc”,将其地址值赋值给 s1。

在这里插入图片描述

  • 然后执行String s2 = new String(chs);,在堆内存中开辟一个空间存放 “abc”,将其地址值赋值给 s2。

在这里插入图片描述

总结

每用一次 new String() 都会再堆内存中开辟一个新的空间,这样会很占内存,所以还是推荐直接赋值的方法创建 String 对象。

字符串比较

“==” 比较

在了解字符串比较方法前,先了解一下 Java 中用 “==” 的比较,分为基本数据类型和引用数据类型:

基本数据类型使用 “==”

“==” 在基本数据类型中比较的是数据值,以下面代码为例:

int a = 10;
int b = 20;
System.out.println(a == b); // false
引用数据类型使用 “==”

“==” 在引用数据类型中比较的是地址值,有 3 种情况,以下面代码为例:

// 1.比较的两个字符串都是通过直接赋值创建的
String s1 = "abc"; // 在串池中开辟的内存空间的地址值
String s2 = "abc"; // 复用已开辟内存空间的地址值
System.out.println(s1 == s2); // true

// 2.比较的两个字符串,一个是直接赋值创建,另一个是通过 new 关键字创建的
String s1 = new String("abc"); // 在堆内存中开辟内存空间的地址值
String s2 = "abc"; // 在串池中开辟的内存空间的地址值
System.out.println(s1 == s2); // false

// 3.比较的两个字符串都是通过 new 关键字创建的
String s1 = new String("abc"); // 在堆内存中开辟内存空间的地址值
String s2 = new String("abc"); // 在堆内存中开辟另一个内存空间的地址值
System.out.println(s1 == s2); // false

注意事项:

先看以下代码:

import java.util.Scanner;
public class StringCompare {
    public static main(String[] args) {
        Scanner input = new Scanner(System.in);
        String s1 = input.next(); // 输入abc
        String s2 = "abc";
        System.out.println(s1 == s2); // false
    }
}

导致两个字符串地址值不相等的原因:由 Scanner 类输入的内容是通过 new 关键字创建的对象。

字符串比较方法

“==” 只能通过比较地址值是否相等来判断内容是否一样,下面的两个方法可以直接比较字符串内容是否相等:

  • boolean equals() 两个字符串相比较,完全一样为 true,否则为 false。

  • boolean equalsIgnoreCase() 忽略大小写的比较

public class StringCompare {
    public static main(String[] args) {
        String s1 = "abc";
        String s2 = "Abc";
        String s3 = new String("abc");
        System.out.println(s1.equals(s3)); // true
        System.out.println(s1.equalsIgnoreCase(s2)); // true
    }
}

规范化表示形式

在 Java 中,字符串对象的规范化表示形式通常指通过字符串池机制,确保相同内容的字符串指向内存中的唯一实例。这是通过 intern() 方法实现的,其核心目的是优化内存使用和比较效率。

关键概念

  1. 字符串池(String Pool)

    • JVM 维护了一个字符串池(位于堆内存中),用于存储字符串字面量(如 "hello")。

    • 当直接使用字面量创建字符串时(如 String s = "hello"),JVM 会先检查池中是否存在该字符串:

      • 若存在,直接引用池中的对象。
      • 若不存在,将字符串添加到池中,再返回引用。
  2. new String() 与字面量的区别

    • 通过 new String("hello") 创建的字符串对象不会直接放入池中,而是直接在堆中生成新对象。
    • 即使内容相同,多次 new String() 会产生不同对象,导致 == 比较失败。
  3. intern() 方法的作用

    • 调用 intern() 会强制将字符串对象添加到字符串池(如果池中不存在该字符串),并返回池中的引用。
    • 若池中已存在,直接返回池中对象的引用。
String s1 = new String("hello");      // 堆中的对象,常量池已存在"hello"(因字面量)
String s2 = s1.intern();              // 返回常量池的引用
System.out.println(s1 == s2);         // false(s1在堆,s2在池)
System.out.println("hello" == s2);    // true(均指向池中的"hello")

String s3 = new String("world");      // 堆中的对象,常量池尚无"world"(假设首次创建)
String s4 = s3.intern();              // 将"world"加入常量池,返回池引用
System.out.println(s3 == s4);         // false(s3在堆,s4在池)

适用场景

  1. 减少重复字符串的内存占用
    适合处理大量内容相同的字符串(如日志处理),通过 intern() 共享唯一实例。
  2. 加速字符串比较
    “==” 比较常量池引用比 equals() 更高效,但需确保所有字符串都经过 intern() 处理。

注意事项

  • 性能权衡
    频繁调用 intern() 可能导致哈希表查询开销,需结合场景测试。
  • 慎用于动态字符串
    对动态生成的字符串(如拼接结果)使用 intern() 可能导致常量池膨胀。

总结

intern() 通过复用常量池中的字符串,优化内存和比较效率,但需权衡性能与场景。在需要处理大量重复字符串时,合理使用它可显著提升程序效率。

遍历字符串

  • public char charAt(int index) 根据索引返回字符。

  • public int length() 返回字符串的长度。

以下是一个遍历字符串的代码示例:

public class printString {
	public static main(String[] args) {
        String s = "hello java";
        for(int i = 0;i < s.length();i++){
       		System.out.println(s.charAt(i));
        }
	}
}

空格在字符串中也占内存,所以输出结果如下:

h
e
l
l
o
    
j
a
v
a

StringJoiner 类

StringJoiner 类概述

  • StringJoiner 类跟 StringBuilder 类一样,可以看作对字符串的操作类,创建之后里面的内容是可变的。
  • 作用:提高字符串的操作效率,而且代码编写特别简洁。
  • JDK 8 新特性。

StringJoiner 类的构造方法

  • public StringJoiner(间隔符号) 创建一个 StringJoiner 对象,指定拼接时的间隔符号。
  • public StringJoiner(间隔符号,开始符号,结束符号) 创建一个 StringJoiner 对象,指定拼接时的间隔符号、开始符号、结束符号。
import java.util.StringJoiner;
public class StringJoinerDemo {
	public static main(String[] args){
        StringJoiner sj = new StringJoiner(",","[","]");
        sj.add("a").add("b").add("c");
        System.out.println(sj); // 输出 [a,b,c]
    }
}
;