![](https://i-blog.csdnimg.cn/blog_migrate/4dd9d6d26e2dd2fe22b65ef381435187.png)
童年的纸飞机,现在终于飞回我手里.
目录
2.1.1 boolean equals(Object anObject) 方法
2.1.2 int compareTo(String s)方法
4、StringBuilder 和 StringBuffer
4.2 介绍StringBuilder 和 StringBuffer
1、字符串的构造
1.1 简单初始化字符串
在我们前面也对字符串进行了简单的使用,在Java当中,String是字符串类型,本质上也是一个类,这个类中提供了很多的方法,我们后续会学习到,现在先来简单看一下String类常见的构造方法:
public static void main(String[] args) {
String str1 = "hello";
String str2 = new String("hello");
char[] array = { 'h','e','l','l','o' };
String str3 = new String(array);
}
还有其他的构造方法,但是常用的就上面这三种,分别是使用常量字符串构造,也就是直接用一个常量字符串赋值给String类型的变量(引用),第二种就是new一个String的对象,第三种是将字符数通过String构造方法转换成字符串。
至于第一种和第二种写法有什么区别,等后面我们学了字符串常量池放在那个时候讲解。
1.2 String是引用类型
因为String是引用类型,所以内部并不存储字符串本身,在String内部原码中,String类实例变量如下:
通过查看String类的原码可以发现,其实一个String引用的对象里面是有两个实例变量的,分别是value 字符数组 和 hash 也就是哈希, 所以在JDK1.8中,字符串实际保存在char类型的数组里头,这里我们先来简单的看一段代码:
public static void main(String[] args) {
// s1和s2引用的是不同对象 s1和s3引用的是同一对象
String s1 = new String("hello");
String s2 = new String("hello");
String s3 = s1;
System.out.println(s1 == s2); // false
System.out.println(s1 == s3); // true
}
为了方便大家更好的了解,我们给上面的代码画一个草图,并不是真正的内存布局图!但不影响我们的理解,等到字符串常量池会给大家画真实的内存布局:
所以上面的代码打印的结果也就是 fasle 和 ture,因为引用使用 == 比较比较的是存储的地址是否相同,而通过上图就能发现 s1 和 s3 存储的地址是相同的,所以打印结果正如我们猜想的一样:
这里我们还需要注意一点,使用 "" 引起来的也是String类型对象,也可也调用对象中可以调用的属性:
// 打印"hello"字符串(String对象)的长度
System.out.println("hello".length());
2、String类常用方法
2.1 String对象的比较方法
在Java中我们有四种的比较方法,当然对于初学者见过最多的就是 == 的比较,但是这个在引用类型比较的是引用的地址是否相同,而基本类型则是比较里面存储的值是否相同,简单来说就是比较变量存储的值,所以 == 号我们不做过多的阐述。
2.1.1 boolean equals(Object anObject) 方法
这个方法其实也不陌生,之前也见过,他是按照字典序也就是字符大小的顺序进行比较,而Sting类中重写了 equals 方法,具体我们可以看一下方法的实现:
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof String) {
String anotherString = (String)anObject;
int n = value.length;
if (n == anotherString.value.length) {
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;
}
这个就是String类中重写 equals 方法的实现代码了,我们简单分析一下:
第一步:首先判断当前引用和 anObject也就是传过来的引用所引用的对象是否相同,因为Object是所有类的父类,这里用到了向上转型,如果引用的是同一个对象则返回 true
第二步:判断 anObject 里面的对象是不是 String 类型对象,如果是就继续比较,不是则直接返回false。
第三步:如果 anObject里面的对象是String类型对象,我们就先把anObject向下转型成String对象,这样就能访问本身对象自己的属性了,接着把当前对象里面字符数组的长度赋值给了n,在判断当前两个对象的里面存放的字符数组长度是否相同,相同则继续比较不是还是返回false。
第四步:如果if里面的字符长度相同,我们就按照字典序逐个字符往后比较,当这个if里面循环成功走完了,也就返回了true,一旦发现有一个字符不相同就会返回false
原码已经简单的分析过了,至于这个方法的使用前面也见到过,所以剩下使用就交给各位感兴趣的下来摸索了。
2.1.2 int compareTo(String s)方法
这个方法是实现的 Comparable 接口里面的 compareTo 方法,与上面不同的是,他返回的是int类型,并不是boolean类型,具体比较方式也是按照字典序进行比较,如果出现不同的字符,就直接返回这两个字符的差值,如果前 k 个字符(k为两个字符串最小的长度值),就返回两个字符串的差值。
public int compareTo(String anotherString) {
int len1 = value.length;
int len2 = anotherString.value.length;
int lim = Math.min(len1, len2);
char v1[] = value;
char v2[] = anotherString.value;
int k = 0;
while (k < lim) {
char c1 = v1[k];
char c2 = v2[k];
if (c1 != c2) {
return c1 - c2;
}
k++;
}
return len1 - len2;
}
这个原码实现的也很简单我们上面也简单说明了一下,就不多说,感兴趣的还是要自己下来用一用这个方法。
2.1.3 int compareToIgnoreCase(String s)方法
这个方法跟上面的 compareTo 方法的区别就是忽略了大小写比较,这里如果要看原码已目前的知识储备是不好理解的,这个知道如何使用就行:
public class Main {
public static void main(String[] args) {
String s1 = "HeLLO";
String s2 = "hEllo";
System.out.println(s1.compareToIgnoreCase(s2));
}
}
2.2 字符串查找方法
字符串查找是很常见的操作,在日常刷题的过程中也会碰到,而Java中用的比较多的就是 char charAte(int index) ,int indexOf(...),int lastIndexOf(...),里面打三个点是因为这些方法都被重载了,形参列表是不同的,具体看下面代码和注释:
public static void strFind() {
String str = "hello";
//返回字符串某个位置上的字符,不能越界也不能是负数
System.out.println(str.charAt(1));
//返回指定字符第一次出现的位置,没有返回-1
System.out.println(str.indexOf('l'));
//从fromIndex位置开始找指定字符第一次出现的位置,没有返回-1
System.out.println(str.indexOf('l', 3));
//返回一个字符串第一次出现的起始位置,没有返回-1
System.out.println(str.indexOf("el"));
//从fromIndex位置开始找指定字符串第一次出现的位置,没有返回-1
System.out.println(str.indexOf("el", 2));
//从后往前找,返回指定字符第一次出现的位置,没有返回-1
System.out.println(str.lastIndexOf('e'));
//从fromIndex位置从后往前找到指定字符第一次出现的位置,没有返回-1
System.out.println(str.lastIndexOf('e', 3));
//从后往前找指定字符串第一次出现的位置,没有返回-1
System.out.println(str.lastIndexOf("el"));
//从fromIndex位置从后往前找指定字符串第一次出现的位置,没有返回-1
System.out.println(str.lastIndexOf("el", 4));
}
这个比较简单,下来多练习下就好了,至于原码的实现不必太过关心,如果每个都去解读原码那得讲到后年马月, 感兴趣的可以下来自己看一看,按Ctrl+鼠标左键,点一下你要进入的方法名就好了。
注意:上面的方法都是要依赖于对象的,也就是实例方法。
2.3 转换相关的方法
2.3.1 数值和字符串之间的转换
public static void strTransform() {
//数字转字符串
String s1 = String.valueOf(123);
String s2 = String.valueOf('b');
String s3 = String.valueOf(12345f);
String s4 = String.valueOf(true);
String s5 = String.valueOf(new Student("张三", 24));
//字符串转数字
int a = Integer.parseInt("1234");
double d = Double.parseDouble("123.4");
}
2.3.2 大小写转换
public static void strTransform() {
//大小写转换
String str1 = "hELlo";
//转换会产生一个新的对象,不会修改原有的字符串
System.out.println(str1.toUpperCase()); //小写转大写
System.out.println(str1.toLowerCase()); //大写转小写
}
2.3.3 字符串转数组
public static void strTransform() {
//字符串转数组
String str2 = "hello";
char[] array1 = str2.toCharArray(); //使用toCharArray方法
for (int i = 0; i < array1.length; i++) {
System.out.print(array1[i] + " ");
}
System.out.println();
//数组转字符串
char [] array2 = new char [] { 'a','b','c' };
String str3 = new String(array2);
System.out.println(str3);
}
2.3.4 格式化
public static void strTransform() {
//格式化
String str4 = String.format("%d-%d-%d", 2022, 8, 19);
System.out.println(str4);
最终输出这个字符串也就是会打印:2022-8-19,学过C语言的小伙伴可能会比较的熟悉,这里照葫芦画瓢即可,得注意一个点,如果是特殊字符格式化比如 %d\%d\%d,这里我们要转义一下即可,如果不知道 %d 是什么意思,可以去参考下C语言的文章。
2.4 字符串替换
将一个字符串的内容替换成新的内容,但是还是会返回一个新的对象不会修改原有的对象,至于为什么后面会讲解:
public static void strReplace() {
String str = "hello world";
//替换所有的指定内容,不会修改原字符串
System.out.println(str.replaceAll("l", "i"));
//替换首个指定内容,不会修改原字符串
System.out.println(str.replaceFirst("l", "i"));
}
2.5 字符串拆分
这里我们使用的是 String[ ] split(String regex) 方法
2.5.1 按指定字符拆分
public static void strSplit() {
String str = "hello world is-is-123";
String[] ret = str.split(" "); //按照空格拆分
for (String x : ret) {
System.out.println(x);
}
ret = str.split(" ", 3); //部分拆分
for (String x : ret) {
System.out.println(x);
}
}
这个方法的返回值是一个String类型的数组,所以我们需要拿对应的数组来接收,接着可以遍历这个循环来打印这个数组的内容,如果看不懂这种打印,可以参考我之前的程序逻辑控制文章,第二个部分拆分怎么理解呢?就是我要按照指定的字符把他拆分成几段,如果是一段那这这个字符串就不会发生变化,大家下来可以自己尝试下,有些特殊字符可能需要用到转义才能正确拆分。
比如我们拆分 ip 地址和 路径:
public static void StrIntercepting() {
//拆分ip地址,特殊字符作为分隔符可能无法正确切分,需要加上转义
String str1 = "192.168.2.1";
//因为\本身就是个特殊字符,所以需要转义成字面的\才能对.进行转义
String[] ret = str1.split("\\.");
for (String x : ret) {
System.out.println(x);
}
String str3 = "lqg\\work\\code";
//这里因为\\表示一个\的字面意思,而一个\又是一个特殊字符
//所以我们还需要\\来表示一个\字面意思,再用它来转义一个\从而才是真正的\
ret = str3.split("\\\\");
for (String x : ret) {
System.out.println(x);
}
}
2.5.2 多次拆分
public static void strSplit() {
//多次拆分
String str2 = "name=Mike&age=24";
String[] ret = str2.split("=");
//第一种方法
for (String x : ret) {
String[] tmp = x.split("&");
for (String s : tmp) {
System.out.println(s);
}
}
//第二种方法
for (int i = 0; i < ret.length; i++) {
String[] tmp = ret[i].split("&");
for (int j = 0; j < tmp.length; j++) {
System.out.println(tmp[j]);
}
}
}
这个多次拆分我们可以分析一下,首先 split 方法返回的是一个数组,所以也就是每个数组里面放的是字符串,然后我们想要再次进行拆分就可以访问对应数组下标的字符串进行拆分,这样也就可以实现多次拆分,上面两种方法提供参考。
还有其他的一些方法比如 String trim() 方法,可以去掉字符串开头和结尾的空格字符等等,可以去查阅下帮助手册自行学习,上面已经介绍了一些常用的方法。
3、字符串常量池
3.1 什么是字符串常量池
字符串常量池在 JVM 中是StringTable类,实际上是一个固定大小的HashTable也就是哈希表(一种高效用来查找的数据结构,后续学习数据结构会详细讲解),在不同JDK版本下的字符串常量池的位置以及默认大小是不同的,但是我们今天是用JDK1.8约等于Java8,字符串常量池的位置在堆中,可以设置大小,有范围限制,默认是1009。
在Java程序中,类似于:1,2,3,3.14,"hello" 等字面常量经常被使用,为了程序的运行速度更快,更节省内存,Java为8中基本数据类型和String类都提供了常量池,我们现在只讲述字符串常量池,至于更详细池的了解会在学习JVM的时候进行讲解。
3.2 从内存的角度理解创建String对象
由于常量池在不同的版本是可能不一样的,目前是在Java8上分析:
我们先来看这样一段代码:
public static void main(String[] args) {
String s1 = "hello";
String s2 = "hello";
String s3 = new String("hello");
String s4 = new String("hello");
System.out.println(s1 == s2); // true
System.out.println(s1 == s3); // false
System.out.println(s3 == s4); // false
}
我们尝试着先来简单分析一下这段代码:
首先 s1 是用常量字符串进行构造,所以会先去字符串常量池存放的地址找对应的链表节点里面对应的对象有没有指向 "hello" 这个字符串(简单理解就是看有没有对应的字符串的对象),如果没有,先用一个哈希桶,每个桶中是链表的结构,链表节点里面包含存储着这个字符串对象的地址,和哈希值,以及其他东西,并且字符串常量池中存放着这个节点的地址,而我们知道字符串对象中有两部分,一部分是数组一部分是哈希,所以数组的那部分就存了 "hello" 字符串的地址
当 s2 创建的时候,发现已经有了 "hello" 字符串对象,所以在创建 s2 这个引用的时候,指向的就是 s1 指向的对象,即不用再新创建新对象了
当 s3 创建的时候,我们还是会去字符串常量池中找,发现有对应的字符串对象,但是我们是 new对象,也就是 new 代表着新,所以会新建一个对象,而这个对象是String类型,里面有两个部分,由于已经存在了 "hello" 字符串,所以 s3 引用的对象里面数组那部分存的是这个字符串的地址,本质上是新创建了一个对象,但是对象里面的数组还是指向那个字符串
当 s4 创建的时候,跟 s3 一模一样,这里我就不多说,下面我们就来看真实的内存图:
所以通过上图我们也可以看出,只要是new对象,都是唯一的!
还可以看到,使用常量串字符串创建String类型对象的效率更高,并且不用创建新对象还更节省空间,用户也可以将创建的字符串对象通过 intern 方式添加进字符串常量池中。
3.3 intern 方法
这个方法是一个 native 方法(native 方法指底层使用C++实现的,看不到实现的原码),这个方法的作用是手动将创建的String对象添加到常量池中。
public static void main(String[] args) {
char[] ch = new char[] {'a', 'b', 'c'};
String s1 = new String(ch); // s1对象并不在常量池中
s1.intern(); // s1.intern();调用之后,会将s1对象的引用放入到常量池中
String s2 = "abc"; // "abc" 在常量池中存在了,s2创建时直接用常量池中"abc"的引用
System.out.println(s1 == s2);
}
这串代码我们可以来简单分析一下:首先 s1 是用字符数组构造的,所以value指向的就是堆中把已有的数组拷贝一份,指向新拷贝的数组,即s1对象并不会放在池中,而后面调用了 intern 方法,也就是将 s1 对象的引用放到常量池,而 s2 发现常量池中有对应这个字符串的对象,就会直接引用 s1 对象的地址。
假设没有使用 intern 这个方法,s1引用的对象与s2并不相同,如果使用了则 s1 与 s2 引用的对象相同!
3.4 一道面试题
在JDK1.8中,请解释一下对象实例化的区别(常量池中都不存在当前字符串):
String str = "hello"; :这个只会开辟一块空间,会把字符串保存在常量池中,然后str共享常量池中的String对象,如果有,则直接引用这个对象。
String str = new String("hello"); :这会开辟两块堆内存空间,字符串"hello"保存在字符串常量池中,然后用常量池中的String对象给新开辟 的String对象赋值。。
String str = new String(new char[] {'h', 'e', 'l', 'l', 'o'}); :先在堆中创建一个String对象,然后利用 coypof 将重新开辟数组空间将参数字符串数组中内容拷贝到String对象中。
4、StringBuilder 和 StringBuffer
4.1 为什么String对象不可变?
前面的很多方法确实证明了String对象的不可变,都会产生一个新的对象,都不会在原有的基础上修改,这是为什么呢?我们再次看一下String类中的原码:
我们先来看String类被 final 修饰了,表示这个类不能被继承,跟里面的 value 数组能不能被修改没有关系,接着再来看 value 数组被 final 修饰了!
那么 final 修饰的数组,表示数组引用的地址不能被改变!也就是 value 引用的对象不能改变,但是可以改变引用对象里面的值,也就是可以改变数组存储的内容!
所以字符串真正不能被改变的原因是前面的 priavte 权限修饰符,表示这个成员变量只能在该类中被访问,而且 String 类并没有提供 getValue 和 setValue 方法!也即没有提供能让你操作这个字符串的方法,你在外部压根访问不到这个数组,你如何修改?这才是String对象不可变的根本原因!
如果要修改字符串的内容如何修改呢?借助StringBuilder和StringBuffer类!
4.2 介绍StringBuilder 和 StringBuffer
我们要尽量避免直接对String类型对象进行修改,因为String类是不能修改的,所有的修改都会创建新对象,效率非常低下。
为了方便字符串的修改,Java就提供了如上两个类,但是这两个类的大部分操作方式是相同的,下面我们就来简单了解下里面常用的方法,用的时候查一下即可:
- StringBuff append(String str) :在尾部追加,相当于String的+=,可以追加:boolean、char、char[]、 double、float、int、long、Object、String、StringBuff的变量
- char charAt(int index):获取index位置的字符
- void setCharAt(int index, char ch):将index位置的字符设置为ch
- StringBuff insert(int offset, String str):在offset位置插入:八种基类类型 & String类型 & Object类型数据
- StringBuffer deleteCharAt(int index):删除index位置字符
- StringBuffer reverse():反转字符串
这些太多了,就不一一列举,下来可以自己查一下。
String和StringBuilder最大的区别在于String的内容无法修改,而StringBuilder的内容可以修改。频繁修改字符串的情况考虑使用StringBuilder。
注意:String 和 StringBuilder类不能直接转换。如果要想互相转换,可以采用如下原则:
- String 变为 StringBuilder: 利用StringBuilder的构造方法或append()方法
- StringBuilder 变为 String: 调用toString()方法。
4.3 三种字符串类型的区别
String 的内容不可修改,StringBuffer 与 StringBuilder 的内容可以修改。
StringBuffer 与 StringBuilder大部分功能是相似的。
StringBuffer 采用同步处理,属于线程安全操作,而 StringBuilder 未采用同步处理,属于线程不安全操作。(后期学习会接触多线程,目前了解即可)
下期预告:【JavaSE 认识异常】