java String类

发布于:2025-07-30 ⋅ 阅读:(24) ⋅ 点赞:(0)

一,如何使用String

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类实例变量如下:
看一段代码:
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类型对象。

// 打印"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,将字符串全部拆分

String[] split(String regex)

这串代码实现了将

"wangyuntong = student & wangkangrui = student & wangyuntong"

这段字符串拆分成几段

通过split方法实现拆分

但是还有一些特殊的符号作为分隔符要拆分还需要转义

举个例子:

我们想要拆分123.98,但是结果却什么也没有输出,这是因为"."也是特殊的符号(正则表达式),我们需要对"."进行转义(//.)
此外还有别的特殊符号,比如
1. 字符 "|" , "*" , "+" 都得加上转义字符,前面加上 "\\" .
2. 而如果是 "\" ,那么就得写成 "\\\\" .
3. 如果一个字符串中有多个分隔符,可以用"|"作为连字符
这样可以省去第二个或多个for循环,输出的结果是一样的

2,将字符串以指定方式,拆分成limit组

String[] split(String regex, int limit)

七,字符串的截取

字符串的截取要用到substring方法

在上述代码中展示了substring的两个用法,即

可以指定索引截取到末尾

也可以制定截取片段,但要注意截取的部分是[ )

注意事项:
1. 索引从0开始
2. 注意前闭后开区间的写法, 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类自身,或者其内部valuefinal修饰而不能被修改。
final修饰类表明该类不想被继承,final修饰引用类型表明该引用变量不能引用其他对象,但是其引用对象中的内容是可以修改的

2,String不可变性存在的意义

1. 方便实现字符串对象池. 如果 String 可变, 那么对象池就需要考虑写时拷贝的问题了.
2. 不可变对象是线程安全的.
3. 不可变对象更方便缓存 hash code, 作为 key 时可以更高效的保存到 HashMap

十,对字符串进行修改

真正对字符串进行修改我们要用到映射,这个方法我们以后再提

注意:尽量避免直接对String类型对象进行修改,因为String类是不能修改的,所有的修改都会创建新对象,效率非常低下。
这种方法不建议使用,因为效率太低,中间产生了太多的临时变量

我们来看一个方法,appened方法,但要注意,这个方法不是对字符串'本身'进行修改,他依然具有字符串的不可变性.

appened常用于字符串构建和数据拼接,主要定义于StringBuilderStringBuffer

它的主要功能是将指定数据追加到当前对象的末尾,并返回当前对象本身(便于链式调用)

  • 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. StringStringBufferStringBuilder的区别
String的内容不可修改,StringBufferStringBuilder的内容可以修改.
StringBufferStringBuilder大部分功能是相似的
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");

执行逻辑拆解(分步骤分析)
  1. 第一步:new String("a")

    • 常量池创建:字面量 "a" 在常量池创建 1 个 String 对象。
    • 堆内存创建:new String("a") 在堆内存创建 1 个 String 对象(拷贝常量池的 "a" )。
    • 共创建 2 个对象(常量池 1 个 + 堆内存 1 个 )。
  2. 第二步:new String("b")

    • 常量池创建:字面量 "b" 在常量池创建 1 个 String 对象。
    • 堆内存创建:new String("b") 在堆内存创建 1 个 String 对象(拷贝常量池的 "b" )。
    • 共创建 2 个对象(常量池 1 个 + 堆内存 1 个 )。
  3. 第三步:new String("a") + new String("b")

    • + 运算符在 Java 中会触发隐式创建 StringBuilder,用于拼接字符串。虽然 StringBuilder 不是 String,但拼接过程会间接影响 String 对象数量:
      • 拼接时,StringBuilder 会先将两个堆内存的 String 对象("a" 和 "b" )的内容取出,拼接成 "ab" 。
      • 由于是非编译期确定的拼接(运行时动态拼接),因此不会在常量池自动创建 "ab" 。
      • 最终通过 StringBuilder.toString() 会在堆内存新建 1 个 String 对象(内容为 "ab" )。
  4. 最终赋值
    变量 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 对象


网站公告

今日签到

点亮在社区的每一天
去签到