前言:Java中有三大特殊类需要我们系统掌握,分别是String类,Object类以及包装类,这里,我们主讲String类,彻底掌握String类的使用
目录
1. 常见的创建字符串的三种方式
2. 字符串的内存布局
2.1 不同的内存空间
2.2 字符串的比较
3. 字符串的常量池
3.1 自动入池
3.2 手工入池
4. 小结一
5. 字符串的不可变性
5.1 为什么有不可变性
5.2 验证不可变性
5.3 字符串的修改
6. StringBuilder类
6.1 String 类与StringBuilder类的相互转化
6.2 StringBuilder类的其他方法
7. String类型和char[ ] 类型、byte[ ]类型的相互转化
7.1 String类型和char[ ] 类型
7.2 String类型和byte[ ]类型
7.3 小结:
8. String类的其他常用操作
8.1 字符串比较:
8.2 字符串查找
8.3 字符串替换
8.4 字符串拆分
8.5 字符串的截取
8.6 其他方法
// 直接赋值法
String str1 = "hello";
// 使用关键字new
String str2 = new String("hello");
// 使用char[]
char[] data = {'h','e','l','l','o'};
String str3 = new String(data);
由第三种创建方式我们可以知道,String内部仍是使用字符数组来存储元素的,下面是部分源码:
同时,由String类前面的final修饰符我们也可以知道,String类是不能再被继承修改的,这样可以保证所有使用JDK的程序员使用的是同一个String类
String是引用数据类型,故其存储的也是地址,在栈上保存,它所创建的对象则在堆上存储(这里,如果不懂栈空间、堆空间等可在上一篇Java中的类和对象这一篇中学习)
内部存储如下:
String str2 = new String("Hello")
这里,hello作为字符串字面常量是一个对象,而因为又使用了new关键字又开辟了一个空间,这个空间会把字符串字面量拷贝复制过来,故而,其实产生了两个对象,字符串字面量是一个垃圾空间
所以,一般由直接赋值法创建对象即可
因为String是引用数据类型,存储的是地址,故而,我们可以得到字符串比较相等时,不可以直接使用 == 进行比较,而要使用equals()方法
(1)对于基本数据类型,== 比较的就是两个变量的值,但是对于引用数据类型,== 比较的其实是其存储的地址,所以不能直接用 == 比较
(2)equals的用法需注意,以下两种形式,建议第二种,尤其是当str1为用户输入时,如果用户未输入,那么str1默认为null,形式一的写法就有可能造成空指针异常
public static void main(String[] args) {
String str1;
Scanner scanner = new Scanner(System.in);
str1 = scanner.next();
// 比较用户输入的字符串是否为“Hello”
// 形式一
System.out.println(str1.equals("Hello"));
// 形式二
System.out.println("Hello".equals(str1));
}
正常输入,两种形式都OK
但如果当str1为默认值Null时,形式一会出现空指针异常
当使用直接赋值法创建字符串时,JVM会对字符串创建一个字符串的常量池,常量池的主要目的就是为了保证效率和高复用
当使用直接赋值法(方式一)创建时,如果所创建的字符串字面值是第一次出现,JVM就会创建对象并将它扔入常量池,而如果该字面值不是第一次出现,JVM会直接从常量池中复用该对象
如下面这段代码:
public static void main(String[] args) {
String str1 = "Hello";
String str2 = "Hello";
System.out.println(str1 == str2);
}
输出结果如下:
== 比较的是str1和str2所存储的地址,输出true说明str1,str2指向的是同一个字符串
其内部存储如下:(在创建str2时,Hello字面量已经存在,所以str2会直接指向而并不会在堆上再创健一个Hello)
(1)当使用其他方法创建字符串时,并不会自动入池,如下面这段代码:
public static void main(String[] args) {
String str1 = new String("Hello");
String str2 = "Hello";
System.out.println(str1 == str2);
}
输出结果:
这里输出的就是false了, 也验证了这种创建方法并不会入常量池
(2)这里我们就可以用intern()方法实现人工入池
public static void main(String[] args) {
String str1 = new String("Hello").intern();
String str2 = "Hello";
System.out.println(str1 == str2);
}
输出结果:
一般,就用直接赋值法创建字符串即可,然后用equals()方法比较值相等
(1)由于String类内部并未提供getter方法,所以外部无法使用到存储字符串真实的char数组,所以,有关修改的操作都并不是真正的修改了原来的字符串,大都是通过创建一个新的字符串来达到看似修改的目的
(2)不可变的好处:
1. 方便实现字符串对象池. 如果 String 可变, 那么对象池就需要考虑何时深拷贝字符串的问题了.
2. 不可变对象是线程安全的.
3. 不可变对象更方便缓存 hash code, 作为 key 时可以更高效的保存到 HashMap 中.
如下面这段代码,
public class StringPractice {
public static void main(String[] args) {
String str = "Hello";
change(str);
System.out.println(str);
}
public static void change(String s){
s = "Hi";
}
}
我们期望的输出结果是将str1的Hello变更为Hi,但我们来看真实的输出结果:
这就是因为字符串的不可变性,change 方法是新创建了一个字符串,并不是修改了原来的字符串,真实的内存如下图:
(1)通常修改字符串的内容,我们选择使用 += 拼接,借助原字符串, 创建新的字符串,如下:
String str1 = "Hello";
str1 += "World";
System.out.println(str1);
但这里其实并不是真正的修改了原字符串,是通过创建新的字符串实现的,先创建了字面量World,然后 + 号创建了HelloWorld,最后 = 使str1指向了新的字符串HelloWorld,但是JVM没有那么笨拙,碰到 + 号,JVM会将字符串转为StringBuilder类
(2)要想真正修改原字符串,只能通过反射来破环封装,这里了解即可,不再延申
(3)通常,如果想大量修改字符串的内容,我们会用到StringBuffer类,和StringBuilder类,这两个类的区别在于,StringBuffer类是线程安全的,StringBuilder类是线程不安全的但效率要高
(1)String -> StringBuilder
构造方法
append()方法,拼接扩展
(2)StringBuilder -> String
toString()方法
互相转化代码实现如下:
public class StringPractice {
public static void main(String[] args) {
String str1 = "Hello";
// String -> StringBuilder
// 构造方法
StringBuilder s1 = new StringBuilder(str1);
// append()方法
StringBuilder s2 = new StringBuilder("hello");
s2.append("world");
// StringBuilder -> String
String s3 = s2.toString();
}
}
(1)reverse()方法,反转字符
(2)delete(int start,int end) 删除指定范围内的字符,左闭右开 ,start,end均为字母索引下标,从0开始数
(3)insert(int start,待插入数据) 将索引下标为start的位置插入数据
public static void main(String[] args) {
StringBuilder s = new StringBuilder("hello");
// 反转字符
s.reverse();
// 输出 olleh
System.out.println(s);
// 删除
s.delete(1,3);
// 输出 oeh
System.out.println(s);
// 插入
s.insert(1,"zzzz");
// 输出 ozzzzeh
System.out.println(s);
}
结果:
(1)char[ ] -> String 即文章开篇写到的第三种创建字符串的方式
(2)String -> char[ ]
public static void main(String[] args) {
String str = "Hello";
char a = str.charAt(1);
char b[] = str.toCharArray();
System.out.println(a);
System.out.println(b);
}
输出结果:
(1)byte[ ] -> String 仍然用String的构造方法,可选择范围也可直接全部转换
(2)String -> byte[ ] getBytes()方法
public static void main(String[] args) {
byte[] b = {'h','i'};
// byte[ ] 全部转为String类型,构造
String str1 = new String(b);
// 从0开始长度为1的数组部分转变为String
String str2 = new String(b,0,1);
System.out.println(str1);
System.out.println(str2);
// String -> byte[]
byte[] b2 = str1.getBytes();
System.out.println(Arrays.toString(b2));
}
结果:
byte[] 是把 String 按照一个字节一个字节的方式处理, 这种适合在网络传输, 数据存储这样的场景下使用. 更适合针对二进制数据来操作.
char[] 是把 String 按照一个字符一个字符的方式处理, 更适合针对文本数据来操作, 尤其是包含中文的时候
1. 相等:返回0.
2. 小于:返回内容小于0.
3. 大于:返回内容大于0
(字符串的比较大小规则, 总结成三个字 "字典序" 相当于判定两个字符串在一本词典的前面还是后面. 先比较第一个字符的大小(根据 unicode 的值来判定), 如果不分胜负, 就依次比较后面的内容)
public static void main(String[] args) {
String str = "HiHello";
String str2 = "H";
System.out.println(str.contains("H"));
System.out.println(str.indexOf("ll"));
System.out.println(str.lastIndexOf("ll"));
System.out.println(str.startsWith("Hi"));
System.out.println(str.endsWith("lo"));
}
输出结果:
true
4
4
true
true
如下,注意区别:
public static void main(String[] args) {
String str = "HiHello";
System.out.println(str.replaceAll("H","zzzz"));
System.out.println(str.replaceFirst("H","yyyy"));
}
结果:
zzzzizzzzello
yyyyiHello
public static void main(String[] args) {
String str = "Hi He llo";
// 以空格分割
String[] s = str.split(" ");
for(String a : s){
System.out.println(a);
}
// 以空格分割,数组长度为2
String[] s2 = str.split(" ",2);
for(String a : s2){
System.out.println(a);
}
}
Hi
He
llo
Hi
He llo
【注意】一些特殊的字符作为切割符可能无法区分,需要加上转义字符
1. 字符"|","*","+"都得加上转义字符,前面加上"\"
2. 而如果是".",那么就得写成"\\."
3. 如果一个字符串中有多个分隔符,可以用"|"作为连字符
如:
public static void main(String[] args) {
String str = "Hi.He.llo";
// 以空格分割
String[] s = str.split("\\.");
for(String a : s){
System.out.println(a);
}
}
public static void main(String[] args) {
String str = "Hi.He.llo";
// 截取字符串
System.out.println(str.substring(1));
System.out.println(str.substring(1,4));
// 字符串长度
System.out.println(str.length());
// 判断是否为空
System.out.println(str.isEmpty());
// 转大写字母
System.out.println(str.toUpperCase());
// 转小写字母
System.out.println(str.toLowerCase());
}
输出结果:
i.He.llo
i.H
9
false
HI.HE.LLO
hi.he.llo
这就是String类的全部内容了,其中最核心的还是要能够理解常量池,以及字符串的不变性