一,如何使用String
java
public static void main(String[] args) {
// 使用常量串构造
String s1 = "hello bit";
System.out.println(s1);
// 直接newString对象
String s2 = new String("hello bit");
System.out.println(s1);
// 使用字符数组进行构造
char[] array = {'h','e','l','l','o','b','i','t'};
String s3 = new String(array);
System.out.println(s1);
}
即三个方法都可以
那我们如何让系统读取我们从键盘输入的话呢?很简单,使用scanner就可以

要注意String而不是int
String的[注意事项]
1. String****是引用类型,内部并不存储字符串本身,
在String类的实现源码中,String类实例变量如下:
看一段代码:
java
public static void main(String[] args) {
// s1和s2引用的是不同对象 s1和s3引用的是同一对象
String s1 = new String("hello");
String s2 = new String("world");
String s3 = s1;
System.out.println(s1.length()); // 获取字符串长度---输出5
System.out.println(s1.isEmpty()); // 如果字符串长度为0,返回true,否则返回false
}

2. 在Java中" "引起来的也是String类型对象。
java
// 打印"hello"字符串(String对象)的长度
System.out.println("hello".length());
二, String****对象的比较
1,引用数据类型储存的是地址
不同于基础数据类型储存的是值,引用数据类型储存的是地址
因此我们要注意使用常量串和使用new来创建对象还是有区别的,什么区别呢,咱们直接上代码


可以看到,明明是一样的内容,在逻辑运算符比较之下却输出的是 false,这是为什么?
我们都知道,在 Java 中,==
运算符用于比较两个引用是否指向内存中的同一个对象 ,也就是比较两个对象的内存地址是否相同。如果两个引用指向同一个对象,==
运算的结果为 true
;如果指向不同的对象,结果则为 false
。
而String就是引用类型,所以两个对象之间比较的是地址而不是内容
就这么简单吗,让我们再来看一段代码


我们可以看到,这次用常量串来构造对象,结果居然是true,这是为什么?
因为JVM 为节省内存,会将字面量创建的字符串放入常量池。若已有相同内容的字符串,则直接复用。因此地址是相同的
而对于new就不一样了,通过new String("abc")
创建的对象会在堆内存中生成新实例,而非直接使用常量池中的对象。因此用new来创建的对象,地址就不一样了
那么我们如何比较内容呢?这时候那我们就要用到equals方法
2,equals方法(比较字符串内容)
通过使用equals方法我们可以比较两个字符串的值,话不多说我们直接看代码


那么又引申出了一个新的问题----我们如何比较字符串的大小呢?
3,compareTo方法(比较字符串大小)
我们先看一段代码:

在这段代码中,我们通过compareTo方法对s1和s2进行了比较
要注意的是,是 s1 和 s2 比较,而不是s2 与 s1比较 因为 e > c 所以 s1 > s2 结果返回的是正数

至于为什么结果是2而不是什么别的正数,是因为在ASCLL码比较下c与e刚好差2(无论大小写)
但是要注意,大写字母+32 = 小写字母

刚才两个字符串的长度相同,那么在长度不相等的情况下,结果会改变吗?


可以看到结果仍然是2,因此实际比较的还是e与c的值
那么,如果两个字符串前面的内容也相同呢?

这次两个字符串前面的内容相同但长度不相同,结果如何?

可以看到,结果是负数,也就是说s1<s2
那么如果大小写不同呢?


此时,s1>s2,因为小写字母 = 大写字母+32(ASCLL码值)
那如果我就是想忽略大小写,我们应该怎么办?
4,compareToIgnoreCase方法(忽略大小写)


代码运行结果为0,说明两个字符串无差值,即 s1 = s2
三,字符串的查找

1,chatAt方法

因为charAt的返回值是char,所以我们用char类型来接受

代码运行结果是第一个字符 ' a '
注意这种给出下标的我们不要越界


一旦越界就会报错
事实上为了防止越界,我们经常用遍历的方式来获取字符,就像这样:


当然如果想让输出结果在同一行,只需要把println改为print就好啦
2,index方法
index方法可以获取到我们字符串的下标
他返回的是所查找字符第一次出现的位置,没有的话就返回-1

此时的"L"默认是hello中的第一个字母L,因此会输出数字2,即第二个下标

此外,index方法还可以构成重载,index方法不仅只能查找第一个字符,还可以指定数位查找

这段代码中我们指定查找了处于第三个数位的"L",代码运行结果是3

当然如果没有找到,结果就会返回-1
那如何构成重载?


可以看到代码运行结果是2,也就是输出的是重载之后的结果
当然我们也可以查找字符串

输出的是第一个字符出现的位置

当然如果没有找到的话依旧返回的是-1
3,lastIndexOf方法
lastIndexOf方法是寻找字符从后往前找

找到的是最后一个"L"的所在位置,即输出3

当然我们也可以指定让他从哪个位置开始找
通过这种方法它会找到字符从后往前找时第一次出现的位置,没有返回-1




很清楚了吧
四,转换
引出:valueOf方法
valueOf方法是一个静态方法,可以把其他类型转化为字符串类型,该方法有多种重载形式,能够转换多种不同类型的数参数
我们打开valueof方法的源代码可以看到确实是由static所修饰的

补充一下,如果你发现一个方法或者成员由类名调用 ,那么这一定会是一个静态方法或静态成员
比如我们常见的System.out,这就是一个类名点一个方法
我们看一下源代码发现确实是由static修饰,也是静态方法

1,把基本数据类型转换为字符串类型


2,将对象转换为字符串

代码运行结果是 20
但是要注意的是
当传入一个对象作为参数时,valueOf()
方法会调用对象的toString()
方法。要是对象为null
,则会返回字符串"null"
。


3,把字符数组转换为字符串


注意,该方法不会抛出NullPointerException异常,如果对象是null值,那么会直接输出null
4,将字符串转换为数字

我们可以通过Integer.parseInt方法将字符串转换为数字.
这里要注意,因为是s1是实例对象,所以要使用非静态方法,如果要使用静态方法,就要在静态方法中创建一个新的Cheer对象(一个静态对象)或者直接将静态方法外面的实例对象变为静态对象


当然除了parseInt方法还有parseDouble方法


5,小写转大写
在字符串中将小写字母转换为大写字母我们需要用到toUpperCase方法



在源代码中,toUpperCase方法返回的是String类型,所以我们要用String类型的对象来接收这个方法
除此之外要注意:
在字符串转换时,一切转变都不是在原字符串上改变,而是创建了一个新的字符串
6,字符串转数组
使用toCharArray方法可以将字符串转换为数组


这是代码运行结果.
要注意的是,在将数组转换为字符串时

这两种写法均可
针对字符数组转字符串,在确保数组非 null
时,二者都能实现转换;考虑到对 null
情况的不同处理,根据实际场景选择即可,若希望更好地处理可能为 null
的数组,String.valueOf
更合适

7,格式化
在 Java 中,格式化(Formatting) 指的是将数据(如数字、日期、字符串等)按照指定的规则转换为特定格式的字符串,以便于展示、存储或传输。格式化的核心目的是让数据呈现更符合人们的阅读习惯,或满足特定场景的格式要求。
格式化主要分为 数字格式化 , 时间格式化 和 字符串格式化,这里我们主要说一下字符串格式化
什么是字符串格式化?
简单来说就是按 指定 占位符规则 拼接 字符串 ,类似 C 语言的 printf
。
怎么进行字符串格式化?
使用format方法可以进行格式化
例如:
- 动态拼接含变量的字符串,如
"姓名:%s,年龄:%d"
填充后为"姓名:张三,年龄:20"



五,字符串的替换
1,replace方法
1,字符的替换


2,字符串的替换


2,replaceFirst方法
该方法的作用是替换第一个字符/字符串


3,replaceAll方法
和replace方法作用相同,但也有一些区别
replace
方法 :- 有两种重载形式 ,一种接收两个
char
类型的参数 ,用于将指定的字符替换为另一个字符;另一种接收两个CharSequence
类型的参数,用于将指定的字符序列(字符串)替换为另一个字符序列(字符串)。 - 它进行的是普通的字符或字符序列匹配,不支持正则表达式。例如,在字符串中遇到目标字符或字符序列就进行替换,不会对字符序列进行特殊的正则解析。
- 有两种重载形式 ,一种接收两个
replaceAll
方法 :- 接收两个
String
类型的参数 ,第一个参数是一个正则表达式,第二个参数是用于替换匹配项的字符串。 - 会按照正则表达式的规则对字符串进行匹配,然后将匹配到的部分替换为指定的字符串。这意味着可以实现更复杂的匹配和替换逻辑,比如匹配特定格式的字符串、数字、单词等。
- 接收两个
- 比如对于字符串
"1abc2abc3abc"
,执行"1abc2abc3abc".replaceAll("\\d", "*")
,这里\\d
是匹配数字的正则表达式,最终会将所有数字替换为*
,得到结果"*abc*abc*abc"
。

总体而言,如果只是简单的字符或字符串替换,使用replace
方法即可;如果需要基于正则表达式进行复杂的匹配和替换,replaceAll
方法则更为合适。
六,字符串的拆分
1,将字符串全部拆分
java
String[] split(String regex)

这串代码实现了将
"wangyuntong = student & wangkangrui = student & wangyuntong"
这段字符串拆分成几段

通过split方法实现拆分
但是还有一些特殊的符号作为分隔符要拆分还需要转义
举个例子:
我们想要拆分123.98,但是结果却什么也没有输出,这是因为"."也是特殊的符号(正则表达式),我们需要对"."进行转义(//.)
此外还有别的特殊符号,比如
- 字符**"|" , "*" , "+"** 都得加上转义字符 ,前面加上**"\\"** .
- 而如果是**"\"** ,那么就得写成**"\\\\"** .
- 如果一个字符串中有多个分隔符,可以用"|"作为连字符
这样可以省去第二个或多个for循环,输出的结果是一样的
2,将字符串以指定方式,拆分成limit组
java
String[] split(String regex, int limit)
七,字符串的截取
字符串的截取要用到substring方法

在上述代码中展示了substring的两个用法,即
可以指定索引截取到末尾
也可以制定截取片段,但要注意截取的部分是**[ )**
注意事项**:**
- 索引从0开始
- 注意前闭后开区间的写法, substring(0, 5) 表示包含 0 号下标的字符, 不包含 5 号下标
八,字符串的trim方法
trim方法可以去掉字符串**左右两边的空格,**但无法去掉中间的


九,字符串的不可变性
1,String不可变性的原因
什么是字符串的不可变性,在前面我们对字符串进行各种操作,比如转换大小写,格式化,替换等等,这些操作并没有改变字符串原本的值,而是创建了一个新的字符串修改,也就是说原本的字符串没有变化.那么为什么字符串具有不可变性呢?
很多书上有写关于字符串不可变的原因是由于final

说是因为final修饰String所以字符串具有不可变性,但其实这并不是关键
因为fianl所修饰的String类,只能说明这个String类是不可以被继承的,并没有说明是不可变
这其实是我们String底层存储带来的不可变性
真正的原因是我们的值value被private和final修饰
被private修饰保证了外部无法直接访问该数组
被final修饰保证了数组引用一旦初始化后就不能指向其他数组对象。即不能引用其它字符数组,但是其引用空间中的内容可以修改。

被private修饰的值只能在当前类中使用,拿不到value,并且被final修饰也无法引用其他字符数组,那我们也就无法修改原来的值,这就是不可变的原因
因此纠正一下,
网上有些人说:字符串不可变是因为其内部保存字符的数组被final修饰了,因此不能改变。
这种说法是错误的,不是因为String类自身,或者其内部value被final修饰而不能被修改。
final修饰类表明该类不想被继承,final修饰引用类型表明该引用变量不能引用其他对象,但是其引用对象中的内容是可以修改的。

2,String不可变性存在的意义
- 方便实现字符串对象池. 如果 String 可变, 那么对象池就需要考虑写时拷贝的问题了.
- 不可变对象是线程安全的.
- 不可变对象更方便缓存 hash code, 作为 key 时可以更高效的保存到 HashMap 中
十,对字符串进行修改
真正对字符串进行修改我们要用到映射,这个方法我们以后再提
注意:尽量避免直接对String类型对象进行修改,因为String类是不能修改的,所有的修改都会创建新对象,效率****非常低下。
这种方法不建议使用,因为效率太低,中间产生了太多的临时变量
我们来看一个方法,appened方法,但要注意,这个方法不是对字符串'本身'进行修改,他依然具有字符串的不可变性.
appened常用于字符串构建和数据拼接,主要定义于StringBuilder 和StringBuffer中
它的主要功能是将指定数据追加到当前对象的末尾,并返回当前对象本身(便于链式调用)
StringBuilder
是非线程安全的,效率较高,适合单线程场景。StringBuffer
是线程安全的(方法加了synchronized
),效率稍低,适合多线程场景。
十一,StringBuilder与StringBuffer
1,为什么要引入这两个类
首先要明确,StringBuilder和StirngBuffer都不是String类型,但是他们都可以操纵字符串
我们先来看一段代码

输出结果:

可以看到,最后在打印的时候,我们使用了toString方法,为什么要使用这个方法?
首先,在 Java 中StringBuilder
是一个可变的字符序列类 ,用于高效地创建和操作字符串。它属于引用类型
而 String
是一个引用类型,用于表示字符串,即一系列字符的有序集合。
因此StringBuilder和String类型不一致,只有 toString()
能把 StringBuilder
转换成真正的 String
类型,满足类型要求。
详细一点说,StringBuilder
重写了 Object
类的 toString
方法,调用 builder.toString()
后,会把 StringBuilder
内部维护的可变字符序列,转换成一个不可变的 String
字符串,这样就能适配 System.out.println
的入参要求,顺利打印出拼接好的字符串内容。
当然,字符串的拼接我们之前学过一段更容易理解的

这样也能输出同样的结果
但是
这样的效率是远远不及StirngBuilder的(对象多时)
我们来用代码说明

这串代码分别计算了符号拼接,StringBuffer拼接和StringBuilder拼接的效率,我们运行一下代码

效率高低一目了然,为什么会这样?
因为符号拼接会不断地创建对象,销毁对象(for循环),而StringBuffer拼接和StringBuilder拼接是在创建后的对象上进行操作,省去了销毁对象的过程,因此效率大大提高


看到了吗,返回值都是this,这是从底层代码来解释的
2,StringBuffer和StringBuilder的联系与区别
首先,StirngBuffer和StirngBuilder这两个类里面的方法几乎是一摸一样的,他们都包含了Stirng类本身没有的方法


那么区别是什么,别着急,现在我们就通过底层代码来看一下(找不同)
StringBuilder:

StringBuffer:

对比一下,有两处不同,最主要的,来看一下StringBuffer 多了一个Synchronized

这个单词的意思是 同步
这个单词在javaEE中我会着重提到,现在不过多讲述
StringBuffer主要用于多线程情况下,可以保证线程安全
StringBuilder主要用于单线程情况下,无法保证线程安全
那为什么不直接用StringBuffer?
我们可以把Synchronized看作是一把锁
把多线程情况比作,有一个公共厕所,很多人去上,那么进去的人要上锁,上完厕所出来再解锁,进去的人再上锁这样的一个过程,这样是有必要的,是可以保证安全的
把单线程情况比作, 放假了 你家里只有你一个人,你要上厕所,那你为什么要上锁解锁呢,这就不存在安全问题了,这样会导致效率没有意义的下降,这是一种资源的浪费
因此在单线程情况 下请尽量使用StringBuilder
小总结
1. String、StringBuffer、StringBuilder的区别
String的内容不可修改,StringBuffer与StringBuilder的内容可以修改.
StringBuffer与StringBuilder大部分功能是相似的
StringBuffer采用同步处理,属于线程安全操作;而StringBuilder未采用同步处理,属于线程不安全操作
3,StringBuffer和StringBuilder特有的方法(部分)
1,字符串逆置(反转)
我们要知道String是没有逆置这个方法的,这是StringBuffer和StringBuilder特有的方法


这里的toString可加可不加
可以看到我们没有给reverse返回值,这是因为他不需要返回值

从他的底层代码来看,方法调用之后直接返回他自己,因此不需要返回值
2,字符串插入


同样不需要返回值,原因也是一样的

十二,String对象创建数量
为了更深一步的了解String对象的创建过程,我们来做一个小练习

请大家先思考一下
解答:
这是java中关于String对象创建数量的经典问题,需要结合字符串常量池和new关键字的特性分析
案例 1:String str = new String("ab");
执行逻辑拆解(分步骤分析)
1,常量池对象创建:
字符串字面量"ab" 会触发常量池检查,由于题目要求"不考虑常量池之前是否存在",因此会在字 符串 常量池中创建1个内容为"ab"的String对象
2,堆内存对象创建:
new String ("ab") 会在堆内存 中再创建1个String对象,这个对象的初识内容拷贝自常量池 的"ab"
3,最终赋值:
变量str指向堆内存中新建的String对象
结论:共创建 2 个 String
对象(常量池 1 个 + 堆内存 1 个 )。
案例 2:String str = new String("a") + new String("b");
执行逻辑拆解(分步骤分析)
-
第一步:
new String("a")
- 常量池创建:字面量
"a"
在常量池 创建 1 个String
对象。 - 堆内存创建:
new String("a")
在堆内存 创建 1 个String
对象(拷贝常量池的"a"
)。 - 共创建 2 个对象(常量池 1 个 + 堆内存 1 个 )。
- 常量池创建:字面量
-
第二步:
new String("b")
- 常量池创建:字面量
"b"
在常量池 创建 1 个String
对象。 - 堆内存创建:
new String("b")
在堆内存 创建 1 个String
对象(拷贝常量池的"b"
)。 - 共创建 2 个对象(常量池 1 个 + 堆内存 1 个 )。
- 常量池创建:字面量
-
第三步:
new String("a") + new String("b")
+
运算符在 Java 中会触发隐式创建StringBuilder
,用于拼接字符串。虽然StringBuilder
不是String
,但拼接过程会间接影响String
对象数量:- 拼接时,
StringBuilder
会先将两个堆内存的String
对象("a"
和"b"
)的内容取出,拼接成"ab"
。 - 由于是非编译期确定的拼接 (运行时动态拼接),因此不会在常量池自动创建
"ab"
。 - 最终通过
StringBuilder.toString()
会在堆内存 新建 1 个String
对象(内容为"ab"
)。
- 拼接时,
-
最终赋值 :
变量
str
指向堆内存中通过StringBuilder
拼接后新建的String
对象(内容"ab"
)。
逐步骤统计:
new String("a")
:2 个对象(常量池 1 + 堆 1 )new String("b")
:2 个对象(常量池 1 + 堆 1 )+
拼接与toString()
:堆内存新增 1 个String
对象(内容"ab"
)
结论:共创建 5 个 String
对象(常量池 2 个 + 堆内存 3 个 )。
关键注意点
-
编译期 vs 运行期拼接 :
如果是编译期确定的拼接(如
String str = "a" + "b";
),结果会直接优化为"ab"
,且只在常量池创建 1 个对象;但本案例是运行期动态拼接 (涉及new String
),因此需走StringBuilder
逻辑,且不会在常量池自动创建"ab"
。 -
StringBuilder
不计数 :案例中
StringBuilder
是中间临时对象,题目只统计String
对象,因此无需计入。
总结:
new String("ab")
→ 2 个String
对象new String("a") + new String("b")
→ 5 个String
对象