【Java】String

String 原理


字符串构造

  1. String是 Java 里代表字符串的引用类型

  2. 4 种常见写法:

    复制代码
            //1. 使用字符串常量直接赋值
            String s1="hello 2026";
            System.out.println(s1);
    
            //2. new String 对象(在堆创建对象)
            String s2=new String("hello 2026");
            System.out.println(s2);
    
            //3. 使用字符数组构造字符串
            char[] array={'h','e','l','l','o',' ','2','0','2','6'};
            System.out.println(array);
    
            //4. 使用字节数组构造对象
            byte[] bytes={97,98,99,100};
            String  s4=new String(bytes);
            System.out.println(s4);
  3. String 源码核心

    复制代码
    public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence,
               Constable, ConstantDesc {
        @Stable
        private final byte[] value; // 底层字节数组,存储字符串内容
        private final byte coder;   // 编码标记:LATIN1 / UTF16
        private int hash;           // 缓存字符串的hash值,默认0
        private boolean hashIsZero; // 标记hash计算后是否真的为0,避免重复计算
    }

    重点:

    1)String被final修饰 → 类不能被继承

    2)value是private final byte[]:数组引用不可变,

    字符串一旦创建内容不可修改(不可变性)


字符串常量池 StringTable

  1. 字符串常量池本质是 JVM 里一个HashTable(哈希表),
    作用就是缓存字符串常量,复用字符串,避免重复创建相同字符串,
    节省内存、提升查找效率


  2. 重点:
    1)JDK6:常量池在永久代 ,永久代内存容易 OOM
    2)JDK7 重大改动:StringTable 从永久代移到堆 ;JDK8 继续保留在堆
    3)底层是 HashTable,HashTable 就是 key-value 结构,查找速度快

  3. 什么是 "池化技术"
    1)池:提前准备好一批资源放在一处,需要的时候直接拿,
    不用每次重新创建资源,提高效率

  4. 例子
    1)每次临时要钱:需要的时候再去申请,流程麻烦、速度慢(不使用池)
    2)一次性把一年生活费放到卡里,要用直接取:池化思想,资源提前备好,随取随用

  5. 其他池例子:
    1)数据库连接池:提前创建一批数据库连接放池子里,程序直接拿,
    不用每次新建连接
    2)线程池:提前创建线程,复用线程,避免频繁创建销毁线程开销

  6. 池化的共同目的:减少创建 / 销毁资源的开销,提升性能

复制代码
   String s1 = "hello";

执行逻辑:

  1. JVM 去StringTable(常量池 HashTable) 查找有没有hello

  2. 存在:直接返回池内字符串的引用,复用

  3. 不存在:在常量池中创建hello字符串,存入哈希表,返回引用

复制代码
   String s2 = new String("hello");
  1. 先去常量池找 "hello",没有就放入常量池

  2. 在堆内存新建一个 String 对象 ,对象内部 value 数组引用常量池的字符数据

结论:s1 == s2 → false,s1 指向常量池对象,s2 指向堆上新对象
9. 面试问题

1)为什么 JDK7 把 StringTable 移到堆

永久代内存空间小,而且永久代 GC 回收效率差,放永久代容易内存溢出;

放到堆中,GC 可以正常回收不再使用的字符串常量

2)StringTable 底层是什么

哈希表 HashTable,哈希表好处:查询快

字符串内存存储


例1
复制代码
           String str1="abc";
           String str2="abc";
           System.out.println(str1==str2); //true

  1. 栈:存放局部变量str1、str2,里面保存引用地址0x112233
    堆内的 StringTable 常量池:存放字符串字面量 abc
  2. 代码逻辑
    1. 执行str1 = "abc":JVM 去常量池查找abc,不存在 → 在常量池创建"abc",
      地址0x112233,栈中str1保存这个地址
    2. 执行str2 = "abc":去常量池查找,已经存在 "abc" ,不会新建,
      直接复用池里对象。栈中str2也保存地址0x112233
    3. str1 == str2:比较引用地址,两者地址完全一样,结果 true
  3. 结论
    双引号直接赋值,优先走常量池,相同字面量会复用同一个对象
例2
复制代码
           String str1=new String("abc");
           String str2=new String("abc");
           System.out.println(str1==str2); //false
  1. 代码逻辑
    1)第一次 new String("abc")
    a)
    先处理双引号字面量"abc":去常量池查找,
    没有就把 abc 放进 StringTable(地址0x112233)
    b) new关键字,在堆上创建一个全新的 String 对象(地址 0x556677) ,
    这个对象内部value数组引用常量池里的"abc"
    c) 栈变量str1保存堆上新对象地址0x556677
    2)第二次 new String("abc")
    a)
    abc 常量池已经存在,不再重复创建常量
    b)new 又在堆上新建另一个 String 对象(地址 0x998877) ,
    内部 value 依然指向常量池的abc
    c) 栈变量str2保存这个新对象地址0x998877
    3)str1 == str2:比较栈里保存的堆对象地址 ,0x998877 ≠ 0x556677 → false
  2. 重点
    1. 只要写new,一定会在堆中创建新 String 对象 ,哪怕内容相同
    2. 两个new String("abc"):字符串内容相同,但是堆上是两个独立对象,
      地址不同
    3. 常量池里的"abc"只会创建一次,被两个堆 String 对象的 value 共享
  3. 面试高频
  4. 踩坑点
    1. ==比较的是引用地址 ,不是字符串内容
    2. 如果想比较字符串内容,要用str1.equals(str2),equals 比较字符内容

常用方法


String 对象比较

  1. == 运算符

    核心规则:

    基本数据类型 :== 比较变量里面存储的值 是否相等

    引用数据类型(String 属于引用类型) :== 比较对象的地址 ,判断是不是同一个对象

    复制代码
            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 两个new出来是不同的对象,地址不同
            System.out.println(s2==s3); //false 地址不同
            System.out.println(s1==s4);  //true s4引用s1,指向同一个对象

    重点: new String() 每次都会在堆上创建新对象 ,
    所以哪怕内容一样,地址也不一样,== 结果是 false

  2. equals() 方法

    1. Object类原生equals()默认就是==比较地址

    2. String 重写了 equals :不再比较地址,逐个比较字符串里面的字符内容是否一样 , 返回boolean(true/false)

      复制代码
           String s1=new String("hello");
           String s2=new String("hello");
           String s3=new String("world");
      
           System.out.println(s1==s2); //false 地址不同
           System.out.println(s1==s3);  //false 地址不同
      
           System.out.println(s1.equals(s2)); //true 二者字符内容相同、
           System.out.println(s1.equals(s3)); //false 二者字符内容不同

    开发场景:判断两个字符串内容是否相等,用equals(),不要用==

  3. compareTo() 方法(字典序比较,返回 int)

    1. 字典序:按照字符的 Unicode 编码大小依次比较

    2. 返回值规则:
      a' 从第一个字符开始对比,遇到不相等字符 → 返回两个字符编码差值
      b' 前面所有字符都相同 → 返回两个字符串长度差值
      a'' 返回 0:两个字符串完全一样
      b'' 返回负数:当前字符串 < 参数字符串
      c'' 返回正数:当前字符串 > 参数字符串

      复制代码
           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,第2位 b比c小,差值为-1
      System.out.println(s1.compareTo(s3)); //0,全部字符相同
      System.out.println(s1.compareTo(s4)); //-3 前3个字符相同,长度差值为3

  4. compareToIgnoreCase()
    1)逻辑和compareTo()完全一致,忽略大小写 进行字典序比较

    复制代码
            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,第2位,c和b字符编码差值为1
            System.out.println(s1.compareToIgnoreCase(s3)); //0,忽略大小写相等
            System.out.println(s1.compareToIgnoreCase(s4)); //-3 前3个字符相同,二者长度差值为3

String 字符串查找

  1. 方法清单

复制代码
           String s="aaabbbcccaaabbbccc";
           //获取下标3位置的字符
           System.out.println(s.charAt(3)); // b

           //从字符串开头往后找,返回字符c第一次出现的下标
           System.out.println(s.indexOf('c')); // 6

           //从下标10的位置开始往后查找,返回字符c 第一次出现的下标
           System.out.println(s.indexOf('c',10)); //15

           //从开头向后查找子串bbb 返回子串bbb的起始下标
           System.out.println(s.indexOf("bbb",10)); //12

           //从字符串末尾向前查找,返回字符 c 最后一次出现的下标
           System.out.println(s.lastIndexOf('c')); //17

           //在下标10 以及他之前的范围,从后往前查找字符 c
           System.out.println(s.lastIndexOf('c',10)); // 8

           //从末尾向前查找子串bbb 返回子串的起始下标
           System.out.println(s.lastIndexOf("bbb")); // 12

           // 在下标10以及它之前的范围,从后往前查找子串bbb
           System.out.println(s.lastIndexOf("bbb",10)); //3 
  1. 1)indexOf:正向查找(左→右) ,找第一次 出现
    2)lastIndexOf:反向查找(右→左) ,找最后一次出现
  2. 带第二个参数fromIndex:
    1)indexOf:从 fromIndex 位置向右搜索
    2)lastIndexOf:从 fromIndex 位置向左搜索
  3. 注意
    1)所有查找方法找不到目标时,统一返回 -1
    2)charAt 如果下标超出范围,不是返回 - 1,而是抛出异常

转换


数值和字符串相互转换
  1. 数字 / 对象 → 字符串:String.valueOf()

    1)String.valueOf(任意类型):可以把数字、布尔、对象全部转为字符串

    2)对象调用 valueOf 时,底层会自动调用对象的 toString() 方法

    如果 Student 类没有重写 toString (),会输出对象地址;重写后输出自定义内容

    3)

    复制代码
    public class Student {
        String name;
        int age;
    
        public Student(String name, int age) {
            this.name = name;
            this.age = age;
        }
    
        @Override
        public String toString() {
          return "Student : "+this.name+" "+this.age;
        }
    }
    
        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("zhangsan",16));
    
            System.out.println(s1);
            System.out.println(s2);
            System.out.println(s3);
            System.out.println(s4);
        }
  2. 字符串 → 数值:包装类的 parseXxx()

    1)规则:

    a)Integer.parseInt(字符串):字符串必须是合法数字,不能带字母,
    否则报错NumberFormatException b)Double.parseDouble(字符串):解析浮点数字符串

    2)

    复制代码
            //字符串转数字
            int data1=Integer.parseInt("1234");
            double data2=Double.parseDouble("12.34");
            System.out.println(data1);
            System.out.println(data2);

    Integer、Double 是基本数据类型对应的包装类

  3. 重要坑点

    1)字符串转数字时,字符串内容必须是纯数字

    复制代码
    Integer.parseInt("12a3"); // 编译报错

    2)String.valueOf() 几乎万能,支持所有基本类型和对象转字符串

    3)区分两个方法:

    a)parseInt:字符串 → int
    b)String.valueOf:任意类型 → String


String 大小写转换
  1. 核心方法

    1)字符串.toUpperCase():小写 → 大写

    2)字符串.toLowerCase():大写 → 小写

    3)Java 中 String 是不可变字符串:

    调用这两个方法,不会修改原来的字符串,会返回一个新字符串

复制代码
           String s1="hello";
           String s2="HELLO";
           //小写转大写
           System.out.println(s1.toUpperCase()); //HELLO
           //大写转小写
           System.out.println(s2.toLowerCase()); //hello
  1. 重点
    1)String 一旦创建,内容不能修改,toUpperCase() 只是生成新字符串,保留结果, 得用变量接收

    复制代码
            String s="abc";
            s.toUpperCase();
            System.out.println(s);  // abc 小写转大写,原字符串没变
    
            String s2=s.toUpperCase();
            System.out.println(s2); //ABC
  2. 补充
    1)字符串里面的数字、符号不受影响,只会转换英文字母

    复制代码
            String str="hello123!";
            System.out.println(str.toUpperCase());  // HELLO123!

字符串与字符数组互转
  1. 核心方法

    1)字符串 → char 字符数组:字符串.toCharArray()

    2)char 字符数组 → 字符串 :new String(字符数组)

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

           //字符数组转回字符串
           String s2=new String(ch);  
           System.out.println(s2);  //hello
  1. 重点
    1)toCharArray() 返回新的字符数组 ,原字符串不会改变(String 不可变)
    2)new String(char[]):根据字符数组创建新字符串
    3)用途场景:需要修改字符串里单个字符的时候,可以先转成 char 数组,
    修改数组元素,再转回字符串

  2. 例:修改字符串的第 0 个字符

    复制代码
            //字符串转字符数组  --> 修改元素后,字符数组转字符串
            String s1="hello";
            char[] arr=s1.toCharArray();
            arr[0]='H';
            for(int i=0;i<arr.length;i++)
            {
                System.out.print(arr[i]+" "); 
            }  //H e l l o 
            System.out.println();
            String newStr=new String(arr);  
            System.out.println(newStr);  // Hello

    toCharArray():把字符串里的每一个字符,提取出来存入char\[\]数组


String.format () 字符串格式化
  1. 常用占位符

    1)%d:整数(decimal)

    2)%s:字符串(string)

    3)%f:浮点数(float)

    4)%c:单个字符(char)

  2. 语法

    复制代码
    String.format(格式字符串, 参数1, 参数2, 参数3...)

    作用:按照指定模板,拼接生成新字符串

复制代码
           String s=String.format("%d-%d-%d",2026,10,6);
           System.out.println(s);

           //%s 字符串占位
           String name="张三";
           String str1=String.format("姓名:%s",name);
           System.out.println(str1);

           // %f 浮点数 .2f代表保留2位小数
           double score=95.678;
           String str2=String.format("成绩 %.2f",score);
           System.out.println(str2);

           //组合使用
           String str3=String.format("姓名:%s , 年龄:%d , 性别:%s , 学号:%s ",
                   "张三",21,"男","2026106");
           System.out.println(str3);
  1. 考点
    1)String.format() 返回新字符串,原字符串不受影响
    2)占位符数量、类型必须和后面参数匹配,否则报错

    复制代码
    String.format("%d", "abc"); // 编译报错,类型没和后面传入的参数类型匹配

    3)和直接+字符串拼接相比:模板更清晰,适合日期、报表等格式化输出


字符串替换
复制代码
           // 字符串替换
           String str="hello world";
           System.out.println(str.replaceAll("l","_")); // he__o wor_d
           System.out.println(str.replaceFirst("l","_")); // he_lo world
  1. replace(CharSequence target, CharSequence replacement),
    它不使用正则,只是纯文本替换

  2. 重点
    1)String 不可变
    替换方法不会修改原来的字符串 ,返回的是全新字符串

    复制代码
            String str="hello world";
            str.replaceAll("l","_");
            System.out.println(str);  //hello world ,字符串替换后,原字符串不会改变

    如果想保存结果,需要用变量接收:

    复制代码
            String str="hello world";
            str.replaceAll("l","_");
            System.out.println(str);  //hello world ,字符串替换后,原字符串不会改变
    
            String newStr=str.replaceAll("l","_");
            System.out.println(newStr);  //he__o wor_d

    2)replaceAll 和 replaceFirst 的第一个参数是正则表达式
    坑举例:. 在正则里代表任意字符

    复制代码
            String s="a.b.c";
            System.out.println(s.replaceAll(".","#"));
            // 输出 ##### . 匹配了所有字符,不是匹配 .  字符
            
            // 匹配文本 .  需要转义
            System.out.println(s.replaceAll("\\.","#"));
            // a#b#c

    a) 匹配文本.,需要转义:replaceAll("\\.", "#")
    b) 如果只是想做普通文本替换,不想碰正则,优先使用 replace("原文本","新文本")

    复制代码
            System.out.println(s.replace("\\.","#"));
            // a#b#c
  3. 小结
    1)replaceAll:全部替换,支持正则
    2)replaceFirst:只替换第一个,支持正则
    3)replace:全部替换,纯文本,不支持正则


相关推荐
Wang's Blog1 小时前
Java框架 SpringCloud 快速入门: 服务拆分之服务远程调用
java·开发语言·spring cloud
Wang's Blog1 小时前
Java框架 SpringCloud 快速入门: 路由断言工厂
java·spring·spring cloud
ym hyd 1111 小时前
门诊挂号系统源码 Java+SpringBoot+Vue3 前后分离
java·vue.js·spring boot·毕设
杨丰玮4181 小时前
C语言核心语法
java·c语言·开发语言·学习方法
海宇AI1 小时前
Java数据工程:利用海宇婚恋风险报告优化高端婚恋实名与涉诉核验合规体验
java·人工智能
CEZ2 小时前
中小企业上 WMS 该先上哪几块:JeeWMS 开源 Java 仓库管理系统的分批上线清单
java·开源
Wang's Blog2 小时前
Java框架 SpringCloud 快速入门: Gateway 路由的过滤器配置
java·spring cloud·gateway
迅猛龙办公室2 小时前
python实现简单进度条
java·前端·python
斯内普吖2 小时前
(开源)宠物饲养系统实战指南 基于 Java + SSM + Vue + MySQL
java·vue.js·mysql·开源·宠物