🎉🎉🎉写在前面:
博主主页:🌹🌹🌹戳一戳,欢迎大佬指点!
博主秋秋:QQ:1477649017 欢迎志同道合的朋友一起加油喔💪
目标梦想:进大厂,立志成为一个牛掰的Java程序猿,虽然现在还是一个小菜鸟嘿嘿
-----------------------------谢谢你这么帅气美丽还给我点赞!比个心-----------------------------
String类
一,常用方法
1.1,字符串的构造方法
在Java里面,我们知道String是一个类,叫做字符串类,那么作为一个类,自然有它自己的构造方法,常用的有下面三种(其余的可以用到的时候去查看API文档):
public class TestDemo220528 {
public static void main(String[] args) {
// 1,使用常量串进行构造,其实和数组的静态赋值一样,这里也是省略的写法
String str1 = "hello";
System.out.println(str1);
// 2,使用关键字new一个对象
String str2 = new String("hahaha");
System.out.println(str2);
// 3,利用字符数组进行构造
char[] arr = {'a','b','c','d'};
String str3 = new String(arr);
System.out.println(str3);
}
}
//输出结果:
//hello
//hahaha
//abcd
注意:
在之前我们就经常说过,String是引用类型,所以由它定义的字符串对象也是引用类型的变量,而不是直接存储的字符串本身,那以上面字符串为例,看看在内存里面字符串到底是怎么存储的。
首先,我们来看看String类的定义的小部分源码:
内 存 图 : \color{orange}{内存图:} 内存图:
1,str1于str2:
2,str3:
因为str3在进行构造的时候,我们传入的是一个字符串数组,源码中定义的方法如下:
也即是说我们传入的字符数组会先被拷贝一份之后让value去引用这个数组。
总结,Java里面的字符串对象,都包含两个属性,就是hash以及value,其中value是一个引用类型,指向的也就是一个字符数组,我们的字符串的内容最终也都是被拆分成了一个个的字符保存在这个字符数组里面。
1.2,String对象的比较
1,== 比较是否引用同一个对象
对于==而言,就是比较的左右两边的值是否一样,内置类型比较的就是数值,引用类型比较的就是地址是否一样。
public class TestDemo220528 {
public static void main(String[] args) {
int a = 10;
int b = 20;
System.out.println(a == b);
String str1 = new String("hello");
String str2 = new String("hello");
System.out.println(str1 == str2);//str1,str2就是两个对象,地址的值肯定不相同
}
}
2,equals方法
String类里面重写了equals方法,可以用来比较两个字符串对象里面的字符串是否相同。注意Object类里面的equals默认还是用双等号来进行比较的,所以在一般情况下都会需要我们去重写equals方法。
public class TestDemo220528 {
public static void main(String[] args) {
int a = 10;
int b = 20;
System.out.println(a == b);
String str1 = new String("hello");
String str2 = new String("hello");
System.out.println(str1 == str2);//str1,str2就是两个对象,地址的值肯定不相同
System.out.println(str1.equals(str2));//true,字符串的内容是一样的
}
}
3,compareTo()方法
和equals方法一样,compareTo()方法也是比较两个字符串对象的内容是否相同,只是在返回值上是int类型。
public class TestDemo220528 {
public static void main(String[] args) {
String str1 = new String("hello");
String str2 = new String("hello");
String str3 = new String("abcd");
String str4 = new String("abcdef");
System.out.println(str1.compareTo(str2));//输出0
System.out.println(str1.compareTo(str3));//输出7
System.out.println(str3.compareTo(str4));//输出-2
}
}
在返回值上,如果说两个字符串的内容一样,那么就返回0。如果是到某一个字符上不相同,那么就是返回两个字符字典序的差值,如果是前k个字符一样(k是较短的那个字符串的长度),那么就是返回两个字符串之间长度的差值。(Java里面的字符串没有什么以斜杠零结尾)
4,compareToIgnoreCase()方法
public class TestDemo220528 {
public static void main(String[] args) {
String str1 = new String("HELLO");
String str2 = new String("hello");
System.out.println(str1.equalsIgnoreCase(str2));//忽略大小写进行比较 true
System.out.println(str1.compareToIgnoreCase(str2));//忽略大小写进行比较 false
}
}
1.3,字符串查找
String类提供的常用查找方法如下:
1,char charAt(int index)
作用:返回字符串index下标处对应的字符,如果index为负数或者越界都会抛出异。 |
public class TestDemo220528 {
public static void main(String[] args) {
String str1 = new String("hello");
for (int i = 0; i < str1.length(); i++) {
System.out.println(str1.charAt(i));//将字符串的字符一个个输出
}
}
}
2,int indexOf(int ch)
作用:返回字符ch第一次出现的位置,没有就返回-1。 |
public class TestDemo220528 {
public static void main(String[] args) {
String str1 = new String("hello");
int ret = str1.indexOf('l');
System.out.println(ret);//输出2
}
}
3,int indexOf(int ch, int fromIndex)
作用:从fromIndex位置开始找ch第一次出现的位置,没有返回-1。 |
public class TestDemo220528 {
public static void main(String[] args) {
String str1 = new String("hello");
int ret = str1.indexOf('l',3);
System.out.println(ret);//输出3,注意,是从fromindex开始找,但是你的下标的参照还是从起始位置开始的哦
}
}
4,int indexOf(String str)
作用:返回str第一次出现的位置,没有返回-1。 |
public class TestDemo220528 {
public static void main(String[] args) {
String str1 = new String("hello");
int ret = str1.indexOf("ll");
System.out.println(ret);//输出2
}
}
5,int indexOf(String str, int fromIndex)
作用:从fromIndex位置开始找字符串str第一次出现的位置,没有返回-1。 |
public class TestDemo220528 {
public static void main(String[] args) {
String str1 = new String("helloll");
int ret = str1.indexOf("ll",4);
System.out.println(ret);//输出5
}
6,int lastIndexOf(int ch)
作用:从后往前开始找字符ch,如果没有则返回-1。 |
public class TestDemo220528 {
public static void main(String[] args) {
String str1 = new String("hello");
int ret1 = str1.lastIndexOf('e');
System.out.println(ret1);//输出1
}
}
7,int lastIndexOf(int ch, int fromIndex)
作用:从fromIndex位置开始找,从后往前找ch第一次出现的位置,没有返 回-1。 |
public class TestDemo220528 {
public static void main(String[] args) {
String str1 = new String("abcdcef");
int ret = str1.lastIndexOf('c',3);
System.out.println(ret);//输出2
}
}
8,int lastIndexOf(String str)
作用:从后往前找,返回str第一次出现的位置,没有返回-1。 |
public class TestDemo220528 {
public static void main(String[] args) {
String str1 = new String("abcdcef");
int ret = str1.lastIndexOf("dc");
System.out.println(ret);//输出3
}
}
9,int lastIndexOf(String str, int fromIndex)
作用:从fromIndex位置开始找,从后往前找str第一次出现的位置,没有返 回-1。 |
public class TestDemo220528 {
public static void main(String[] args) {
String str1 = new String("abcdcedcf");
int ret = str1.lastIndexOf("dc",5);
System.out.println(ret);//输出3
}
}
1.4,字符串转化
1,数值转字符串
class Student{
public int age;
public Student(int age) {
this.age = age;
}
@Override
public String toString() {
return "Student{" +
"age=" + age +
'}';
}
}
public class TestDemo220529 {
public static void main(String[] args) {
// 将其他数据类型转换成字符串
String str1 = String.valueOf(123);
String str2 = String.valueOf(11.2);
String str3 = String.valueOf(new Student(18));//也可以将一个对象转换成字符串
System.out.println(str1);//输出123
System.out.println(str2);//输出11.2
System.out.println(str3);//输出 Student{age=18}
}
}
对于valueOf而言,内部都会调用一个toString()的方法,只是根据你传进的参数类型不同,调用的是不同类的toString()方法而已。
2,字符串转数值
public class TestDemo220529 {
public static void main(String[] args) {
int a = Integer.valueOf("100");
int b = Integer.valueOf("100",8);//八进制转化
System.out.println(a);//输出100
System.out.println(b);//输出64
int c = Integer.parseInt("123");
System.out.println(c);//输出123
}
}
在大多数的情况下,我们用parseInt()之类的方法比较多。
3,大小写转化
public class TestDemo220529 {
public static void main(String[] args) {
String s1 = new String("HELLO");
System.out.println(s1.toLowerCase());//输出hello
System.out.println(s1);//输出HELLO
String s2 = new String("hello");
System.out.println(s2.toUpperCase());//输出HELLO
System.out.println(s2);//输出hello
}
}
字符串大小写转化是非常简单的,需要注意的几点是在大小写转换的时候是只会考虑字符串中的英文字母的,比如说有中文在里面照样还是只会转换英文。另外,在进行大小写转换后是会产生新的字符串对象的,所以根本就不是在原字符串上进行修改的。
4,字符串转数组
public class TestDemo220529 {
public static void main(String[] args) {
// 字符串转数组
String s1 = new String("hello");
char[] ch = s1.toCharArray();//字符串转数组的方法
for (char x:ch) {
System.out.println(x);
}
// 数组转字符串
char[] ch1 = {'h','e','l','l','o'};
String s2 = new String(ch1);
System.out.println(s2);
}
}
5,格式化输出为字符串
public class TestDemo220529 {
public static void main(String[] args) {
String s1 = String.format("%d - %d - %d",2022,5,29);
System.out.println(s1);//输出字符串 “2022 - 5 - 29”
}
}
1.5,字符串替换
public class TestDemo220529 {
public static void main(String[] args) {
String s1 = new String("abcdefabdddd");
//替换单个字符
String ret = s1.replace('a','t');//替换生成一个新的对象
System.out.println(ret);//tbcdeftbdddd
//替换字符串
String ret1 = s1.replace("ab","qq");
System.out.println(ret1);//qqcdefqqdddd
//替换字符串中的某一个全部的内容
String ret2 = s1.replaceAll("ab","ss");
System.out.println(ret2);//sscdefssdddd
//替换第一次出现的内容
String ret3 = s1.replaceFirst("ab","hh");
System.out.println(ret3);//hhcdefabdddd
}
}
替换的时候可以替换单个字符,也可以是字符串,但是无论怎样,字符串的内容是不能够修改的,所以替换后都是生成的新的字符串。
1.6,字符串拆分
1,将字符串全部拆分
//注意split的参数是字符串不是字符
public class TestDemo220530 {
public static void main(String[] args) {
String s1 = new String("I am a student");
String[] ret = s1.split(" ");
for (String s:ret) {
System.out.println(s);
}
}
}
字符串拆分的方法split的返回值是字符串数组,按照传入的字符串,把原字符串进行拆分。比如上面的空格,虽然是一个字符,但是你得当作一个字符串传入,也就是 “ ”。
2,将字符串进行部分的拆分
public class TestDemo220530 {
public static void main(String[] args) {
String s1 = new String("I am a student");
String[] ret = s1.split(" ",2);
for (String s:ret) {
System.out.println(s);
}
}
}
//输出 I
// am a student
注意,String[] split(String regex, int limit) ,后面的limit是拆分成多少组的限制条件,不是拆分几次,这个点不要混淆了。还有就是如果你给的分的部分的限制数已经大于本身就能分成的部分数时,那这个时候就是能分多少个部分就分多少个部分。
注意:
1,字符"|","*","+"都得加上转义字符,前面加上 "\\" 。 |
因为某些字符代表有特殊的含义,但是作为分隔符的时候就是仅仅代表这个字符本身,所以在使用的时候我们必须要进行转义。
public class TestDemo220530 {
public static void main(String[] args) {
String s1 = new String("192.168.11.11");
String[] ret = s1.split(".");
for (String s:ret) {
System.out.println(s);
}
}
}
//按照常理而言,好像并没有任何的使用错误,但是实质就是没有任何的输出,调试后也会发现ret数组里面根本什么都没有,也即是没有拆分
//正确做法:
public class TestDemo220530 {
public static void main(String[] args) {
String s1 = new String("192.168.11.11");
String[] ret = s1.split("\\.");
for (String s:ret) {
System.out.println(s);
}
}
}
这个时候就体现了转义字符的用处,两个斜杠表示为一个斜杠的意思,一个斜杠加上一个点号,就是表示单纯的点号这个标点符号。例如两个斜杠的分隔符要表示的话必须要四个斜杠才可以转义为两个斜杠最原始的意思。
2,如果存在多个分割符,可以用|来作为连字符。也就是表示|左右两边都是分隔符。 |
public class TestDemo220530 {
public static void main(String[] args) {
String s1 = new String("name==zhangsan&&age==20");
String[] ret = s1.split("==|&&");
for (String s:ret) {
System.out.println(s);
}
}
}
//输出
//name
//zhangsan
//age
//20
当然,这是选择用|,其实也可以嵌套使用split达到相同的效果
public class TestDemo220530 {
public static void main(String[] args) {
String s1 = new String("name==zhangsan&&age==20");
String[] ret = s1.split("&&");//首先按照&&拆分一次
for (String s:ret) {
String[] ret1 = s.split("==");//把按照&&拆分的每一部分再按照==拆分一次就好
for (String ss:ret1) {
System.out.println(ss);
}
}
}
}
1.7,字符串截取
public class TestDemo220530 {
public static void main(String[] args) {
String s = new String("hello world");
//截取beginIndex到字符串的末尾
String ret1 = s.substring(3);
//截取beginIndex到endIndex,但是注意这个范围是一个左闭右开的!!!!!
String ret2 = s.substring(3,7);
System.out.println(ret1);//输出 lo world
System.out.println(ret2);//输出 lo w
}
}
1.8,其他操作方法
String trim()//去掉字符串左右两边的空格
public class TestDemo220530 {
public static void main(String[] args) {
String s = new String(" ab ababab abab ");
System.out.println(s);
System.out.println(s.trim());
}
}
程序运行截图:
1.9,字符串常量池
1,为什么引入常量池
在java程序中,类似于"hello","12.2"等字面类型的字符串常量会经常被使用,所以为了程序的运行速度更快,也更加节省内存,就引入了常量池这个概念,而有了池之后,像这些经常使用的字符串就会被直接保存在池之中,用的时候就不需要再去创建了。
当然,除了字符串常量池之外,还有以下的池可以先认识认识:
- Class文件常量池:每个.Java源文件编译后生成.Class文件中会保存当前类中的字面常量以及符号信息
- 运行时常量池:在.Class文件被加载时,.Class文件中的常量池被加载到内存中称为运行时常量池,运行时常量池每个类都有一份
2,字符串常量池的介绍
字符串常量池在JVM中是StringTable类,实际是一个固定大小的HashTable(哈希表)。
3,再谈字符串对象的创建
3.1,直接使用字符串常量进行赋值
对于字符串常量而言,在进行直接赋值的时候,会先看常量池里面有不有相同的字符串常量,有的话就可以直接把这个字符串常量对应的对象赋值给我们自己的字符串对象,如果字符串常量池里面没有相同的字符串常量,那么就会先创建,然后再进行赋值。
当然,所有的字符串常量需要创建的,在字节码文件加载的时候就已经创建好了。
public class TestDemo220530 {
public static void main(String[] args) {
String s1 = "hello";
String s2 = "hello";
System.out.println(s1 == s2);
}
}
对于上面的这段代码,可能主观上会觉得s1与s2是两个对象,所以输出结果为false,但是实际是true,但是为什么就是true呢?从内存图上看的会清楚,如下:
在对s1进行赋值的时候就会在常量池中创建这个一个元素会指向"hello"这个字符串对象,然后当s2赋值的时候,因为内容是相同的,就会直接去使用这个常量池中已经存在的”hello“字符串对象,所以最终s1与s2引用的是同一个字符串对象,自然s1 == s2就是true。字符串常量池里面的每一个元素都是一个链表。
3.2,通过new创建String对象
public class TestDemo220530 {
public static void main(String[] args) {
String s1 = "hello";
String s2 = new String("hello");
String s3 = new String("world");
System.out.println(s1 == s2);//输出 false
}
}
内
存
图
:
\color{orange}{内存图:}
内存图:
通过内存图可以看出,如果是我们直接使用字符串常量进行赋值的话,那么就会直接去常量池定义一个字符串对象,然后对象的value引用一个字符数组存储着每一个字符。但是当我们利用new关键字来定义字符串对象的时候,我们还是会现在堆上定义一个字符串对象,然后看字符串在常量池中存不存在,存在就让这个字符串对象的value直接引用已经存在的字符数组,如果不存在,就需要重新在常量池上又定义一个字符串对象,然后这个对象的value属性引用一个字符数组,然后在堆上的那个字符串对象的value属性也会引用这个字符数组。
所以,总的来说,直接使用常量串进行赋值会比new字符串对象在空间的节省以及速度上都会好很多。
3.3,intern方法
public class TestDemo220530 {
public static void main(String[] args) {
char[] ch = {'a','b','c'};
String s1 = new String(ch);
String s2 = "abc";
System.out.println(s1 == s2);//输出false
}
}
上面这段代码毫无疑问的会输出false,因为s1,s2是两个不同的对象的引用,地址肯定是不同的。
因为s1我们是通过字符数组来对字符串对象进行构造的,而不是双引号引起来的内容,所以这个字符串对象是不会开辟在常量池里面的,但是s2是开辟在常量池里面的,所以可以很清楚的看到s1所引用的对象与s2所引用的对象是不一样的。
也正是如此,我们的intern方法可以将不是在常量池中的字符串对象添加到常量池里面,比如这里的s1。
public class TestDemo220530 {
public static void main(String[] args) {
char[] ch = {'a','b','c'};
String s1 = new String(ch);
s1.intern();
String s2 = "abc";
System.out.println(s1 == s2);//输出true
}
}
内存图:
通过intern()方法将s1添加进常量池后,因为s1与s2的字符串的内容是一样的,所以在常量池里面就只会有一个相同的字符串对象,那么自然s1 == s2就是true了。
注意:
intern()方法的作用是将字符串对象添加进常量池,但是这个添加是有前提条件的,那就是常量池里面不能存在有相同内容的字符串对象,如果存在,那么就不会进行添加了,因为没有必要。
public class TestDemo220530 {
public static void main(String[] args) {
char[] ch = {'a','b','c'};
String s1 = new String(ch);
String s2 = "abc";
s1.intern();
System.out.println(s1 == s2);
}
}
如同现在这段代码就会直接输出false,因为常量池中已经存在有相同内容的字符串对象s2,所以s1.intern()不会把s1添加进常量池,所以s1 == s2是不成立的。
总结,既然几种情况下的String对象的创建都已经介绍清楚了,那下面来看看面试题,进行下相关的总结:
【面试题:请解释String类中几种对象实例化的区别】
下面所有的情况均是在常量池不存在"hello"的情况下开展的:
1,String str = “hello” 这种情况下一共定义了一个对象,保存在常量池里面。
2,String str = new String(“hello”) 这种情况下一共定义了两个对象,一个就是单纯存在在堆上,另一个是在常量池中,但是两个字符串对象的value属性所引用的那个字符数组是一个。
3,String str = new String(new char[]{‘h’,‘e’,‘l’,‘l’,‘o’}) 这种情况下一共定义了三个对象,都是在堆上,没有在常量池,一个对象是char[]这个数组对象,一个是str这个字符串对象,还有一个是针对char[]这个数组拷贝出来供字符串对象引用的字符数组对象。
1.10,字符串的不可变性
String是一种不可变对象. 字符串中的内容是不可改变。字符串不可被修改
首先看看String类顶一个源码,可以看到,我们的String类是被final修饰的,所以不能被继承,同时value是被paivate所修饰的,所以在类外根本就拿不到value这个引用,所以又谈何能够修改他所指向的那个字符数组里面的内容呢。
那可能有的同学看到过有的说法说的是因为value被final所修饰所以改变不了字符串的值,我只想说,大错特错!
如图所示,value[0] = 'e’改变值是可以的,我们要清楚final其实就是定义这个变量为常量,也就是说它的值一旦赋值后就不能在改变,只是对于value这个变量而言比较特殊的是它里面存放的是地址,也就是说value的指向是不能够修改的。
总结,也正是因为我们的字符串不可变,所以上面所有的对字符串操作的函数都不是在原字符串上进行修改的,而是修改后产生了新的字符串对象。
1.11,字符串的修改
字符串虽然说是能够修改的,但是因为其自身是不可变的原因,所以在进行修改的时候会创建很多新的对象进行辅助操作,效率很低下。
//以拼接字符串为例
public class TestDemo220603 {
public static void main(String[] args) {
String s = "hello";
s += "world";
System.out.println(s);//输出helloworld
}
}
我们知道String类型的变脸是不能够修改的,但是这里实现了拼接实际上是底层上进行了优化实现的。底层上会优化为StringBuilder类来进行修改,我们反编译看到的详细过程如下:
//模拟实现下上面的编译过程
public class TestDemo220603 {
public static void main(String[] args) {
String s = "hello";
// 想要实现在后面追加一个world
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append("hello");
stringBuilder.append("world");
s = stringBuilder.toString();
System.out.println(s);
}
}
那正如上面代码所展示的,我们在对字符串进行修改的时候底层上利用的是StringBuilder,那为什么StringBuilder就可以呢?
我们查看源码发现,StringBuilder是继承于AbstractStringBuilder类的,成员属性value是一个字符数组,用来存储字符串的每一个字符,但是与String类的最大区别就是它的访问权限是public,所以在类外可以访问到并进行修改。我们在利用StringBuilder实现字符串的修改时,“hello”,"world"这些双引号直接引起来的字符串常量照样还是在常量池中,只不过我们在调用append()方法时,会利用这些常量池中的字符串来对我们的StringBuilder类的对象进行构造,所以最后修改追加完后,StringBuilder类的对象的value属性中就保存了修改完成的内容,最后调用toString()方法,会利用value的值来new一个String对象返回出去,然后你在用原来的Strinf对象去接收,那就是修改好的字符串了。
总结,因为String修改的底层上会优化为StringBuilder,所以会涉及到创建中间的临时StringBuilder对象,如果说像类似的追加次数很多,那么效率就会很低下,所以我们如果说是像实现类似的功能的话,不要利用String类去做文章,最好的办法就是直接使用StringBuilder或者StringBuffer。
二,StringBuilder,StringBuffer
上面既然用到了StringBuilder与StringBuffer,那么下面就给大家详细介绍下这两个类。
由于String类的不可修改性,所以为了方便字符串的修改,Java就提供了StringBuilder与StringBuffer这两个大类,他们里面拥有很多String类里面没有的方法。具体很多方法的使用大家可以去查看api文档,会有详细介绍,下面简单的为大家展示几个方法…
public class TestDemo220603 {
public static void main(String[] args) {
StringBuilder stringBuilder1 = new StringBuilder("hello");//"hello"还是在常量池
stringBuilder1.append("world");
System.out.println(stringBuilder1);//追加 相当于String类的 +=
System.out.println(stringBuilder1.length());//求字符串的长度
stringBuilder1.reverse();//将字符串进行逆置
String s1 = stringBuilder1.toString();//用String类型的变量接收
System.out.println(s1);
}
}
//输出结果:
//helloworld
//10
//dlrowolleh
【String类与StringBuilder类的转化问题:】
1,String转化为StringBuilder:利用StringBuilder的构造方法或者append方法
StringBuilder stringBuilder1 = new StringBuilder("hello");
StringBuilder stringBuilder2 = new StringBuilder();
stringBuilder.append("hello");
2,StringBuilder转化为String:利用StringBuilder的toString方法
StringBuilder stringBuilder1 = new StringBuilder("hello");
String s1 = stringBuilder1.toString();
【常见面试题:】
1,String,StringBuilder,StringBuffer的区别
String的内容不可以修改的,但是StringBuilder,StringBuffer的内容可以修改。
StringBuilder,StringBuffer的大部分功能相似。但是StringBuffer采用了同步处理机制,属于线程安全操作,StringBuilder未采用同步处理机制,属于线程不安全操作。(以append方法举例:)
可以看到StringBuffer的append()方法是有synchronized修饰的,你可以把它当成是一把锁,在多线程的情况下,当某一个进程在使用这个方法的时候,就会把这个方法锁起来,其余方法无法使用,使用完成后,锁会自动打开。但是也不是说有了这么一个锁,StringBuffer就是最好的,因为开锁关锁都是需要耗费资源时间的,所以在一般情况下,StringBuilder使用的会多一些。
2,以下总共创建了多少个String对象【前提不考虑常量池之前是否存在】
String str = new String("ab"); // 会创建多少个对象
会创建两个对象 new String()一个,然后常量池"ab"中一个
String str = new String("a") + new String("b"); // 会创建多少个对象
会创建六个对象 new String()一个,常量池"a"一个,new String()一个,常量池"b",前后拼接会创建一个StringBuilder对象
,赋值给str默认调用toString()方法的时候会new一个String对象
最后,今天的文章分享比较简单,就到这了,如果大家觉得还不错的话,还请帮忙点点赞咯,十分感谢!🥰🥰🥰