夯实Java基础(十三)----字符串
1、String的介绍
字符串无论是在Java语言中还是在其它语言中,都应该是用的最频繁、最多的,可见字符串对于我们来说是多么的重要,所以我们非常有必要去深入的了解一下。
String就代表字符串,在Java中字符串属于对象。我们刚刚接触Java时,在学习数据类型的时候应该提到过String。Java有基本数据类型和引用数据类型,而String就是一个引用数据类型,它是一个类,位于java.lang包下,既然它是一个类,那我们就来看看它的源码结构。
从上面的图可以看出,String类是用final修饰的,表明它不能再被继承了。同时还实现了序列化(Serializable)、比较排序(Comparable)、字符序列(CharSequence)这三个接口,表明字符串可以被序列化、可以用于比较排序。关于实现了CharSequence接口下面有讲到。我们还可以看到String类中定义了一个char型数组value[ ],这个是用于存储字符串的内容,并且使用final修饰的,表明这个是常量。所以字符串一旦被初始化,就不可以被改变,表示String是不可变性,这就导致每次对String的操作都会生成新的String对象。
以上可以得出字符串的一些特点:
- 字符串的一个重要特点就是字符串不可变。这种不可变性是通过内部的
private final char[]
字段,以及没有任何修改char[]
的方法实现的。修改一个字符串变量值,相当于新生成一个字符串对象。 - 上面这个字符数组
private final char value[];
也是字符串对象的内部存储形式。所以"abc"
等效于char[] data={ 'a' , 'b' , 'c' }
。
JDK1.9之前使用的是char[] value数组,JDK1.9之后改成了byte[]数组
- 字符串字面量也是一个String类的实例,存储在字符串常量池中,相同的字符串字面量表示的对象在内存中只有一份。
- 字符串String类型本身是final声明的,意味着我们不能继承String,也就意味着我们不能去重写他的方法。
2、CharSequence接口介绍
关于String实现CharSequence接口这里,我去百度一下,介绍如下:
CharSequence是一个接口,表示char值的一个可读序列。此接口对许多不同种类的char序列提供统一的自读访问。此接口不修改该equals和hashCode方法的常规协定,因此,通常未定义比较实现 CharSequence 的两个对象的结果。他有几个实现类:CharBuffer、String、StringBuffer、StringBuilder。
CharSequence与String都能用于定义字符串,但CharSequence的值是可读可写序列,而String的值是只读序列。
对于一个抽象类或者是接口类,不能使用new来进行赋值,但是可以通过以下的方式来进行实例的创建:
CharSequence cs="hello";
CharSequence的使用简单实例:
public class StringTest {
public static void main(String[] args) {
String str = "String";
StringBuffer sBuffer = new StringBuffer("StringBuffer");
StringBuilder sBuilder = new StringBuilder("StringBuilder");
show(str);
show(sBuffer);
show(sBuilder);
}
//如果参数类型为String则不能接收StringBuffer和StringBuilder
public static void show(CharSequence cs) {
System.out.println(cs);
}
}
运行的结果如下所示:
可能是这样理解的吧,CharSequence是一个接口,本身是没有什么读写意义的。String只是它的一个实现类,虽然String是只读,但是CharSequence的实现类还有StringBuffer,StringBuilder这些可写的,所以用CharSequence作为参数可以接收String,StringBuffer,StringBuilder这些类型。
此处参考链接:https://blog.csdn.net/a78270528/article/details/46785949
3、String字符串创建
字符串的创建比较简单,创建String的实例有两种方式,一种是直接给String的变量赋值,另一种是使用String的构造器创建实例。那么这两种方式创建的实例有什么区别呢?区别就是前者会创建一个对象,而后者会创建两个对象,这个后面讨论。下面是String中的构造方法:
public String()
:初始化新创建的 String对象,以使其表示空字符序列。String(String original)
: 初始化一个新创建的String
对象,使其表示一个与参数相同的字符序列;换句话说,新创建的字符串是该参数字符串的副本。public String(char[] value)
:通过当前参数中的字符数组来构造新的String。public String(char[] value,int offset, int count)
:通过字符数组的一部分来构造新的String。public String(byte[] bytes)
:通过使用平台的默认字符集解码当前参数中的字节数组来构造新的String。public String(byte[] bytes,String charsetName)
:通过使用指定的字符集解码当前参数中的字节数组来构造新的String。
上述字符串构造器简单代码示例:
//字符串常量对象,推荐
String str = "hello";
// 无参构造,不推荐
String str1 = new String();
//创建"hello"字符串常量的副本,不推荐
String str2 = new String("hello");
//通过字符数组构造
char chars[] = {'a', 'b', 'c','d','e'};
String str3 = new String(chars);
String str4 = new String(chars,0,3);
// 通过字节数组构造
byte bytes[] = {97, 98, 99 };
String str5 = new String(bytes);
String str6 = new String(bytes,"GBK");
然后再来分析一下直接给String的变量赋值和使用String的构造器创建实例分别创建了几个对象,这里好像也是面试的时候经常问的一个问题 ,举例代码如下:
public class StringTest {
public static void main(String[] args) {
//方式一:直接赋值
String s1 = "abc";
String s2 = "abc";
//方式二:new+构造器
String s3 = new String("abc");
String s4 = new String("abc");
System.out.println(s1 == s2);//true
System.out.println(s1 == s3);//false
System.out.println(s3 == s4);//false
System.out.println(s1.equals(s3));//true
}
}
运行结果一目了然,String的值是常量,它的值是放在方法区的处理池中,而常量池中相同的值在只会存在一份。我们知道==比较的地址,equal()比较的是内容,s1和s2指向同一个引用,所以地址相同,而s3和s4它们分别创建了两个对象,地址值显然不同。
通过这个图我们也容易分析出创建实例时创建了几个对象。
String s1 = "abc"创建对象的过程:首先检查常量池中是否存在内容为"abc"的字符串,如果有,则不再创建对象,直接让s1变量指向该字符串的引用,如果没有则在常量池中创建"abc"对象然后让s1引用该对象。
String s3 = new String("abc")创建实例的过程:首先在堆创建一个String的对象,并让s3引用指向该对象,然后再到常量池中查看是否存在内容为"abc"字符串对象,如果存在,则将String对象中的value引用指向常量对象,将new出来的字符串对象与字符串常量池中的对象联系起来,如果不存在,则在字符串常量池中创建一个内容为"abc"的字符串对象,并将堆中的String对象与之联系起来。
4、String字符串拼接
字符串的拼接有三种方式:①、直接使用"+";②、使用concat()方法;③、使用append()方法。这里我主要来讨论一下"+"问题,举例:
注:如果都能做对说明已经掌握的很好了👍
这里先给出String字符串拼接的一些结论(重要!!!):
- ①、常量与常量的拼接结果必定是常量,它们是在常量池中完成,因为常量在编译阶段就可以确定下来,在编译阶段就会自动拼接完成。
- ②、只要涉及到有变量的(非常量),结果都是在堆内存中完成的,因为变量只有在运行阶段才可以确定。
- ③、直接赋值
String str="a"
与构造器new String("a")
创建的字符串必定不会相等。 - ④、如果拼接后的结果调用了intern()方法,则返回值就是在常量池中。
- ⑤、字符串只有值传递,没有引用传递。
(1)、直接给String赋值的拼接举例:
@Test
public void test1() {
String s1 = "Hello";
String s2 = "World";
String s3 = "HelloWorld";
String s4 = "Hello" + "World";
String s5 = s1 + "World";
String s6 = "Hello" + s2;
String s7 = s1 + s2;
String s8 = s7.intern();
System.out.println("(1)" + (s3 == s4));
System.out.println("(2)" + (s3 == s5));
System.out.println("(3)" + (s3 == s6));
System.out.println("(4)" + (s3 == s7));
System.out.println("(5)" + (s3 == s8));
System.out.println("(6)" + (s5 == s6));
System.out.println("(7)" + (s5 == s7));
System.out.println("(8)" + (s6 == s7));
}
运行结果如下图所示:
(2)、直接赋值与使用构造方法创建字符串的拼接方式:
@Test
public void test2() {
String str1 = "1";
String str2 = "2";
String str3 = new String("1");
final String str4 = "2";
final String str5 = new String("2");
String str6 = "12";
String str7 = "1" + "2";
String str8 = str1 + "2";
String str9 = str1 + str2;
String str10 = str3 + str4;
String str11 = "1" + str4;
String str12 = "1" + str5;
String str13 = (str1 + str2).intern();
System.out.println("(1)" + (str1 == str3));
System.out.println("(2)" + (str2 == str4));
System.out.println("(3)" + (str4 == str5));
System.out.println("(4)" + (str6 == str7));
System.out.println("(5)" + (str6 == str8));
System.out.println("(6)" + (str6 == str9));
System.out.println("(7)" + (str6 == str10));
System.out.println("(8)" + (str6 == str11));
System.out.println("(9)" + (str6 == str12));
System.out.println("(10)" + (str6 == str13));
}
运行结果如下图所示:
(3)、字符串的值传递:
@Test
public void test3() {
String str1 = "hello";
String str2 = new String("hello");
char[] ch = new char[]{'h', 'e', 'l', 'l', 'o'};
String str3 = new String(ch);
change(str1, str2, ch, str3);
System.out.println("str1 = " + str1);
System.out.println("str2 = " + str2);
System.out.println("ch = " + String.valueOf(ch));
System.out.println("str3 = " + str3);
}
public static void change(String str1, String str2, char[] arr, String str3) {
str1 = "change";
str2 = "change";
str3 = "change";
arr[0] = 'c';
arr[1] = 'h';
arr[2] = 'a';
arr[3] = 'n';
arr[4] = 'g';
}
运行结果如下图所示:
5、String字符串中常用的方法
String中的常用方法我们需要熟练的掌握, 这样平时做一些字符串的常规操作就可以快速知道,而不用去查找API了。
①、常规:
- int length():返回字符串的长度: return value.length
- boolean isEmpty():判断是否是空字符串:return value.length == 0
- String trim():返回字符串的副本,忽略前导空白和尾部空白
- String concat(String str):将指定字符串连接到此字符串的结尾。 等价于用“+”
- String toLowerCase():使用默认语言环境,将 String 中的所有字符转换为小写
- String toUpperCase():使用默认语言环境,将 String 中的所有字符转换为大写
- char[] toCharArray():将字符串转为char型数组。
②、比较:
- boolean equals(Object obj):比较字符串的内容是否相同
- boolean equalsIgnoreCase(String anotherString):与equals方法类似,忽略大小写
- int compareTo(String anotherString):比较两个字符串的大小
- boolean matches(String regex):判断此字符串是否匹配给定的正则表达式。
③、查找:
- char charAt(int index): 返回某索引处的字符return value[index]
- int indexOf(String str):返回指定子字符串在此字符串中第一次出现处的索引。注:indexOf和lastIndexOf方法如果未找到都是返回-1
- int indexOf(String str, int fromIndex):返回指定子字符串在此字符串中第一次出现处的索引,从指定的索引开始
- int lastIndexOf(String str):返回指定子字符串在此字符串中最右边出现处的索引
- int lastIndexOf(String str, int fromIndex):返回指定子字符串在此字符串中最后一次出现处的索引,从指定的索引开始反向搜索
- boolean endsWith(String suffix):测试此字符串是否以指定的后缀结束
- boolean startsWith(String prefix):测试此字符串是否以指定的前缀开始
- boolean startsWith(String prefix, int toffset):测试此字符串从指定索引开始的子字符串是否以指定前缀开始
- boolean contains(CharSequence s):当且仅当此字符串包含指定的 char 值序列时,返回 true
④、替换:
- String replace(char oldChar, char newChar):返回一个新的字符串,它是通过用 newChar 替换此字符串中出现的所有 oldChar 得到的。
- String replace(CharSequence target, CharSequence replacement):使用指定的字面值替换序列替换此字符串所有匹配字面值目标序列的子字符串。
- String replaceAll(String regex, String replacement):使用给定的 replacement 替换此字符串所有匹配给定的正则表达式的子字符串。
- String replaceFirst(String regex, String replacement):使用给定的 replacement 替换此字符串匹配给定的正则表达式的第一个子字符串。
⑤、截取:
- String substring(int beginIndex):返回一个新的字符串,它是此字符串的从beginIndex开始截取到最后的一个子字符串。
- String substring(int beginIndex, int endIndex) :返回一个新字符串,它是此字符串从beginIndex开始截取到endIndex(不包含)的一个子字符串。
⑥、切片:
- String[] split(String regex):根据给定正则表达式的匹配拆分此字符串。
- String[] split(String regex, int limit):根据匹配给定的正则表达式来拆分此字符串,最多不超过limit个,如果超过了,剩下的全部都放到最后一个元素中。
⑦、其它
- boolean matches(String regex):用于配置正则表达式
- String trim():去除字符串前后的空格,返回一个新的对象
- static String valueOf(int i):将各种基本数据类型转为字符串
- public native String intern():如果字符串常量池中已经包含一个等于此String对象的字符串,则返回代表池中这个字符串的String对象;否则,将此String对象包含的字符添加到常量池中,并返回此String对象的引用
如果还想学习更多的方法,可以自行去看String的API或者源码。
6、StringBuffer和StringBuilder
StringBuffer和StringBuilder十分相似,它们都代表可变的字符序列,可以对字符序列进行增删改查操作,此时不会产生新的对象,而且它两内部的方法也是一样的。那么String、StringBuffer和StringBuilder的区别是什么。
- String(JDK1.0):字符串常量,不可变字符序列,线程安全,效率低。
- StringBuffer(JDK1.0):字符串变量,可变字符序列,线程安全,效率低。
- StringBuilder(JDK5.0):字符串变量,可变字符序列,线程不安全,效率高。
为什么说String和StringBuffer是线程安全,StringBuilder是线程不安全呢?
因为String是final修饰的常量,它是不可变的字符串,所有的操作都是不可能改变它的值,所以线程是安全的。再通过看StringBuffer和StringBuilder的源码,可以很明显发现,StringBuffer是线程安全的,因为其下的所有方法都加上了synchronized。而StringBuilder则没有加这个关键字。
它们之间常用的方法:
- StringBuffer append(xxx):用于进行字符串的拼接
- cahr charAt(int index):返回char在指定索引在这个序列值。
- StringBuffer delete(int start,int end):删除指定位置的内容
- StringBuffer deleteCharAt(int index):删除char在这个序列中的指定位置。
- StringBuffer replace(int start,int end,String str):把[start,end)位置上的元素替换为str
- void sedtCharAt(int n,cahr ch):将指定索引位置的字符改成ch
- StringBuffer insert(int offset,xxx):在指定位置插入xxx
- StringBuffer reverse():将字符序列反转
- int indexOf(String str):返回指定子字符串第一次出现的字符串内的索引。
- int lastIndexOf(String str):返回指定子字符串最右边出现的字符串内的索引。
- String subString(int start,int end):返回一个新的 String,其中包含此序列中当前包含的字符的子序列。
7、String、StringBuffer和StringBuilder三者效率对比
String、StringBuffer和StringBuilder涉及可变序列与不可变序列、线程是否安全情况,这些因素必然影响到它们之间的运行效率,所以我们来比较一下他们之间的运行效率。
简单代码示例:
public class StringTest {
public static void main(String[] args) {
long startTime = 0L;
long endTime = 0L;
String text = " ";
StringBuffer buffer = new StringBuffer("");
StringBuilder builder = new StringBuilder("");
//String
startTime = System.currentTimeMillis();
for (int i = 0; i < 50000; i++) {
text = text + i;
}
endTime = System.currentTimeMillis();
System.out.println("String执行时间:" + (endTime - startTime));
//StringBuffer
startTime = System.currentTimeMillis();
for (int i = 0; i < 50000; i++) {
buffer.append(String.valueOf(i));
}
endTime = System.currentTimeMillis();
System.out.println("StringBuffer执行时间:" + (endTime - startTime));
//StringBuilder
startTime = System.currentTimeMillis();
for (int i = 0; i < 50000; i++) {
builder.append(String.valueOf(i));
}
endTime = System.currentTimeMillis();
System.out.println("StringBuilder执行时间:" + (endTime - startTime));
}
}
运行结果可能需要等个5-8秒,运行结果如下:
我们多次运行的结果也大致相同,所以运行效率为:StringBuilder > StringBuffer > String。String如此的慢是因为它是字符串常量,在创建对象后是不可改变的,然而每次改变String类型的值都会在常量池中新建一个常量对象,所以非常耗时间。而StringBuffer和StringBuilder的可变的字符序列,它们只是在原有内容发生了改变,并没有新创建对象。所以经常改变内容的字符串最好不要用 String类型,推荐使用StringBuffer,因为每次生成对象都会对系统性能产生影响,特别当内存中无引用对象多了以后, JVM 的 GC 就会开始工作,性能就会降低。
8、String字符串小结
通过上面的学习,我们简单小结一下:
- String:不可变字符串,应该是线程安全的,因为它是不可变的字符串,适用于少量字符串操作的情况。
- StringBuffer:可变字符串,线程安全,适用多线程下在字符缓冲区进行大量操作的情况。
- StringBuilder:可变字符串,线程不安全,适用于单线程下在字符缓冲区进行大量操作的情况。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列:基于图像分类模型对图像进行分类
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 25岁的心里话
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器
· 零经验选手,Compose 一天开发一款小游戏!
· 通过 API 将Deepseek响应流式内容输出到前端
· 因为Apifox不支持离线,我果断选择了Apipost!