一、String类
JDK中的String类:
为什么String类被final修饰呢?被final修饰后无法被继承,不存在子类,这样就可以保证所有使用JDK的人,大家用到的String类仅此一个,大家都相同。否则的话会导致子类行为不一致的问题。
二、创建字符串的四种方式
1.直接赋值
代码如下:
String str = "hello world";
2.通过构造方法产生对象
代码如下:
String str2 = new String("hello world");
3.通过字符数组产生对象
代码如下:
char[] data = new char[] {'a','b','c'};
String str = new String(data);
4.通过String的静态方法valueOf(任意数据类型)转为字符串
代码如下:
String str = String.valueOf(10);
最常用的是方式一和方式四
三.字面量和常量池
1.字面量
直接写出来的数值就成为字面量。
比如:10,就是int字面量;10.1,double字面量;true,boolean字面量;“abc”,String字面量。字符串(String)是一个引用数据类型,实际上字符串的字面量就是一个字符串的对象。
String str = “hello world”;字符串字面量,也是字符串的对象。str就是字符串的一个引用。
public class StringTest {
public static void main(String[] args) {
String str ="hello world";
String str1 = str;
str1 = "Hello";
System.out.println(str);
}
}
结果如下:
此时str1 = “Hello”;Hello也是字符串的字面量,是一个新的字符串对象,str1实际上指向了新的字符串对象”Hello”;str仍指向原字符串对象”hello world”。
2.字符串比较相等
- 所有引用数据类型比较相等时,使用equals方法比较,JDK常用类,都已经覆写了equals方法,大家直接使用即可。
- 引用数据类型使用”==”比较的仍然是数值(就是地址是否相等)。
1.equals方法
String str1 = "hello";
String str2 = "hello";
System.out.println(str1==str2);
结果如下:
这并不是说他俩的值相等,而是他们两个的地址相等,指向的是同一块地址。
String str1 = new String("hello");
String str2 = new String("hello");
System.out.println(str1==str2);
结果如下:
为什么这个时候false呢?因为new出来的是两个不同的对象。他们的地址肯定是不同的,我们需要使用equals方法比较他们的数值是否相等。
String str1 = new String("hello");
String str2 = new String("hello");
System.out.println(str1.equals(str2));
结果如下:
2.equalsIgnoreCase方法。
我们如果把str2中的hello改为Hello,此时再运行就会发现输出false,因为用equals方法比较是区分大小写的,我们要想不区分大小写来比较需要使用equalsIgnoreCase方法。
String str1 = new String("hello");
String str2 = new String("Hello");
System.out.println(str1.equalsIgnoreCase(str2));
运行结果如下:
假设此时我们有一个由用户输入的用户名:
那么是第一种输出方式好呢还是第二种,当然是第二种,我们要记住,所有牵扯到用户自己输入的都需要进行判空,如果此时userName是一个null那么再通过userName去”.”equals方法,就可能会发生空指针异常。
更推荐使用第二种方式。因为我们要比较的特定内容本身就是字符串字面量,一定不是空对象,把要比较的内容放在equals的前面,就可以方便处理userName为空的问题。
3.compareTo方法
String str1 = "abc";
String str2 = "Abc";
System.out.println(str1.compareTo(str2));
运行结果如下:
这个32是 a 和 A 的ASCII码值的差值。A是65,a是97.
按照字典序排列字符串:就是按照字符串内部的字符的ASCII码大小排序。
相等返回0
小于返回 负值
大于返回 正值
3.关于字符串的常量池问题
所谓的”池”,都是一种共享设计模式的思想,当前进程中,所有常量池中的常量都是共享的。当字符串产生之后大部分情况下都是用来进行输出的,也就是打印它的内容。也就是说”hello”只要有一个就行了,这样做可以节省空间。
String str1 = "hello";
String str2 = "hello";
String str3 = "hello";
System.out.println(str1==str2);
System.out.println(str2==str3);
运行结果如下:
说明这三个引用指向了相同的内存。
String str1 = new String("hello");
String str2 = new String("hello");
String str3 = new String("hello");
System.out.println(str1==str2);
System.out.println(str2==str3);
运行结果如下:
这三个引用指向不同的字符串对象。
我们来分析一下:
- 当使用直接赋值法产生对象时,JVM会维护一个字符串常量池,若该对象还在堆中不存在,则产生一个新的字符串对象加入字符串的常量池中。
- 当继续使用直接赋值法产生字符串对象时,JVM发现该引用指向的内容在常量池中已经存在了,则此时不再新建字符串对象,而是复用已有对象。
- JDK8之后将常量池放在了JVM的堆中存储。
我们来看一下上面两种产生字符串方式的内存分析:
上图中三行代码实际上只产生了一个字符串。
那当我们采用new产生新的字符串时又是怎么一回事呢?
String str1 = new String("hello");
程序的执行是从右向左,当字符串字面量”hello”第一次出现的时候,字符串常量池中还没有这个东西,就产生一个”hello”存入常量池中。下来前面又有一个new,有new就有新空间,在堆中就又产生了一个新的字符串对象,这个字符串对象的值也是”hello”,并且这个字面量不会进入常量池,就在堆中存储。就相当于这一行代码,产生了两对象。str指向了堆中的普通的”hello”,当执行第二行代码时,此时常量池中已经有”hello”了,所以不再产生新的字符串字面量,然后又new一个新的”hello”在堆中存储,str2指向它。以此类推,就相当于这三行代码,产生了四个字符串对象。其中一个在常量池中,其余三个在堆中。
内存图如下:
4.手工入池 intern方法
String类提供的intern方法。
这是一个本地方法,调用intern方法会将当前字符串引用指向的对象保存到字符串常量池中。它有下面两种情况:
- 若当前常量池中已经存在了该对象,则不再产生新的对象,返回常量池中的String对象。
- 若当前常量池中不存在该对象,则将该对象入池,返回入池后的地址。
我们来看看使用intern方法的几种例子:
eg1:
//这个str1指向堆中普通的字符串对象
String str1 = new String("hello");
str1.intern();
String str2 = "hello";
System.out.println(str1 == str2);
结果如下:
为什么时false呢?
- 当执行第一行代码时,字符串字面量”hello”还从来没有出现过,此时就产生一个”hello”存入常量池中。下来前面又有一个new,有new就有新空间,在堆中就又产生了一个新的字符串对象,这个字符串对象的值也是”hello”,并且这个字面量不会进入常量池,就在堆中存储。str1指向了堆中的普通的”hello”。 这一行代码产生了两个对象。
- 当执行第二行代码的时候 str1.intern(); 常量池中已经存在”hello”,不会再产生一个新的字符串对象,而是返回常量池中已经存在的字符串对象的地址。此时str1只是调用了一下intern方法,并没有去接收这个方法的返回值。
- 当执行第三行代码的时候,直接给str2赋值一个”hello” 此时常量池中已经有这个”hello”了,不是第一次出现,所以str2指向的就是常量池中的这个”hello”。
- 所以他们俩地址不相等。
内存图如下:
那如何让他们俩相等呢?只需要让str1去接收一下intern方法的返回值即可:
String str1 = new String("hello");
str1 = str1.intern();
String str2 = "hello";
System.out.println(str1 == str2);
运行结果如下:
因为此时str1接收的是常量池中”hello”的地址,所以它两相等。
内存图如下:
eg2:
char[] data = new char[] {'a','b','c'};
String str1 = new String(data);
str1.intern();
String str2 = "abc";
System.out.println(str1 == str2);
运行结果如下:
为什么此时又是true呢?
- 执行第一行代码时,首先先new一个字符数组,有new就有新空间,在堆中开辟一个字符数组存放a,b,c三个值。
- 执行第二行代码时,我们new String (data);时,这里面没有字符串,data是一个字符数组,还没有字符串对象呢,所以常量池中还是空。此时只是在堆中产生了一个普通的字符串对象,它的值为”abc”,由data数组赋值而来的。str1就指向了普通的”abc”。这一行代码产生了一个对象。
- 执行第三行代码时,str1.intern();此时常量池中没有该对象,就把普通的”abc”挪到常量池中,也就是入池,并不是拷贝过去。str1还是指向这个”abc”。
- 执行第四行代码时,此时的常量池中已经有了”abc”,不再产生新的字符串对象,而是直接复用已经存在的”abc”,所以str2指向常量池中已经存在的”abc”,和str1指向的是同一个地址。所以运行结果是true。
内存图如下:
没调用intern方法之前,str1指向的”abc”是在常量池外面的。
四.字符串的不可变性
1.不可变性
所谓的字符串不可变指的是字符串对象的内容不能变,而不是字符串引用不能变。
比如:
String str = "hello";
str = str + "world";
str = str + "!";
System.out.println(str);
运行结果如下:
这里的不可变指的是 “hello” “world” “helloworld” “!” “helloworld!” 这几个字符串对象不可变,一但声明后就无法修改其内容。
- 刚开始的时候栈中只有str一个引用,堆中有常量池
- 执行第一行代码的的时候常量池中只有”hello”这个对象,str指向它。
- 执行第二行代码的时候先 + 了一个”world”,现在常量池中产生了一个”world”,然后进行拼接,常量池中产生一个”helloworld”,str指向了”helloworld”。
- 执行第三行代码的时候,现在常量池中产生了一个”!“,然后进行拼接常量池中又产生了一个”helloworld!”,str又指向了”helloworld!”。
内存图如下:
字符串的引用是可以随意改变的。
为什么字符串的对象无法修改内容而其他类的对象能修改内容? 我们来看看JDK8中String类的源码。(在JDK17中是byte数组)
字符串其实就是一个字符数组 char[ ],字符串保存的值实际上在数组中保存。这个value数组被private修饰封装,String类的外部无法访问value这个数组。String类内部并没有提供关于value属性的getter和setter方法。对于String类的外部而言,value完全无法使用。因此字符串对象的内容无法修改(String类的外部根本拿不到这个value数组)。
内存图如下:
value是一个数组。
2. 修改字符串的内容
- 在运行时通过反射破坏value数组的封装(不推荐,以后再讲)。
- 更换使用StringBuilder或者StringBuffer类。但是这两个已经不是String类型了。StringBuffer是线程安全,性能较差,使用起来和StringBuilder没什么区别。
- 若需要频繁进行字符串的拼接,我们使用StringBuilder类的append方法。
- StringBuilder类可以修改对象的内容(不是String类)。
public class StringBuilderTest {
public static void main(String[] args) {
StringBuilder sb = new StringBuilder();
sb.append("hello");
sb.append("world");
sb.append("!");
System.out.println(sb);
}
}
运行结果如下:
这里面的值在不停的修改,并不是在拼接。
3.StringBuilder类和String类的转换
StringBuilder和String类是两个独立的类,StringBuilder类就是为了解决字符串拼接问题产生的。因为String的对象无法修改内容,为了方便字符串的拼接操作,产生了StringBuilder类,StringBuilder类的对象是可以修改内容的。
- 将String类转换为StringBuilder类
- 使用StringBuilder的构造方法或者append方法
这里的”hello”就是一个字符串字面量,我们调用了StringBuilder的构造方法,将一个字符串的字面量转换为了StringBuilder对象。
这也是将字符串字面量转换为StringBuilder类
- 将StringBuilder类转换为String类
调用toString方法:
运行结果如下:
4.StringBuilder类的其他方法
1. 字符串的反转操作
调用reverse();方法。
StringBuilder sb = new StringBuilder("hello");
sb.append("123");
sb.reverse();
String str = sb.toString();
System.out.println(str);
运行结果如下:
2. 字符串删除指定范围数据
调用delete(int start, int end);方法
删除从start索引开始,end之前的所有内容。左闭右开,按照索引删除
StringBuilder sb = new StringBuilder("hello");
sb.append("world");
sb.delete(5,10);
String str = sb.toString();
System.out.println(str);
运行结果如下:
3. 插入操作
insert(int start,各种数据类型);
将新元素插入当前StringBuilder对象,插入后新值的其实索引为start
StringBuilder sb = new StringBuilder("hello");
sb.append("world");
sb.delete(5,10);
sb.insert(5,10);
String str = sb.toString();
System.out.println(str);
运行结果如下:
插入之后,10这个元素的位置就是5。
4.String、StringBuilder、StringBuffer的区别
- String的对象无法修改, StringBuilder和StringBuffer可以修改
- StringBuffer是线程安全的操作,性能较差;StringBuilder是线程不安全,性能较高。除此之外,他们两的其他操作完全一样,方法名都一样。
五.字符串和字符数组的互相转换
1.String转为char:
- 取出字符串中,指定索引的字符:
调用String类的charAt方法
String str = "hello";
System.out.println(str.charAt(1));
输出结果如下:
- 将字符串中的内容转为字符数组 String->char[ ]
调用 toCharArray() 方法
String str = "hello";
char[] data = str.toCharArray();
System.out.println(data);
System.out.println(data[0])
运行结果如下:
说明这个字符数组索引为0的值是h
2.char转为String:
- 通过String类的构造方法:
char[] ch = new char[] {'a','b','c'};
String str = new String(ch);
System.out.println(str);
运行结果如下:
我们怎么确定他就转换成功了呢? 我们可以通过调用String类的方法来确定
System.out.println(str.length);
结果如下:
- 通过String类的valueOf方法:
char[] ch = new char[] {'a','b','c'};
String str1 = String.valueOf(ch);
System.out.println(str1);
运行结果如下:
其实这个valueOf在JDk内部还是调用的String类的构造方法。
- 将字符数组的部分内容转为字符串对象
还是通过构造方法:
char[] ch = new char[] {'a','b','c'};
String str = new String(ch);
String str1 = new String(ch,1,2);
System.out.println(str);
System.out.println(str1);
运行结果如下:
构造方法里的1代表字符数组开始的索引,2代表转换的字符个数。
3.判断一个字符串的对象是由纯数字组成
- 自己写一个循环去遍历
public class StringTest {
public static void main(String[] args) {
String str1 = "123";
String str2 = "123a";
System.out.println(isNumber(str1));
System.out.println(isNumber(str2));
}
public static boolean isNumber(String str) {
//转为字符数组
char[] data = str.toCharArray();
//循环遍历data中的每个字符,判断这个字符是否是数字字符
for (char c : data) {
if (c < '0' || c > '9') {
return false;
}
}
return true;
}
}
- 待用Character包装类的 isDigit() 方法:
public static boolean isNumber(String str) {
//转为字符数组
char[] data = str.toCharArray();
//循环遍历data中的每个字符,判断这个字符是否是数字字符
for (char c : data) {
//判断字符c是否是数字,返回true或false
if (!Character.isDigit(c)) {
return false;
}
}
return true;
}
运行结果如下:
这两个方法干的事完全一样。不过一个是我们自己写的,一个是JDK给我们写好的。
总结
要使用String类,就采用直接赋值的方式。要比较内容是否相等使用equals方法。还有一些字符串的其他操作。
关于String类还有其他常见的操作,要是再写下去文章未免过于臃肿,不利于大家阅读。
如文中有不足之处,还请批评指出。