【Java】一篇文章带你玩转字符串API

(>_< )字符串的常用操作无外乎 :

 

  1. 创建
  2. 切片
  3. 查找
  4. 替换
  5. 分割
  6. 反转
  7. 插入
  8. 删除
  9. 格式化输出
  10. 大小写转换
  11. 比较
  12. 拼接
  13. 优化

★速查表位于文末

 
 

0.创建

有两种方式来创建字符串:

①通过"String"关键字 ; ②"new"一个String对象

直接看代码

String str1 = "loli";
String str2 = "loli";
String str3 = new String("loli");
//我们用String创建了两个;用new创建了一个

它们有什么区别呢?

  此时先不做讨论(在之后涉及到字符串的比较、连接、优化时,再做分析;在接下来要提到的所有字符串操作中,你可以将他们视为完全等同的)
 
 

1.切片

即由"Loli Suki"得到"Loli"、"li"等任意的部分

利用substring方法:

String str1 = "Loli suki !!!";
String str2 = str1.substring(0 , 4);
String str3 = str1.substring(5);

打印出str2和str3 :

Loli		//str2
suki !!! 	//str3

上面用到了substring的两个重载 :①传入两个整型参数;②传入一个整型参数

它们的作用显而易见,分别是:

①返回下标从0到4的部分(不包括4!!
②返回下标从5一直到末尾的部分
 
 

2.查找

indexOf(从前往后找)
lastIndexOf(从后往前找)
直接看例子,显而易见 :

String str = "Loli suki suki suki !!!";
int[] num = new int[3];		//创建数组储存数字,便于打印而已
num[0] = str.indexOf("suki");				//5 (找到了第一个suki)
num[1] = str.lastIndexOf("suki");			//15 (找到了第三个suki)

值得注意的是,无论是从前往后找还是从后往前找,只返回找到的第一个符合的字符串的首字母的下标
 

还有一个很好用的重载方法(多传入一个整形数字) :

//接着上面的代码
num[2] = str.indexOf("suki",6);			    //10 (找到了第二个suki)

即“从下标6开始查找” ;
注 : lastIndexOf也有此重载方法
 
 

3.替换

replace方法
依旧直接看例子 :

String str = new String("Loli suki suki !!!");

System.out.println(str.replace('s' , 'r'));				//"Loli ruki ruki !!!"
System.out.println(str.replace("suki" , "saikou"));		//"Loli saikou saikou !!!"

Q :从上面的这两个输出来看(输出写在注释上),你发现了什么细节呢?

  一是replace会全部替换(例子中所有的 ‘s’ 都被替换成了 ‘r’ )

  二是只能char to char ,或者String to String ;比如,replace( ‘s’ , “saikou” )会报错,根本就没有replace( char , String ) 或者 replace( String , char )这种重载方法

 
Q :当我们只需要替换字符串中出现的第一个"suki"时,该怎么办呢?

  replaceFirst :只替换第一个

  replaceAll :替换全部

  它们的参数均是String,并且能够对其进行解析,也就是说,可以传入正则表达式(regex)
  比如 : str.replaceAll("\\d" , “!”) ,把str字符串中的所有数字替换成"!" ;而replace只会傻乎乎地去寻找“\\d”字符串本身并替换,却无法解析正则表达式。

 

4.分割

split()方法
这是一个典例,通常我们用for语句迭代输出split方法返回的数组

String str = "Lolisuki";
for(String temp : str.split("s")){
	System.out.println(temp);
}

便可以得到 Loli  uki
 
split方法进行切割时,有一些小细节;下面列举出来 :

❶ split()中的参数必须存在且是字符串,这很严格。str.split()会报错;str.split(‘o’)会报错

❷ 精确控制分割后的最大份数,使用重载方法split(String , int);如限制为3份:str.split("" , 3);

❸ 以下作为了解,但必须戒备

  Ⅰ.split方法切割后,会将位于末尾的所有空字符自动舍弃
  Ⅱ.重载方法split(String , int)的第二个(int型)传入一个任意的负数,则标志着阻止自动舍弃
  Ⅲ.对份数的控制(int型传入正数),可以让本会被自动舍弃的空字符被保留;如果份数超过最大份数,则按最大份数切割,也不会报错

验证代码 :

String str = "abbb";
String[] arr1 = str.split("b");
String[] arr2 = str.split("b" , 3);
String[] arr3 = str.split("b" , -1);

System.out.println(arr1.length);		// 1
System.out.println(arr2.length);		// 3
System.out.println(arr3.length);		// 4

输出结果写在注释上了。(一个小tip:3个分隔符会把一个串分割为4份)

最好在纸上手动分割一下,数一数,不难也不麻烦,只是容易遗忘忽视。

 

 

看这里!!!

从这里开始,我们需要一些必要的知识,才能理解接下面的字符串操作 >>>

  ❶除了String有许多API可以操作字符串,还有许多其他类是用来操作字符串的,比如StringBufferStringBuilder ;事实上,由于储存机制、执行效率、特定功能的原因,它们都是极为常用的 ;

  ❷StringBuffer与StringBuilder的API极为相似。下面的代码以StringBuffer为例,列举出了你在创建StringBuffer对象时,几乎所有你可能遇到的场景

String str1 = "FBI! Open the door!!!";
StringBuffer s1 = new StringBuffer(str1);

String str2 = new String("FBI! Open the door!!!");
StringBuffer s2 = new StringBuffer(str2);

StringBuffer s3 = new StringBuffer("FBI! Open the door!!!")

StringBuffer s4 = new StringBuffer().append("FBI! Open the door!!!")

  ❸String类是否有能够进行字符串反转插入删除的函数方法呢?很可惜并没有。因此我们的思路是:创建一个内容与String对象一样的StringBuffer对象,进行操作后再转化回String类型
  (这个思路很基础、很常用、也很重要)

  ❹其实,利用String类的substring,replace等等也是可以实现这些操作的;但我们不必用C语言的思想亲自去写这样的函数为难自己,也不用细致的弄清这些底层实现;用造好的轮子,即现成的方法不香吗?
 
 

5.反转

StringBuffer类的reverse方法

直接看例子,弄清每一行代码在做什么 :

String str1 = "Loli NO.1";

StringBuffer s = new StringBuffer(str1);	// 创建了一个内容与str1一样的StringBuffer对象
s.reverse();								// 反转
String str2 = s.toString();					// 将StringBuffer类型转化为String类型

System.out.println(str2);

中间的三行一步到位 :

String str2 = new StringBuffer(str1).reverse().toString();

 
 
此时,我们注意到了一个之前从来没有想过的问题

String str1 = new String("Loli Saikou !!!");
str1.replace("Loli" , "FBI");					//用"FBI"替换"Loli"(上面讲过的!!!)

StringBuffer str2 = new StringBuffer("Loli Saikou !!!");
str2.reverse();									//字符串反转(刚刚讲过)

System.out.println("str1 : " + str1);		//打印str1
System.out.println("str2 : " + str2);		//打印str2

运行结果可能和你想的不太一样 :

str1 : Loli Saikou !!!
str2 : !!! uokiaS iloL

结果是 : str1中的"Loli"没有被替换 ; 而str2的的确确被反转了

问题的关键在哪里呢?

我们把replace和reverse这个行为单独列成一行,为什么replace看似没有发挥作用呢,而reverse却达到了想要的效果呢?

其实你猜的完全没错。(在本文靠近末尾的位置,会给出详细答案。)

 
 

6.插入

StringBuffer类的insert方法

不必多言 :

StringBuffer str = new StringBuffer("Lolita");
str.insert(1 , "aaa");

打印出str :

Laaaolita

这很简单 : “在下标为1处插入字符串aaa” ;关键是要能想的到对StringBuffer类的使用
 
 

7.删除

StringBuffer的deleteAtdelete方法

deleteAt( int n) :删除下标为n的字符

delete( int a , int b) :删除下标从a到b的字符串(不包括b

也不必多说 :

StringBuffer str1 = new StringBuffer("Lolita");
str1.deleteAt(2);

StringBuffer str2 = new StringBuffer("Lolita");
str2.delete(2,4);

运行后str1与str2的值

str1 : loita
str2 : lota

 
 

4.5分割(续)

  我们已经知道,有许多与字符串相关的类可以对字符串进行特定的操作。下面介绍一个与String类的split方法同样常用的StringTokenizer类及其相关API :

尝试理解下面的代码:

String str = "a bcdef";

StringTokenizer st1 = new StringTokenizer(str);
while(st1.hasMoreElements()){
	System.out.println(st1.nextElement());
}

StringTokenizer st2 = new StringTokenizer(str , "c");
while(st2.hasMoreElements()){
	System.out.println(st2.nextElement());
}

StringTokenizer st3 = new StringTokenizer(str , "ce");
while(st3.hasMoreElements()){
	System.out.println(st3.nextElement());
}

StringTokenizer st4 = new StringTokenizer(str , "ce" , true);
while(st4.hasMoreElements()){
	System.out.println(st4.nextElement());
}

  初次就想理解恐怕有点困难,你只需对这段代码中API的用法感到熟悉即可

  hasMoreElements:“还有未被取出的元素吗?”

  nextElement:“我来取出下一个元素!”

  这些“元素(Element)”就是分割后返回的一个个片段;由于涉及到枚举与迭代机制,不再赘述;你需要做的是先记住此用法,因为在其他类中,它们的组合使用同样常见

大致理解代码思路后,运行结果还是有些出人意料 :

这四小段代码,切割的结果分别是 :
>>> a	 	 bcdef
>>> a b		  def
>>> a b		  d		 f
>>> a b		  c		 d		 e		 f

原因分别是 :

>>> 默认按空格进行分割
>>> 按传入的"c"进行分割
>>> 不是按照"ce"进行分割的!是按照"c""e"两个分割符进行分割的!!!
>>> true意味着,分割符也要返回

把代码、结果、原因对应着多看几遍

另外值得一提的是,split方法会舍弃末尾的空格,而StringTokenizer舍弃了所有的空格

 
 

8.格式化输出

format方法的使用

如果你学过C语言或者Python,那么对接下来的内容应该会感到很熟悉

System.out.println(String.format("%s and %d" , "loli" , 123));
//输出  loli and 123
System.out.println(String.format("%1$s and %2$s and %1$s" , "you" , "me"));
//输出  you and me and you

你也可以直接 :

System.out.format("%s" , "loli");
//输出  loli

第二条需要仔细思忖一下,在纸上画一画对应关系,并不难

 
 

9.大小写转换

直接用String类toUpperCasetoLowerCase方法

String str = "abcDEF";
System.out.println(str.toUpperCase());
System.out.println(str.toLowerCase());
ABCDEF
abcdef

 

 
 

必须理解的重点(Δ)

  在开始时我们提到了两种创建字符串的方法,但没有说明它们的不同,下面着重对字符串创建时的存储位置进行解释

  这涉及到堆(heap)栈(stack)常量池机制,请做好心理准备

复习

  变量有两种:primitive主数据类型引用

  对于String str = “Lolisuki”, 其实质是用一个名称为str的引用指向一个String类型的对象;即str 和"Lolisuki"分别储存在

  引用名称本身储存在上,对象全部储存在

正题
 

▲不使用new,称之为静态创建。

在创建时,会先搜寻常量池中是否有相同的字符串,如果有的话,则该引用指向那个已存在的字符串的地址的复制(可以理解为直接指向了那个已存在的字符串,与另一个引用共用同一个字符串对象),而不会创建新的字符串对象;若没有,则在常量池中生成一个字符串对象,引用指向它。

 

这就是常量池机制。

 
 

▲使用了new:既然是new,它便会不管三七二十一直接在堆中开辟一片新的区域,创建一个新的String对象,并且让引用指向这个对象。

 

以这种方法创建字符串,根本不会有搜寻常量池的动作,不会触发常量池机制。每new一次就是新建一个对象。

 
 

▲关键在何处呢?

关键在于,如果触发常量池机制并使得两个引用指向常量池中的同一个对象,那么它们的地址是完全相同的!

而使用new会创建一个新的对象,它的地址也必定是独一无二,不会与已存在的任何字符串对象的地址相同!

 
接下来的例子很好的解释了上面的文字描述

>>> String str1 = "loli";
>>> String str2 = "loli";
>>> String str3 = new String("loli");

过程:

>>> 未使用new ; 在常量池中未搜寻到内容为"loli"的对象,因此在常量池中创建一个,str1引用指向此对象
>>> 未使用new ; 在常量池中搜存到字符串"loli",不再新建对象,str2引用直接指向此对象
>>> 使用new ; 不触发常量池机制,在常量池外新建一个字符串对象,str3引用指向此对象

图解:

对照图解,再读一遍上面的内容与代码,一切就都能明了了

 
▲常量池机制有什么作用?

  其实从图解很容易看出,常量池中的相同的字符串使用同一个对象,即实现了对象的共享,这不仅节省了内存空间,也避免了频繁地创建和销毁对象而影响系统性能
 
多个引用指向同一个对象,当通过其中一个引用修改了这个字符串对象,其他引用不会出现风险吗?

  事实上,你想通过其中任意一个引用来修改这个字符串(对象),根本就是不可能的String类中没有任何一个方法可以改变字符串(对象)本身。所有的方法都是创建了一个新的对象,对字符串的操作都体现在这个新的对象上(因此,当你没有及时让一个引用指向这个被操作后的对象,这个对象就会因为没有别任何引用指向而被遗弃,它的下场只能是被垃圾处理机制回收)
  而且,String是被final的,也就是说,通过继承来扩展它的特性也做不到。想改变String对象本身,根本不可能。这使得常量池机制绝对安全,即使一个对象被多个引用也没有风险。
  有了以上的知识储备后,在 5.反转 板块的末尾提出那个问题已经有了答案:(请翻到那个问题)
  

  String类的API对字符串的操作,实际上都是新创建一个String对象,并不改变原先的String对象本身

  StringBuffer与StringBuilder类的API方法直接在原先的对象上进行操作,没有新建任何对象

 
 
 

10.比较

如果有其他的高级语言基础,首先想到的会是 “==
我们直接使用上面使用过的代码 :

//创建
String str1 = "Loli";
String str2 = "Loli";
String str3 = new String("Loli");

//比较
System.out.println(str1 == str2);
System.out.println(str1 == str3);

结果还是有些出乎意料 :

str1 == str2  :  true
str1 == str3  :  false

关键在于 : "=="比较的是字符串对象的地址而非内容

在比较地址时,一定注意字符串的创建方法:是否使用new? 是否触发常量池机制?

直接看图,豁然开朗 :

然而,在大多数情境中,我们想要比较的是“内容”。我们希望的是依据内容进行比较使得 “str1 == str3” 返回 true;而不是比较地址,得到 false。

  实际上,如果用"=="进行比较返回了true,则其内容也必定相等。虽说两个字符串内容相同,其地址未必,但地址相同的字符串,必然位于常量池中,内容必定相同。
  "==“比较地址的效率是很高的,比接下来要讲的方法效率都高很多。如果你能保证你创建的字符串都位于常量池,最好就用”=="进行比较。

 
☀ 接下来要介绍的方法,都是对内容进行比较的 ,以何种方式进行创建并不重要 :

首先明确,比较字符串都是从第一个字符开始比较,如果相同就比较二者的第二个字符,依次比较下去直到出现不同
 

① String类的compareTo方法

  compareTo :返回一个int型数字,含义是字符的ASCII码差值

  compareToIgnoreCase :返回一个int型数字,含义是字符的ASCII码差值(忽视大小写)

对照例子一看便知(注意看注释)

String str1 = "A loli";
String str2 = "a loli";
		
System.out.println(str1.compareTo(str2));					//-32  ('A'-'a' = 65-97 = -32)
System.out.println(str2.compareTo(str1));					//32   ('a'-'A' = 97-65 =  32)
System.out.println(str1.compareToIgnoreCase(str2));			//0    (忽视大小写)

② String的equals方法(实际上equals方法继承自Object基类)

  equals :返回一个boolean型,内容相同则 true ,不同则 false

  equalsIgnoreCase :返回一个boolean型,内容相同则 true ,不同则 false(忽视大小写)

接着上面的代码 :

System.out.println(str1.equals(str2));						//false
System.out.println(str1.equalsIgnoreCase(str2));			//true

③ String的regionMatches方法

上面的两种方法虽然返回的类型不同,但都是默认从第一个字符依次比较;而regionMatches方法,可以指定两个字符串各自任意的区域进行比较,非常灵活

regionMatches方法的参数较多,看清楚了 :

String str1 = "Gothic_Loli";
String str2 = "Maid_Loli";

boolean isMatch1 = str1.regionMatches(7, str2, 5, 4); 			//true

一共四个参数,在上面的例子中,其意义分别是 :

7   :str1下标为7的字符开始
str2 :代表与str2进行比较
5   :str2下标为5的字符开始
4   :str1,str2分别从其开始的位置,比较4个字符的长度

还有一种重载方法用于忽略大小的区域比较(在第一个参数的位置传入true):

boolean isMatch2 = str1.regionMatches(true, 7, str2, 5, 4); 			//true

 
 

11.拼接

常见的有 :

连接符"+"

String的 concat 方法

StringBuffer或StringBuilder的 append 方法

直接读代码 :

String str1 = new String("Loli");
String str2 = new String("Suki");

//第一种方法 : 连接符"+"
String str3 = str1 + str2 ;

//第二种方法 : concat
String str4 = str1.concat(str2);

//第三种方法 : append
StringBuilder s5 = new StringBuilder(str1);
s5.append(str2);
打印出来都是 :
LoliSuki
LoliSuki
LoliSuki

对三种方法稍作比较 :

⑴ 连接符"+"能将字符串与多种类型进行连接,也可与int,boolean等连接

⑵ “str3 = str1 + str2"的实质是"str3 = new StringBuilder(str1).append(str2).toString()

⑶ 前两种方法都不是在原有的字符串上进行操作的,而是新建了一个对象并对其进行操作后,紧接着将一个引用指向这个对象

⑷ 第三种方法利用了StringBuilder类,它于StringBuffer类一样,其方法都是对该字符串本身进行操作,而不创建新的对象

⑸ 在进行轻量操作时,"+"简单且可读性强;进行大量字符串的操作时,StringBuffer与StringBuilder有时间和空间上的优势

 
 

11.优化

String类的intern方法

  当一个位于堆上的对象使用intern方法时,会去常量池中寻找相同的字符串,如果未找到,则将其位于堆上的地址拷贝到常量池中(作用相当于这个字符串对象被放到了常量池中);如果找到了,则原引用直接直接指向位于常量池中的对象的地址的拷贝

可以将其效果理解为 :

将堆上常量池外的对象加到了常量池中。

看了下面的代码和图解,就能很好地理解 :

String str1 = "Loli";
String str2 = "Loli";
String str3 = new String("Loli");

System.out.println(str1 == str2);				//true
System.out.println(str1 == str3);				//false
System.out.println(str1 ==str3.intern());		//true

在这里插入图片描述
 
 
 
 
 

★速查表

 

操作API
切片String.substring
查找String.indexOf \ lastIndexOf
替换(Ⅰ)Sting.replace
替换(Ⅱ)String.replaceFirst \ replaceAll
分割String.split
反转StringBuffer.reverse
插入StringBuffer.insert
删除StringBuffer.deleteAt \ delete
大小写转换String.toUpperCase \ tolowerCase
比较(地址)" == "
比较(Ⅰ)String.compareTo \ compareToIgnoreCase
比较(Ⅱ)String.equals \ equalsToIgnoreCase
比较(Ⅲ)String.regionMatches
拼接(Ⅰ)String.concat
拼接(Ⅱ)StringBuilder.append
拼接(Ⅲ)" + "
优化String.intern

 
 

 
 

 
 

※ 后记

0 . 对字符串和数组的操作,是JAVA SE的重要内容
1 . 如有错误,欢迎指正
2 . 转载标明作者、出处

  • 3
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值