2021年1月4日学习记录
1.字符串
1.1 字符串基本介绍及声明定义方式
字符串是一种常见且常用的数据类型,在Java中,字符串(String)是一种引用类型,其底层实现使用到了字符型数组,但是需要注意的是字符串和字符型数组不是同一个东西,二者是有区别的,因此在学习Java时千万不可将字符串和字符型数组混淆。
字符串使用双引号进行定义,例如:
String str = "See you Cowboy Bebop";
String str1 = "";
由上图可见,字符串的声明并不用new语句来创建对象,因此很多人会误以为字符串类型是一种基本类型,但需要注意的是,字符串是一种引用类型,千万不要因为它的声明方式将其记为基本类型。
1.2 空串与长度为零的串
字符串中存在一种特殊的字符串,叫做空串,其很容易与长度为零的字符串相混淆,需要注意的是,二者有着本质上的差别。
String emptyStr = "";//长度为零的串
String aStr;//空串
上面的emptyStr和aStr是完全不同的,其中emptyStr叫做空字符串,它确确实实指向一片地址,只不过这片地址上面并没有字符数据,而aStr则处于未被定义的状态,它没有指向任何地址,它的地址是一个空地址null,因此和有地址指向的emptyStr完全不同。我们可以记为:长度为零的串有值空间,它指向了一片地址,但是这片地址上没有任何字符数据;空串则是没有值空间,它根本没有长度,使用length方法会报错。
1.3 子串
字符串中的一部分,或者说一个字符串连续的子集被称为子串,如See you Cowboy Bebop中的加粗部分连带中间的空格,就叫做这个字符串的子串,子串必须是位于字符串上的,且连续的一段。如果学过数据结构,肯定对字符串的子串有着深刻的印象,模式匹配算法便是针对子串进行操作的。在Java中,String类型拥有一个切割子串的方法:substring(m,n),它的作用是取字符串中从第m个开始,到第n-1个结束的子串出来。它取得下标区间为[m,n),是一个左闭右开的区间,简单来说,是取第m个,然后一直往右取,直到发现字符串位数已经到了n,便不再取,其中第n个不取。下面是使用方法:
String str = "See you Cowboy Bebop";
String str1 = str.substring(3,8);
结果为“ you ”,包括空格在内的5个字符。字符串的下标索引从0开始,这一点和C语言中的数组一样,需要记住,在进行字符串切割的时候一定要注意这一点。
1.4 字符串拼接
在C语言中,字符串拼接是一个很麻烦的过程,但是在Java中字符串拼接显得非常容易,直接用“+”号就可以将两个字符串拼接起来,同时加号还可以连接字符串类型和其他基本类型,需要注意的是字符串加上其他基本类型仍然是字符串类型。
String str = "See you";
String str1 = "Cowboy Bebop";
System.out.println(str + " " + str1);//字符串拼接的一种用法,其输出结果为See you Cowboy Bebop
String str2 = "Hello";
System.out.println(str2 + "World");//输出结果为HelloWorld
String str3 = "GoGoGo!";
int a = 111;
str3 = str3 + a;
System.out.println(str3);//输出结果为gogogo111,在str3 = str3 + a;中,int类型a被转换成了字符串类型并拼接到了str3后边
1.5 不可变字符串
让学过C语言的人难以接受的是,Java中并没有提供修改字符串的方法,也就是说我们无法像C语言的字符数组一样通过下标精确的修改某个值。如果我们想用比较小的代价修改一个字符串,通常可以使用字符串切割加拼接的方法修改,这听上去十分麻烦。这是为什么呢,这是因为Java的特殊机制,在Java中,有着比C语言更加严格的安全策略,为了防止字符串数据进行溢出,设计者宁缺毋滥,取消了字符串的精确修改的机制,因为如果能够用操作数组的方式修改字符串,就会使得字符串有变得过长而影响到其他数据的风险,尽管字符串很难大到这种程度,但是,只要有可能,就不行。
在这里我们应该了解一下Java中以及大部分系统中为变量分配空间的机制,在大部分操作系统中,内存都被分成了大小为4kb的存储单元,这是为了提升内存速度,这一点我将在附录1中进行详细解释。由于这个机制的存在,Java为所有变量都会进行基本的空间分配,对于字符串型的变量,在初始化时,也会根据其长度分配一个固定的空间,这个空间至少为4kb,不论它多么小。如果我们能够对字符串进行任意的修改,那么这个字符串很有可能会越来越长直到最后超过为它分配的基本空间,这时就有可能覆盖其他程序,系统此时就危险了。这种危险在C语言中经常出现,在C语言中这被称为数组溢出,Java为了从根本上杜绝这种现象,为了让我们无论如何拼接字符串都不会溢出,采取了这种策略:在我们用“+”拼接字符串时,系统会新申请一块大小符合拼接后的字符串长度的新空间,将新字符串复制到新空间中之后,将新地址赋给一个变量,这个变量之前可能指向其他字符串,但此时它将抛弃原来的字符串并指向新的字符串地址,被放弃的字符串地址空间被Java自动回收。因此,我们发现,在Java中String类型不可在原地址上进行修改,因此也被命名为不可变字符串,我们可以理解为一个地址上的字符串一旦被写上,就不能被修改,只能被放弃,被复制,但是其本身,不能被进行修改。因此String类型又被称为不可变字符串。
既然如此,在Java中,字符串变量可以灵活的改变其指向的地址,这是为什么呢?这是因为Java中的特殊字符串存储机制,即字符串常量池机制。
为了理解字符串常量池机制,我们首先要了解Java中的引用类型和基本类型的存储方式。在Java中,一个变量的声明为:
int k = 10;//左侧的k被称为句柄,右侧的10被称为值
String a = "hello";//左侧的a被称为句柄,右侧的hello被称为值
其中在等号左边的变量名被称为句柄,右边的被称为值,对于基本变量类型来说,值得大小是不会改变的,无论定什么值,只要在范围内,其大小都是固定的位数,因此,在存储时,基本变量类型的句柄和值在内存上是相邻的,而对于引用类型变量来说,它们的大小并不固定,因为有长字符串也有短字符串,但它们都是字符串类型。因此,对于字符串,就出现了字符串常量池,对于一个字符串,它的真实值存储在一个地方,这个地方和句柄并不相邻,但句柄指向这个地址,根据句柄也可以找到这个值,而当用这个句柄为其他字符串赋值时,不会再新建一个字符串值,而是将这个字符串值的地址直接传送给新句柄,也就是说值通常是唯一的,不同的字符串变量如果值相同,他们有可能指向了同一个值。这个机制在进行字符串拷贝时可能会非常方便,但是如果修改某一个字符串,就会有造成其他字符串被异常修改的风险,因此,不允许修改字符串也有这里的一部分原因。字符串连接池的图示如下:
如图,若存在字符串str = “See you CowBoy Bebop”,我们新建一个字符串类型str1,让str1 = str时,即有如下操作时:
String str = "See you CowBoy Brbop";
String str1 = str;
结果就会如上图所示,即字符串常量池会自动检索str的值,并获取其地址,最终将这个地址赋予给str1,而不是再新建一个名称一样的字符串值赋予str1。同理,如果此时进行这种操作:
String aaa = "Hello world";
那么就会进行这种过程:系统在常量池中检索有没有名为“Hello World”的值,如果有的话就获取其地址,然后将地址发送给aaa句柄,aaa句柄此时直接指向这个地址,也就是说最终常量池会变成这样:
当我们进行一下操作时:
str = "Yuuuwawuuu";
str1 = "hahahahaha";
常量池的变化是这样的:
之前的字符串“See you CowBoy Bebop”将被直接遗弃并进行回收,而不是在它的地址上进行修改。那么,字符串常量池中会有相同值的字符串值吗,也就是说如果我们的程序中存在相同的字符串时,他们的句柄有可能指向不同的地址吗?这是有可能的,如果我们需要重新开辟一块空间来存储相同的字符串,可以使用new语句来强调开辟一片新空间,如下:
String bbb = new String("Hello world");
用这种new语句重新声明的字符串会在常量池中新开辟一片空间进行存取,最终字符串常量池会变得像这样:
综上所述,我们可以得到结论:1.字符串是一种不可变字符串,原因是在我们修改字符串时,并不是在原地址上直接进行修改,因为整个Java的机制不允许字符串的值被修改,我们在修改字符串时的真实操作其实是修改了字符串的句柄的指向,它指向的是一个新的字符串,旧的字符串其实是被舍弃了。整个过程中变化的不是字符串,而是字符串变量指向的地址
2.因为字符串连接池机制的存在,关于字符串的拷贝变得相当快速,但是也存在了一个问题即多个字符串变量指向同一个字符串值,因此,Java机制中禁止对字符串的值进行修改。
3.我们使用字符串拼接以及字符串裁剪得到的新字符串都是创建的新字符串,而不是在原来字符串地址上就行字符串实时修改得到的结果。
4.这种机制导致在字符串修改时效率很慢,但是在Java中很少进行字符串修改的操作,也很少进行字符串拼接的操作,字符串拷贝的操作多一些,因此这种机制还是相当实用的。
1.6 字符串的拷贝
基于上述信息,我们知道字符串拷贝通常是将地址指向新的变量,当我们用“=”进行字符串拷贝时,确实是使用的这种方法,然而字符串中还存在一种拷贝方式,名为深拷贝,它可以确确实实的新建一个区域赋给新字符串变量,相比于深拷贝,用等号进行赋值的方式被称为浅拷贝。但是深拷贝通常是用底层的C++自行实现,Java本身并没有提供直接可用的深拷贝方法。
1.7 字符串比较
字符串的比较通常不能使用“==”来进行比较,“==”比较的是两个字符串变量的地址,值相同的字符串的地址很有可能指向了同一个地址,这时的判断结果似乎没什么问题,而值不相同的字符串值也一定不会在同一位置,然而在字符串常量池中,很有可能同样的字符串值被放在了不同的位置上,这时结果就会异常,比如使用“+”或者substring方法产生的相同字符串就不会和值一样的字符串共享,他们会新建在另一个位置。这里使用equals()方法进行比较,equals()方法会切实的比较二者在字符上时候相同,相同则返回true,不同则返回false。
boolean a1 = str.equals(str1);//此为具体用法,a1是判断结果
1.8 字符串的API
API看上去是一个很高端的词,实际上API就是不带SDK的使用方法说明,它通常是告诉你方法的返回值,形参类型,方法名以及方法能干什么。字符串的常用API如下:
char charAt(int index);
String str a = "Hello world";
char h = a.charAt(3);
获得的h是从a字符串中从0开始数到第三个位置的代码单元,因为这个字符串中不存在辅助字符,因此用这个方法可以获取字符串中任意一个指定字符,在这里获取的是“l”,需要注意的是,这个方法尽量别用,除非你真的对代码单元感兴趣。当字符串中存在辅助字符时,盲目地使用该方法可能会取到一个辅助字符两个代码单元中的其中一个,这时会出现乱码,事与愿违,因为这个方法真的不是在取字符,而是在取代码单元,只不过通常情况下一个代码单元确实表示一个字符。
int codePointAt(int index);
String str a = "Hello world";
int n = a.codePointAt(4);
这是获取码点的方法,可以返回指定位置的码点并将码点值转换成一个int类型的值
int offsetByCodePoints(int startIndex,int cpCount)
该方法可以从第startIndex开始获取往后的第cpCount个代码单元的位置,这样可以精确的得到码点的位置,不会收到双代码单元字符的影响。
注:在实际操作中我发现了这里有些问题,我将在另一篇博文“深入探讨码点与代码单元”中进行说明
String replace(char searchChar, char newChar)
String str = "Ha Ha Ha";
System.out.println(str.replace("a","ei"));
使用后输出结果为”hei hei hei“,此方法将str字符串中的“a”换成了字符串“ei”
int length(String str)
此方法可以获得字符串的长度,或者说码点的个数。
String split(String cstr)
此方法是根据某个子串进行切割,切割完毕后的结果按照每部分一行的方式输出出来。如:
String str = "大家好,我是国服中单李明,我是来自河北石家庄的国服中单选手,我虽然英语不好但不影响我国服中单的练成";
String arr = str.split("国服中单");//将字符串根据等号进行切割
for(String a : arr){
System.out.println(a);
}
输出结果为:
大家好,我是
李明,我是来自河北石家庄的
选手,我虽然英语不好但不影响我
的练成
split方法不是根据某个字符进行切割,而是根据某个子串进行切割,这一点需要注意。
同时,还有一些非常重要的需要经常使用的方法,这里不再列举,直接给出这些方法。
附录:
附录1:
如果我们大家学过操作系统,就会发现内存里边是分页的,而磁盘中也是分页的,CPU往内存中拿数据的时候是按页拿,这个页的大小其实就非常影响系统效率,这是为什么呢?CPU读取数据的时间是非常快的,CPU读一次数据的时间是15到20纳秒,只要一次读取的大小接近或者不太大,时间就可以把握在这个区间里,如果我们分页过小,空间利用效率确实会得到提升,但是相对应的,读取同样大小的文件所需要的次数就会上升,而时间也就随之上升。如果我们将这个页的大小扩大,那么它的读取次数就会少的多,时间也就会缩短。如果需要读一个大文件,那么很快就可以将其读完。如今根据多方面测评,认为4kb的页大小是最为合适的,很多不到4kb的文件其真实大小都是4kb,如图:
我们注意到,对于大小比较小的内存,这个4kb所占的比例比在硬盘中4kb占得比例大的多得多,一般硬盘的大小都要比内存大很多,因此4kb在硬盘中显得更加渺小,这也就导致硬盘的存储更慢,而内存的读取速度更快,在格式化时,我们可以改变硬盘的分区大小,这个分区大小其实改变的就是页大小,这个大小越大,硬盘存取就越快,但是相应的,空间浪费也就越多,内存和硬盘是有性能偏重的,内存更注重速度而硬盘更注重存储。