参考笔记:java String类 万字详解(通俗易懂)-CSDN博客
本文可以和我的另一篇博客 "Java中String的内存原理" 结合一起看:【Java SE】Java中String的内存原理-CSDN博客
目录
1.基本介绍
① String 是引用数据类型,用于保存字符串,也就是一组字符序列。例如:"你好"、"12.97"、"boy"等
② 字符串的字符使用 Unicode 字符编码,一个字符(不区分字母还是汉字)占两个字节
2.String类的final关键字和value属性
Java 的 String 类部分源码如下:
① String 类用 final 关键字修饰,因此 String 类是不可被其他类继承的
② 由 String 类中的 value 属性及其源码描述,可以得知 String 类底层实际是一个字符数组char[ ] 。即 new 出的 String 对象在堆空间中,该对象的空间里有 value 属性,用于存放字符串的内容(实际指向的是字符串常量池地址)
案例:String str = new String("小马")
可以看到,字符串对象 str 的 value[0] = '小',value[1] = '马'
内存原理图
③ value 数组的访问控制修饰符是 private ,意味着外部无法直接获取该字符数组,所以在 ② 的例子是无法使用 "str.value" 获取的,而且 String 没有提供 value 的 get 和 set 方法
④ value 数组使用 final 关键字修饰,而 value 的数据类型是 char[ ],即引用数据类型,所以用 final 关键字修饰表示 value 指向的地址是不可变的,但指向的地址存放的内容可变
结论:由于 ③ 和 ④ 的共同作用,Java 中的 String 具有不可变性,简单来说就是在字符串常量池中对于每个字符串常量,其内容一直不会被修改
只看文字描述不好理解,在 "6.String类的不可变性" 会举例子帮助理解
3.String类的继承关系图、常用的构造器
3.1 继承关系图
蓝色实线:继承extends
绿色虚线:实现implements
String 类除了继承 Object 类之外,还实现了 3 个接口。只需要记住关键的两个接口:
① Serializable接口:实现该接口使得 String 类型可串行化,串行化后,String 类型可以进行网络传输
② Comparable接口:实现该接口使得 String 类型的对象可以进行“比较”的操作
3.2 常用的构造器
String 类的所有构造器如下,本文只讲几个常用的:
① String():
该构造器初始化一个 String 对象,使其指向一个空字符序列
② String(byte[] bytes):
该构造器初始化一个 String 对象,并将指定字节数组中的数据转化成字符串
③ String(char[] value):
该构造器初始化一个 String 对象,并将指定字符数组中的数据转化成字符串
④ String(char[] value, int offset, int count):
该构造器初始化一个 String 对象,并将指定字符数组从 offset 下标开始的 count 个字符转化成字符串
⑤ String(String original):
该构造器初始化一个 String 对象,使该对象的字符串内容与 original 相同
案例演示
public class demo {
public static void main(String[] args) {
//演示 : String类的常用构造器
//1 —— String()
String str_0 = new String();
System.out.println(str_0);
System.out.println("--------------------");
//2 —— String(byte[] bytes)
byte[] bytes = {65, 66, 67, 68}; //65~68对应ASCII码表A~D
String str_1 = new String(bytes);
System.out.println(str_1);
System.out.println("--------------------");
//3 —— String(char[] value)
char[] value = {'唱', '跳', 'r', 'a', 'p', '篮', '球'};
String str_2 = new String(value);
System.out.println(str_2);
System.out.println("--------------------");
//4 —— String(char[] value, int offset, int count)
String str_3 = new String(value, 0, 4);
System.out.println(str_3);
System.out.println("--------------------");
//5 —— String(String original)
String str_4 = new String("ftt!");
System.out.println(str_4);
}
}
4.不同方式创建String类对象
4.1 直接赋值方式
String 类在实际开发中的使用场景非常多,因此 Java 在底层提供了针对 String 类的优化,即可以不通过构造器来创建 String 对象,而是直接赋值一个字符串
eg : String str = "CYC";使用这种方式来创建 String 对象,JVM 会先在字符串常量池中查看是否有 "CYC" 字符串的数据空间:
① 若有,直接将空间地址返回给 str
② 若无,在字符串常量池种开辟 "CYC" 的数据空间,并将空间地址返回给 str
因此,直接赋值方式下,无论是 ① 还是 ② , str 引用最终直接指向的都是字符串常量池中 "CYC" 的空间地址
内存原理图
4.2 new方式赋值
String 类的 value 属性:private final char value[ ]
eg : String str = new String("CYC");
使用这种方式来创建 String 对象,JVM 会先在堆中给 String 对象开辟空间,这片空间中维护了 value 数组这一属性。然后 JVM 在字符串常量池中查看是否有 "CYC" 字符串的数据空间:
① 若有,直接将空间地址返回给 value
② 若无,先在字符串常量池种开辟 "CYC" 的数据空间,然后将空间地址返回给 value
因此,new 方式赋值下,无论是 ① 还是 ②,str 引用直接指向的都是在堆中开辟的空间地址, value 引用最终指向的都是字符串常量池中 "CYC" 的空间地址
内存原理图
4.3 其他赋值方式
其他赋值方式和内存原理图可以看我之前写的一篇博客的 "4.3 new和直接赋值混合"、"4.4 字符串拼接" 部分:
【Java SE】Java中String的内存原理-CSDN博客
5.String类常用方法
String 类中的方法非常多,本文仍然是只讲常用的
5.1 判断功能的方法
① boolean equals(Object anObject) :判断两个字符串的内容是否相同
源码如下,还是比较简单的,我也作了注释
public boolean equals(Object anObject) {
if (this == anObject) {//如果引用地址相同,那肯定不用说,字符串内容肯定也相同
return true;
}
if (anObject instanceof String) {//判断传进来的anObject是否是String类型
String anotherString = (String)anObject;//向下转型
int n = value.length;
if (n == anotherString.value.length) {//判断两个字符串的长度是否相等,不相等肯定返回false
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
while (n-- != 0) {//一个字符一个字符作比较
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
return false;
}
② boolean equalsIgnoreCase(String anotherString):判断两个字符串的内容是否相同,不区分大小写
③ boolean startsWith(String prefix):判断当前字符串是否以 prefix 开头
④ boolean isEmpty():判断当前字符串是否为空
案例
public class demo {
public static void main(String[] args) {
//演示 : String常用方法之判断功能的方法
//1 —— boolean equals(Object anObject)
String str_0 = "cyan";
System.out.println("\"Cyan\"字符串与str_0字符串的内容相等吗?" + "Cyan".equals(str_0));
System.out.println("================");
//2 —— boolean equalsIgnoreCase(String anotherString)
System.out.println("\"Cyan\"字符串与str_0字符串的内容相等吗?" + "Cyan".equalsIgnoreCase(str_0));
System.out.println("================");
//3 —— boolean startWith(String prefix)
System.out.println("str_0字符串是否以\"cy\"开头?" + str_0.startsWith("cy"));
System.out.println("str_0字符串是否以\"Cy\"开头?" + str_0.startsWith("Cy"));
System.out.println("================");
//4 —— boolean isEmpty()
System.out.println("str_0字符串是否为空?" + str_0.isEmpty());
}
}
5.2 获取功能的方法
① int length():获取当前字符串的长度
② char charAt(int index):获取字符串指定位置索引的字符
③ int indexOf(String str):获取指定字符(串) 第一次出现的索引,若返回 -1,则表示没有该指定字符(串)
④ int indexOf(String str, int t):功能与 ③ 方法相同,但默认从字符串索引第 t 位开始找
⑤ int lastIndexOf(String str):获取指定字符(串)最后一次出现的索引,若没有,返回 -1
⑥ int compareTo(String anotherString):返回两个字符串对象的比较结果,有 3 种情况:
(1)字符串长度相同,内容相同:返回 0
(2)字符串长度相同,内容不同:从两个字符串的第一个字符开始比较,返回第一个不相等的字符的 ASCII 码差值
(3)字符串长度不相同,但较长字符串的前面部分正好是较短字符串:返回两个字符串的长度差
⑦ String substring(int beginIndex):获取指定索引位置(包含该位置) 之后的字符串。
⑧ String substring(int beginIndex, int endIndex):获取从索引 beginIndex 开始,到索引 endIndex 之间的字符串(区间为前闭后开)
案例
public class demo {
public static void main(String[] args) {
//演示 : String常用方法之获取功能的方法
//1 —— int length()
String str_0 = "We are the best.";
System.out.println("当前字符串str_0 : " + str_0);
int length = str_0.length();
System.out.println("str_0字符串的长度为:" + length);
System.out.println("========================");
//2 —— char charAt(int index)
char ch_0 = str_0.charAt(0);
char ch_1 = str_0.charAt(5);
System.out.println("str_0字符串中,索引为0的字符是:" + ch_0);
System.out.println("str_0字符串中,索引为5的字符是:" + ch_1);
System.out.println("========================");
//3 —— int indexOf(String str)
int i_0 = str_0.indexOf('e');
System.out.println("字符\'e\'在字符串str_0中首次出现时的索引为:" + i_0);
int i_2 = str_0.indexOf("are");
System.out.println("字符串\"are\"在字符串str_0中首次出现时的索引为:" + i_2);
System.out.println("========================");
//4 —— int indexOf(String str, int t)
int i_1 = str_0.indexOf('e', 7);
System.out.println("从字符串str_0索引为7的地方开始找,字符\'e\'在字符串str_0中首次出现时的索引为:" + i_1);
System.out.println("========================");
//5 —— int lastIndexOf(String str)
int i_3 = str_0.lastIndexOf('t');
System.out.println("字符\'t\'在字符串str_0中最后一次出现时的索引为:" + i_3);
System.out.println("========================");
//6 —— int compareTo(String anotherString)
//第一种情况 : 俩字符串相同
int i_4 = str_0.compareTo("We are the best.");
System.out.println("compareTo方法返回0说明俩字符串相等: " + i_4);
//第二种情况 : 俩字符串长度相同,内容不同
int i_5 = str_0.compareTo("You are so good."); //W : 77, Y : 79.
System.out.println("\'W\'和\'Y\'的ASCII码差值为:" + i_5);
//第三种情况 : 俩字符串长度不相同,但长的字符串的前一部分与短的字符串相同。
int i_6 = str_0.compareTo("We are the best.we are the king!");
int i_7 = "We are the best.we are the king!".compareTo(str_0);
System.out.println("str_0字符串与传入字符串的长度差值为:" + i_6);
System.out.println("传入字符串与str_0字符串的长度差值为:" + i_7);
System.out.println("========================");
//7 —— String substring(int beginIndex)
String str_1 = str_0.substring(11); //"best."
System.out.println("str_0字符串中,从索引11开始到结束的字符串为:" + str_1);
System.out.println("========================");
//8 —— String substring(int beginIndex, int endIndex)
String str_2 = str_0.substring(7, 10); //[7,10)——注意区间的格式。
System.out.println("str_0字符串中,从索引7到10(不包括10)的字符串为:" + str_2);
}
}
5.3 转换功能的方法
① byte[] getBytes():将字符串转换成字节数组,字节数据中存放每个字符的 ASCII 码值
② char[] toCharArray():将字符串转换成字符数组
③ static String valueOf(...):将指定类型数据转换成字符串
④ String replace(char oldChar, char newChar):将字符串中的 oldChar 字符全部替换为 newChar ,然后返回一个新的字符串。原字符串不受影响
⑤ String[] split(String regex):以传入的字符(串)为标准来切割原字符串,返回切割后的字符串数据(保存在数组中),以 String 类型数组作为接收。原字符串不受影响
⑥ String trim():去掉字符串两端的空白字符(可用于登录账号和密码的处理)
⑦ String concat(String str):将传入的字符(串)拼接到当前字符串的末尾
⑧ String toUpperCase():将当前字符串全部转换成大写后返回
⑨ String toLowerCase():将当前字符串全部转换成小写后返回
⑩ static String format(String format, Object... args):将指定参数以指定的输出控制格式来输出。形参 "String format" 即输出格式控制,是包含占位符的 String 类型。形参 "Object... args" 即要输出的参数,如下所示:
案例
public class demo {
public static void main(String[] args) {
//演示 : String常用方法之转换功能的方法
//1 —— byte[] getBytes()
String str_0 = "ABCD"; //A~D对应ASCII码值 = 65~68
byte[] bytes = str_0.getBytes();
for (int i = 0; i < bytes.length; i++) {
System.out.println("字节数组的第" + (i + 1) + "个元素 = " + bytes[i]);
}
System.out.println("-----------------------");
//2 —— char[] toCharArray()
char[] chars = str_0.toCharArray();
for (int i = 0; i < chars.length; i++) {
System.out.println("字符数组的第" + (i + 1) + "个元素 = " + chars[i]);
}
System.out.println("-----------------------");
//3 —— static String valueOf(...)
String str_1 = String.valueOf(1111);
String str_2 = String.valueOf('h');
String str_3 = String.valueOf(123.6666);
System.out.println("int类型转String类型:" + str_1);
System.out.println("char类型转String类型:" + str_2);
System.out.println("double类型转String类型:" + str_3);
System.out.println("-----------------------");
//4 —— String replace(char oldChar, char newChar)
String str_4 = "You_are_so_beautiful!";
String str_5 = str_4.replace('_', ' '); //以' '替换掉'_'
System.out.println("原字符串:" + str_4);
System.out.println("新字符串:" + str_5);
System.out.println("-----------------------");
//5 —— String[] split(String regex)
String str_6 = "What hahaa hahacoincidence haha!";
String[] strings = str_6.split("haha"); //以"haha"来切割字符串。
for (String string : strings) {
System.out.print(string);
}
System.out.println("\n-----------------------");
//6 —— String trim()
String str_7 = " hey,man! ";
String str_8 = str_7.trim();
System.out.println("去掉两端空白字符前的字符串:" + str_7);
System.out.println("去掉两端空白字符后的字符串:" + str_8);
System.out.println("-----------------------");
//7 —— String concat(String str)
String str_9 = "I ";
String str_10 = str_9.concat("love ").concat("programming!");
System.out.println("拼接后的字符串str_10 = " + str_10);
System.out.println("-----------------------");
//8 —— String toUpperCase()
String str_11 = "i love you!";
System.out.println("大写之前的字符串:" + str_11);
System.out.println("大写之后的字符串:" + str_11.toUpperCase());
System.out.println("-----------------------");
//9 —— String toLowerCase()
System.out.println("小写之前的字符串:" + str_11.toUpperCase());
System.out.println("小写之后的字符串:" + str_11.toUpperCase().toLowerCase());
System.out.println("-----------------------");
//10 —— static String format(String format, Object... args)
String name = "Cyan";
int age = 21;
String hobby = "programming!";
String sss = String.format("My name is %s, and I'm %d years old, my hobby is %s", name, age, hobby);
System.out.println(sss);
}
}
6.String类的不可变性(非常重要)
在本文的 "2.String类的final关键字和value属性" ③ 和 ④ 的共同作用下,Java 中的 String 具有不可变性,简单来说就是在字符串常量池中对于每个字符串常量,其内容一直不会被修改
结论:String 是不可变的,对于 String 的任何修改操作都会生成新对象,而原引用不受影响
下面举一些例子来帮助理解
6.1 直接修改字符串
示例代码
public class test {
public static void main(String[] args) {
String s = "123";
System.out.println("\"123\"地址对应的hashCode值:"+s.hashCode());
/*
(1)正确解读:s = "小马" 等价于 String s = "小马",创建了个新的字符串对象,其引用是字符串常量池中"小马"的空间地址。
"123" 仍然在字符串常量池中,地址也未变
(2)错误解读:s = "小马" 使得原引用中的 "123" 被替换为 "小马" 。PS:这是很多人容易理解错的地方
*/
s = "小马";
System.out.println("\"小马\"地址对应的hashCode值:"+s.hashCode());
}
}
运行结果:
内存原理GIF图
6.2 在方法中修改字符串
虽然 String 类是引用数据类型,但由于 String 类的不可变性,在方法中修改字符串有以下特点:
① 方法内对 String 参数的修改(如拼接、替换)会生成新对象,原对象内容不变
② 方法外部的原始引用仍然指向原对象
示例代码
public class test {
public static void main(String[] args) {
String str = "Hello";
changeString(str);
System.out.println(str); // 输出仍然是Hello
}
//方法内部修改字符串
public static void changeString(String str) {
str = "World";//生成新对象"World",内部的str指向新对象,外部的str不受影响
}
}
在此例中,方法 changeString 接收了 str 引用的拷贝,即 str 也是指向 "Hello"
然后在方法内部,str 被重新赋值为 "World" ,但这只是改变了 changeString 方法内部的 str 的引用,外部的 str 仍然指向 "Hello" 。这证明了虽然传递的是引用的拷贝,但由于 String 的不可变性,原对象不会被修改
内存原理GIF图