一、什么是String类?
众所周知,String类代表字符串类型。Java中所有被双引号包裹的字符串都是String类的对象。
(比如:"zhangsan" , "lisi" , "博主是帅哥" , "123Abc"......)
引用数据类型与基本数据类型?
String类是和int, double, boolean......不一样,String是引用数据类型。那么引用数据类型和基本数据类型有什么区别呢?
引用数据类型一般情况下需要创建一个实例对象来对数据进行存储和操作,而基本数据类型可以直接使用,下面我们举个例子:
我们可以清楚地发现,以String为代表地引用数据类型需要像类一样创建一个实例化对象来使用,而下方的基本数据类型则直接使用即可。
而实际上,引用数据类型就是一个被封装好的类,String也不例外
----- 那么有同学们就好奇了,我们之前使用String的时候不都是和基本数据类型一样直接使用的吗?
是的,现在的String类型可以和基本数据类型一样直接使用,不需要导包和手动新建一个String对象再对其进行操作:
可实际上,类就是类,虽然不需要手动导包和新建一个实例对象,但是Java会在为str变量赋值的时候自动创建一个String类对象,其依然会在堆或者字符串常量池中开辟一个内存空间存放这个实例对象。而
String
类是 Java 标准库 (java.lang)API 的一部分,java.lang包下的类是不需要导包即可直接调用的。而作为一个类,其处理方式和操作方式当然也多种多样,我们会在后面的学习中一一接触。
二、String类的特点都有哪些?
- 字符串不可变,它们的值在创建后不能被更改【重点】
这意味着一旦一个 String 对象被创建,它的内容就不能被改变。但是为什么我们在日常使用的时候好像它是可以被我们改变的呢?具体来说,当你做如下赋值:
String str = "hello"; str = "world";
实际上发生了以下几件事:
- 第一行代码 String str = "hello"; 创建了一个指向字符串常量池中 "hello" 的引用。
- 如果 "hello" 已经存在于字符串常量池中,那么 str 就指向已有的 "hello" 字符串对象(节约内存);否则,就在常量池中创建一个新的 "hello" 字符串对象。
- 第二行代码 str = "world"; 并没有改变原来的 "hello" 字符串。相反,它创建了一个指向字符串常量池中 "world" 的新引用。
- 如果 "world" 已经存在于字符串常量池中,那么 str 就指向已有的 "world" 字符串对象;否则,就在常量池中创建一个新的 "world" 字符串对象。之后,str 引用将指向 "world",而 "hello" 仍然存在于字符串常量池中,只是 str 不再引用它。
- 当你使用 new String("hello") 创建一个新的 String 对象时,该对象会在堆上分配内存,
- 同时字符串 "hello" 也会被检查是否已经在字符串常量池中存在。如果不存在,它会被添加到常量池中。但无论如何,new String("hello") 创建的 String 对象本身总是在堆上。
- 不可变性的好处之一是提高了安全性(因为无法更改字符串的内容),同时也提高了性能(因为相同的字符串可以被多个地方共享,减少了内存开销)。此外,不可变性还使得 `String` 对象天生就是线程安全的,可以在多线程环境中安全地共享。
上面详细描述了String的底层工作机制,如果觉得文字看起来头有点晕,没关系,我绘制了一幅String底层机制图在下方,可以结合文字进行理解,其实不难:
创建字符串两种方式区分好就可以:
- new 来创建对象
- 直接赋值创建对象
- 注意:若无变量参与,直接拼接(如:"a" + "b" + "c")会直接得到拼接后的结果,复用字符串常量池中的字符串。
- 虽然 String 的值是不可变的,但是它们可以被共享【重点】
- 分享的特性指的就是上面所说的字符串常量池:如果在池中存在的字符串,可以直接被引用,不需要新建一个实例对象。
- 字符串效果上相当于字符数组( char[] ),但是底层原理是字节数组( byte[] )【了解即可】
拓展【了解即可】:至于为什么字符串值不能被改变,我们需要联想一下我们平时使用的类,如果你想要改变类中属性的值,比如name,是不是要用Setter方法去修改,而String类似乎没有这个方法给大家提供。这是设计者故意而为止的。原因有很多,下面给大家参考一下,如果想详细了解可以自己去深入学习:
安全性:
字符串不可变性确保了字符串内容不会被意外修改。这对于很多应用场景非常重要,特别是在涉及到安全相关的领域,如密码、密钥等。线程安全:
由于字符串是不可变的,因此可以在多线程环境中安全地共享。不需要额外的同步机制来保证字符串的一致性。性能优化:
- 字符串常量池的存在使得相同的字符串可以被多次使用而无需重新创建。这减少了内存消耗,尤其是在频繁创建相同字符串的情况下。
- 不可变性允许 JVM 进行一些优化,如内联(inlining)字符串操作等,进一步提高性能。
哈希一致性:
字符串的不可变性保证了其哈希码(hash code)在对象生命周期内是一致的。这对于使用哈希表(如HashMap
)等数据结构非常重要,因为哈希码必须在整个生命周期内保持不变。简化编程模型:
不可变性简化了编程模型,使得程序员不必担心对象的状态变化。这使得代码更容易理解和维护。垃圾回收优化:
由于字符串对象不会被修改,垃圾回收器可以更有效地管理内存,因为不需要担心对象状态的变化影响垃圾回收策略。
三、String类的构造方法
既然String是一个类,那么就肯定有构造方法,下面来介绍String几种常见的构造方法:
这些构造方法在后面的开发中会有很多应用场景,尽量熟悉,下面我将以代码演示的方式展示一下上述构造方法的用法:
public String():
public String(char[] chs):
public String(byte[] bys):
String s = "abc":
四、String类的常用操作
String作为一个类,里面封装了非常多的方法供开发者使用,下面我介绍一些比较常用的操作,这一部分内容在开发中也会被经常用到,所以非常重要。
4.1 字符串的比较
我们都知道,基本数据类型在比较的时候通常会用“==”号,那么字符串比较也是用“==”号吗?
那就要看你想比较的是什么了。
- 如果你想比较两个字符串对象的地址,那么使用的确实是“==”号:
- 如果你想比较内容,那就要使用到String类的equals方法了:
关于==号作用的总结
- 比较基本数据类型:比较的是具体的值
- 比较引用数据类型:比较的是对象地址值
4.2 equals方法
public boolean equals(String s)
如上所示,equals方法可以传入一个字符串与调用这个方法的字符串进行比对,如果内容完全一致(包括字母的大小写),则会返回一个布尔值true,否则,返回false
下面我将继续用代码进行String类equals方法的使用:
- 通过上图我们可以看到,s1和s2都是new出来的String字符串,所以它们肯定是两个不同的实例化对象;如果用==进行对比,对比的是对象的地址值,不相等;equals方法对比的是内容值,相等。
- 而s3,s4是直接赋值的,它们的值会被存放在字符串常量池中被复用,所哟它们是同一个实例化对象 ;如果用==进行对比,对比的是对象的地址值,相等;equals方法对比的是内容值,也相等。
这样我们就可以清楚地知道==和equals方法的区别。
除此之外,equals还有一个衍生方法要介绍一下,那就是equalsIgnoreCase。
4.2.2 equalsIgnoreCase方法
那么这个方法有什么来头呢?实际上也是对比字符串的内容,不过equalsIgnoreCase方法在比对的时候没有equals那么严格,它不会对比字母的大小写,下面也是通过代码给大家演示:
尽管字符串内出现了大小写不一致的情况,其依然会返回一个true——忽略大小写。
拓展:String字符串在Scanner中的创建机制
举个简单的例子,我们将创建一个Scanner类对数据进行输入,分别输入两次相同内容的字符串,最终会创建两个字符串对象还是会存放在字符串常量池中被复用?
从结果中很容易看出来,其实sc.next()的底层并不是在字符串常量池中创建对象,而是new出来的对象。
4.3 通过遍历字符串学习两个方法length和charAt
例子:输入一个字符串,并对其进行遍历。
看到这发现了两个新的方法,那么他们分别都有什么作用呢?
4.3.1 length方法
public int length()
顾名思义,这个方法就是用来返回字符串长度的,字符串中有多少个字符,它就会返回一个多大的整数。
4.3.2 charAt方法
public char charAt (int index)
这个方法也很好理解,那就是返回字符串中对应索引的字符值,比如s1.charAt(0):传回s1字符串索引为0的字符,结合上图可以知道,返回的应该是'a'。(注意,字符串索引从0开始)
4.3.3 toCharArray方法
public char[] toCharArray()
该方法会把字符串的内容一个个取出,生成一个char类型的字符数组
4.3.4 substring方法
public String substring(int begin, int end)
public String substring(int index)
substring方法的作用是截取字符串,它还有一个重载方法,接下来我介绍一下这两个方法的使用和注意事项:
在上述例子中,采用substring方法对字符串进行截取,截取索引为0-3的字符串值
但是运行后发现,只截取到了索引为0-2的字符串值,"app",这是因为substring方法是包头不包尾的!如果填写的是0和3,substring会截取0,1,2索引上的字符串值。
我们再来看下面一个例子,介绍一下substring一个重载的方法,这个方法也很重要,在后续javaWeb中使用AliOSS对象存储的时候给图片或者文件重命名的时候会使用到。现在我先举一个简单的例子:
在上述例子中,我们把开始索引0给去掉,只保留一个3,这代表着我们要截取索引3到最后一个索引的字符串值。
4.3.5 replace方法
我们通过一个有趣的情景来认识一下这个方法。顾名思义,replace就是字符串替代的意思。在日常生活中,我们会遇到很多敏感词,比如TMD,那么一般我们会使用***给它们替代掉,下面我们就用replace来实现这个功能(后面我们还会学习到正则表达式来进行更加便捷规范的字符串代替):
除此之外,如果敏感词太多,我们还可以放在数组中进行统一替代:
这样,敏感词就滴水不漏啦哈哈~
五、两个实用的字符串处理工具
我们先来思考一个问题,根据前面所学的知识我们知道,每两个字符串拼接在一起(由于不能改变字符串的值)都会创建一个新的字符串对象,这样如果拼接数据的量比较大的话,性能就会非常差,处理速度会很慢。
那么我们有没有办法,让字符串可以“改变值”呢?其实是有的,但实际上只是能达到这个性能目的,并不是真正地改变字符串的值。下面我来介绍一下这两个方法:
5.1 StringBuilder类
什么是StringBuilder呢?实际上这也是一个Java给我们写好的类,我们可以把它看成是一个容器,创建之后里面的内容是可以变的。使用这个容器,我们拼接字符串的时候,就不需要创建一堆字符串对象了。其实它的作用没有那么复杂,我通过一张图给大家展示一下:
上图我们可以清晰看到,我们使用了Java提供的容器,在容器中处理好需要拼接的字符串,最终节省了很多内存空间和新建对象所耗费的时间和性能。
下面简单介绍一下这个容器的基本使用:
5.1.1 创建对象
和创建其他类一样,直接new:
或者给容器一个初始值:
注意:stringBuilder对象打印出来的是属性值不是地址值
但是还要注意啊,这个值只是容器的属性值,并不是字符串值,现在它还不是一个字符串类型的值。
5.1.2 添加元素
添加元素会使用append方法:
这样就可以使内容拼接在一起了。
通过查看源码可以发现,append方法将传入的字符串内容拼接好后返回的是这个对象本身,
那我们就可以采用链式编程来对其属性进行添加:
当然append方法不仅仅能传入一个字符串,还可以传入其他值,可以自行了解......
5.1.3 反转
除了拼接内容,容器还有操作其属性值的其他能力:
5.1.4 获取长度
5.2 StringJoiner类
StringJoiner跟StringBuilder一样,也可以看成是一个容器,创建之后里面的内容是可变的。
作用:提高字符串的操作效率,而且代码编写特别简洁,但是目前市场上很少有人用。
StringJoiner是JDK8出现的。
但是在基本使用上,StringJoiner跟StringBuilder稍微有点区别:
5.2.1 创建对象
StringJoiner创建对象给构造方法传入的是元素与元素之间的间隔符
5.2.2 添加元素
如上面展示StringJoiner添加元素调用的是add方法,和StringBuilder的append方法类似,但是add方法只能添加字符串类型。
5.2.3 获取长度
和StringBuilder一样,StringJoiner也是使用length方法获取内容的长度。
5.3 转换为字符串类
我们在使用容器创建出来的“字符串”真的是字符串吗?其实不是的,这些内容是容器的对象,如果想要把他们转换为字符串,调用字符串的功能还要追加一步:
调用toString方法
StringJoiner跟StringBuilder一样,调用toString方法转换为字符串类型。只有转换为字符串类,其相关的功能才能被使用:
经过上面的学习,大家对Java字符串类会有一个更深入的了解,String类在后续的开发中是一个很常见的知识点,加深对String类的了解对理解Java的面向对象特性也颇有裨益。希望大家学有所获~