String 类
1. String 类的基本概念
(1)String 类对象用于保存字符串,也就是一组字符序列。字符串常量是一组用双引号括起来的字符序列,如:“你好”,"hello"等。
(2)String 类对象与字符串常量的区别:String 类对象存储在堆内存中,而字符串常量存储在方法区的常量池中;存放在常量池中的字符串常量会有一个地址,通常该地址会返回给指向该字符串常量的String 类对象的 value 属性;但是注意:字符串常量本身就是一个存储在常量池中的String 类对象。
(3)字符串的字符使用Unicode 字符编码,一个字符(无论是字母还是汉字)都占两个字节。
- 前3点后面会再深入解释,概念有点绕。
(4)String 类实现了接口 Serializable【说明了 String 类对象可以串行化:可以在网络传输】;String 类还实现了接口 Comparable [说明了String 对象可以比较大小]。
- 如下图:
(5)String 类是 final 类,不能被其他的类继承。
(6)String 类中有属性 private final char value[],这是一个 char 类型的常量数组,用于存放字符串中的每一个字符。
(7)注意:value 数组 是一个 final 类型, 不可以被修改,即数组 value 不能再指向新的数组地址,但是数组中单个字符内容是可以改变的。
- 代码说明:
final char[] value = {
'a','b','c'};
char[] v2 = {
't','o','m'};
value[0] = 'H';// 可以改变数组中的元素的值
//value = v2; 错误,不可以修改 value地址
2. String 类对象的创建方式
(1)方式一:直接赋值 String s = “lineryue”;
(2)方式二:调用构造器 String s2 = new String(“lineryue”);
- 两种创建方式的流程分析
- 方式一:直接赋值 String s = “lineryue”;
(1)首先在方法区的常量池中查看是否存在一个存储了 “lineryue” 的空间;
(2)如果存在,就在栈空间中声明一个对象名 s ,然后将该空间的地址返回给 s ,s 最终指向的是常量池的空间地址(说明 s 是可变的,同时该字符串常量也是一个对象;
(3)如果不存在,就在常量池中开辟一个空间,将 “lineryue” 存储进去,然后在栈空间中声明一个对象名 s ,再将该空间的地址返回给 s ,s 最终指向的是常量池的空间地址(说明 s 是可变的),同时该字符串常量也是一个对象。
- 方式二:调用构造器 String s2 = new String(“lineryue”);
(1)首先在堆内存中开辟了一个空间,这个空间是真正的String 类对象,该空间里面存在一个char 数组类型的 value 属性,value 指向了常量池;
(2)接着String 类对象在方法区的常量池中查看是否存在一个存储了 “lineryue” 的空间;
(3)如果存在,就将该空间的地址返回给 value ,value 最终指向的是常量池的空间地址;(注意:value 是 final 类型的)
(4)如果不存在,就在常量池中开辟一个空间,将 “lineryue” 存储进去,再将该空间的地址返回给 value ,value 最终指向的是常量池的空间地址;
(5)最后,在栈内存中声明一个对象名 s2 ,将堆内存中 String 类对象空间的地址返回给 s2 ,s2 最终指向的是堆空间中的String 类对象地址。
- 创建String 类对象的内存分布图:
- 小结:两种创建String 类对象的方式,虽然都是创建了字符串 “lineryue” ,但创建的途径是不同的,上面的图和流程分析是非常重要的,把它熟记在心。
3. String 类对象经典例题
(1)观察下面代码,输出结果是什么?
String a = "abc";
String b = "abc";
System.out.println(a.equals(b));// T;equals() 是比较两个对象的字符串是否相同,"abc"和"abc" 是相同的。
System.out.println(a == b);// T;“==” 是比较两个对象的地址是否相同,根据创建String类对象的流程,a 和 b都指向常量池中的同一个地址,所以 a == b。
String s1 = new String("bcde");
String s2 = new String("bcde");
System.out.println(s1.equals(s2));// T;equals() 是比较两个对象的字符串是否相同,"bcde"和"bcde" 是相同的。
System.out.println(s1 == s2); // F;只要有new ,都是在堆内存中创建一个新对象,s1 和 s2 分别指向了两个对象空间,地址不同,所以错误。
(2)观察下面代码,输出结果是什么?
String a = "hsp"; //a 指向 常量池中的 String类对象“hsp”
String b = new String("hsp");//b 指向堆中的String类对象,再由该对象的 value 属性指向常量池中的“hsp”。
System.out.println(a.equals(b)); // T;"hsp" 和 "hsp"相同
System.out.println(a == b); // F;对象名a 和 b指向的地址不同
(3)观察下面代码,输出结果是什么?
String a = "hsp";
String b = new String("hsp");
/* 当某个String 类对象调用intern() 方法时,
如果常量池中已经存在一个等于此String对象的字符串(用equals 方法确定是否等于),
则该方法返回常量池中该字符串的地址;
否则,将String对象的字符串 添加到常量池中,该方法返回存储字符串的地址;*/
System.out.println(a == b.intern()); // T;b.intern() 返回了常量池中的存储"hsp"的地址,因此a 指向的地址和 b.intern() 返回的地址相同。
System.out.println(b == b.intern()); // F;b 对象名指向的是堆内存中的对象地址,和 b.intern() 返回的不同。
(4)观察下面代码,输出结果是什么?
public class StringExercise04 {
public static void main(String[] args) {
Person p1 = new Person();
p1.name = "hspedu";
Person p2 = new Person();
p2.name = "hspedu";
System.out.println(p1.name.equals(p2.name)); // T;两者都是"hspedu",正确
System.out.println(p1.name == p2.name); // T;两者都指向了常量池中的"hspedu",地址相同,正确
System.out.println(p1.name == "hspedu"); // T;p1.name指向了常量池中的"hspedu"的地址,"hspedu"本身就是该地址,两者相同,正确
}
}
class Person {
public String name;
}
- 小结:在字符串对象的比较时,注意使用(==)还是 equals() 方法,前者比较的是对象的地址,后者比较的是字符串的内容。需要牢牢把握创建String类对象的两种方式的流程,这是本质,多绕的题都会迎刃而解。
4. String 类对象的特性
(1)String 类是一个 final 类,代表不可变的字符序列。
(2)String类对象是不可变的,指的是一个String 类对象一但被创建,该对象的内容就不可以再改变(例如:属性 value );但对象的引用(对象名)可以改变,也就是说对象名可以再指向另外的对象。
- 思考1,对于String类对象不可变的理解:
- 在 String 类的源码中,主要的成员变量只有两个:
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[];
/** Cache the hash code for the string */
private int hash; // Default to 0
-
分析
- 可以知道,String 类就是对char 数组 value 的封装,这里先不用管 hash 属性;String类使用 private final 修饰 value,且没有提供修改value的 setValue 方法,这代表了 一但String 类对象被创建,该对象就不可以再更改其 value 的值了。 所以,我们可以认为 String 类对象是不可变的。(使用反射时有例外,这个在反射的章节再讲解)
-
思考2,对于String 类对象的引用(对象名)可以改变的理解:
- 看下面代码,输出结果是什么?
String s = "abc";
System.out.println("s=" + s);// 输出:s=abc
s = "123";
System.out.println("s=" + s);// 输出:s=123
- 分析:
- 上面代码中,执行语句 String s = “abc”,在常量池中创建了一个String类 对象"abc",并将该对象的地址返回给了对象名 s ;再执行语句 s = “123”,并不是将刚才的"abc" 修改为"123",而是在常量池中又创建了一个 String 类对象 “123”,并将新对象的地址返回给了 s ;
- 所以,改变的是对象的引用 s ,而"abc" 这个 String 类的对象还存在于常量池中,只不过是没有对象名引用它了。
5. String 类对象经典面试题
(1) 看下面代码,创建了几个对象?
String s = "hello" + "abc";
- 答案: 只创建了一个对象;编译器会自动优化代码,上面代码在优化后等价于 String s = “helloabc”;因此只是在常量池中创建了一个 String 类对象。
(2) 看下面代码,创建了几个对象?
String a = "hello";
String b = "abc";// a 和 b 都指向常量池中的对象
String c = a + b;// c 指向堆内存中的对象
- 答案:
- 创建了三个对象;先在常量池中创建了 “hello” 和 “abc” 这两个对象,然后 String c = a + b 这一语句,底层先创建了一个 StringBuilder 类对象,然后使用该对象的 append 方法,将 “hello” 和 “abc” 拼接在一起,在常量池中创建了 “helloabc”,但最终返回了一个堆中的 String 类对象,该对象的 value 指向常量池的 “helloabc”,而 c 指向了堆内存中的对象。
(3) 看下面代码,创建了几个对象?输出结果是什么?
String s1 = "hspedu"; //s1 指向池中的 “hspedu”
String s2 = "java"; // s2 指向池中的 “java”
String s3 = s1 + s2;// s3 指向堆中的对象,该对象再指向池中的“hspedujava”
String s5 = "hspedujava"; // s5 指向池中的 “hspedujava”
String s6 = s3.intern();// s6 指向池中的 “hspedujava”
System.out.println(s3 == s5); // F
System.out.println(s5 == s6); // T
System.out.println(s5.equals(s6));// T
(4) 看下面代码,输出结果是什么?
class Test1 {
String str = new String("hsp");
final char[] ch = {
'j', 'a', 'v', 'a'};
public void change(String str, char[] ch) {
str = "java";
ch[0] = 'h';
}
public static void main(String[] args) {
Test1 ex = new Test1();
ex.change(ex.str, ex.ch);
System.out.print(ex.str + " and ");
System.out.println(ex.ch);// 可以直接打印出char数组中的元素
}
}
- 答案: hsp and hava;
- 本题中是引用传递,即把对象的引用复制了一份,复制品也指向了对象,两个引用不会相互影响,但是他们共同指向了同一个对象空间。在 change 方法栈中,执行了 str = “java”; 即复制的引用指向了新的常量池中的字符串 “java”;change 方法栈退出后,main 方法中原有的 str 引用还是指向原来的 “hsp” 对象,因此 ex.str 输出的是 hsp 。
- 而同样是引用传递,在 change 方法栈中 数组 ch 内的元素的值被改变了,则 main 方法栈中 ex.ch 输出的结果也会被影响。本题画内存图分析会思路会清晰,如果博主的解释不够清楚,可以在评论区提出问题。
6. String 类中的常用方法
- 代码实现:
public class StringMethod01 {
public static void main(String[] args) {
//1. equals :比较内容是否相同,区分大小写
String str1 = "hello";
String str2 = "Hello";
System.out.println(str1.equals(str2));
// 2.equalsIgnoreCase :忽略大小写的判断内容是否相等
String username = "johN";
if ("john".equalsIgnoreCase(username)) {
System.out.println("Success!");
} else {
System.out.println("Failure!");
}
// 3.length :获取字符的个数,字符串的长度
System.out.println("韩顺平".length());
// 4.indexOf :获取字符在字符串对象中第一次出现的索引,索引从0开始,如果找不到,返回-1
String s1 = "wer@terwe@g";
int index = s1.indexOf('@');
System.out.println(index);// 3
System.out.println("weIndex=" + s1.indexOf("we"));// 0
// 5.lastIndexOf :获取字符在字符串中最后一次出现的索引,索引从0开始,如果找不到,返回-1
s1 = "wer@terwe@g@";
index = s1.lastIndexOf('@');
System.out.println(index);//11
System.out.println("ter的位置=" + s1.lastIndexOf("ter"));// 4
// 6.substring :截取指定范围的子串
String name = "hello,张三";
// 下面name.substring(6) 从索引6开始截取后面所有的内容
System.out.println(name.substring(6));// 截取后面的字符
// name.substring(0,5)表示从索引0开始截取,截取到索引 5-1 = 4 位置
System.out.println(name.substring(2,5));// llo
}
}
- 代码实现:
public class StringMethod02 {
public static void main(String[] args) {
// 1.toUpperCase:转换成大写
String s = "heLLo";
System.out.println(s.toUpperCase());// HELLO
// 2.toLowerCase :转换成小写
System.out.println(s.toLowerCase());// hello
// 3.concat :拼接字符串
String s1 = "宝玉";
s1 = s1.concat("林黛玉").concat("薛宝钗").concat("together");
System.out.println(s1);// 宝玉林黛玉薛宝钗together
// 4.replace 替换字符串中的字符
s1 = "宝玉 and 林黛玉 林黛玉 林黛玉";
// 在s1中,将 所有的 林黛玉 替换成薛宝钗
// 解读: s1.replace() 方法执行后,返回的结果才是替换过的.
// 注意对 s1没有任何影响
String s11 = s1.replace("宝玉", "jack");
System.out.println(s1);//宝玉 and 林黛玉 林黛玉 林黛玉
System.out.println(s11);//jack and 林黛玉 林黛玉 林黛玉
// 5.split: 分割字符串, 对于某些分割字符,我们需要 转义比如 | \\等
String poem = "锄禾日当午,汗滴禾下土,谁知盘中餐,粒粒皆辛苦";
//解读:
// 1. 以 , 为标准对 poem 进行分割 , 返回一个数组
// 2. 在对字符串进行分割时,如果有特殊字符,需要加入转义符 \
String[] split = poem.split(",");
poem = "E:\\aaa\\bbb";
split = poem.split("\\\\");
System.out.println("==分割后内容===");
for (int i = 0; i < split.length; i++) {
System.out.println(split[i]);
}
// 6.toCharArray :转换成字符数组
s = "happy";
char[] chs = s.toCharArray();
for (int i = 0; i < chs.length; i++) {
System.out.println(chs[i]);
}
// 7.compareTo :比较两个字符串的大小,如果前者大,
// 则返回正数,后者大,则返回负数,如果相等,返回0
// 解读:
// (1) 如果长度相同,并且每个字符也相同,就返回 0
// (2) 如果长度相同或者不相同,但是在进行比较时,可以区分大小
// 就返回 if (c1 != c2) {
// return c1 - c2;
// }
// (3) 如果前面的部分都相同,就返回 str1.len - str2.len
String a = "jcck";// len = 3
String b = "jack";// len = 4
System.out.println(a.compareTo(b)); // 返回值是 'c' - 'a' = 2的值
// 8.format : 格式字符串
// 占位符有: %s 字符串 %c 字符 %d 整型 %.2f 浮点型
String name = "john";
int age = 10;
double score = 56.857;
char gender = '男';
// 将所有的信息都拼接在一个字符串.
String info =
"我的姓名是" + name + "年龄是" + age + ",成绩是" + score + "性别是" + gender + "。希望大家喜欢我!";
System.out.println(info);// 很麻烦,使用占位符
// 解读
// 1. %s , %d , %.2f %c 称为占位符
// 2. 这些占位符由后面变量来替换
// 3. %s :表示后面由 字符串来替换
// 4. %d :是整数来替换
// 5. %.2f :表示使用小数来替换,替换后,只会保留小数点两位, 并且进行四舍五入的处理
// 6. %c :使用char 类型来替换
String formatStr = "我的姓名是%s 年龄是%d,成绩是%.2f 性别是%c.希望大家喜欢我!";
String info2 = String.format(formatStr, name, age, score, gender);
System.out.println("info2=" + info2);
}
}
总结
- 本文详细总结讲解了 String 类(字符串) 的知识,并深入解释了 String类 的注意事项和细节,举了很多很多的例子来讲解。String 类(字符串)是Java 的重难点,理解并掌握这一部分的知识,需要有较好的面向对象的基础。小白博主已经尽力整理了,希望小伙伴们看后能有所收获!
- 最后,如果本文有什么错漏的地方,欢迎大家批评指正!一起加油!!我们下一篇博文见吧!