杰瑞科技汇

Java字符串String有哪些核心特性?

目录

  1. String 的基本概念
  2. String 的不可变性
  3. String 的常用方法
  4. String 的创建方式
  5. StringStringBuilderStringBuffer 的区别
  6. String 的内存布局与常量池
  7. 最佳实践

String 的基本概念

String 类代表一个不可变的字符序列,在 Java 中,字符串被广泛用于存储和操作文本数据。

Java字符串String有哪些核心特性?-图1
(图片来源网络,侵删)
  • 本质String 不是基本数据类型,它是一个对象。
  • 声明:使用 String 关键字来声明字符串变量。
    String greeting = "Hello, World!";

String 的不可变性

这是 String 最核心、最重要的一个特性,一旦一个 String 对象被创建,它的内容就不能被改变。

如何体现不可变性?

当你对字符串进行任何修改操作(如 concat(), replace(), substring() 等)时,Java 不会修改原始的 String 对象,而是会创建一个新的 String 对象来保存修改后的结果。

示例代码:

Java字符串String有哪些核心特性?-图2
(图片来源网络,侵删)
String str1 = "hello";
System.out.println("str1 的初始值: " + str1); // 输出: hello
System.out.println("str1 的内存地址: " + System.identityHashCode(str1)); // 获取对象哈希码
// str1.concat(" world") 会返回一个新的字符串对象
// str1 本身并没有改变
str1 = str1.concat(" world"); 
System.out.println("str1 修改后的值: " + str1); // 输出: hello world
System.out.println("str1 修改后的内存地址: " + System.identityHashCode(str1)); // 地址发生了变化

输出分析:

str1 的初始值: hello
str1 的内存地址: 1163157884  (假设的地址)
str1 修改后的值: hello world
str1 修改后的内存地址: 1956725890  (地址不同,说明是新的对象)

为什么 String 要设计成不可变?

  1. 线程安全:不可变对象天生就是线程安全的,多个线程可以同时访问一个 String 对象,而无需担心数据被其他线程修改,因此不需要额外的同步机制。
  2. 安全性:字符串被广泛用于 Java 类的加载、网络连接的 URL、文件路径等,如果字符串是可变的,这些关键信息可能会被恶意代码篡改,导致严重的安全问题。
  3. 性能优化(哈希缓存):因为字符串内容不会改变,String 类的 hashCode() 方法在第一次被调用后,会缓存计算好的哈希值,之后每次调用 hashCode() 都直接返回缓存值,这大大提高了在哈希表(如 HashMap, HashSet)中作为键的性能。
  4. 作为 HashMap 的键HashMap 要求键对象的 hashCodeequals 在对象的生命周期内保持不变。String 的不可变性完美地满足了这一要求。

String 的常用方法

String 类提供了非常丰富的方法来操作字符串。

方法 描述 示例
length() 返回字符串的长度。 "abc".length() -> 3
charAt(int index) 返回指定索引处的字符。 "abc".charAt(1) -> 'b'
substring(int beginIndex) 返回从 beginIndex 开始到末尾的子字符串。 "abcdef".substring(2) -> "cdef"
substring(int begin, int end) 返回从 beginIndexendIndex-1 的子字符串。 "abcdef".substring(2, 4) -> "cd"
concat(String str) 将指定字符串连接到此字符串的末尾。 "a".concat("b") -> "ab"
replace(char old, char new) / replace(CharSequence old, CharSequence new) 替换所有出现的旧字符/序列为新字符/序列。 "hello".replace('l', 'p') -> "heppo"
contains(CharSequence s) 判断字符串是否包含指定的字符序列。 "hello world".contains("world") -> true
toUpperCase() / toLowerCase() 将字符串转换为大写/小写。 "Hello".toLowerCase() -> "hello"
trim() 去除字符串两端的空白字符。 " hello ".trim() -> "hello"
split(String regex) 根据给定的正则表达式分割字符串。 "a,b,c".split(",") -> ["a", "b", "c"]
equals(Object anObject) 比较两个字符串的内容是否相等(区分大小写)。 "abc".equals("ABC") -> false
equalsIgnoreCase(String anotherString) 比较两个字符串的内容是否相等(不区分大小写)。 "abc".equalsIgnoreCase("ABC") -> true
compareTo(String anotherString) 按字典顺序比较两个字符串。 "abc".compareTo("abd") -> -1
indexOf(int ch) / lastIndexOf(int ch) 返回指定字符第一次/最后一次出现的索引。 "hello".indexOf('l') -> 2
valueOf(primitive type or Object) 将基本数据类型或对象转换为字符串。 String.valueOf(123) -> "123"

String 的创建方式

在 Java 中,创建字符串主要有两种方式,它们在内存中的行为有很大不同。

字面量赋值

String str1 = "hello";
String str2 = "hello";
  • 机制:这种方式会首先在 JVM 的字符串常量池 中查找是否存在 "hello" 这个字符串,如果存在,则直接引用该对象;如果不存在,则在常量池中创建 "hello",然后再引用它。
  • 比较str1 == str2 的结果是 true,因为它们引用的是常量池中的同一个对象。
    System.out.println(str1 == str2); // true

new 关键字创建

String str3 = new String("hello");
String str4 = new String("hello");
  • 机制:这种方式会在堆内存中创建一个新的 String 对象,其内容是 "hello",它不会去检查常量池中是否已经存在 "hello"(尽管 "hello" 这个字面量本身会被放入常量池)。
  • 比较str3 == str4 的结果是 false,因为它们是堆中两个不同的对象。
    System.out.println(str3 == str4); // false
特性 字面量赋值 (String s = "abc") new 关键字 (String s = new String("abc"))
创建位置 字符串常量池 堆内存
内存检查 会检查常量池,有则复用,无则创建 直接在堆中创建新对象,不检查常量池(但字面量本身会入池)
比较 引用同一个对象时为 true 永远是 false(除非是同一个变量)
性能 更高效,节省内存 较低,会创建不必要的额外对象

最佳实践:在大多数情况下,优先使用字面量赋值,除非你有明确的理由需要在运行时动态创建字符串。


StringStringBuilderStringBuffer 的区别

由于 String 的不可变性,在进行大量字符串拼接操作时,会频繁创建新对象,导致性能低下。StringBuilderStringBuffer 就是为了解决这个问题而设计的。

特性 String StringBuilder StringBuffer
可变性 不可变 可变 可变
线程安全 线程安全(不可变) 非线程安全 线程安全
性能 差(频繁创建对象) 稍低(因为有同步开销)
主要用途 存储和操作少量、固定的文本 单线程环境下进行大量字符串修改 多线程环境下进行大量字符串修改
示例方法 concat(), replace() append(), delete(), reverse() append(), delete(), reverse()

如何选择?

  1. 如果字符串内容不会改变:使用 StringString name = "John";
  2. 如果需要在单线程下进行大量字符串拼接、修改:使用 StringBuilder,这是最常见的场景,性能最好。
    StringBuilder sb = new StringBuilder();
    sb.append("Hello");
    sb.append(" ");
    sb.append("World");
    String result = sb.toString(); // "Hello World"
  3. 如果需要在多线程环境下进行字符串修改:使用 StringBuffer,它的方法大多被 synchronized 修饰,保证了线程安全,但速度比 StringBuilder 慢。

String 的内存布局与常量池

理解常量池对于深入理解 String 至关重要。

  • 字符串常量池:是 JVM 内存中一个特殊的区域,专门用于存储字符串字面量,它的目的是为了复用字符串,减少内存开销。

  • intern() 方法:这是一个不常用但很重要的方法,它的作用是:如果当前字符串在常量池中不存在,则将其添加到常量池中,并返回常量池中的引用;如果已存在,则直接返回常量池中的引用。

    String s1 = new String("hello");
    String s2 = s1.intern(); // s2 现在指向常量池中的 "hello"
    String s3 = "hello";
    System.out.println(s1 == s2); // false, s1在堆,s2在常量池
    System.out.println(s2 == s3); // true, s2和s3都指向常量池中的同一个对象

最佳实践

  1. 优先使用字面量:除非有特殊需求,否则始终用 String s = "hello"; 的方式创建字符串。

  2. 避免不必要的拼接:在循环中不要使用 或 concat() 来拼接字符串,这会产生大量中间对象,应使用 StringBuilder

    // 错误示例
    String result = "";
    for (int i = 0; i < 1000; i++) {
        result = result + i; // 每次循环都创建新对象
    }
    // 正确示例
    StringBuilder sb = new StringBuilder();
    for (int i = 0; i < 1000; i++) {
        sb.append(i);
    }
    String result = sb.toString();
  3. 使用 equals() 比较内容:要比较两个字符串的内容是否相同,务必使用 equals()equalsIgnoreCase() 方法。 比较的是内存地址(引用),这在大多数情况下不是你想要的。

  4. 注意 split() 的正则表达式split() 方法接收的是一个正则表达式,而不是普通的字符串,如果你要分割的字符串中包含正则表达式中的特殊字符(如 、、 等),需要进行转义。

    // 错误:. 在正则中匹配任意字符
    String[] parts1 = "192.168.1.1".split(".");
    System.out.println(parts1.length); // 输出可能是 0,而不是 4
    // 正确:使用 \\ 进行转义
    String[] parts2 = "192.168.1.1".split("\\.");
    System.out.println(parts2.length); // 输出 4

希望这份详细的讲解能帮助你彻底理解 Java 中的 String

分享:
扫描分享到社交APP
上一篇
下一篇