【Java篇】存在即不变:深刻解读String类不变的艺术

文章目录

  • 认识String类:Java字符串的奥秘
    • 本节目标
    • 一、String类的重要性
    • 二、常用方法
      • [2.1 字符串构造](#2.1 字符串构造)
      • [2.2 String对象的比较](#2.2 String对象的比较)
      • [2.3 字符串查找](#2.3 字符串查找)
      • [2.4 转化](#2.4 转化)
        • [2.4.1 数值和字符串转化](#2.4.1 数值和字符串转化)
        • [2.4.2 大小写转换](#2.4.2 大小写转换)
        • [2.4.3 字符串转数组](#2.4.3 字符串转数组)
        • [2.4.4 格式化](#2.4.4 格式化)
      • [2.5 字符串替换](#2.5 字符串替换)
      • [2.6 字符串拆分](#2.6 字符串拆分)
      • [2.7 字符串截取](#2.7 字符串截取)
      • [2.8 去除字符串前后空格](#2.8 去除字符串前后空格)
      • [2.9 字符串的不可变性](#2.9 字符串的不可变性)
      • [2.10 字符串修改](#2.10 字符串修改)
        • [2.10.1 字符串直接修改的低效性](#2.10.1 字符串直接修改的低效性)
        • [2.10.2 使用StringBuffer和StringBuilder](#2.10.2 使用StringBuffer和StringBuilder)
    • [3. StringBuilder和StringBuffer](#3. StringBuilder和StringBuffer)
      • [3.1 StringBuilder的介绍](#3.1 StringBuilder的介绍)
      • [3.2 面试题](#3.2 面试题)
    • 四、总结和展望

认识String类:Java字符串的奥秘

💬 欢迎讨论:如果你对本篇内容有任何疑问或想深入探讨,欢迎在评论区留言交流!

👍 点赞、收藏与分享:觉得内容有帮助就请点赞、收藏并分享给更多学习Java的小伙伴!

🚀 继续学习之旅:本篇文章将详细讲解String类的基本概念、常用操作、不可变性及相关优化技巧,带你领略Java字符串的奥秘!


本节目标

  1. 认识String类的重要性
  2. 了解String类的基本用法
  3. 熟练掌握String类的常见操作
  4. 认识字符串常量池
  5. 认识StringBuffer和StringBuilder

一、String类的重要性

在C语言中已经涉及到字符串了,但是在C语言中要表示字符串只能使用字符数组或者字符指针,可以使用标准库提供的字符串系列函数完成大部分操作,但是这种将数据和操作数据方法分离开的方式不符合面向对象的思想,而字符串应用又非常广泛,因此Java语言专门提供了String类。

在开发和校招笔试中,字符串也是常客,比如:

  • 字符串转整形数字
  • 字符串相加

而且在面试中也频繁被问到,比如:StringStringBuffStringBulider之间的区别等。


二、常用方法

2.1 字符串构造

String类提供的构造方式非常多,常用的就以下三种:

其他方法需要用到时,大家参考Java在线文档:String官方文档

java 复制代码
public static void main(String[] args) {
    // 使用常量串构造
    String s1 = "hello bit";
    System.out.println(s1);
    // 直接new String对象
    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. String是引用类型,内部并不存储字符串本身,在String类的实现源码中,String类实例变量如下:

    java 复制代码
    public static void main(String[] args) {
        // s1和s2引用的是不同对象,s1和s3引用的是同一对象
        String s1 = new String("hello");
        String s2 = new String("world");
        String s3 = s1;
        System.out.println(s1.length()); // 获取字符串长度---输出5
        System.out.println(s1.isEmpty()); // 如果字符串长度为0,返回true,否则返回false
    }
  1. 在Java中""引起来的也是String类型对象。
java 复制代码
   // 打印"hello"字符串(String对象)的长度
   System.out.println("hello".length());

2.2 String对象的比较

字符串的比较是常见操作之一,比如:字符串排序。Java中总共提供了4种方式:

  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
    }
  2. boolean equals(Object anObject) 方法:按照字典序比较
    String类重写了父类Object中equals方法,Objectequals默认按照==比较,String重写equals方法后,按照如下规则进行比较,比如: s1.equals(s2)

    代码说明如下:

    java 复制代码
    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;
    }
java 复制代码
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
}
  1. int compareTo(String s) 方法: 按照字典序进行比较
    equals不同,compareTo返回的是int类型。具体比较方式:
    1. 先按照字典次序大小比较,如果出现不等的字符,直接返回这两个字符的大小差值;
    2. 如果前k个字符相等(k为两个字符串长度最小值),返回值为两个字符串长度的差值。
java 复制代码
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.compareTo(s2)); // 不同输出字符差值-1
    System.out.println(s1.compareTo(s3)); // 相同输出 0
    System.out.println(s1.compareTo(s4)); // 前k个字符完全相同,输出长度差值 -3
}
  1. int compareToIgnoreCase(String str) 方法:
    compareTo方式相同,但是忽略大小写比较。
java 复制代码
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
}

2.3 字符串查找

字符串查找也是字符串中非常常见的操作,String类提供了常用查找的方法:

方法 功能
char charAt(int index) 返回index位置上的字符,如果index为负数或者越界,抛出IndexOutOfBoundsException异常
int indexOf(int ch) 返回ch第一次出现的位置,没有返回-1
int indexOf(int ch, int fromIndex) 从fromIndex位置开始找ch第一次出现的位置,没有返回-1
int indexOf(String str) 返回str第一次出现的位置,没有返回-1
int indexOf(String str, int fromIndex) 从fromIndex位置开始往后找str第一次出现的位置,没有返回-1
int lastIndexOf(int ch) 从后往前找,返回ch第一次出现的位置,没有返回-1
int lastIndexOf(int ch, int fromIndex) 从fromIndex位置开始往前找ch第一次出现的位置,没有返回-1
int lastIndexOf(String str) 从后往前找,返回str第一次出现的位置,没有返回-1
int lastIndexOf(String str, int fromIndex) 从fromIndex位置开始往前找str第一次出现的位置,没有返回-1

注意:上述方法均为实例方法。

java 复制代码
public static void main(String[] args) {
	String s = "aaabbbcccaaabbbccc";
	System.out.println(s.charAt(3)); // 'b'
	System.out.println(s.indexOf('c')); // 6
	System.out.println(s.indexOf('c', 10)); // 15
	System.out.println(s.indexOf("bbb")); // 3
	System.out.println(s.indexOf("bbb", 10)); // 12
	System.out.println(s.lastIndexOf('c')); // 17
	System.out.println(s.lastIndexOf('c', 10)); // 8
	System.out.println(s.lastIndexOf("bbb")); // 12
	System.out.println(s.lastIndexOf("bbb", 10)); // 3
}

2.4 转化

2.4.1 数值和字符串转化
java 复制代码
public static void main(String[] args) {
    // 数字转字符串
    String s1 = String.valueOf(1234);
    String s2 = String.valueOf(12.34);
    String s3 = String.valueOf(true);
    String s4 = String.valueOf(new Student("Hanmeimei", 18));
    System.out.println(s1);
    System.out.println(s2);
    System.out.println(s3);
    System.out.println(s4);
    System.out.println("=================================");
    // 字符串转数字
    // 注意:Integer、Double等是Java中的包装类型,这个后面会讲到
    int data1 = Integer.parseInt("1234");
    double data2 = Double.parseDouble("12.34");
    System.out.println(data1);
    System.out.println(data2);
}

2.4.2 大小写转换
java 复制代码
public static void main(String[] args) {
    String s1 = "hello";
    String s2 = "HELLO";
    // 小写转大写
    System.out.println(s1.toUpperCase());
    // 大写转小写
    System.out.println(s2.toLowerCase());
}

2.4.3 字符串转数组
java 复制代码
public static void main(String[] args) {
    String s = "hello";
    // 字符串转字符数组
    char[] ch = s.toCharArray();
    for (int i = 0; i < ch.length; i++) {
        System.out.print(ch[i]);
    }
    System.out.println();
    // 数组转字符串
    String s2 = new String(ch);
    System.out.println(s2);
}

2.4.4 格式化
java 复制代码
public static void main(String[] args) {
    String s = String.format("%d-%d-%d", 2019, 9, 14);
    System.out.println(s);
}

2.5 字符串替换

使用一个指定的新字符串替换掉已有的字符串数据,可用的方法如下:

java 复制代码
public static void main(String[] args) {
    String str = "helloworld";
    System.out.println(str.replaceAll("l", "_"));
    System.out.println(str.replaceFirst("l", "_"));
}

注意:由于字符串是不可变对象,替换操作不会修改当前字符串,而是产生一个新的字符串。

常用方法说明:

  • String replaceAll(String regex, String replacement)

    替换所有匹配正则表达式regex的内容为replacement。

  • String replaceFirst(String regex, String replacement)

    仅替换第一个匹配的内容。


2.6 字符串拆分

可以将一个完整的字符串按照指定的分隔符划分为若干个子字符串。可用方法如下:

java 复制代码
public static void main(String[] args) {
    String str2 = "hello world hello bit";
    // 按照空格拆分
    String[] result = str2.split(" ");
    for(String s: result) {
        System.out.println(s);
    }
    
    String[] result2 = str2.split(" ", 2);
    for(String s: result2) {
        System.out.println(s);
    }
}

注意事项:

  1. 特殊字符如"|", "*", "+"需加上转义符(前面加 "\\")。
  2. 而如果是 "\" ,那么就得写成 "\\\\"
  3. 若字符串中有多个分隔符,可用"|"作为连接符。

例如,拆分IP地址:

java 复制代码
public static void main(String[] args) {
    String str = "192.168.1.1";
    String[] result = str.split("\\.");
    for(String s: result) {
        System.out.println(s);
    }
}

多次拆分:

java 复制代码
String str = "name=zhangsan&age=18";
String[] result = str.split("&");
for (int i = 0; i < result.length; i++) {
    String[] temp = result[i].split("=");
    System.out.println(temp[0] + " = " + temp[1]); // 输出键值对
}

2.7 字符串截取

从一个完整的字符串中截取出部分内容。可用方法如下:

  • String substring(int beginIndex)

    从指定索引截取到结尾。

  • String substring(int beginIndex, int endIndex)

    截取部分内容。

java 复制代码
public static void main(String[] args) {
    String str = "helloworld";
    System.out.println(str.substring(5));   // 输出 "world"
    System.out.println(str.substring(0, 5)); // 输出 "hello"
}

注意事项:

  1. 索引从0开始。
  2. 注意前闭后开区间的写法,substring(0, 5) 表示包含0号下标的字符,不包含5号下标。

补充:

如果截取的字符串就是原串,那这时不会返回新的对象,而是原对象的引用


2.8 去除字符串前后空格

方法 功能
String trim() 去掉字符串中的左右空格,保留中间空格

代码示例:

java 复制代码
// 使用trim()方法去除字符串两端的空格
String str = "  hello world  ";
System.out.println("[" + str + "]");
System.out.println("[" + str.trim() + "]"); // 输出 "hello world"

注意: trim 会去掉字符串开头和结尾的空白字符(空格、换行、制表符等)。


2.9 字符串的不可变性

String是一种不可变对象,字符串中的内容是不可改变的。字符串不可被修改,是因为:

  1. String类被final修饰,表明该类不能被继承,因此避免子类修改破坏其的不可变性。
  2. value数组被final修饰 ,表明value自身的值不能改变,即不能引用其它字符数组,但其引用空间中的内容可以修改。但是即使如此,value 数组是 private 的,且 String 类没有提供修改的方法,这才是String类字符串不可变性的根本原因
  3. 所有涉及到可能修改字符串内容的操作都会创建一个新对象,改变的是新对象。例如,replace方法。

注意事项:

网上有些人说:字符串不可变是因为其内部保存字符的数组被final修饰了,因此不能改变。这种说法是错误的,final修饰类表明该类不想被继承,final修饰引用类型表明该引用变量不能引用其他对象,但其引用对象中的内容是可以修改的,原因上面已经说过了。

不可变对象的优点:

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

2.10 字符串修改

2.10.1 字符串直接修改的低效性

尽量避免直接对String类型对象进行修改,因为String类是不可变的,所有的修改都会创建新对象,效率非常低。

示例代码:

java 复制代码
String s = "hello";
s += " world"; // 字符串拼接,生成了一个新的对象
System.out.println(s); // 输出:hello world

效率问题:

直接修改字符串会不断创建新的对象,效率低下。例如,以下代码拼接字符串时,效率非常差:

java 复制代码
long start = System.currentTimeMillis();
String s = "";
for (int i = 0; i < 10000; ++i) {
    s += i; // 低效的拼接方式
}
long end = System.currentTimeMillis();
System.out.println(end - start);
2.10.2 使用StringBuffer和StringBuilder

为了解决String修改效率低的问题,Java提供了StringBufferStringBuilder,它们都支持可变的字符序列,适合在大量修改字符串时使用。


3. StringBuilder和StringBuffer

3.1 StringBuilder的介绍

由于String的不可更改特性,为了方便字符串的修改,Java中提供了StringBuilder和StringBuffer类。它们大部分功能相同,这里主要介绍StringBuilder常用的方法。需要注意的是,String和StringBuilder之间不能直接转换,转换原则如下:

  • String转为StringBuilder: 利用StringBuilder的构造方法或append()方法。
  • StringBuilder转为String: 调用toString()方法。

下面是StringBuilder常用方法及其说明:

方法 说明
StringBuilder append(String str) 在尾部追加,相当于String的"+=",可追加boolean、char、char[]、double、float、int、long、Object、String、StringBuilder的变量
char charAt(int index) 获取index位置上的字符
int length() 获取字符串的有效长度
int capacity() 获取底层保存字符串空间的总大小
void ensureCapacity(int minimumCapacity) 扩容,确保内部容量至少为minimumCapacity
void setCharAt(int index, char ch) 将index位置的字符设置为ch
int indexOf(String str) 返回str第一次出现的位置;如果不存在则返回-1
int indexOf(String str, int fromIndex) 从fromIndex位置开始查找str第一次出现的位置;如果不存在则返回-1
int lastIndexOf(String str) 返回str最后一次出现的位置;如果不存在则返回-1
int lastIndexOf(String str, int fromIndex) 从fromIndex位置开始向前查找str最后一次出现的位置;如果不存在则返回-1
StringBuilder insert(int offset, String str) 在offset位置插入指定的字符串,可插入八种基本类型、String类型和Object类型的数据
StringBuilder deleteCharAt(int index) 删除index位置上的字符
StringBuilder delete(int start, int end) 删除[start, end)区间内的字符
StringBuilder replace(int start, int end, String str) 将[start, end)区间内的字符替换为指定的str
String substring(int start) 从start开始一直到末尾的字符,以String的方式返回
String substring(int start, int end) 截取[start, end)区间内的字符,以String的方式返回
StringBuilder reverse() 反转当前字符串
String toString() 将StringBuilder中的字符转换为String

示例代码:

java 复制代码
public static void main(String[] args) {
    StringBuilder sb1 = new StringBuilder("hello");
    StringBuilder sb2 = sb1;
    
    // 追加操作:追加字符、字符串和整型数字
    sb1.append(' ');          // 追加一个空格,使内容变为 "hello "
    sb1.append("world");      // 内容变为 "hello world"
    sb1.append(123);          // 内容变为 "hello world123"
    
    System.out.println(sb1);  // 输出:hello world123
    System.out.println(sb1 == sb2);  // 输出:true,sb1和sb2引用的是同一个对象
    
    // 获取和设置字符
    System.out.println(sb1.charAt(0)); // 输出第0号位字符 'h'
    System.out.println(sb1.length());  // 输出当前字符串长度
    System.out.println(sb1.capacity()); // 输出底层数组的总大小
    
    sb1.setCharAt(0, 'H');    // 将首字符修改为 'H', 结果为 "Hello world123"
    
    // 插入字符串
    sb1.insert(0, "Hello world!!!"); // 在开头插入 "Hello world!!!"
    System.out.println(sb1);
    
    // 查找子字符串位置
    System.out.println(sb1.indexOf("Hello"));       // 返回 "Hello" 第一次出现的位置
    System.out.println(sb1.lastIndexOf("hello"));     // 返回 "hello" 最后一次出现的位置
    
    // 删除操作
    sb1.deleteCharAt(0);      // 删除首字符
    sb1.delete(0, 5);         // 删除索引区间 [0, 5) 内的字符
    
    // 截取字符串
    String str = sb1.substring(0, 5);  // 截取[0, 5)区间内的字符
    System.out.println(str);
    
    // 反转字符串
    sb1.reverse();
    str = sb1.toString();     // 转换为String
    System.out.println(str);
}

从上述示例可以看出,StringBuilder提供了丰富的方法来修改字符串内容,使其在频繁修改场景下具有较高的效率。

StringBuilder在线文档


3.2 面试题

面试题:

  1. String、StringBuffer、StringBuilder的区别:
    • String: 内容不可修改,属于不可变对象。
    • StringBuffer和StringBuilder: 内容可以修改,属于可变对象,功能基本相同。
    • 线程安全性:
      • StringBuffer采用同步机制,线程安全;
      • StringBuilder未采用同步机制,线程不安全,但在单线程环境下效率更高。

四、总结和展望

本文主要介绍了Java中String类的各项操作技巧,从字符串的构造、比较、查找、截取、转化、替换、拆分等基本方法,到字符串不可变性的原理及其优势,再到针对频繁修改场景下的StringBuilder与StringBuffer的介绍。通过丰富的示例代码,帮助大家直观理解每种方法的使用和背后的设计理念,使读者能在实际开发中更准确地选择和应用不同的字符串处理工具。

下一篇文章我们将重点讲解异常处理机制,深入解析异常捕获、抛出以及自定义异常的实现,为编写健壮高效的程序提供实用指导。希望大家在不断实践中,能逐步掌握Java语言的核心技术,并不断提高代码质量与开发效率。


以上就是关于【Java篇】存在即不变:深刻解读String类不变的艺术的内容啦,各位大佬有什么问题欢迎在评论区指正,或者私信我也是可以的啦,您的支持是我创作的最大动力!❤️

相关推荐
lemon_sjdk2 小时前
java学习——枚举类
java·开发语言·学习
FreeBuf_2 小时前
Next.js 发布扫描工具:检测并修复受 React2Shell 漏洞(CVE-2025-66478)影响的应用
开发语言·javascript·ecmascript
2022.11.7始学前端2 小时前
n8n第九节 使用LangChain与Gemini构建带对话记忆的AI助手
java·人工智能·n8n
JosieBook3 小时前
【Spring Boot】Spring Boot调用 WebService 接口的两种方式:动态调用 vs 静态调用 亲测有效
java·spring boot·后端
a程序小傲3 小时前
京东Java面试被问:Spring拦截器和过滤器区别
java·面试·京东云·java八股文
御形封灵3 小时前
基于原生table实现单元格合并、增删
开发语言·javascript·ecmascript
应茶茶4 小时前
从 C 到 C++:详解不定参数的两种实现方式(va_args 与参数包)
c语言·开发语言·c++
Data_agent4 小时前
1688获得1688店铺列表API,python请求示例
开发语言·python·算法
2401_871260024 小时前
Java学习笔记(二)面向对象
java·python·学习