理解String 类
引言
我们都知道在C语言中是没有字符串类型的,C语言中,最常用的就是将字符串放进一个数组中,之后,对数组进行一些操作。而在 Java / C++ 中,有直接表示字符串的类型。在Java 中,String 类型就是表示字符串类型,同时它也是引用类型。比方说:
String str = "abcd";
没错,在 Java 中,字符串就是上述这样定义并初始化的,而 abcd 的末尾没有像 C语言那样有 \0,在 Java 中,里面就是 abcd.
在上面的一行代码中,被双引号引起来的 abcd 就叫做字符串,它是有a、b、c、d 这四个字符组成的,而 abcd 又属于字面值常量,其本质是常量,不可以被更改。
一、创建字符串的方式
1. 直接赋值
程序清单1:
public class Test1 {
public static void main(String[] args) {
String str1 = "hello";
System.out.println(str1);
//输出结果:hello
}
}
2. 通过使用构造方法
程序清单2:
public class Test2 {
public static void main(String[] args) {
String str2 = new String("world");
System.out.println(str2);
//输出结果:world
}
}
3. 通过使用字符数组转换成字符串
程序清单3:
public class Test3 {
public static void main(String[] args) {
char[] chars = {'x','y','z'};
String str3 = new String(chars);
System.out.println(str3);
//输出结果:xyz
}
}
二、字符串的引用变量比较相等
1. 情况一
在程序清单4中,例如(str1 == str2)的这种形式,比较的其实是两个引用的地址,而不是字符串的内容 " hello ",这点需要理解,请往下看:
程序清单4:
public class Test4 {
public static void main(String[] args) {
String str1 = "hello";
String str2 = "hello";
System.out.println(str1 == str2);
}
}
输出结果:
图解上述代码:
String 类的设计使用了共享设计模式:
如果现在采用了直接赋值的模式进行String类的对象实例化操作,那么该实例化对象(字符串内容)将自动保存到这个对象池之中。如果下次继续使用直接赋值的模式声明 String 类对象,此时对象池之中如若有指定内容,将直接进行引用。
如若没有,则开辟新的字符串对象而后将其保存在对象池之中以供下次使用。
2. 情况二
程序清单5:
public class Test5 {
public static void main(String[] args) {
String str1 = new String("hello");
String str2 = new String("hello");
System.out.println(str1 == str2);
}
}
输出结果:
图解上述代码:
3. 情况三
程序清单6:
public class Test6 {
public static void main(String[] args) {
String str1 = "hello";
String str2 = new String("hello");
System.out.println(str1 == str2);
}
}
输出结果:
图解上述代码:
总结:
-
直接赋值:只会开辟一块堆内存空间,并且该字符串对象可以自动保存在对象池中以供下次使用。
-
构造方法:会开辟两块堆内存空间,不会自动保存在对象池中,可以使用intern( ) 方法手工入池。
常量池的一些概念:
运行时常量池:当程序把编译好的字节码文件,加载到JVM中后,会生成一个运行时常量池(方法区),实际上是 Class 文件常量池。
字符串常量池:主要存放字符串常量,本质上是一个哈希表,StringTable,双引号引起来的字符串常量。
三、理解字符串不可变
String 类的内部实现是基于 char[ ] 来实现的,但是 String 类并没有提供 set 方法之类的来修改内部的字符数组。
程序清单7:
public class Test7 {
public static void main(String[] args) {
String str1 = "abc";
System.out.println(str1);
str1 = "xyz";
System.out.println(str1);
}
}
输出结果:
注意:在这里,我们并不是通过把字符串的内容修改了,而是将 [ str1 引用 ] 引用了新的对象。
程序清单8:
public class Test8 {
public static void main(String[] args) {
String str = "hello" ;
str = str + " world" ;
str += " !!!" ;
System.out.println(str);
}
}
输出结果:
同样地,在程序清单8中,我们并不是将 " hello " 中的内容修改了,而是先通过 " hello " 和 " world " 创建了一个新的对象 " hello world ",之后又通过 " hello world " 和 " !!! " 创建了另一个新的对象 “hello world !!!”。对于这样的操作,相当于每次都要通过关键字 new 实例化一个对象。所以,程序清单8是一个不好的示范,对于程序员来说,不应该这么做。
四、字符、字节、字符串、数组
1. 将字符数组转换成字符串
程序清单9:
public class Test9 {
public static void main(String[] args) {
char[] chars = {'a','b','c','d'};
String str = new String(chars);
System.out.println(str);
}
}
//输出结果:abcd
2. 拿到字符串中的连续字符
程序清单10:
public class Test10 {
public static void main(String[] args) {
char[] chars = {'a','b','c','d'};
String str = new String(chars, 1, 2);
System.out.println(str);
}
}
//输出结果:bc
String str1 = new String(char[] chars,int offset, int count);
在 String 构造方法中,其中 offset 表示偏移量,count 表示偏移个数,若 offset 为 1,count 为 2,那么,实现 str1 就从数组下标1开始,往后拿2个元素。当然,我们应该注意不能越界哦。
3. charAt( ) 方法 [ 常用 ]
程序清单11:
public class Test11 {
public static void main(String[] args) {
String str = "abcde";
char ch = str.charAt(2);
System.out.println(ch);
}
}
//输出结果:c
charAt(int index)
index 表示指定位置的索引,字符串第一个字符索引为 0。
同样地,我们应该注意不能越界哦。
4. 将字符串转变成字符数组 [ 常用 ]
程序清单12:
public class Test12 {
public static void main(String[] args) {
String str = "abcde";
char[] chars = str.toCharArray();
System.out.println(Arrays.toString(chars));
}
}
//输出结果:[a, b, c, d, e]
上面四个程序清单说明的是字符数组与字符串之间的一些操作,如果把字符数组换成字节数组,整型数组等等…其对应的思想是一样的。感兴趣的小伙伴可以自己试一下字节与字符串之间的关系。
5. 判断字符串是是由字符构成还是由数字构成
判断一个字符串是否是由字符构成
思路:我们创建一个 judge( ) 方法来判定每个字符是否由字母组成即可,所以我们遍历整个字符串的长度,然后通过下面两行代码来验证每一个字符:
char ch = str.charAt(i);
boolean sign = Character.isLetter(ch);
程序清单13:
public class Test13 {
public static boolean judge(String str){
for (int i = 0; i < str.length(); i++) {
char ch = str.charAt(i);
boolean sign = Character.isLetter(ch);
if(sign == false){
return false;
}
}
return true;
}
public static void main(String[] args) {
String str = "abcde";
System.out.println(judge(str));
}
}
//输出结果:true
当然,我们也可以判定是否每个字符是数字,只不过逻辑需要改变一下,代码如下:
char ch = str.charAt(i);
boolean sign = Character.isDigit(ch);
五、字符串比较
1. equals( ) 方法
查看底层代码,通过 equals( ) 方法比较字符串的内容,返回类型是布尔类型。
程序清单14:
public class Test14 {
public static void main(String[] args) {
String str1 = "hello";
String str2 = new String("hello");
System.out.println(str1.equals(str2));
String str3 = null;
String str4 = "hello";
//System.out.println(str3.equals(str4)); //空指针异常
System.out.println(str4.equals(str3));
}
}
输出结果:
注意,在程序清单14中,str3 和 str4 不能互换位置,因为 str3 这个引用本身不指向任何对象,如果继续使用 str3 的话,会造成空指针异常,我已经通过注释标明出来了。
在程序清单15中,我们对程序清单14做出了一些改变,当我们忽视大小写的时候,可以使用 equalsIgnoreCase( ) 方法。
程序清单15:
public class Test15 {
public static void main(String[] args) {
String str1 = "hello";
String str2 = "HeLLo";
System.out.println(str1.equalsIgnoreCase(str2));
}
}
输出结果:
2. compareTo( ) 方法
当查看底层代码,我们发现 String 类型实现了 Comparable接口,那么String 类就会重写 compareTo( ) 方法,返回值是整型。
接下来,我们通过程序清单16来演示一下 compareTo( ) 方法是怎么使用的。
程序清单16:
public class Test16 {
public static void main(String[] args) {
String str1 = "abc";
String str2 = "ABC";
int ret1 = str1.compareTo(str2);
System.out.println(ret1);
System.out.println("--------------");
String str3 = "abc";
String str4 = "acb";
int ret2 = str3.compareTo(str4);
System.out.println(ret2);
System.out.println("--------------");
String str5 = "abcde";
String str6 = "ab";
int ret3 = str5.compareTo(str6);
System.out.println(ret3);
}
}
输出结果:
在程序清单16中,compareTo( ) 方法是通过对比字符串中的字符一个一个进行比较的,字符对应的 Unicode 编码之差就是返回值( ASCII 码 )。如果两个字符串长度不等,返回的就是字符串长度之差。
六、字符串查找
1. contains( ) 方法
主串通过 contains( ) 方法 调用子串
程序清单17:
public class Test17 {
public static void main(String[] args) {
String str1 = "abcdef";
String str2 = "cde";
System.out.println(str1.contains(str2));
}
}
输出结果:
2. indexOf( ) 方法
主串通过 indexOf( ) 方法 调用子串
如果主串中有子串的数据,那么就返回主串中的索引,即子串对应的第一个字符的位置,如果主串中没有子串的数据,则返回 -1.
程序清单18:
public class Test18 {
public static void main(String[] args) {
String str1 = "abcdef";
String str2 = "cde";
String str3 = "dec";
System.out.println(str1.indexOf(str2));
System.out.println(str1.indexOf(str3));
}
}
输出结果:
3. lastIndexOf( ) 方法
indexOf( ) 方法是在主串中从前往后找子串,lastIndexOf( ) 方法是在主串中从后往前找子串。虽然 lastIndexOf( ) 方法是从后往前找,但是输出的索引下标依然是从前往后算的。只不过,如果两个方法面对重复的字符,会出现不同的结果。 如下所示:
程序清单19:
public class Test19 {
public static void main(String[] args) {
String str1 = "{abcd}}";
String str2 = "}";
System.out.println(str1.lastIndexOf(str2));
System.out.println(str1.indexOf(str2));
}
}
输出结果:
拓展:indexOf( ) 方法 和 lastIndexOf( ) 方法中可以添加第二个整型参数 : ( fromIndex ),表示从某个位置开始找,感兴趣的小伙伴可以自己测试一下。
4. startsWith( ) 方法
字符串通过调用 startsWith( ) 方法,来判断主串的头部是否是我们要输入的数据。传参的时候可以是一个参数,也可以是两个参数。第一个参数可以是字符,也可以是字符串;第二个参数表示的是偏移量,旨在通过某个位置开始判断。
代码如下:
程序清单20:
public class Test20 {
public static void main(String[] args) {
String str1 = "abcdef";
System.out.println(str1.startsWith("a"));
System.out.println(str1.startsWith("bcd"));
System.out.println(str1.startsWith("c",2));
//表示从偏移位置2 往后判断
}
}
输出结果:
5. endsWith( ) 方法
startsWith( ) 方法是从头部开始判断,而 endsWith( ) 方法是从尾部开始判断,思想是相同的,但是 endsWith( ) 方法 在传参的时候,只能传一个参数,即我们要判断的字符或字符串。
程序清单21:
public class Test21 {
public static void main(String[] args) {
String str1 = "abcdef";
String str2 = "cde";
System.out.println(str1.endsWith("f"));
System.out.println(str1.endsWith("bcd"));
}
}
输出结果:
七、字符串替换
1. replace( ) 方法
我们看到下图 replace( ) 方法,在底层的代码,当替换成功的时候,返回的是一个新创建的对象,而需要替换的字符的内容和输入字符的内容相同的时候,说明无须替换,那么这个时候,就返回原来的引用。
这就说明一个问题,在上文中的目录,我提到了一个关键点:字符串的内容不可变,也就是说,当我们表面上好像改变了原先字符串的数据,但实际上底层操作的时候,为我们 new 了一个新的对象,所以说我们看到被替换后的字符串,实际意义上是一个完完整整的一个新串,与原先的字符串没有半点关系。
程序清单22:
public class Test22 {
public static void main(String[] args) {
String str = "abcabeabdabxy";
String ret1 = str.replace('a','z');
String ret2 = str.replace("ab","zz");
String ret3 = str.replace("ab","zzz");
String ret4 = str.replaceFirst("ab","zz");
System.out.println(ret1);
System.out.println(ret2);
System.out.println(ret3);
System.out.println(ret4);
}
}
输出结果:
八、字符串拆分
1. split( ) 方法
我们看到 split( ) 方法底层代码的返回类型是 String[ ] 类型,即一个字符串数组的类型。
程序清单23:
public class Test23 {
public static void main(String[] args) {
String str = "name=Jack&age=23";
String[] strings = str.split("&");
for (String s:strings) {
System.out.println(s);
}
}
}
输出结果:
在我们通过 IDEA 编译器调试之后,我们可以发现字符串数组中下标 0 和 1 放了两个字符串。
我们需要注意的是:如果我们单纯地调用 split( ) 方法,只会分割一次,因为底层代码就是这么实现的,在后面我会提到多次分割。
程序清单24:
public class Test24 {
public static void main(String[] args) {
String str = "name=Jack&age=23";
String[] strings = str.split("=", 2);
for (String s:strings) {
System.out.println(s);
}
}
}
输出结果:
这里我们需要注意下面的代码:在 split( “=”, 2 ) 中的参数2,代表的是按 " = " 分割成两组,而不是均匀分割成两组。
String[] strings = str.split("=", 2);
2. 使用 split( ) 方法 进行二次分割
程序清单25:
public class Test25 {
public static void main(String[] args) {
String str = "name=Jack&age=23";
String[] strings1 = str.split("&");
for (String s:strings1) {
String[] strings2 = s.split("=");
for (String ss: strings2) {
System.out.println(ss);
}
}
}
}
输出结果:
在这里我就不进行调试了,调试的过程中是动态的,感兴趣的小伙伴可以自己她是过后观察。
3. 字符串拆分的特殊情况
对于一些转义字符的特殊情况,我们在给 split( ) 方法传参的时候,需要考虑进去。比方说:在程序清单26中,我们通过字符 " . " 来分割字符串,然而我们不能直接就这么传参了,我们需要通过 ’ \ ’ 转义才能达到我们的目的。
此外,字符 " | ‘’," * " , " + " 等等都得加上转义字符。
程序清单26:
public class Test26 {
public static void main(String[] args) {
String str = "192.168.1.1" ;
String[] strings = str.split("\\.") ;
for(String s: strings) {
System.out.println(s);
}
}
}
输出结果:
4. 按连接符实现多次拆分
在程序清单26中,我们可以通过连接符 ’ | ’ 实现不同分隔符之间的拆分。
程序清单26:
public class Test26 {
public static void main(String[] args) {
String str = "name=Jack&age=23";
String[] strings = str.split("=|&|=");
for (String s:strings) {
System.out.println(s);
}
}
}
输出结果:
九、字符串截取
1. subString( ) 方法
subString( ) 方法的底层代码,可以看到其返回类型是 String 类型。
并且,返回时创建了新的对象。
我们先来展示程序清单27,再来分析代码是如何实现的。
程序清单27:
public class Test27 {
public static void main(String[] args) {
String str1 = "abcdef";
String str2 = str1.substring(2);
String str3 = str1.substring(0);
String str4 = str1.substring(2,5);
System.out.println(str2);
System.out.println(str3);
System.out.println(str4);
}
}
输出结果:
分析程序清单27的代码:
输出 str2 :是从字符串下标索引为2的字符开始往后截取(包括2下标),底层代码 new 了一个新对象。
输出 str3 :字符串下标索引为0的字符开始往后截取,等于拿到了原先的对象,这时候并没有重新创建对象。
输出 str4 :subString( ) 方法遵循左闭右开,左边下标索引为2可以取到字符,右边下标索引为5取不到字符。同时,底层代码 new 了一个新对象。
十、其他的一些字符串操作
1. trim( ) 方法
trim( ) 方法 用来除去字符串两边的空格,代码如下:
程序清单28:
public class Test28 {
public static void main(String[] args) {
String str1 = " abc xyz ";
System.out.print(str1);
System.out.println("------------");
String str2 = str1.trim();
System.out.print(str2);
System.out.println("------------");
}
}
输出结果:
2. 将字符串中的字符转换成大写 / 小写 [ 常用 ]
toUpperCase( ) 方法 和 toLowerCase( ) 方法
程序清单29:
public class Test29 {
public static void main(String[] args) {
String str1 = "abcdWXYZ123";
String str2 = str1.toUpperCase();
String str3 = str1.toLowerCase();
System.out.println(str2);
System.out.println(str3);
}
}
输出结果:
3. 拼接字符串
程序清单30:
public class Test30 {
public static void main(String[] args) {
String str1 = "abcd";
String str2 = "wxyz";
String str3 = str1.concat(str2);
System.out.println(str3);
}
}
输出结果:
4. 求字符串长度 [ 常用 ]
求字符串长度并不难,而我想说的是,这里需要区分:字符串求长度的格式 str.length( ) 和 数组求长度的个数的格式 arr.length。注意点:一个有括号,一个没括号。
程序清单31:
public class Test31 {
public static void main(String[] args) {
String str = "abcde";
System.out.println(str.length());
int[] arr = {1,2,3,4,5};
System.out.println(arr.length);
}
}
输出结果:
5. 判断字符串是否为空
程序清单32:
public class Test32 {
public static void main(String[] args) {
String str1 = "abcd";
String str2 = " ";
String str3 = "";
System.out.println(str1.isEmpty());
System.out.println(str2.isEmpty());
System.out.println(str3.isEmpty());
}
}
输出结果:
十一、理解引用
程序清单33:
public class Test {
public void change(String s, char[] ch){
s = "world";
ch[0] = 'g';
}
String str = new String("hello");
char[] chars = {'a', 'b', 'c'};
public static void main(String args[]){
Test test = new Test();
test.change(test.str, test.chars);
System.out.println(test.str);
System.out.println(test.chars);
}
}
输出结果:
这一题在我第一次做的时候,自己认为输出结果会是下面代码:
//错误答案
world
gbc
后来,我才想明白,当我们传参的时候,传了一个是 String 类型,一个是 char[ ] 类型,这两者在传参的时候传的其实都是引用类型。(这其实就是实参和形参之间对应的关系,通过改变形参并不影响传入的实参状态)
说白了,就是把对象的地址传过去了。那么在 change( ) 方法接收的时候,等于拿到了两者的地址。后来,我们改变了引用变量 s ,让它指向了 " world “,那么就不再指向原先的” hello ",所以最终引用 s 与主函数中的引用 str 没有半毛钱关系。而引用 ch 就不同了,它是通过 ch[0] 直接访问到原先数组,通过改变数组下标为 0 的元素值,这会破坏原先数组的结构,所以最终数组元素变成了 [ gbc ]。 下图辅助理解:
十二、StringBuilder 类 和 StringBuffer 类
我们查看底层代码,发现 StringBuilder 类 在底层实现的时候,调用了 append( ) 方法,在 append( ) 方法中返回的是原先的对象,并没有重新 new 一个新的对象。append( ) 方法在底层代码实现的作用就是:可以拼接字符串,然后一次一次地将字符串放入原先的对象中。
我们查看底层代码,发现 StringBuffer 类 在底层实现的时候,和上面的 StringBuilder 类很相似,唯一不同的就是 StringBuffer 类多了一个限定符 synchronized,这表示 StringBuffer 采用了同步处理,属于线程安全操作,会使程序执行起来更加安全,当然也会使程序执行起来相对 StringBuilder 于更慢一点。
在介绍过 StringBuilder 类 和 StringBuffer 类 两者对应的底层代码之后,因为两者很多地方是相似的,那么我直接以 StringBuilder 类 来演示一些操作字符串的方法。
1. append( ) 方法
append( ) 方法可以用来拼接很多不同类型的变量放入一个 StringBuilder 类的变量中,我们先来看一下编译器中 append( ) 方法,可以拼接的东西应有尽有!
程序清单34:
public class Test34 {
public static void main(String[] args) {
StringBuilder str = new StringBuilder("abc");
System.out.println(str);
System.out.println("----------------");
str.append("opq");
System.out.println(str);
System.out.println("----------------");
str.append("xyz");
System.out.println(str);
}
}
输出结果:
在程序清单34中,我们可以发现 append( ) 方法在放字符串的时候,每次都放入了 str 中,那么实际上被改变的就是原先对象中的字符串内容。
2. 字符串逆置
程序清单35:
public class Test35 {
public static void main(String[] args) {
StringBuilder str = new StringBuilder("abcde");
System.out.println(str);
System.out.println(str.reverse());
}
}
输出结果:
3. StringBuilder 类 和 StringBuffer 类 的其他一些方法
在程序清单36中,通过 delete( ) 方法,我们删除对应索引下标的字符,遵循左闭右开原则。
程序清单36:
public class Test36 {
public static void main(String[] args) {
StringBuffer str = new StringBuffer("abcdefg");
System.out.println(str);
System.out.println(str.delete(1,4));
}
}
输出结果:
在程序清单37中,通过 insert( ) 方法,我们在对应的索引下标处添加对应的数据。
程序清单37:
public class Test37 {
public static void main(String[] args) {
StringBuffer str = new StringBuffer("abcde");
System.out.println(str);
System.out.println(str.insert(0, "你好"));
System.out.println(str.insert(2, "hello"));
}
}
输出结果:
4. String、StringBuffer、StringBuilder 三者的区别
① String的内容不可修改,StringBuffer与StringBuilder的内容可以修改
② StringBuffer与StringBuilder大部分功能是相似的
③ StringBuffer采用同步处理,属于线程安全操作;而StringBuilder未采用同步处理,属于线程不安全操作
5. String 和 StringBuffer、StringBuilder 之间的转换
在这里,我们需要理解一点:String 表示 String 类,而 StringBuilder 是另一种类,StringBuffer 同样也是另一种类,这三者性质不同,但是可以互相转换。
在程序清单38中,我演示了两种转换形式:
① StringBuilder / StringBuffer 类 =》String 类
调用 toString( ) 方法
② String 类 =》StringBuilder / StringBuffer 类
利用 StringBuilder / StringBuffer 的构造方法
程序清单38:
public class Test {
public static String transform1(){
StringBuilder strb = new StringBuilder();
strb.append(111);
strb.append("hello");
return strb.toString();
//return strb; //error
}
public static StringBuilder transform2(){
String str = "world";
return new StringBuilder(str);
//return str; //error
}
public static void main(String[] args) {
System.out.println(transform1());
System.out.println(transform2());
}
}
输出结果:
十三、其他类型与字符串类型之间的转换
1. 整型转字符串
程序清单39:
public class Test {
public static String transform1(){
return Integer.toString(123);
}
public static String transform2(){
return Character.toString('c');
}
public static void main(String[] args) {
System.out.println(transform1());
System.out.println(transform2());
}
}
输出结果:
在上面,我演示了 toString( ) 方法
当然,我们也可以使用 String.valueOf ( ) 方法,此外,我们可以进行转换的类型有很多很多。
2. 字符串转整型
在下面的程序中,我先将整个字符串分割为一个数组元素中的三个元素,我将数组的第一个元素转换成了整数,使用了【 Integer.parseInt() 】方法,后两个元素直接赋值给了字符串。后来我又将刚刚数组的第一个元素转换成了字符串,通过给其加上一个空字符即可。
程序清单40:
public class Test {
public static void main(String[] args) {
String str = "123;opq;xyz";
String[] result = str.split(";");
for (String s : result) {
System.out.print(s +" ");
}
int a = Integer.parseInt(result[0]);
String b = result[1];
String c = result[2];
System.out.print("\n"+a +" ");
System.out.print(b +" ");
System.out.print(c +" \n");
System.out.println("------------------");
String str2 = result[0] + "";
System.out.println(str2);
}
}
输出结果: