String 原理
字符串构造
String是 Java 里代表字符串的引用类型
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);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
字符串常量池本质是 JVM 里一个HashTable(哈希表),
作用就是缓存字符串常量,复用字符串,避免重复创建相同字符串,
节省内存、提升查找效率
重点:
1)JDK6:常量池在永久代 ,永久代内存容易 OOM
2)JDK7 重大改动:StringTable 从永久代移到堆 ;JDK8 继续保留在堆
3)底层是 HashTable,HashTable 就是 key-value 结构,查找速度快什么是 "池化技术"
1)池:提前准备好一批资源放在一处,需要的时候直接拿,
不用每次重新创建资源,提高效率例子
1)每次临时要钱:需要的时候再去申请,流程麻烦、速度慢(不使用池)
2)一次性把一年生活费放到卡里,要用直接取:池化思想,资源提前备好,随取随用其他池例子:
1)数据库连接池:提前创建一批数据库连接放池子里,程序直接拿,
不用每次新建连接
2)线程池:提前创建线程,复用线程,避免频繁创建销毁线程开销池化的共同目的:减少创建 / 销毁资源的开销,提升性能
String s1 = "hello";执行逻辑:
JVM 去StringTable(常量池 HashTable) 查找有没有
hello存在:直接返回池内字符串的引用,复用
不存在:在常量池中创建
hello字符串,存入哈希表,返回引用
String s2 = new String("hello");
先去常量池找 "hello",没有就放入常量池
在堆内存新建一个 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
栈:存放局部变量str1、str2,里面保存引用地址0x112233
堆内的 StringTable 常量池:存放字符串字面量 abc- 代码逻辑
- 执行
str1 = "abc":JVM 去常量池查找abc,不存在 → 在常量池创建"abc",
地址0x112233,栈中str1保存这个地址- 执行
str2 = "abc":去常量池查找,已经存在 "abc" ,不会新建,
直接复用池里对象。栈中str2也保存地址0x112233str1 == str2:比较引用地址,两者地址完全一样,结果 true- 结论
双引号直接赋值,优先走常量池,相同字面量会复用同一个对象例2
String str1=new String("abc"); String str2=new String("abc"); System.out.println(str1==str2); //false
- 代码逻辑
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- 重点
- 只要写
new,一定会在堆中创建新 String 对象 ,哪怕内容相同- 两个
new String("abc"):字符串内容相同,但是堆上是两个独立对象,
地址不同- 常量池里的
"abc"只会创建一次,被两个堆 String 对象的 value 共享- 面试高频
- 踩坑点
==比较的是引用地址 ,不是字符串内容- 如果想比较字符串内容,要用
str1.equals(str2),equals 比较字符内容
常用方法
String 对象比较
==运算符核心规则:
基本数据类型 :
==比较变量里面存储的值 是否相等引用数据类型(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
equals()方法
Object类原生equals()默认就是==比较地址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(),不要用==
compareTo()方法(字典序比较,返回 int)
字典序:按照字符的 Unicode 编码大小依次比较
返回值规则:
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个字符相同,长度差值为3compareToIgnoreCase()
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 字符串查找
方法清单
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)indexOf:正向查找(左→右) ,找第一次 出现
2)lastIndexOf:反向查找(右→左) ,找最后一次出现- 带第二个参数
fromIndex:
1)indexOf:从 fromIndex 位置向右搜索
2)lastIndexOf:从 fromIndex 位置向左搜索- 注意
1)所有查找方法找不到目标时,统一返回-1
2)charAt 如果下标超出范围,不是返回 - 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); }字符串 → 数值:包装类的 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 是基本数据类型对应的包装类
重要坑点
1)字符串转数字时,字符串内容必须是纯数字
Integer.parseInt("12a3"); // 编译报错2)String.valueOf() 几乎万能,支持所有基本类型和对象转字符串
3)区分两个方法:
a)
parseInt:字符串 → int
b)String.valueOf:任意类型 → String
String 大小写转换
核心方法
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)String 一旦创建,内容不能修改,toUpperCase() 只是生成新字符串,保留结果, 得用变量接收
String s="abc"; s.toUpperCase(); System.out.println(s); // abc 小写转大写,原字符串没变 String s2=s.toUpperCase(); System.out.println(s2); //ABC补充
1)字符串里面的数字、符号不受影响,只会转换英文字母
String str="hello123!"; System.out.println(str.toUpperCase()); // HELLO123!
字符串与字符数组互转
核心方法
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)toCharArray()返回新的字符数组 ,原字符串不会改变(String 不可变)
2)new String(char[]):根据字符数组创建新字符串
3)用途场景:需要修改字符串里单个字符的时候,可以先转成 char 数组,
修改数组元素,再转回字符串例:修改字符串的第 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); // HellotoCharArray():把字符串里的每一个字符,提取出来存入char\[\]数组
String.format () 字符串格式化
常用占位符
1)%d:整数(decimal)
2)%s:字符串(string)
3)%f:浮点数(float)
4)%c:单个字符(char)
语法
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)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
replace(CharSequence target, CharSequence replacement),
它不使用正则,只是纯文本替换重点
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_d2)
replaceAll和replaceFirst的第一个参数是正则表达式
坑举例:.在正则里代表任意字符
String s="a.b.c"; System.out.println(s.replaceAll(".","#")); // 输出 ##### . 匹配了所有字符,不是匹配 . 字符 // 匹配文本 . 需要转义 System.out.println(s.replaceAll("\\.","#")); // a#b#ca) 匹配文本.,需要转义:replaceAll("\\.", "#")
b) 如果只是想做普通文本替换,不想碰正则,优先使用 replace("原文本","新文本")
System.out.println(s.replace("\\.","#")); // a#b#c小结
1)replaceAll:全部替换,支持正则
2)replaceFirst:只替换第一个,支持正则
3)replace:全部替换,纯文本,不支持正则







