(>_< )字符串的常用操作无外乎 :
- 创建
- 切片
- 查找
- 替换
- 分割
- 反转
- 插入
- 删除
- 格式化输出
- 大小写转换
- 比较
- 拼接
- 优化
★速查表位于文末
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可以操作字符串,还有许多其他类是用来操作字符串的,比如StringBuffer,StringBuilder ;事实上,由于储存机制、执行效率、特定功能的原因,它们都是极为常用的 ;
❷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的deleteAt和delete方法
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类的toUpperCase和toLowerCase方法
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 . 转载标明作者、出处