懵了!看了阿里 p7 大佬耗时三天三夜整理的 String 类,我居然不会

今日分享开始啦,请大家多多指教~
字符串是我们以后工作中非常常用到的类型. 使用起来都非常简单方便, 我们一定要使用熟练。
那么 C 语言中是否有字符串类型? 答案是 “ 没有 ” !!
char *p = " hello";
那么 p 的类型是一个字符串类型么? 不是,p 是一个指针!!
而在 Java 当中是有字符串类型的——String
一、定义方式
创建字符串的方式有很多种,常见的构造 String 的方式如以下:
方式一:直接赋值法
String str1 = "hello";
方式二: new String()
String str2 = new String("hello");
方式三:创建一个字符数组 ch,new String ( ch )
char chs[] = {'h','e','l','l','l','o'};
String str3 = new String(chs);
二、内存
在此之前我们要先引入一个概念 字符串常量池
Sting constant pool 字符串常量池 的特性
1.在 JDK.7 开始,字符串常量池 被挪到堆里了
2.池内的数据不存在重复
下面我们通过一系列的练习来熟悉 字符串常量池以及 字符串类型数据在内存中的存放。

我们来看这样的代码,str 代表的是引用\地址,请判断 两次打印分别是什么?
我们来看结果

这个结果说明 str1 和 str2 存放的地址是不一样的, str1 和 str3 存放的地址是一样的。
好的,为什么是这样的结果呢?我们来看一下这几个字符串类型变量的内存。

"hello"如果存放在常量池当中,就会占用内存,假如这块空间的地址为 111,那么 str1 中存放的就是 111.
str2 new 一个 String 对象,那么肯定在堆上开辟内存,假设内存地址是 888,在这个 String 对象中,存在一个 value[] 保存着 orginal 传入的字符串,这个 val ==“hello”,因为在字符串常量池中已经有了"hello",所以 val 直接指向 常量池中的"hello".但是 str2 指向的依然是 888 在堆中的空间。


所以 str1 不等于 str2。
之后呢,str3 也等于"hello",他也准备把 hello 放在常量池当中.此时常量池中已经存在"hello",那么之后 str3 在存放"hello"地址的时候,就指向的是常量池中原来 hello 的地址。
所以 str1 等于 str3
再看一组练习

请判断两次打印的结果…
结果如下:

下面我们来分析,这组代码中 str 变量的内存存放

str1 指向字符串常量池中的 “hello”
str2 是"hel"与"lo" 组合而成的,常量在编译的时候就已经确定了,所以在编译时,已经被处理为"hello",所以也指向 常量池中的"hello"。
所以 str1 等于 str2
str3 首先 new 了一个 String(“hel”)对象,在堆中开辟一块空间,这个对象中的"hel"同时存放在常量池中,之后又在常量池中开辟一块空间存放 “lo”。两块部分之间的"+",将 String 的对象 与常量池中的 "lo"结合在堆中再次开辟一块新的空间,这块内存中的 val ==“hello”,str3 指向的是合并之后的对象 ,地址为 999.
所以 str1 不等于 str3.
再看一组练习

请看一下,我们将 String str 作为参数,改变 str 的内容,以及传入 数组 val 改变 数组元素,其打印结果是什么?

我们看到 String str 的内容并未改变,但是数组 val 的元素却改变了。
我们从内存的角度来分析。

str1 指向字符串常量区的"hello",地址为 888
val 作为数组引用,指向堆中开辟的数组空间,地址为 777
str 作为函数的形参,接收 str1 实参的值,也就是 888,此时 str 指向常量区的”hello“,但是在方法的内部,str = “abcde”,在字符串常量区中有开辟一块"abcde"的内存,地址为 000,最后 str 存放的地址为 000.
array 作为函数的形参,接收 val 实参的值,也就是 777,此时 array 指向堆中 开辟的数组空间,此时通过 array 来改变数组元素的内容,最终 改变的也同样是 val 实参的内容.
三、字符串比较相等
如果现在有两个 int 型变量,判断其相等可以使用 == 完成。

如果说现在在 String 类对象上使用 == ?
代码 1

看起来貌似没啥问题, 再换个代码试试, 发现情况不太妙.
代码 2

在上面的几个练习中,我们 用 str1 == str2 比较的是两个字符串的引用/地址,如果比较字符串里面的内容,我们需要用到 equals 方法。

最后的打印结果

打印的结果符合字符串的内容比较。
常用的比较方式:
我们再来看一种情况,

这时候运行程序,就会出现以下情况:

空指针异常,因为 null. 任何方法都会出现异常。
所以一定要保证 str1 不能为 null。
那么如果我们改一下,

所以我们知道 equals(),括号里可以是 null,但是 点之前一定不能是 null.

当我们写代码遇到以上的情况时,我们应该尽量选方式 2,这样保证 equals 之前一定不为 null,以防出现异常.
四、字符串常量池
在上面的例子中, String 类的两种实例化操作, 直接赋值和 new 一个新的 String.
(1) 直接赋值

String 类的设计使用了共享设计模式
在 JVM 底层实际上会自动维护一个对象池(字符串常量池)
如果现在采用了直接赋值的模式进行 String 类的对象实例化操作,那么该实例化对象(字符串内容)将自动保存到这个对象池之中.
如果下次继续使用直接赋值的模式声明 String 类对象,此时对象池之中如若有指定内容,将直接进行引用
如若没有,则开辟新的字符串对象而后将其保存在对象池之中以供下次使用
理解 “池” (pool)
“池” 是编程中的一种常见的, 重要的提升效率的方式, 我们会在未来的学习中遇到各种 “内存池”, “线程池”, “数据库连接池” …然而池这样的概念不是计算机独有, 也是来自于生活中.
举个例子:现实生活中有一种女神, 称为 “绿茶”, 在和高富帅谈好对象的同时, 还可能和别的屌丝搞暧昧. 这时候这个屌丝被称为 “备胎”. 那么为啥要有备胎? 因为一旦和高富帅分手了, 就可以立刻找备胎接盘, 这样 效率比较高.如果这个女神, 同时在和很多个屌丝搞暧昧, 那么这些备胎就称为 备胎池.
(2)采用构造方法
类对象使用构造方法实例化是标准做法。分析如下程序:
String str = new String("hello");

这样的做法有两个缺点:
1.如果使用 String 构造方法就会开辟两块堆内存空间,并且其中一块堆内存将成为垃圾空间(字符串常量 “hello” 也是一个匿名对象, 用了一次之后就不再使用了, 就成为垃圾空间, 会被 JVM 自动回收掉).
2.字符串共享问题. 同一个字符串可能会被存储多次, 比较浪费空间.
(3)intern 的使用
String str1 = "hello";
String str2 = new String("hello").intren();
从上面的由 构造方法定义字符串,我们会浪费内存空间,而这里有一个方法 ,叫做 intern(),手动入池。
那这是什么意思呢?
这是先看一下传入构造方法的字符串在字符串常量池中是否存在,如果有的话,就把常量池中的引用传给当前的引用类型变量。

综上所述,我们一般使用 直接赋值法来 创建 String 对象。

我们再来看这样一组代码,来画一下它的内存结构

在第一步的代码中,new 了两个字符串"1",在堆中创建了两个对象,指向常量池中的"1",拼接在一起,s3.interb(),s3 手动入池,“11"在池中没有,所以就把 堆中的"11"的引用 555 传入常量池中。s4 指向池中的"11”,而这时池中已经有了"11"的引用,所以 s4 指向的就是 s3 在入池的引用。
所以结果为 true。

所以呢,我们解决了一个疑问
在常量池当中,可以放 字符串的字面值常量,也可以放引用。什么时候放引用,就是类似于上面的那种情况之下,s3.intern(),s3 所指向的这个对象在字符串常量池中是不存在的,那么入池的时候就把堆中 s3 的引用放入。
五、理解字符串不可变
字符串是一种不可变对象. 它的内容不可改变.这是什么意思呢?

对于这种代码,乍一看我们以为成功的将 str 每次与其他的字符串拼接,但是这样是不可以的, str 原来指向的是"hello",但是 在与" world"拼接之后,又会产生一个新的对象"helll world",再次拼接一个"!!!",那么又会产生一个新的对象"hello world!!!",在内存中就会产生多个对象。

我们最后需要的是"hello world!!!",但是却开辟了 5 块内存空间。
如果在一个循环中拼接,那么会开辟更多的内存空间!!
所以这样的代码是极为不可取的!!!
那么如何拼接呢,具体在之后的 StringBuff、StringBuilder 中介绍。
六、字符、字节、字符串
(1)字符与字符串
字符串内部包含一个字符数组,String 可以和 char[] 相互转换

1.字符数组转字符串


此时我们 的 str 结果就是 “hello”,同时他也可以再给两个参数.
2.将部分字符数组中的内容转换为字符串

offset–偏移量
count-- 转换几个

此时我们将 val 中偏移 1 个,转换之后的两个数组元素为字符串
打印结果应该为 el
运行结果如下:

3.将字符串中对应索引转换为字符


索引从 0 开始,我们输入 1,所以转换的为字符串中的 e
运行结果如下:

4.将字符串转换为字符数组


我们用字符数组接收 str 转换后的字符。
运行结果如下:

好了,了解了这几种字符与字符串的方法,我们通过几个练习来继续熟悉。
练习一给定字符串一个字符串, 判断其是否全部由数字所组成.
思路: 将字符串变为字符数组而后判断每一位字符是否是" 0 “~”‘9’"之间的内容,如果是则为数字.

(2)字节与字符串
字节常用于数据传输以及编码转换的处理之中,String 也能方便地和 byte[] 相互转换
常用方法:

1.字节数组转换为字符串


运行结果:

字符串中的内容是字节数组与 Ascii 码表中对应的字符。
2.部分字节数组的内容转换为字符串


运行结果:

3.字符串转换为字节数组


运行结果:

(3) 小结
那么何时使用 byte[], 何时使用 char[] 呢?
byte[] 是把 String 按照一个字节一个字节的方式处理, 这种适合在网络传输, 数据存储这样的场景下使用. 更适合针对二进制数据来操作.
char[] 是吧 String 按照一个字符一个字符的方式处理, 更适合针对文本数据来操作, 尤其是包含中文的时候.
七、字符串的常见操作
(1)字符串比较
上面使用过 String 类提供的 equals()方法,该方法本身是可以进行区分大小写的相等判断。除了这个方法之外,String 类还提供有如下的比较操作.
1.区分大小写比较


运行结果:

我们常用的 equals 方法 是区分大小写的,这点要注意。
2.不区分大小写的比较


运行结果:

这种不区分大小写的比较还是很常见的,比如应用于验证码上,不区分大小写。
3.比较两个字符串的大小关系


运行时结果


掌握了字符串比较相等的方法,下来我们来做一道练习题
比较字符串是否相等

题解思路:
将 word1 字符串数组的内容都在 str1 追加,word2 字符串数组的内容在 str2 追加,最终 equals 比较 str1 str2 字符串的内容,相等返回 true,不等返回 false.
注意:参数等问题要考虑全面


(2)字符串查找
从一个完整的字符串之中可以判断指定内容是否存在,对于查找方法有如下定义:

判断一个字符串中是否存在子字符串

我们可以先看一下 contains 方法的源码

contains 方法的使用

运行结果

所以可判断在"badabc" 这个字符串中存在 这个 “abc” 的子字符串。
找到子字符串的下标

我们先来看一下一个参数的 index 方法的源码

带一个参数的 index 方法的使用

运行结果:

两个参数的 index 方法的使用

在下面我们又看到了一个 index 方法,这说明 默认情况下,index 是从 0 下标开始查找的,如果再给他一个下标参数,那么就从指定的下标位置进行字符串查找。
使用:

运行结果:

从后往前查找到子字符串的位置

lastIndexOf 是从后向前查找 子字符串的位置
lastIndexOf 方法的使用

运行结果:

同时 lastIndexOf 也有两个参数的方法,从指定下标开始从后向前进行查找。
判断是否由 参数字符串开头的

同时也有两个参数的方法,从指定位置判断是否由 指定字符串开头

判断是否由指定字符串进行结尾的

(3)字符串替换

(1)替换所有的指定内容

replaceAll 的使用

运行结果:

成功的把所有的 “ab” 替换成为 “AB”.
(2)替换首个要替换的内容.

replaceFirst 的使用

运行结果:

注意说明:
由于字符串是不可变对象, 替换不修改当前字符串, 而是产生一个新的字符串.
(4)字符串拆分
可以将一个完整的字符串按照指定的分隔符划分为若干个子字符串

1.将字符串全部拆分

接收的类型是字符串数组类型,传参数时,传一个我们想要分割的符号。split 的使用

我们在用 split 方法是, 以 空格 为分割符,将我们的 str 字符串 进行拆分
我们来看拆分的效果

2.带两个参数的 split 方法

还是以上面的字符串为例

运行结果:

我们除了将字符串作为参数,还将 limit 设置为 2,那么拆分后的数组长度就为 2,所以运行结果就如上所示。
难点:
拆分是特别常用的操作. 一定要重点掌握. 另外有些特殊字符作为分割符可能无法正确切分, 需要加上转义字符
示例 1
拆分 IP 地址
比如说我们要分割 IP 地址,192.168.1.1,以 “.” 分割。
当我们运行时会发现 打印为空,这是为什么呢?
有些符号比较特殊,必须用到转义字符
“ \. ”才能表示一个真正的 “.”
同时"\"也需要进行转义,那么就又要再加一个斜杠。
“\\.”这时字符串才只能被 “ . ”分割。

运行结果

1. 字符"|","*","+"都得加上转义字符,前面加上"\\".
2. 而如果是"\",那么就得写成"\\".
3. 如果一个字符串中有多个分隔符,可以用"|"作为连字符.
连字符 “ | ” 的使用

运行结果:

(5)字符串截取
从一个完整的字符串之中截取出部分内容。可用方法如下:

1.从指定下标截取到字符串结束

方法的使用

运行结果:

2.带有两个参数的 subString 方法,截取指定下标范围内的字符串内容

方法的使用

运行结果

注意:
1.指定下标范围 是 左闭右开的区间
2.截取后的字符串是一个新的对象
(6)其他操作方法
字符串操作还有很多其他的方法,在这里我们只进行简单介绍。
八、StringBuffer 和 StringBuilder
StringBuffer 和 StringBuilder 又是一种新的字符串类型。
通常来讲 String 的操作比较简单,但是由于 String 的不可更改特性,为了方便字符串的修改,提供 StringBuffer 和 StringBuilder 类。
StringBuffer 和 StringBuilder 在功能上大部分是相同的,在这里我们着重介绍 StringBuffer.
(1)append 方法
在 String 中使用"+"来进行字符串连接,但是这个操作在 StringBuffer 类中需要更改为 append()方法。
String 和 StringBuffer 最大的区别在于:String 的内容无法修改,而 StringBuffer 的内容可以修改。频繁修改字符串的情况考虑使用 StingBuffer。

运行结果:

我们来看一下 StringBuffer 的 append 方法的源码

最后返回的是 this,在字符串本身拼接字符串。同时 StringBuffer 有自己重写的 toString 方法,可以直接进行打印。
我们来看一下 以下的代码:

我们对以上代码进行编译一下:

在编译的过程中,我们发现 StringBuilder.append 方法的出现;
我们将这个过程用 StringBuilder 写一下:

说明:
String 的“+” 拼接,会被底层优化为一个 StringBuilder ,拼接的时候会用到 append 方法
(2)注意
注意:String 和 StringBuffer 类不能直接转换。如果要想互相转换,可以采用如下原则:
String 变为 StringBuffer:利用 StringBuffer 的构造方法或 append()方法
StringBuffer 变为 String:调用 toString()方法。
除了 append()方法外,StringBuffer 也有一些 String 类没有的方法:
字符串反转:
public synchronized StringBuffer reverse();
(3)区别
String 和 StringBuilder 及 StringBuffer 的区别
String 进行拼接时,底层会被优化为 StringBuilder
String 的拼接会产生临时对象,但是后两者每次都只是返回当前对象的引用。
String 的内容不可修改,StringBuffer 与 StringBuilder 的内容可以修改.
StringBuilder 和 StringBuffer 的区别
我们来看一下这两个类型的 append 方法

所以 StringBuffer 和 StringBuilder 的区别主要体现在线程安全上 。
1.StringBuffer 与 StringBuilder 大部分功能是相似的 2.StringBuffer 采用同步处理,属于线程安全操作;而 StringBuilder 未采用同步处理,属于线程不安全操作
字符串操作是我们以后工作中非常常用的操作. 使用起来都非常简单方便, 我们一定要使用熟练.
今日份分享已结束,请大家多多包涵和指点!
评论