我们知道,C语言中字符串以字符数组的形式来表示,这种方式可以说很糟糕了。现代的编程语言基本都会有一个基本的类型来表示字符串,本篇博客我们来介绍一下Java中的字符串类型String。
字符串的创建
首先,我们来看一下,如何创建一个字符串。我们介绍三种常见的创建方式。
- 方式一:
String str = "hehe";
。内存布局如下: - 方式二:
String str2 = new String("hehe");
。内存布局如下: - 方式三:
char[] arr = {'h', 'e', 'h', 'e'}; String str3 = new String(arr);
。内存布局如下:
结论:
- 从上述三种字符串定义方式的内存布局可以看出,方式一不仅写起来简单,也是最省空间的。所以在使用中我们更推荐使用方式一。
注意:
"hehe"
这样的字符串字面值常量,类型也是String;- Java中除了四类八种基本的数据类型(byte、short、int、long、float、double、char、boolean)之外,全部都是引用类型;
- 既然String也是引用类型,我们来看一下这段代码的内存布局。
String str1 = "hehe"; String str2 = str1;
。
修改str1的引用,并不会让str2发生改变。str1 = "world";
。修改之后,内存布局如下:
实际上,str1 = "world";
这样的代码并不算是修改字符串,而是让str1这个引用指向了一个新的String对象。
字符串比较相等
- 我们知道,基本类型的变量是否相等,可以通过
==
操作符进行比较。
内存布局如下:
这里的x == y;
比较的就是x和y的值是否相等。 - 那么String类对象能否使用==进行比较相等呢?我们来试一下。
这里,我们可以看到,结果是true,而str1和str2的内容确实也相等,都是"hehe"
。这是不是说明字符串类型可以使用==来比较相等呢?
我们再来看一个例子:
可以看到,这里str1和str2中的内容是相等的,都是"hehe"
。但是为什么结果是false呢?
其实这里的str1 == str2;
比较的是str1和str2的身份是否相同,也就是比较的是str1和str2引用的是否是同一个对象。
我们画一下二者的内存布局来理解一下: - 那么,我们如何来两个字符串内容是否相等呢?可以使用String类提供的方法equals。
下面,来看一下代码演示:
注意事项:
- 我们来看一个场景,我们有
String str = "hehe";
,这时候,我们想要判断str中的内容是否是"hehe",应该怎么做? - 这里,我们有两种方法。如下:
可以看到,两种方法都可以达到我们想要的效果。 - 但是,这里我们更推荐使用方法二,因为如果str为null,那么方法一将会抛异常。我们具体来看一下:
字符串常量池
- 首先,我们来看一个例子。
从运行结果可以看出,str1、str2和str3指向的是同一个"hehe",内存布局如下: - 为什么这里没有开辟新的空间,来创建多份"hehe"呢,这是因为String类的设计使用了共享设计模式。
在JVM底层自动维护了一个对象池(字符串常量池)。
采用直接赋值的方式实例化一个String类对象,如:String str1 = "hehe";
。首先会到字符串常亮池中去找指定内容"hehe"
,如果此时字符串常亮池之中有指定内容"hehe"
,将直接进行引用;如果字符串常量池中没有指定内容"hehe"
,则创建新的字符串对象并将其保存到字符串常量池中以供下次使用。 - 下面,我们看一下使用构造函数实例化String类对象的内存布局,
String str = new String("hehe");
:
可以看到,此时"hehe"
有两份,一份在字符串常量池,一份是我们new出来的,在堆上。我们能不能让"hehe"
变成一份呢?字符串常亮池中的我们没办法改变,我们能不能将new出来的对象放到字符串常亮池中呢?这时候就可以让"hehe"变成一份。答案是可以的。 - 我们可以使用String的intern()方法,手动把String对象加入到字符串常量池中。下面看操作:
使用intern()方法,会先去字符串常量池中查找是否在池中。如过在池中,则直接返回池中对象的引用;如果不在池中,则在池中实例化一个相同的对象,返回这个池中对象的引用。
这时候,我们再来看String类两种对象实例化方式的区别:
- 方式一:
String str = "hehe";
。直接赋值,该种示例化方式只会开辟一块堆内存空间,并且该字符串对象可以自动保存在字符串常亮池中以供下次使用。 - 方式二:
String str = new String("hehe");
。使用构造方法,会开辟两块堆内存空间,不会自动保存在对象池中,可以使用intern()方法手动入池。
综上所述,我们可以更推荐使用直接赋值的方式,来实例化一个字符串对象。
注意:JDK1.8版本中,将字符串常量池从方法区的运行时常量池分离到堆中。
“池”(pool)
“池”是编程中的一种常见的,重要的提升效率的方式。常见的有“内存池”、“线程池”、“数据库连接池”等等;
“池”的概念被广泛的应用在服务器端软件的开发上。使用池技术可以明显提高应用程序的速度,改善效率和降低系统资源的开销。
我们可以举个例子来更好的理解“池”:
假如现在有大量的客户并发的访问服务器时,我们如何提供提供服务呢?
- 方案一:我们可以为每一个客户提供一个新的服务对象进行服务。这种方法看起来很简单,但是在实际应用中如果采用这种方法会有很多问题,显而易见的是不断的创建和销毁新的服务对象将会耗费大量系统资源,导致系统性能下降。
- 方案二:针对这种场景,我们就可以使用“池”的方式。“池”可以想象成就是一个容器保存着我们需要的对象。我们对这些对象进行复用,进而提高系统性能,可以省去不断创建和销毁服务对象所带来的开销。
“池”从结构上来看,它应该具有容器对象和具体的元素对象。从使用方法上来看,我们可以直接获取“池”中的元素来用,也可以把我们要做的任务交给它来处理。所以从使用方法上看,“池”分为两种类型:
- 用于处理客户提交的任务的,比如说:线程池;
- 资源池:客户从池中获取有关的对象进行使用,如:前面说的字符串常量池、数据库连接池等等。
字符串不可变
字符串是一种不可变对象。它的内容不能被改变。String类的内部实现也是基于char[]来实现的,就是String类并没有提供set之类的方法来修改内部的字符数组。String类的不可变是通过封装来实现的。
下面,我们来看一段代码:
可能有人会说,这不是把str的内容改变了吗?str已经变成了"hello world!!!"
。其实不然,我们还是来画一下内存布局:
从上述可以看出,并不是"hello"
被改变了,而是str的引用变了,改为引用"hello world!!!"
。在这个过程中一共创建了五个对象,如上图所示,其中:"hello"
、"world"
和"!!!"
在字符串常亮池;而"hello world"
和"hello world!!!"
在堆上,其中"hello world"
会变成垃圾,因为没有指向它的引用了。
注意:字符串常量池中的字符串就算没有引用指向它,也不会变成垃圾。
下面看一个经典错误代码:
这个代码就会产生大量的临时对象,效率比较低。
不可变字符串对象的修改
现在有一个需求,需要将字符串str = "hehe";
改为str = "Hehe";
。有什么办法?
方法有两个:
方法一:借助原字符串,创建新的字符串。
方法二:
使用“反射”操作可以强行破坏封装,访问一个类内部的private成员。
关于反射:
- 反射是面向对象编程的一种重要特性,有些编程语言也称为“自省”。
- 指的是在程序运行过程中,获取/修改某个对象的详细信息(类型信息、属性信息等)。相当于让一个对象更好的“认清自己”。
String对象不可变的好处
- 方便实现字符串常量池;
- 线程安全;
- 不可变对象更方便缓存hashcode,作为key时可以更高效的保存到HashMap中。
字符、字节和字符串
字符与字符串
字符串内部包含一个字符数组,String可以和char[]相互转换。我们看下面几个方法:
No. | 方法名称 | 类型 | 描述 |
1. | public String(char value[]) | 构造 | 使用字符数组中的所有内容构造一个字符串 |
2. | public String(char value[], int offset, int count) | 构造 | 使用字符数组中以offset下标开始的连续count个字符构造一个字符串 |
3. | public char charAt(int index) | 普通 | 获得字符串中指定索引的字符 |
4. | public char[] toCharArray() | 普通 | 将字符串变为字符数组返回 |
我们一一来演示一下上述方法的使用:
- 使用字符数组中的所有内容构造一个字符串。
- 将部分字符数组中的内容变为字符串。
- 取得字符串指定索引的字符。
- 字符串转为字符数组。
字节与字符串
No. | 方法名称 | 类型 | 描述 |
1. | public String(byte bytes[]) | 构造 | 将字节数组变为字符串 |
2. | public String(byte bytes[], int offset, int length) | 构造 | 将部分字节数组中的内容变为字符串 |
3. | public byte[] getBytes() | 普通 | 将字符串以字节数组的形式返回 |
4. | public byte[] getBytes(String charsetName) throws UnsupjportEncodingException | 普通 | 将字符串转换为指定编码方式,并以字节数组的形式返回 |
我们一一来演示一下上述方法的使用:
- 将字节数组变为字符串。
首先,我们来构造字符串的字节数组,我们使用的字符串为"蒙蒙",我们知道Java中使用的是Unicode字符集,实现方式为utf-8。
通过查表,我们知道蒙的Unicode码为0x8499,另外我们还需要了解一下UTF-8的编码规则,UTF-8编码规则有两条:①对于单字节字符,字节的第一位设为0,后面7位为这个符号的Unicode码。因此对于英文字母,UTF-8编码和ASCII码是相同的。②对于n字节的符号(n > 1),第一个字节的前n位都设为1,第n + 1位设为0,后面字节的前两位一律设为10。剩下的没有提及的二进制位,全部为这个符号的Unicode码。
我们来构建这么一个字节数组,代码如下: - 将部分字节数组中的内容变为字符串。
- 将字符串以字节数组的形式返回。
- 将字符串以指定编码方式的字节数组形式返回。
字符和字节什么场景下使用?
- char[]是把String按照一个字符一个字符的方式处理,更适合针对文本数据来操作,尤其是包含中文的时候;
- byte[]是把String按照一个字节一个字节的方式处理,这种适合在网络传输,数据存储这样的场景下使用。更适合针对二进制数据来操作。
字符串常见操作
字符串比较
No. | 方法名称 | 类型 | 描述 |
1. | public boolean equals(Object anObject) | 普通 | 区分大小写的比较 |
2. | public boolean equalsIgnoreCase(String anotherString) | 普通 | 不区分大小写的比较 |
3. | public int compareTo(String anotherString) | 普通 | 比较两个字符串大小关系 |
下面,我们来一一演示一下这些方法的使用:
- 区分大小写的相等比较。
- 不区分大小写的相等比较。
- 比较两个字符串的大小关系。
compareTo是字符串的字典序比较。
字符串查找
所谓字符串查找就是从一个完整的字符串之中可以判断指定内容是否存在,有如下方法:
No. | 方法名称 | 类型 | 描述 |
1. | public boolean contains(CharSequence s) | 普通 | 判断一个子字符串是否存在 |
2. | public int indexOf(String str) | 普通 | 从左开始向右查找指定字符串是否存在,查到了返回位置的开始索引,如果查不到返回-1 |
3. | public int indexOf(String str, int fromIndex) | 普通 | 从指定位置开始查找子串的位置 |
4. | public int lastIndexOf(String str) | 普通 | 由右向左查找子字符串位置 |
5. | public int lastIndexOf(String str, int fromIndex) | 普通 | 从指定位置由右向左查找子串 |
6. | public boolean startsWith(String prefix) | 普通 | 判断是否以指定字符串开头 |
7. | public boolean startsWith(String prefix, int toffset) | 普通 | 从指定位置开始判断是否以指定字符串开头 |
8. | public boolean endsWith(String suffix) | 普通 | 判断是否以指定字符串结尾 |
下面来一一演示一下这些方法的使用:
- 判断一个子串是否存在。
- 从左向右查找第一个子串位置。
- 从指定位置开始查找第一个子串位置。
- 从右向左找第一个子串位置。
- 从指定位置由右向左查找第一个子串位置。
- 判断字符串是否以指定字符串开头。
- 从指定位置开始判断是否以指定字符串开头。
- 判断是否以指定字符串结尾。
字符串替换
使用一个指定的新的字符串替换掉已有的字符串数据。
No. | 方法名称 | 类型 | 描述 |
1. | public String replaceAll(String regex, String replacement) | 普通 | 替换所有的指定内容 |
2. | public String replaceFirst(String regex, String replacement) | 普通 | 替换首个内容 |
下面,我们一一演示一下上面方法的使用:
- 替换所有的指定内容。
- 替换首个内容。
字符串拆分
可以将一个完整的字符串按照指定的分隔符划分为若干个子字符串。
No. | 方法名称 | 类型 | 描述 |
1. | public String[] split(String regex) | 普通 | 将字符串全部拆分 |
2. | public String[] split(String regex, int limit) | 普通 | 将字符串部分拆分,最多切分为limit份 |
我们一一来演示一下这些方法:
- 将字符串全部切分。
- 将字符串全部切分,设置最多切分的份数。
注意这里的参数limit的功能,假如说原本字符串可以切分成4份,但是这里将limit设置为3之后,字符串将会切分为3份。
我们再来看一个例子,IP地址的切分。
可以看到这里什么都没有切分出来,这是为什么呢?
因为.在正则表达式中是特殊字符,需要转义。
这里需要加两个反斜杠,第一个反斜杠是Java中String的转义要求,第二个反斜杠是正则表达式的要求。
下面在来看一个多次切分的例子:
注意事项:
- 字符"|","*","+"都需要加上转义字符反斜杠;
- 而如果是".",那么需要写成"\.";
- 如果一个字符串中有多个分隔符,可以用"|"作为连字符。
字符串截取
从一个完整的字符串之中截取出部分内容。可用如下方法:
No. | 方法名称 | 类型 | 描述 |
1. | public String substring(int beginIndex) | 普通 | 从指定索引截取到结尾 |
2. | public String substring(int beginIndex, int endIndex) | 普通 | 截取部分内容,截取部分为:[beginIndex, endIndex) |
我们来一一演示一下这些方法的使用:
- 从指定索引截取到结尾。
- 截取部分内容。
其他操作方法
No. | 方法名称 | 类型 | 描述 |
1. | public String trim() | 普通 | 去掉字符串中的左右空格,保留中间空格 |
2. | public String toUpperCase() | 普通 | 字符串转大写 |
3. | public String toLowerCase() | 普通 | 字符串转小写 |
4. | public native String intern() | 普通 | 字符串入池操作 |
5. | public String concat(String str) | 普通 | 字符串连接,等同于"+",不入池 |
6. | public int length() | 普通 | 取得字符串长度 |
7. | public boolean isEmpty() | 普通 | 判断字符串是否为空(不是null,而是长度为0) |
下面一一看这些方法的具体使用:
- 去掉字符串中的左右空格。
- 字符串转大写。
- 字符串转小写。
- 字符串拼接。
- 获得字符串长度。
- 判断字符串是否为空。
StringBuffer和StringBuilder
StringBuffer和StringBuilder是为了解决String是不可变对象带来的麻烦。
我们来看一下之前那个频繁拼接字符串的代码:
String str = "hehe";
for (int i = 0; i < 100; ++i) {
str += i;
}
System.out.println("str = " + str);
我们知道String是不可变对象,所以这个代码的效率很低,就是在频繁的创建新的对象。我们使用StringBuffer和StringBuilder来分别优化一下这个代码:
String、StringBuffer和StringBuilder的区别
- String是不可变的,StringBuffer和StringBuilder都是可变的;
- StringBuffer和StringBuilder大部分功能都是类似的;
- StringBuffer采用同步处理,属于线程安全;StringBuilder没有采用同步操作,线程不安全。
来源:https://blog.csdn.net/sss_0916/article/details/102656830