【Java 基础语法】万字解析 Java 的 String 类

x33g5p2x  于2021-09-30 转载在 Java  
字(18.0k)|赞(0)|评价(0)|浏览(359)

在 C 语言中并没有字符串类型,而 Java 中却有着 String。之前就介绍过 Java 的数据类型,但是 String 类型还有着很多的知识没有介绍到。今天这节就是深度解析一下 Java 中的字符串类型,让我们更加了解它,更容易玩转字符串。

学习 Java 时我总是离不开它的 api 文档,因为很多知识都不一定要全部记得,有些方法随用随查就行了。而今天我们剖析 String 类型之前,我们就可以借助 Java 的 api 文档,让我们对 String 类型有个整体的认识。先看我在 api 中对 String 的一张截图

从这张图中我们就可以获取到关于 String 的几个知识点

  • String 是在 Java.lang 包中的,我们之前说过使用这个包的一些类(含 String 类)前不需要手动导包
  • String 是一个类
  • String 类继承了这三个接口:Serializable(下面会介绍)、CharSequence(下面会介绍)、Comparable< String >(在万字解析 Java 的多态、抽象类和接口这章介绍了)
  • String 类被 final 修饰,故它不可以被继承,是密封类

1. 创建字符串

为什么要借用 api 开头呢?
因为我们既然了解了 String 是一个类,那么用这个类去创建对象的时候我们就要清楚它有哪些构造方法

因此我就又截了 api 中关于 String 构造方法的图片,我们来看看

虽然 String 的构造方法比较多,但是我们常用的就三种
*
方式一:

String str1 = "Hello Java";

方式二:

String str2 = new String("Hello Java");

方式三:

char[] array = {'a', 'b', 'c'};
String str3 = new String(array);

至于想要知道方法三的原理的小伙伴,可以通过 ctrl + 鼠标点击 String 跳到 String 的定义,去寻找有数组相关的成员变量和构造方法,之后相信理解起来就很简单

注意:

“hello” 这样的字符串字面值常量,类型也是 String

问题: String 是引用类型,println 打印的应该是一个地址,为什么结果可以正常打印字符串呢?

  1. 通过找到 String 的重写方法,我们可以看到

  2. 再通过找到 println 的定义,我们可以看到

  3. 然后我就先不解释了,哈哈。其实我们知道 String 返回的是 this,但在 println 中又被转换成字符串。如果你想知道怎样转换的话,可以在 println 的定义中一直跟踪到整个过程

2. 字符串常量池

在我们实例化字符串的时候,其实就分为了两种方式

  • 直接赋值
  • 采用构造方法(new 一个新的 String)

a)直接赋值

我们可以先看一个代码,猜猜结果是啥

String str1 = "Hello Java";
String str2 = "Hello Java";
System.out.println(str1 == str2);

结果其实就是:true。因为我们知道 str1 和 str2 的值(这里指的是他俩的引用值)是一样的。

但是为什么在创建字符串的时候没有开辟出新的内存空间呢?为什么内存是这样存储的呢?

因为 String 类的设计使用了共享设计模式

在 JVM 底层实际上会自动维护一个对象池,这里指的是字符串常量池

什么是字符串常量池呢?
在堆中有一个区域存储着字符串常量,即字符串常量池。但是在 JVM 当中并没有划分区域指定哪里是字符串常量池。它的本质其实就是一个哈希表

那么字符串常量池有什么作用呢?

如果对 String 类的对象实例化,并且之前字符串常量池中没有该实例化对象,那么该实例化对象将自动保存到这个字符串常量池中,如

String str1 = "Hello Java";

此时 "Hello Java" 这个字符串,将保存到字符串常量池中
*
如果对 String 类的对象实例化,但字符串常量池之前就已经含有该实例化对象,那么将直接对其进行引用

String str1 = "Hello Java";
String str2 = "Hello Java";
System.out.println(str1 == str2);

故这个代码先是 str1 实例化对象,并将 “Hello Java” 保存到了字符串常量池中。之后 str2 实例化时,则可以直接对这个字符串进行引用,因此它们两个的引用值是相同的

2)采用构造方法

我们来看一个代码

String str2 = new String("Hello Java");

按照上述字符串常量池的概念,我们思考下实例化时在内存中是怎样的

我们知道 String 类有一个含数组的构造方法,数组名就是 Value。我们 new 的时候,相当于在堆上开辟一块空间,里面有一个变量是 Value,而构造时出现了字符串常量 “Hello Java”,所以他会自动保存到字符串常量中,而 Value 存的参数就是 “Hello Java”,并且此时他存的应该是这个字符串的地址,即相当于 Value 的引用又指向 “Hello Java”

因此我们会发现如果采用构造方法实例化 String 的对象,则会有下面的缺点:

  • 如果使用 String 构造方法会开辟两块堆内存空间,并且其中一块堆内存空间将成为垃圾空间
  • 同一个字符串可能会被多次次存储,比较浪费空间

问题: 请解释 String 类中两种对象实例化的区别

  • 直接赋值:只会开辟一块堆内存空间
  • 构成方法:会开辟两块堆内存空间

3. 字符串比较相等(包含 equals、intern)

首先我先介绍字符串比较时会出现的各类情况,在各种复杂的情况中,让你理解 String 类实例化时在内存中的情况

3.1 情况一(含 equals):

String str1 = "Hello Java";
String str2 = new String("Hello Java");
System.out.println(str1 == str2);

我们知道他们的输出是一样的,那么下面这个代码的答案是什么呢?按理说是:true

但结果是:false,为什么呢?
== 其实就是比较的是值相不相等,但是对于上述代码的值不是数值而是引用值,而 new 的对象相当于在堆上新开辟了一块空间,引用值肯定不同,所以这样比较的结果是错误的

正确的比较字符串的方式是使用 equals

equals 是比较引用所指向的对象是否相同

故将上述代码改成

System.out.println(str1.equals(str2));

结果就变成了:true,因为两个引用是不同的但是它们所指向的对象相等

3.2 情况二:

String str1 = "Hello Java";
String str2 = "Hello " + "Java";
System.out.println(str1 == str2);

结果是:ture,为什么呢?

因为常量在编译的时候就已将被运算了,即上述代码中的 "Hello " + "Java" 在编译时就已经被拼接成了 “Hello Java”。

3.3 情况三:

String str1 = "Hello Java";
String str2 = "Hello ";
String str3 = str2 + "Java";
System.out.println(str1 == str3);

结果是:false,为什么呢?

因为 str2 是变量(变量在编译的时候是不知道里面的值的,只有在运行时才知道),所以 str3 不能在编译时确定值是什么,我们可以用一张图看一下上述代码的存储

3.4 情况四:

String str1 = "Hello Java";
String str2 = "Hello " + new String("Java");
System.out.println(str1 == str2);

结果是:false,为什么呢?

我们直接上内存看看

结果就是引用不同

3.5 情况五:

String str1 = "Hello Java";
String str2 = new String("Hello ") + new String("Java");
System.out.println(str1 == str2);

结果是:false,至于为什么其实和上述情况类似,最终比较的两个对象引用值不同

3.6 情况六(含 intern 讲解):

String str1 = "Hello Java";
String str2 = new String("Hello Java");
str2.intern();
System.out.println(str1 == str2);

结果是:false,为什么呢?

这个情况和情况一类似,就是多了 intern 那行代码。

intern 就是手动将字符串入池

我们这个情况属于 str2 实例化的对象在字符串常量池已经存在了,故其实就不做处理了,和情况一是一样的,内存图就是这样

那么如果实例化之前字符串常量池不存在又会出现什么结果呢?看情况七

3.7 情况七:

String s1 = new String("1") + new String("1");
s1.intern();
String s2 = "11";
System.out.println(s1 == s2);

结果是:true,为什么呢?

我们先直接上一个图

从 s1 看起,先是两个字符串对象实例化,并将 “1” 存入了字符串变量池。之后拼接成一个新的对象,此时的数值为 “11”。后面由于有 intern,并且字符串 “11” 在变量池中不存在,所以进行入池,故字符串池中存了字符串 “11”(注意:此时存的是字符串 “11” 的地址)。再 s2 进行实例化,因为字符串变量池已经存在 “11”,所以就直接引用,故最终 s1 和 s2 的引用相同

3.8 情况八:

String str1 = new String("Hello Java");
str1.intern();
String str2 = "Hello Java";
System.out.println(str1 == str2);

结果是:false,为什么呢?

其实写看这种题目就是要细心,我们直接上图理清思绪

3.9 情况九:

String s1 = new String("1") + new String("1");
String s2 = "11";
s1.intern();
System.out.println(s1 == s2);

结果是:false,为什么呢?

这个情况其实和情况七就一点不同,直接上图理解

3.10 情况十:

String str1 = "Hello Java";
String str2 = str1;
str2 = "Hello World";
System.out.println(str1 == "Hello Java");

结果其实是:true,为什么呢?

str1 = "Hello World" 其实是将 str1 这个引用指向了一个新的 String 对象,即上述代码的整个过程可以理解为这个图

3.11 情况十一:

public static void func(String str1){
    str1="abc"
}
public static void main(String[] args){
    String str = "Hello Java";
	func(str);
	System.out.println(str == "Hello Java");
}

结果其实还是:true,这是为啥呢?

这个其实和情况十是一种情形,虽然加了个函数,但函数里的形参的引用是指向实参引用的地址,然后将形参指向一个新的对象,对实参其实没有影响

介绍了这么多种情况我们再回顾下 == 和 equals 的用法吧!

3.12 == 和 equals

String 使用 == 比较并不是在比较字符串的内容,而是比较两个引用是否指向同一个对象(即引用值)

这两种有什么不同吗?
面向对象编程语言中,涉及到对象的比较有三种方式:比较身份(即比较引用值)、比较值、比较类型

一般编程语言中 == 是用来比较值的,但是 Java 中是用来比较身份的(不需要记,看内存的存储就行)

比较值我们好理解,那么这个身份什么意思呢?我们来看下面一张图

诶对,大魔王现在去取快递,它的快递就放在那个红框框里。

我们可以把那个柜子的位置看看成“第二行,从左数第四个”或者是“第二行,从右数第第三个”,由于这两个位置都指向一个柜子,所以就表示身份相同,是我大魔王装快递的柜子

我们也可以在“第一行从左数第一个”柜子和“第一行从左数第二个“柜子都放入同样的物品,虽然它们不是同一个柜子,但是打开都是相同的物品,这就叫值相同,但是这

大家可以自细细感悟下,并且我们还能得到这样的结论:身份相同值一定相同,值相同身份不一定相同

因此比较字符串的时候,如果我们用 == 比较,可能明明它们的对象其实都是同一个字符串,但因为引用值的不同使结果出现错误。

故我们就要使用 equlas 去比较字符串,因为它是比较引用所指向的对象

注意:
当我们使用 equals 去比较字符串的时候,这样写代码要注意

String str1 = null;
String str2 = "abc";
System.out.println(str1.equals(str2));

上述代码会抛出 java.lang.NullPointerException 异常,所以我们要注意 str1 的引用是非为空,故最好直接使用字面常量字符串的形式去比较,例如

String str1 = "abc";
String str2 = "abc";
System.out.println("abc".equals(str2));

4. 理解字符串不可变

4.1 分析

字符串是一种不可变的对象,它的内容不可以改变
定义 String 类中的数组我们其实可以看到它是被 final 修饰的,无法修改

这是什么意思嘞,我们先看一段代码

String str = "hello ";
str += "java";
str += "!!!";
System.out.println(str);

结果是:"hello java!!!" ,就是你会感觉好像字符串被修改了对吧,就像是小时候的神奇宝贝进化一样,本来是小火龙,后来变成火恐龙,最好进化成喷火龙。始终都是这一只,但小火龙进化后,原本的小火龙就没有了

但是字符串是不会向上述那样,虽然最终输出的是:"hello java!!!",但是 "hello ""java""!!!" 这几个字符串都没有改变,还存在着。我们可以通过内存去理解一下

因为不是动态的所以看起来没那么顺畅,首先是1号线,表示代码的第一行,代码的第二行就是加了一个 “java”,由于常量是不可以改变的,所以不可能直接加在原有的 “hello” 后面,就新开辟一个内存,将拼接的新的字符串存入,此时 str1 的地址应该变成 0x678。3号线也是和2号线一样的步骤。所以最后其实是开辟了五个内存。原有的字符串没有改变,而是增加了新的字符串

4.2 修改字符串方式(含反射简单介绍)

那么如果我们想要修改字符串该怎么办呢?

注意:
字符串是不可以修改的,我这里说的修改其实是得到和原字符串上进行改变的新的字符串,但原字符串是不会改变的,例如将字符串 str = "hello" 改成 str = "Hello"

方式一:借助原字符串,创建新的字符串

采用 substring 方法来提取字串

String str = "hello";
str = "H" + str.substring(1);
System.out.println(str);

结果为:Hello,但是我们要知道

这样修改并没有改变原字符串,只是提取了它的字串,并进行了新的拼接

那么我就是想将字符串真正的改变而不是创建新的字符串有办法吗?有啊!这里需要用到反射

那什么是反射呢?
反射是 Java 类的一种自省的方式。通常情况下:类的内部细节有时候在类外是看不到的,但是通过反射就可以看到。

这其实就可以形象的理解为我们的行李箱接受安检的时候,行李箱通过检查的机器就可以直接看到行李箱内部的物品。

因此如果我们可以拿到 String 内部的字符串将它直接修改,就 🆗。那么怎么做呢?

String str = "hello";
// 拿到字节码对象
Class c = String.class;
// 获取 String 类的 value 字段
Field field = c.getDeclareField("value");
// 修改该字段的权限,使其访问属性为 true
field.setAccessible(true);
// 将 str 中的 value 属性获取到
char[] vals = (char[]) field.get(str);
// 将第一个字符修改成 'H'
vals[0] = 'H';
System.out.println(str);

结果为:“Hello”,并且是直接将原字符串修改

但是我们为什么要让 String 不可变呢?

  • 方便实现字符串常量池。如果可变那么对象池就需要考虑何时深拷贝字符串的问题
  • 不可变对象是线程安全的
  • 不可变对象更方便缓存 hash code,作为 key 时可以更高效的保存到 HashMap 中

5. 字符、字节、字符串

5.1 字符串与字符

之前我们就介绍到字符串内部含一个数组,即 String 应该可以和 char[] 相互转换

我搜集了下列方法

No.方法名称类型描述
1public String(char value[])构造将字符数组中的所有内容变为字符串
2public String(char value[], int offset, int count)构造将部分字符数组中的内容变为字符串,offset 为偏移量,从0开始
3public char charAt(int index)普通取得指定索引的字符,索引从0开始
4public char[] toCharArray()普通将字符串变为字符数组返回

接下来进行一一演示

示例一: 将字符数组中的所有内容变为字符串

char[] value = {'a', 'b', 'c', 'd', 'e'};
String str = new String(value);
System.out.println(str);

结果为:abcde

示例二: 将部分字符数组中的内容变为字符串

char[] value = {'a', 'b', 'c', 'd', 'e'};
String str = new String(value, 1, 3);
System.out.println(str);

结果为:bcd

示例三: 取得指定索引的字符,索引从0开始

String str = "abcde";
char c = str.charAt(2);
System.out.println(c);

结果为:c

示例四: 将字符串变为字符数组返回

String str = "abcde";
char[] value = str.toCharArray();
System.out.println(Arrays.toString(value));

结果为:[a, b, c, d, e]

练习: 判断一个字符串是否都由数字组成

String str = "12213";
char[] value = str.toCharArray();
for(int i=0; i<value.length; i++){
    if(value[i]<'0' || value[i]>'9'){
        System.out.println("不是都由字母组成");
        return;
    }
}
System.out.println("都是由字母组成");

思路:

将字符串变为字符数组,然后判断每一位是否为数字

5.2 字符串与字节

字节常用于数据传输以及编码转换的处理,字符串 String 也能和字节数组 byte[] 相互转换

我搜集了下列方法

No.方法名称类型描述
1public String(byte bytes[])构造将字节数组变成字符串
2public String(byte bytes[], int offset, int length)构造将部分字节数组中的内容变为字符串
3public byte[] getBytes()普通将字符串以字节数组的形式返回
4public byte[] getBytes(String charsetName)throws java.io.UnsupportedEncodingException普通编码转换处理

接下来进行一一演示

示例一: 将字节数组变成字符串

byte[] bytes = {97, 98 ,99 ,100};
String str = new String(bytes);
System.out.println(str);

结果为:abcd

示例二: 将部分字节数组中的内容变为字符串

byte[] bytes = {97, 98 ,99 ,100};
String str = new String(bytes, 1, 2);
System.out.println(str);

结果为:bc

示例三: 将字符串以字节数组的形式返回

public static void main(String[] args) {
    String str = "abcde";
    byte[] bytes = str.getBytes();
    System.out.println(Arrays.toString(bytes));
}

结果为:[97, 98, 99, 100, 101]

示例四: 编码转换处理

public static void main(String[] args)throws java.io.UnsupportedEncodingException {
    String str = "魔王";
    byte[] bytes = str.getBytes("GBK");
    System.out.println(Arrays.toString(bytes));
}

结果为:[-60, -89, -51, -11]

如果我们将编码方式 “GBK” 改成 “utf-8”,则会有不同的结果

public static void main(String[] args)throws java.io.UnsupportedEncodingException {
    String str = "魔王";
    byte[] bytes = str.getBytes("utf-8");
    System.out.println(Arrays.toString(bytes));
}

结果为:[-23, -83, -108, -25, -114, -117]

注意:
细心的朋友在演示示例二的时候会发现,当我们只输了第二个参数时,String 被打了一个横线

这是为什么呢?通过转到定义

我们看到了 @Deprecated 这样的注解,这个表示这个方法已经被弃用了,这在我们今天介绍 String 的构造方法那张图里面就有这个方法,也被表明被弃用

5.3 字节数组和字符数组使用场景

对于字节数组:
byte[] 是把 String 按照一个字节一个字节的方式处理,适合用在网络传输数据存储针对二进制数据操作的场景

对于字符数组:

char [] 是把 String 按照一个字符一个字符的方式处理,适合用在对文本数据操作尤其是包含中文的时候

那啥又是文本数据和二进制数据呢?其实可以这样记忆

看的懂得就是文本数据(例如 .java 文件),看不懂的就是二进制数据(例如 .class 文件

6. 字符串常见操作

6.1 字符串比较

上述介绍到 equals 可以比较字符串是否相等,并且是区分大小写的。而除了它,String 类还有其他比较字符串的方法

我搜集了下列方法

NO.方法名称类型描述
1public boolean equals(Object anObject)普通区分大小写的比较
2public boolean equalsIgnoreCase(String anotherString)普通不区分大小写的比较
3public int compareTo(String anotherString)普通比较两个字符串大小关系

接下来进行一一演示

示例一: 区分大小写的比较

String str1 = "hello";
String str2 = new String("Hello");
System.out.println(str1.equals(str2));

结果为:false

示例二: 不区分大小写的比较

String str1 = "hello";
String str2 = new String("Hello");
System.out.println(str1.equalsIgnoreCase(str2));

结果为:true

示例三: 比较两个字符串大小关系

String str1 = "hello";
String str2 = new String("Hello");
System.out.println(str1.compareTo(str2));

结果为:32,这个结果是怎么来的呢?

字符串的大小比较,其实是“字典序”的比较,将两个字符串从第一个字母开始,一一比较,如果相等就比较下一个,如果不等就停止比较,结果返回的是较大的字母减去较小字母的差值

6.2 字符串查找

我们做算法题经常碰到某个字符串中是否存在一个指定的内容,而 String 类中就有专门的查找方法

我搜集了下列方法

No.方法名称类型描述
1public boolean contains(CharSequence s)普通判断一个字符串是否存在
2public int indexOf(String str)普通从头开始查找指定字符串的位置,查到了返回位置的开始索引,查不到返回-1
3public int indexOf(String str, int fromIndex)普通从指定位置开始查找子字符串位置
4public int lastIndexOf(String str)普通由后向前查找子字符串位置
5public int lastIndexOf(String str, int fromIndex)普通从指定位置由后向前查找
6public boolean startsWith(String prefix)普通判断是否以指定字符串开头
7public boolean startsWith(String prefix, int toffset)普通从指定位置开始判断是否以指定字符串开头
8public boolean endWith(String suffix)普通判断是否以指定字符串结尾

接下来进行一一演示

示例一: 判断一个字符串是否存在

String str = "helloworld";
System.out.println(str.contains("hello"));

结果为:true

示例二: 从头开始查找指定字符串的位置,查到了返回位置的开始索引,查不到返回-1

String str = "helloworld";
System.out.println(str.indexOf("world"));

结果为:5

示例三: 从指定位置开始查找子字符串位置

String str = "helloworldhelloworld";
System.out.println(str.indexOf("world", 6));

结果为:15

示例四: 由后向前查找子字符串位置

String str = "helloworld";
System.out.println(str.lastIndexOf("world"));

结果为:5

示例五: 从指定位置由后向前查找

String str = "helloworldhelloworld";
System.out.println(str.indexOf("world", 6));

结果为:15

示例六: 判断是否以指定字符串开头

String str = "helloworld";
System.out.println(str.startsWith("hello"));

结果为:true

示例七: 从指定位置开始判断是否以指定字符串开头

String str = "helloworld";
System.out.println(str.startsWith("hello", 2));

结果为:false

示例八: 判断是否以指定字符串结尾

String str = "helloworld";
System.out.println(str.endsWith("world"));

结果为:true

6.3 字符串替换

String 类中也有方法将一个指定的新字符串替换掉已有的字符串数据

我搜集了下列方法

No.方法名称类型描述
1public String replaceAll(String regex, String replacement)普通替换所有的指定内容
2public String replaceFirst(String regex, String replacement)普通替换首个内容

接下来进行一一演示

示例一: 替换所有的指定内容

String str = "helloworld";
System.out.println(str.replaceAll("world", "java"));

结果为:“hellojava”

示例二: 替换首个内容

String str = "helloworld";
System.out.println(str.replaceFirst("l", "-"));

结果为:he-loworld

注意:
由于字符串是不可变对象,替换不修改当前字符串,而是产生一个新的字符串

6.4 字符串拆分

String 类中也有方法将一个完整的字符串按照指定的分隔符划分为若干子字符串

我搜集了下列方法

No.方法名称类型描述
1public String[] split(String regex)普通将字符串全部拆分
2public String[] split(String regex, int limit)普通将字符串部分拆分,该数组长度就是 limit 极限

接下来进行一一演示

示例一: 将字符串全部拆分

String str = "hello world hello java";
String[] result = str.split(" ");
for(String s : result){
    System.out.println(s);
}

结果为:
hello
world
hello
java

示例二: 将字符串部分拆分,该数组长度就是 limit 极限

String str = "hello world hello java";
String[] result = str.split(" ", 2);
for(String s : result){
    System.out.println(s);
}

结果为:
hello
world hello java

其中第二个参数指最多分的组数,这个代码的参数是2,所以最多分两组

示例三: 拆分 IP 地址

String str = "192.168.1.1";
String[] strings = str.split(".");
for(String s: strings){
    System.out.println(s);
}

但是这个代码输出不了结果,如果大家不信可以上机试试。那怎样写才是对的呢?看下面代码

String str = "192.168.1.1";
String[] strings = str.split("\\.");
for(String s: strings){
    System.out.println(s);
}

此时结果就是:
192
168
1
1

这是由于在分割字符中,如果是以字符 “|”、"/*"、"+" 分隔,分割时都得加上转义字符 " \ ",而这里之所以要加两个,是因为通过第一个 " \ ",先将第二个 " \ " 变成了转义符号

示例四: 一个代码中有多个分隔符要进行多次拆分

String str = "java string-split#test";
String[] strings = str.split(" |-|#");
for(String s: strings){
    System.out.println(s);
}

结果为:
java
string
split
test

是不是很方便,如果一个字符串中有多个分隔符并要对他们都要进行分割,可以用 “|” 作为连字符

示例五: 多次分割

String str = "name=zhangsan&age=18";
String[] strings = str.split("&");
for(String s: strings){
    String[] tmp = s.split("=");
    for(String ss: tmp) {
        System.out.println(ss);
    }
}

结果为:
name
zhangsan
age
18

注意:

  • 字符串 “|”、"/*"、"+" 作为分割符时,参数前要加转义字符 " \ ",但是要写成 " \ \ ",因为第一个斜杠是为了将第二个斜杠变成转义字符
  • 如果一个字符串中有多个分隔符需要被分割,可以将这些分隔符用 “|” 号连接,写到参数里面

6.5 字符串截取

上面稍微介绍过了 substring 方法,它是用来截取字符串的。它的参数不仅可以只有一个,我们来了解下

No.方法名称类型描述
1public String substring(int beginIndex)普通从指定索引截取到结尾
2public String substring(int beginIndex, int endIndex)普通截取部分内容

接下来进行一一演示

示例一: 从指定索引截取到结尾

String str1 = "helloworld";
String str2 = str1.substring(5);
System.out.println(str2);

结果为:“world”

示例二: 截取部分内容

String str1 = "helloworld";
String str2 = str1.substring(2,5);
System.out.println(str2);

结果为:“llo”

注意:
截取部分内容,参数其实是一个左闭右开的区间,意思就是不包含 endIndex 位置的字符

6.6 其他操作

除了上述的一些操作字符串的方法,其实还有很多其他的方法,下面我就列举了几个

No.方法类型描述
1public String trim()普通去掉字符串中的左右空格,保留中间空格
2public String toUpperCase()普通字符串转大写
3public String toLowerCase()普通字符串转小写
4public native String intern()普通字符串入池操作
5public String concat(String str)普通字符串连接,等同于 “+”
6public int length()普通取得字符串长度
7public boolean isEmpty()普通判断字符串是否为空(空不是 null,而是长度为0)

这里的方法就不一一展示示例了,如果大家能够把上述字符串的方法都记得的话,那么去做一些题目那真的可以说是更加轻松了。并且字符串的方法不止这些,大家可以去 Java 的 api 中去查看学习

7. StringBuffer 和 StringBuilder

7.1 了解 StringBuffer 和 StringBuilder

StringBuffer 和 StringBuilder 是什么呢?遇到没学过的我们可以借助 api 查看一下

从上面两张图中我们可以获取到关于 StringBuffer 和 StringBuilder 的几个知识点

  • StringBuffer 和 StringBuilder 都是类
  • 两者都在 java.lang 包中,即不用手动导包
  • 两者都继承了这三个接口:SerializableAppendableCharSequence
  • 两者都继承了 Object 类

此时大家应该对这两个类应该都有印象了,我们通过分析到这两个类和 String 类一样都继承了 CharSequence 接口,而这个接口描述的是一系列的字符集会,故我们可以理解,StringBuffer 和 StringBuilder 和 String 类一样都是和字符串相关的

7.2 StringBuffer、StringBuilder 和 String 的区别

那么字符串既然有了 String 类,为什么还要有这两个类呢?
通常来讲 String 的操作比较简单,但是由于 String 的不可更改特性,为了方便字符串的修改,则提供 StringBuffer 和 StringBuilder类

由于 StringBuffer 和 StringBuilder 大部分功能相似,下面则只用 StringBuffer与 String 进行比较找出区别,之后再将这两者进行比较

区别一:
我们知道 String 类可以采用直接赋值和构造两者方法。但是 StringBuffer 只可以采用构造法,如

StringBuffer sb = new StringBuffer("hello");

区别二:

在 String 中拼接字符是使用 “+” 来进行拼接的,但是 StringBuffer 中不可以,而是使用 append() 方法,如

StringBuffer sb = new StringBuffer("hello");
sb.append("java").append("!!!");
System.out.println(sb);

结果为:“hellojava!!!”

注意:append() 方法可以接着该方法进行多个字符串拼接,如上述代码

区别三(重要):

虽然 String 和 StringBuffer 都是和字符串有关的类,但是他俩不能直接转换,如果想要转换,方法如下:

String 变为 StringBuffer: 利用 StringBuffer 的构造方法或 append() 方法

String str = "abc";
// 利用 StringBuffer 的构造方法
StringBuffer sb = new StringBuffer(str);
// 利用 append() 方法
StringBuffer sb = new StringBuffer();
sb.append(str);

StringBuffer 变为 String: 调用 toString() 方法

StringBuffer sb = new StringBuffer("abc");
String str = sb.toString();

区别四(重要):

String 不可修改,而 StringBuffer 可以修改。这是为什么呢?我们可以看他俩的定义

其中 StringBuffer 修改后返回的是当前的对象。意思是拼接时将新的字符串直接赋予给了当前对象,并没有产生新的对象,故 StringBuffer 可以修改

区别五:

StringBuffer 中有着一些 String 没有的方法,比如:字符串反转

StringBuffer sb = new StringBuffer("abc");
System.out.println(sb.reverse());

结果为:“cba”

还有其他的方法,大家可以直接查阅 api 了解

扩充:

首先我们先写一个代码

String str = "abc";
str = str + "def";
System.out.println("str");

这个代码很简单,但是通过反汇编时,我们发现它执行的代码好像有点不同,我将它翻译后是这样的(大家可以搜反汇编的命令自己去查看)

String str = "abc";
StringBuffer sb = new StringBuffer();
sb = sb.append(str).append("def").toString();
System.out.println("str");

这个代码好像更高级对吧,哈哈,但是输出的结果是一样的。我们可以先理解为它是被优化了,那么为什么要优化呢?这两个代码有什么不同吗?

我们知道使用 StringBuffer 修改时不会产生新的对象,如果我们拼接的字符串很多,单纯的使用 String 的方法拼接的话将会产生很多个对象,例如这样的代码

String str = "A";
for(int i=0; i<9; i++){
    str+=i;
}
System.out.println(str);

而此时我们再通过反汇编,并将反汇编的指令翻译后的代码是这样的

String str = "A";
StringBuffer sb = new StringBuffer(str);
for(int i=0; i<9; i++){
    sb.append(i);
}
str=sb.toString();
System.out.println(str);

这样的话,整个循环是没有新增对象的,这样大大减缓了内存损耗

7.3 StringBuffer 和 StringBuilder 的区别

细心的兄弟在看 StringBuffer 和 StringBuilder 的定义时,就可以发现它们有不同之处

如图所示,StringBuffer 的方法中都有 synchronized,而 StringBuilder 的方法中没有

那么这个 synchronized 有什么用作呢?
它表示采用同步处理,能保证线程是安全的。

不知道线程的朋友可以借助这样一个形象的比喻去了解:小明现在去上厕所,这个厕所门没有锁,故其他他人想进来就进来,诶这就可以理解为线程是不安全的,而如果厕所门有把锁,小明可以很惬意的上厕所,就可以理解为线程是安全的

故 StringBuffer 更适合于多线程情况,StringBuilder 更适合于单线程情况,String 也适合于单线程情况

7.4 面试题

请解释 String、StringBuffer、StringBudiler 的区别

  • String 的内容不可修改,StringBuffer 和 StringBuilder 的内容可以修改
  • StringBuffer 和 StringBuilder 大部分功能是相似的
  • StringBuffer 采用同步处理,属于线程安全操作;而 StringBuilder 未采用同步处理,属于线程不安全操作

8. 总结

写这篇博客的途中,我花了几天时间。由于是第一次学习 Java,所以写写停停,反复思考又继续写写停停,就感觉这些知识很快就刻在我的脑海中了,从开始写某一节卡壳到要回顾前文,到后面就很顺气自然的一气呵成。不过肯定有我理解有误的地方,如果小伙伴们有发现的话可以直接评论哦!

最后希望给你们也带来帮助,然后来一个收藏支持一下这篇文章呀!

相关文章