目录
String基本概念
字符串的不可变性
众所周知,String的特点是不可变,所以为什么是不可变呢?怎么实现的不可变呢?
来看它的源码:
可以看到,我们平时 String str = "abc",其中的"abc"都是存储于String内的数组中的,但是String并不是使用它完成“不可变”这一特点的,String的不可变是因为没有对外提供改变内部数组的方法,并且由于String是被final修饰的类,无法被继承/实现,那么也就无法通过子类去修改它。
所以它的不可变特点是如何完成的?
答案是:内部字符串数组不可达。
我们对str操作时,总会感觉改变了str的值,其实你只是把str换了个指向罢了
str1 = "abc";
str1 = "def";
你只是把str1从指向"abc"变成了指向"def",并没有改变str1的值哦
字符串的常量池
同时,你会不会为搞不懂常量池、不知道怎么判断两个字符串是否相等而发愁?如下:
String str1 = "abc";
String str2 = "abc";
String str3 = new String("abc");
str1 == str2
str1 != str3
由于String类型实在是太常用了,为了节约空间,JAVA类库的设计者在实现时增添了一个东西,名为常量池。
(以前常量池在方法区,现在在堆区)
每生成一个字符串,该字符串都将在常量池"登记",当要第二次使用时,就共享它,而不是再创建一个新的同样的字符串。
当你直接将str1指向字符串"abc"时,会先在常量池中寻找这个字符串,如果找到了,直接将str1指向"abc",如果没找到,会在常量池中创建一个"abc",并将str1指向它。看一下代码执行过程:
String str1 = "abc"; 这一步执行时,str1没找到"abc",所以它会在常量池创建"abc",并将str1指向"abc"。
String str2 = "abc"; 这一步执行时,因为str1已经组建了"abc",所以str2直接指向"abc"。
接下来看看String str3 = new String("abc");
它并不是直接将str3指向"abc",而是在堆区的其他地方new 一个String类大小的空间,开头说过,String内部其实是有一个数组存放字符的,此时这个数组就会指向"abc"。注意,并不是str3指向"abc"哦,是str3内部的数组。
在了解字符串的不可变性和常量池之后,你是不是对String的理解更为深入了呢?来看看这几道题吧。
1、在执行以下操作后 , Str1 = ______;
String str1 = "hello";
String str2 = str1;
str2 = "abc";
答案 :hello
解析 :只是改变了str2的指向,而不是它的内容
2、一大波题目正在来临
String s1 = "Hello";
String s2 = "Hello";
String s3 = "Hel" + "lo";
String s4 = "Hel" + new String("lo");
String s5 = new String("Hello");
String s6 = "H";
String s7 = "ello";
String s8 = s6 + s7;
1、System.out.println(s1 == s2); // true
2、System.out.println(s1 == s3); // true
3、System.out.println(s1 == s4); // false
4、System.out.println(s1 == s8); // false
5、System.out.println(s4 == s5); // false
1、s1 == s2 :上面已经讲过,都直接指向常量池,相等
2、s1 == s3 :相等,如果右侧都是常量,常量池中直接连接这两个字符串;如果右侧出现变量,本质是new了一个StringBuilder然后一个一个连接,
3、s1 == s4 :s4可以看作右侧是一个常量+一个变量,故本质是new Stringbuilder(),不相等
4、s1 == s8 :s8是两个变量,本质是new Stringbuilder(),故 s1 != s8。
5、s4 == s5 :虽然都是new 出的String,但是在堆区地址不同,故不相同
插一嘴,其实使用Scanner输入的String字符串是直接在堆区new哦。
StringBuilder、StringBuffer
刚才一直说什么“这个过程运用到了new StringBuilder”,现在就开始讲讲具体过程。
String类是不可变的,但是StringBuilder和StringBuffer是可变的。
接下来就是StringBuilder类和StringBuffer类。
刚才一直说,
String str3 = str1 + str2;
这个过程其实是StringBuilder来完成的,那么它到底是怎么实现的呢?
我们通过反编译软件XJad来完成这件事,
如果你没有XJad或者你不会使用,可以先阅读这篇博客,两种方式教你查看反编译代码:
代码:
String str1 = "abc";
String str2 = "def";
String str3 = "abc" + "def";
String str4 = srt1 + str2;
运行后将.class文件放入XJad中后显示:
可以看到str3创建时,直接将"abc"和"def"从常量池中取出连接起来。
同样可以看到str4 的创建过程很复杂,它先是new 了一个StringBuilder类,又将str1和str2拼接上,最后再转为String类赋值给str4。这就是字符串拼接的本质。
再看看我们平时打印一个数是怎么打印的,是System.out.println(),然后在里面放一个数1、2.0、true、、、然后直接输出他们呢?不,其实也是借助String来完成的。
来看看关于System.out.println的源代码:
以输出boolean和char来举例 (忽略synchronized,这是线程方面的,不影响阅读)
先是调用print方法,再换行,那我们再来看看print方法
看到了吗?其实不管你用print输出的是什么,它都会先给你转成字符串再打印到控制台
那这又与StringBuilder有什么关系呢?
我们用XJad查看以下代码的反编译文件
String name = "小明";
int age = 18;
System.out.println("我叫" + name + ", 我今年" + age + "岁了");
其实它的过程比你想象的更加复杂哦:
你看,只要里面有字符串拼接,编译器都会帮你,先创建一个StringBuilder 类,由于它是可变的,可以使用append()方法将其他int、double、boolean...连接起来,最后,再调用toString()方法转换成String类。即:print方法内部只能有String,有其他的也会转换成String。
接下来说说StringBuilder和StringBuffer的区别:
StringBuilder | 线程不安全,效率高 |
StringBuffer | 线程安全,效率低 |
什么叫“线程安全”呢?就是StringBuffer类的操作是原子级别的,不存在这个线程正在执行StringBuffer的操作呢,另外一个线程直接插入进来了。但是为了实现线程安全,就付出了效率的代价。
关于线程,简单的提一嘴就结束了(++操作不是原子级别哦~挺好玩的)
由于本篇博客只是浅浅涉猎javase的String类,所以要想知道更多关于StringBuilder和StringBuffer的内容,可以参考这位大佬的博客:StringBuilder和StringBuffer的区别
常用方法
0.求字符串长度,length
String a = "shsiwie";
int n = a.length();
1. 字符串转数组,toCharArray
将String字符串转换为字符数组。
String 类的内容和长度是固定的(final),要对其操作就要借助一些方法,虽然这些方法可以对字符串的内容进行操作,但这不改变对象实例,而是生成了一个新的实例。
有些题给了字符数组,没给字符串,而字符串是不能单个操作的。就要把它转换成字符数组进行操作。
如:给已知字符串排序
String str = "sojssj";
char[] a = str.toCharArray();
Arrays.sort(a);
先将str字符串转换成字符数组,调用Arrays.sort方法进行升序排列
查看源码:
发现他其实是根据String底层的字符数组使用System.arraycopy()方法拷贝一份给你。
2.字符串比较,equals
由于刚才已经详细讲过字符串常量池,这里就不重复了。
想要比较字符串内容时,用到equals方法。
String a = "abc";
String b = new String("abc");
boolean isSame;
isSame = a.equals(b);
//isSame == true
当遇到 常量与变量进行比较时,建议把常量放在前面。
boolean isSame;
String a = "abc";
isSame = "abc".equals(a);
//isSame = true;
原因:
因为如果把变量写在前面
a.equals("abc") 万一 str 是 NULL ,程序会报错,空指针异常 NullPointerExcption
忽略大小写比较:equalsIgnoreCase()
a.equalsIgnoreCase(b);
3.字符串切割,split
类似C语言中的strtok,比它牛逼。底层较为复杂,很少使用。
strtok切割后返回的是地址,而split切割后返回的是已经切割好的字符串数组,故需要 String[] 接收
String a = "aa,bb,cc";
String[] b = a.split(",");
//遍历打印b : aabbcc
for(int i = 0; i < b.length; i++) {
System.out.print(b[i]);
}
注意: split 方法中的参数是正则表达式,如果想切割字符串中的".",会切割失败
如:
String a = "c.c.c";
String[] b = a.split(".");
能运行但是没有输出(因为切割失败,b数组的长度为0)。
想要切割".",就要写成"\\."的形式
String a = "abc.def.gh";
String[] b = a.split("\\.");
切割成功,b数组的长度为3
4.替换指定内容,replace
将字符串的所有符合要求的内容替换为指定内容。返回值为字符串
String a = "aaabaaabaaa";
String b = a.replace("b","a");
//b : aaaaaaaaaaa
String c = "你他妈的,你他妈的";
String d = c.replace("你他妈的","***");
// d : ***,***
5.查找子串 indexOf
int a.indexOf(b) 返回b在a中第一次出现的位置
String a = "abcde";
String b = "bc";
System.out.println(a.indexOf(b)); // 1
返回值为字串第一次出现的位置
找不到则返回-1
来做几道关于String常用方法的练习题:
2、反转字符串