在实际开发过程中,程序员往往要对字符串进行比较内容、搜索、替换、截取等操作。在String类中,针对这些常用的操作都定义了相应的方法,本小节将详细讲述字符串的常用操作。
9.2.1 比较字符串的值
本小节所讲述的字符串比较是一个广义的概念,例如,判断字符串是否以某个子字符串开头或结尾的方法也被认为是字符串比较。
在9.1小节曾经讲过:不能用==运算符判断两个String类对象的值是否相同。如果想判断两个字符串的值是否相同,需要调用equals()方法。这个方法的返回值是一个boolean型数据,如果两个字符串值相同,就返回true,否则返回false。equals()方法本身很好理解,但需要注意两个细节:
首先,equals()方法是在Object类当中就已经定义了的方法,Object类在定义这个方法时把参数定义成了Object类型,而String类当中的equals方法是重写了Object类当中的那个equals,所以equals()方法的参数的类型仍然是Object类型,因此,在实际编写代码的时候,可以用任意类型的对象当作equals()方法的参数。
其次,只有在参数为String类型、并且参数的内容与当前字符串的值完全相同的情况下,方法运行才会返回true,其他情况一律返回false。此处所说的“完全相同”,是指连字母的大小写都要全部相同,只有在这种情况下方法才会返回true。如果给equals()方法传递的参数是空,这时方法运行并不会抛出异常,而是直接返回false。
由于equals()方法只有在参数是String类对象时才能对其值是否相同进行判断,但实际开发过程中,有时会用StringBuffer和StringBuilder这两个类来表示字符串,在这种情况下无论两个字符串的值是否相同,方法都直接返回false。为了能在参数不是String类对象的情况下仍然能够判断两个字符串的值是否相同,String类专门提供了一个contentEquals()方法,这个方法能够在参数是StringBuffer和StringBuilder这两个类对象的情况下判断字符串的值是否相同。这里必须强调一个细节:contentEquals()方法在参数为空的情况下会抛出空指针异常,因此在调用这个方法时还需要对参数是否为空进行判断。
在做字符串值比较的时候,有时希望能忽略字符串当中字母大小写的差别。例如,很多程序在判断用户所输入的验证码是否正确时,就往往忽略大小写的差别。想实现做忽略大小写的比较,可以调用String类的equalsIgnoreCase(),这个方法在比较字符串时完全忽略字母的大小写。但使用这个方法时也需要注意:equalsIgnoreCase()方法只能接收String类的对象作为参数,如果参数为空,方法并不会在运行时抛出异常,而是直接返回false。下面的【例09_02】展示了如何使用各种方法判断字符串的内容是否相同。
【例09_02 比较字符串内容】
Exam09_02.java
String类当中还定义了一个compareTo()方法,这个方法能够按字典顺序比较两个字符串。这个方法在运行时,会对两个字符串相同位置上的每一对字符进行比较,当第一次出现不同字符时,就计算这两个字符的编码差值并且返回计算结果。compareTo()方法的工作原理不好理解,下面通过【例09_03】为读者讲解compareTo()方法的作用。
【例09_03 按字典顺序比较字符串】
Exam09_03.java
【例09_03】的运行结果如图9-3所示:
图9-3 【例09_03】运行结果
【例09_03】中,第1次进行比较的两个字符串是s1和s2,这两个字符串的前两个字符完全相同,不相同的字符第一次出现是在第3个字符的位置。compareTo()方法就会计算这s1和s2第3个位置上字符的编码差值。s1的第3个字符是k,s2的第3个字符是d,k的字符编码是107,d的字符编码是100,107与100的差就是7,因此第一行输出语句输出结果是“第1次比较结果:7”。
第2次进行比较的两个字符串是s1和s3,这两个字符串的第1个字符相同,当继续向后查找时,却发现s3已经没有后面的字符了,这样就无法找到两个字符串当中第一个不同的字符。在这种情况下compareTo()方法会计算这两个字符串长度的差值。s1的长度为4,s3的长度为1,它们的长度差为3,因此会在控制台上输出“第2次比较结果:3”
第3次进行比较的两个字符串是s1和s4,这两个字符串第1个字符就不相同,虽然它们的第一个字符是同一个字母,但按照字符编码规则来讲,m和M的编码值并不相同,m的编码值为109,而M的编码为77,差值为32,因此控制台上第3条输出语句的输出结果是“第3次比较结果:32”。
第4次进行比较的两个字符串是s1和s5,这两个字符串完全相同,没有任何差异,因此它们的比较结果为0。
从【例09_03】可以看出,compareTo()方法在对字符串进行比较时会区分字母的大小写,如果希望在比较时忽略字母的大小写,可以调用compareToIgnoreCase()方法进行比较。需要注意:compareTo()和compareToIgnoreCase()这两个方法在参数为空时候都会出现异常,因此在调用这两个方法的时候需要提前判断参数是不是为空。
有时候不需要对整个字符串的内容进行判断,可能只是判断字符串的部分内容。例如有时需要判断一个字符串是不是以某个字符串开头或者是结尾,这时候可以调用startsWith()和endsWith()方法来完成判断。startsWith()方法用来判断当前字符串是不是以某个字符串开头,而endsWith()方法用来判断当前字符串是不是以某个字符串结尾。需要注意:这两个方法在判断过程中都会区分字母的大小写。另外,startsWith()还能设定以字符串的什么位置作为开头,例如:
字符串中的每个字符的下标是从0开始计数的。这条语句中,“internationalization”虽然不是以“er”开头,但如果把下标为3的位置作为开头,那么这个字符串就是以“er”开头的,因此上面的语句运行结果为true。下面的【例09_04】展示了如何使用这两个方法判断字符串的开头和结尾。
【例09_04 判断字符串的开头和结尾】
Exam09_04.java
【例09_04】比较简单,读者可以自行运行案例代码并观察运行结果,仔细体会startsWith()和endsWith()这两个方法的作用。
9.2.2判断空字符串和空白字符
不包含任何字符的字符串被称为空字符串,需要注意:空字符串和空对象(null)并不是一个概念,因为空字符串本质上也是一个对象。实际开发过程中,经常会判断某个字符串是不是空字符串,例如:用户在完成注册操作时,如果在用户名一栏中没有填写任何信息,那么计算机接收到的用户名就是一个空字符串。程序必须能够判断出它是一个空字符串并且向用户发出相应的提示信息。
一个字符串的长度,是指组成字符串的字符的个数。空字符串中不包含任何字符,所以空字符串的长度为0。程序员可以根据字符串的长度是否为0来判断这个字符串是不是空字符串。获取字符串长度的可以调用String类的length()方法,这个方法的返回值就是字符串的长度值。
实际上,String类还提供了一个专门判断空字符串的isEmpty()方法,这个方法返回一个布尔类型的结果,如果是空字符串,方法返回true,否则返回false。但实际开发过程中,判断空字符往往不用length()方法和isEmpty()方法。如果有一个String类对象str,判断它是不是空字符串的代码往往会写成:
之所以不用length()方法和isEmpty()方法来判断str是不是空字符串,是因为str有可能是一个空对象(null),直接调用它的方法可能会出现空指针异常。而代码中的“”就是一个String类对象,它是一个空字符串,但并不是空对象(null),因此调用这个对象的equals()方法一定不会产生空指针异常。即使str是空对象,equals()方法也只是会返回false而不会抛出异常。
有的时候,除了要判断空字符串还要判断一个字符串是不是全部都由空白字符组成。所谓空白字符是指那些在文本编辑工具中看不见的字符,主要包括:空格、制表符、换行符等等。之所以要判断空白字符是因为在某些场景下,全部由空白字符组成的字符串属于不合理字符串,例如很多网站都不允许把用户名注册为一个或多个空格组成的字符串。String类中的isBlank()方法就能够判断字符串是不是全部由空白字符组成,它的返回值也是boolean型数据,如果返回值为true表示该字符串全部由空白字符组成,返回false表示该字符串中除空白字符以外还有其他字符。下面的【例09_05】演示了length()、isEmpty()和isBlank()这三个方法的作用:
【例09_05判断空字符串和空白字符】
Exam09_05.java
9.2.3搜索字符串
搜索字符串是指从某个字符串中搜索特定的内容。String类中有一个contains()方法,这个方法用来判断字符串当中是否包含了参数所指定的字符串。contains()方法的返回值是boolean型,返回值为true表示字符串中包含参数指定的字符串,返回值为false表示不包含。contains()方法区分字母大小写,因此在字符串“abc”中用contains()方法判断是否包含“AB”所得到的运算结果为false。此外,contains()方法并不要求参数必须是String类型的对象,StringBuffer和StringBuilder类所表示的字符串也可以被用来判断是不是在字符串中。
有时候程序员不仅仅要判断字符串当中是否包含哪些内容,还希望知道被搜索内容在字符串中的位置。String类的indexOf()方法可以获得要搜索的内容在整个字符串当中的下标,需要注意这个下标是从0开始计算的。如果一个字符是“abcde”,而被搜索的内容是“bcd”,其中字母b在字符串中的下标是1,c的下标是2,d的下标是3,在这种情况下indexOf()方法会返回被搜索内容当中第一个字符在整个字符串当中的下标,因此“abcde”.indexOf(“bcd”)的运算结果为1。如果被搜索的内容不在字符串中,indexOf()方法的返回值为-1。假如被搜索的内容在字符串中出现了多次,那么indexOf()方法只返回被搜索内容第一次出现的位置。
其实,indexOf()方法不仅仅能搜索字符串,还能搜索单个字符,只要把单个字符本身或它的字符编码当作参数传入到indexOf()方法中就可以。但indexOf()方法却不能搜索以StringBuffer或StringBuilder表示的字符串。
通常来讲,搜索操作都是从字符串的开头进行的。如果程序员希望从某个字符串中间的某个位置开始搜索,就需要调用indexOf()方法的另外一个重载版本,这个重载版本有两个参数,第一个参数是被搜索的内容,第二个参数就是开始搜索的位置,它的返回值仍然是被搜索内容在整个字符串当中的位置。
前文讲过,indexOf()方法总是返回被搜索内容在字符串中第一次出现的位置,如果希望知道被搜索的内容最后一次在字符串中出现的位置,可以调用一个叫做lastIndexOf()的方法,这个方法在对字符串进行搜索时,是从右向左反向进行的。lastIndexOf()的方法也可以指定搜索的起始位置,但无论是否指定搜索的起始位置,lastIndexOf()方法总是按从右向左的顺序进行搜索。下面的【例09_06】展示了contains()、indexOf()和lastIndexOf()方法的作用。
【例09_06 搜索字符串】
Exam09_06.java
9.2.4替换子字符串
所谓“子字符串”就是字符串中一部分连续字符所组成的字符串,例如有字符串“abcde”,而“bcd”是它的一部分连续的字符,因此“bcd”就是“abcde”的子字符串。替换子字符串,就是把一个字符串当中的子字符串替换成其他字符串的操作。
第一个替换子字符串的是replaceFirst()方法,这个方法有两个参数,它在执行过程中会搜索第一个参数所指定的子字符串,当第一次找到这个子字符串时,就会把它替换为第二个参数所指定的字符串,但后面如果再次出现相同的子字符串时就不再进行替换。下面的【例09_07】展示了replaceFirst()方法的特性。
【例09_07 replaceFirst()方法的使用】
Exam09_07.java
【例09_07】的运行结果如图9-4所示:
图9-4 【例09_07】运行结果
从图9-4可以看出,str1当中第一个“girl”被替换为“baby”,但第二个“girl”并不会被替换。在语句①中,原本希望把str2中的第一个“.”替换为“*”,但并没有达到替换效果,方法运行的结果是把str2中的第一个字符替换成了“*”。这是因为replaceFirst()方法在运行过程中,被替换的子字符串可以不是一个固定的值,它只要符合一个正则表达式定义的格式就可以。“.”在正则表达式中有特殊意义,它代表任意一个字符,所以在replaceFirst()方法执行时,就把str2当中的第一个字符替换成了“*”。关于正则表达式的内容将在本书后面的章节中进行讲解。
如果希望把str2当中的第一个“.”替换为“*”,就需要用转义字符来代替,在语句②中,把replaceFirst()方法的第一个参数写成了转义字符“\\.”,这个转义字符就代表普通的字符“.”而不是正则表达式中具有特殊意义的符号,因此在第三条输出语句的输出结果中,str2的第一个“.”被替换为“*”。
假如希望把一个字符串中所有满足条件的子字符串都替换为其他字符串,就需要调用replaceAll()方法来完成替换操作。replaceAll()方法与replaceFirst()方法一样,都会把被替换的子字符串当作正则表达式来看待,所以如果希望替换str2中的“.”,在书写参数时也要写成“\\.”。实际上,在String类的很多方法中都会把参数中的字符串看作正则表达式而不是其字面含义。如何分别一个方法把那些参数当作了正则表达式呢?在IDEA中,把鼠标悬停到这个方法之上,如果看到一个参数是String类型的,并且它的名称是regex,那么这个参数就会被当作一个正则表达式。
很多初学编程的读者都不了解正则表达式,不知道哪些字符在正则表达式中有特殊意义。为了解决这个问题,String类又提供了一个replace()方法,在这个方法当中,被替换的子字符串就被看作一个普通的字符串,而不是把它当作一个正则表达式。下面的【例09_08】展示了replaceAll()与replace()这两个方法的不同特性。
【例09_08 替换子字符串】
Exam09_08.java
【例09_08】的运行结果如图9-5所示:
图9-5 【例09_08】运行结果
从图9-5中可以看出:replaceAll()方法把第一个参数当作了正则表达式,所以当第一个参数为“.”时,会替换字符串中所有字符,因为“.”在正则表达式中代表任意一个字符。而replace()方法则没有这个特性,它会把第一个参数“.”当作一个普通的字符串,因此不需要把“.”以转义字符的形式书写。此外从程序中的语句① 和② 也可以看出,replace()方法还可以接收char型、StringBuffer以及StringBuilder类型的数据作为参数。此处再次强调:String类是不可变类,所以上述三个方法都不是对原String对象做出修改,而是创建出了一个新的String对象。
9.2.5截取字符串
截取字符串就是按照一定的规则,从一个完整的字符串当中截取出其中的一部分的操作。String类的substring()方法是实际开发过程中最常用的一个截取字符串的方法,它可以截取出参数所指定位置的一个字符串。substring()方法有两个重载版本,第一个版本有两个int型的参数,这两个参数分别表示截取操作在字符串中的开始位置和结束位置。第二个版本只有一个int型参数,它表示截取操作在字符串中的开始位置,由于这个版本的方法不能设定截取的结束位置,所以虚拟机在做完成截取操作时,会从开始位置一直截取到字符串的末尾。
在String类中,还定义了一个与substring()相类似的方法,它就是subSequence()方法,这个方法没有重载版本,它也有两个int型参数,这两个int型参数也分别代表了截取操作的开始和结束位置。但与substring()方法不同,subSequence()方法的返回值类型不是String,而是CharSequence。下面的【例09_09】展示了substring()和subSequence()这两个方法的执行效果。
【例09_09 截取字符串】
Exam09_09.java
【例09_09】的运行效果如图9-6所示:
图9-6 【例09_09】运行效果
在【例09_09】的语句①中,为substring()方法设定的参数为3和5,表示从下标为3的位置开始截取,一直截取到下标为5的位置。字符串str下标为3的字符是k,而下标为5的字符是J,但从图9-6可以看出,substring()方法只截取了“ke”,下标为5的字符J并没有被截取下来。因此,substring()方法的第二个参数实际上表示截取到此位置之前,但不包含此位置的字符。而语句②中的subSequence()方法也具有相同效果。在语句③中,只给substring()方法传递了一个参数,这个参数就是开始截取操作的位置,在这种情况下,substring()方法在进行截取操作时会从开始位置把整个字符串剩余部分全部截取下来。另外还需要强调:如果给这两个方法传递了超出范围的参数,将会引发“字符串下标越界异常”,例如,如果给语句①中的substring()方法传递的参数是20和30,由于这两个下标值都超出了合理下标范围,程序运行将会出现异常。
有的时候,程序员需要单独截取出一个字符串上特定位置上的字符,这种操作也可以被看作截取字符串。完成这个操作可以调用charAt()方法,这个方法的参数就是要提取字符的下标,例如:
这条语句就能从字符串“abcde”中截取出下标为2的字符c。
9.2.6去除空白字符
实际开发过程中,有时需要把字符串两端的空白字符去除掉。String类定义了一个trim()方法,这个方法能够去掉字符串两端的空白字符。但是需要强调:trim()方法不能去除位于字符串中间的空白字符。
trim()方法有一定的局限性,它仅能去除字符串两端的半角空白字符,为了弥补这个缺陷,JDK11开始又增加了strip()、stripLeading()和stripTrailing()这三个方法。这三个方法都能去除半角及全角的空白字符,其中strip()方法能够去掉字符串两端的空白字符,stripLeading()方法能够去除字符串左端的空白字符,而stripTrailing()方法则能够去除字符串右端的空白字符。下面的【例09_10】展示了这些去除空白字符方法的执行效果。
【例09_10 去除空白字符】
Exam09_10.java
【例09_10】中,首先打印了str这个字符串,之后又打印了调用各种去除空白字符方法的结果。因为str两端都包含空白字符,为了便于观察输出结果,在进行打印操作时都在字符串左右两端添加了几个#号。【例09_10】的运行结果如图9-7所示:
图9-7 【例09_10】运行结果
从图9-7中可以看出:这些方法都不能去掉位于字符串中间的空白字符。
9.2.7拆分字符串
拆分字符串就是把一个完整字符串拆分成多个子字符串的操作。拆分操作往往不是随意进行的,一般情况下都会以某些特定字符作为分割符对字符串进行拆分。String类定的split()方法就能完成字符串的拆分,它在进行拆分操作时会以参数指定的分割符对原字符串进行拆分。当一个字符串被拆分成好几部分后,会用一个数组来存放每一个子字符串。下面的【例09_11】展示了split()方法的执行效果。
【例09_11 split()方法的使用】
Exam09_11.java
例子中字符串str的内容是一首古诗,古诗总共有四句,每一句之间都有一个#号。语句①调用了str的split()方法,并且给方法传递的参数是“#”,这表示说要以#号为分割符对字符串进行拆分。被拆分出来的各个部分会存放到数组中。【例09_11】的运行结果如图9-8所示:
图9-8 【例09_11】运行结果
通过图9-8可以看到:字符串被拆分成4部分,而每个部分中都不包含分割符,这说明分割符只是用来标记在字符串的什么位置进行拆分,而它自身并不属于任何一个部分。假如两个#号分割符就挨到了一起,在这种情况下,分割符之间其实就是一个空字符串,经过拆分之后,这个空字符串也会被当作一个元素存放到数组当中。如果字符串中不包含参数所指定的分割符,例如以“@”作为分割符对字符串str进行拆分,这种情况下无法对字符串进行拆分,但split()方法运行仍然会产生一个数组,这个数组中只有一个元素,就是str这个字符串自身。实际上split()方法还有另外一个版本,这个版本有两个参数,第一个参数仍然是用来指定分割符,而第二个参数指定字符串最多被拆分成几段。在拆分过程中,会按照分割符的顺序依次进行拆分,如果拆分出来的子字符串数量已经达到参数所指定的数量,就停止拆分。例如把例子中的语句①改为如下形式:
以上语句中为split()方法设置的第二个参数为2,在这种情况下,str会被拆分成两段,程序的运行结果如图9-9所示:
图9-9 指定字符串拆分为两段
从图9-9可以看出,由于字符串被指定最多拆分成两段,那么在第二段字符串中仍然会保留分割符#号,因为它们原本就是str字符串的一部分。有读者可能不理解为什么字符串“最多”被拆分成两段,而不是“一定”被拆分成两段?这是因为如果设定了其他分割符,例如以@作为分割符,str这个字符串只能被拆分成一段,无法被拆分为两段,因此是“最多”而不是“一定”。
需要注意:split()方法的第一个参数,也就是指定分割符的这个参数是一个正则表达式,所以读者在给这个参数传递值的时候,要传递一个正则表达式,如果需要用正则表达式当中的特殊字符来当作分割符,则需要使用转义字符的形式来书写。
9.2.8拼接字符串
在Java语言中,使用加号就能完成String类对象的拼接操作。实际上String类还提供了专门用于拼接的方法。第一个能够完成拼接的是concat()方法,这个方法的作用是把参数所指定的字符串连接到当前字符串的末尾。例如:
这条代码能够把“hello”和“world”拼接成“helloworld”。concat()方法与使用加号拼接字符串在性能上有一定区别。首先,concat()方法只能接受String类型的对象作为方法的参数,而使用加号连接字符串,可以把一个String类对象与任何类型的数据相连接。其次,如果concat()方法的参数为空对象,方法执行时会产生空指针异常,而使用加号去连接字符串的时候,加号的左右两边都可以是空对象,不会产生异常。基于以上两个原因,笔者不推荐大家在实际开发过程中使用concat()方法,而是直接使用加号进行字符串的拼接。
concat()方法只能完成两个String类对象的拼接,如果希望把多个字符串拼接起来,可以调用join()方法。join()方法是一个静态方法,它有两个参数,第一个参数用来指定连接符,第二个参数是一个可变参数,程序员用第二个参数来指定被连接的多个字符串。下面的【例09_12】展示了concat()和join()方法的执行效果。
【例09_12 字符串拼接】
Exam09_12.java
【例09_12】的运行结果如图9-10所示:
图9-10 【例09_12】运行结果
程序中的语句①调用了join()方法连接字符串,在做连接操作时以“-”作为连接符,因此各个字符串在拼接时中间都会加上一个“-”。如果希望各个字符串在连接时中间不添加连接符,可以用空字符串作为join()方法的第一个参数。另外,从程序中可以看到:join()方法不仅仅能连接String类对象,还可以连接StringBuffer或StringBuilder对象。
9.2.9字符串的其他操作
程序中有时会需要对字符串进行大小写的转换操作。如果需要把字符串中的字母全部转换为大写形式,可以调用toUpperCase()方法,反之,如果希望把字符串中的字母全部都转换成小写形式,就可以调用toLowerCase()方法。下面的【例09_13】展示了如何对字符串的大小写进行转换。
【例09_13字母大小写转换】
Exam09_13.java
【例09_13】非常简单,各位读者可以自行运行程序并观察运行结果。
我们都知道:字符串是由多个字符组成的,因此字符串可以被转换为字符数组。调用toCharArray()方法就可以把字符串转换成一个字符数组,例如:
以上这条语句能够让字符串“abcde”中的所有字符组成一个char型数组。实际开发过程中,有时还需要把其他类型的数据转换为字符串。无论是基础类型数据还是引用类型数据,都可以调用String类的valueOf()静态方法将其转换成String类对象。除此之外,valueOf()方法还能把字符数组转换为String类对象,并且在转换时还能指定用数组中的哪一部分元素进行转换。例如:
在这条语句中,valueOf()方法的第一个参数是一个字符数组,表示要把这个字符数组的一部分转换成String对象。第二个参数是2,表示从下标为2的位置开始转换,第三个参数是3,但这个参数并不表示转换到下标为3的位置结束,而是表示把数组中3个连续的字符转换为String对象。因此,以上代码的运行结果为“llo”
实际上String类还专门定义了一个把char型数组转换为String类对象的copyValueOf()方法,这个方法既可以把整个数组都转换为String类对象,也可以只转换数组的一部分。下面的【例09_14】展示了如何把其他类型的数据转换为String类对象。
【例09_14 其他类型数据转换为String类对象】
Exam09_14.java
本文字版教程还配有更详细的视频讲解,小伙伴们可以点击这里观看。