文章目录1.看看源码2.不可变有什么好处呢2.1可以缓存hash值2.2StringPool的使用2.3安全性2.4线程安全3.再来深入了解一下String3.1"+"连接符3.2“+”连接符的效率4.字符串常量4.1为什么使用字符串常量?4.2实现字符串常量池的基础5.String类常见的面试题5.1判断字符串是否相等5.2创建多少个字符串对象?1.看看源码
大家都知道,String被声明为final,因此它不可被继承。(Integer等包装类也不能被继承)。我们先来看看String的源码。
在Java8中,String内部使用char数组存储数据。
ubcfinalclassString
imlementsjava.io.Seriazable,Comarable<String>,CharSequence{
**Thevalueisusedforcharacterstorage.*
rivatefinalcharvalue[];
}在Java9之后,String类的实现改用byte数组存储字符串,同时使用r来标识使用了哪种编码。
ubcfinalclassString
imlementsjava.io.Seriazable,Comarable<String>,CharSequence{
**Thevalueisusedforcharacterstorage.*
rivatefinalbyte[]value;**Theidentifieroftheencodingusedtoenthebytesin{@value}.*
rivatefinalbyter;
}value数组被声明为final,这意味着value数组初始化之后就不能再引用其它数组。并且String内部没有改变value数组的方法,因此可以保证String不可变。
2.不可变有什么好处呢
2.1可以缓存hash值
因为String的hash值经常被使用,例如String用做HashMa的key。不可变的特性可以使得hash值也不可变,因此只需要进行一次计算。
2.2StringPool的使用
如果一个String对象已经被创建过了,那么就会从StringPool中取得引用。只有String是不可变的,才可能使用StringPool。
2.3安全性
String经常作为参数,String不可变性可以保证参数不可变。例如在作为网络连接参数的情况下如果String是可变的,那么在网络连接过程中,String被改变,改变String的那一方以为现在连接的是其它主机,而实际情况却不一定是。
2.4线程安全
String不可变性天生具备线程安全,可以在多个线程中安全地使用。
3.再来深入了解一下String
3.1“+”连接符
字符串对象可以使用“+”连接其他对象,其中字符串连接是通过StringBuilder(或StringBuffer)类及其aend方法实现的,对象转换为字符串是通过toString方法实现的。可以通过反编译验证一下:
**
*测试代码
*
ubcclassTest{
ubcstaticvoidmain(String[]args){
inti=10;
Strings="abc";
System.out.rintln(s+i);
}
}**
*反编译后
*
ubcclassTest{
ubcstaticvoidmain(Stringargs[]){删除了默认构造函数和字节码
bytebyte0=10;
Strings="abc";
System.out.rintln((newStringBuilder()).aend(s).aend(byte0).toString());
}
}
由上可以看出,Java中使用"+"连接字符串对象时,会创建一个StringBuilder()对象,并调用aend()方法将数据拼接,最后调用toString()方法返回拼接好的字符串。那这个“+”的效率怎么样呢?
3.2“+”连接符的效率
使用“+”连接符时,JVM会隐式创建StringBuilder对象,这种方式在大部分情况下并不会造成效率的损失,不过在进行大量循环拼接字符串时则需要注意。比如:
Strings="abc";
for(inti=0;i<10000;i++){
s+="abc";
}这样由于大量StringBuilder创建在堆内存中,肯定会造成效率的损失,所以在这种情况下建议在循环体外创建一个StringBuilder对象调用aend()方法手动拼接(如上面例子如果使用手动拼接运行时间将缩小到1200左右)。
与此之外还有一种特殊情况,也就是当"+"两端均为编译期确定的字符串常量时,编译器会进行相应的优化,直接将两个字符串常量拼接好,例如:
System.out.rintln("Hello"+"World");**
*反编译后
*
System.out.rintln("HelloWorld");4.字符串常量
4.1为什么使用字符串常量?
JVM为了提高性能和减少内存的开销,在实例化字符串的时候进行了一些优化:使用字符串常量池。每当创建字符串常量时,JVM会首先检查字符串常量池,如果该字符串已经存在常量池中,那么就直接返回常量池中的实例引用。如果字符串不存在常量池中,就会实例化该字符串并且将其放到常量池中。由于String字符串的不可变性,常量池中一定不存在两个相同的字符串。
4.2实现字符串常量池的基础
实现该优化的基础是因为字符串是不可变的,可以不用担心数据冲突进行共享。
运行时实例创建的全局字符串常量池中有一个表,总是为池中每个唯一的字符串对象维护一个引用,这就意味着它们一直引用着字符串常量池中的对象,所以,在常量池中的这些字符串不会被垃圾收集器回收。
我们来看个小例子,了解下不同的方式创建的字符串在内存中的位置:
Stringstring1="abc";常量池
Stringstring2="abc";常量池
Stringstring3=newString("abc");堆内存
5.String类常见的面试题
5.1判断字符串是否相等
ubcstaticvoidmain(String[]args){
Strings1="123";
Strings2="123";
Strings3="1234";
Strings4="12"+"34";
Strings5=s1+"4";
Strings6=newString("1234");
System.out.rintln(s1==s2);true
System.out.rintln(s1.equals(s2));true
System.out.rintln(s3==s4);true
System.out.rintln(s3==s5);false
System.out.rintln(s3.equals(s5));true
System.out.rintln(s3==s6);false
}解析:
s1和s2:
Strings1="123";先是在字符串常量池创建了一个字符串常量“123”,“123”常量是有地址值,地址值赋值给s1。接着声明Strings2=“123”,由于s1已经在方法区的常量池创建字符串常量"123",进入常量池规则:如果常量池中没有这个常量,就创建一个,如果有就不再创建了,故直接把常量"123"的地址值赋值给s2,所以s1==s2为true。
由于String类重写了equals方法,s1.equals(s2)比较的是字符串的内容,s1和s2的内容都是"123",故s1.equals(s2)为true。
s3和s4:
s3创建了一个新的字符串"1234",s4是两个新的字符串"12"和"34"通过"+“符号连接所得,根据Java中常量优化机制,“12”和"34"两个字符串常量在编译期就连接创建了字符串"1234”,由于字符串"1234"在常量池中存在,故直接把"1234"在常量池的地址赋值给s4,所以s3==s4为true。
s3和s5:
s5是由一个变量s1连接一个新的字符串"4",首先会在常量池创建字符串"4",然后进行"+“操作,根据字符串的串联规则,s5会在堆内存中创建StringBuilder(或StringBuffer)对象,通过aend方法拼接s1和字符串常量"4”,此时拼接成的字符串"1234"是StringBuilder(或StringBuffer)类型的对象,通过调用toString方法转成String对象"1234",所以s5此时实际指向的是堆内存中的"1234"对象,堆内存中对象的地址和常量池中对象的地址不一致,故s3==s5为false。
看下JDK8的API文档里的解释:Java语言为字符串连接运算符(+)提供特殊支持,并为其他对象转换为字符串。字符串连接是通过StringBuilder(或StringBuffer)类及其aend方法实现的。字符串转换是通过方法来实现toString,由下式定义0bject和继承由在Java中的所有类。有关字符串连接和转换的其他信息,请参阅Gosng,Joy和Steele,Java语言规范。不管是常量池还是堆,只要是使用equals比较字符串,都是比较字符串的内容,所以s3.equals(s5)为true。Java常量优化机制:给一个变量赋值,如果等于号的右边是常量,并且没有一个变量,那么就会在编译阶段计算该表达式的结果,然后判断该表达式的结果是否在左边类型所表示范围内,如果在,那么就赋值成功,如果不在,那么就赋值失败。但是注意如果一旦有变量参与表达式,那么就不会有编译期间的常量优化机制。s3和s6:
Strings6=newString("1234");在堆内存创建一个字符串对象,s6指向这个堆内存的对象地址,而s3指向的是字符串常量池的"1234"对象的地址,故s3==s6为false。5.2创建多少个字符串对象?
Strings0="123";
Strings1=newString("123");
Strings2=newString("1"+"2");
Strings3=newString("12")+"3";解析:
Strings0=“123”;
字符串常量池对象:“123”,1个;
共1个。
Strings1=newString(“123”);
字符串常量池对象:“123”,1个;
堆对象:newString(“123”),1个;
共2个。
Strings2=newString(“1”+“2”);
字符串常量池对象:“12”,1个(Jvm在编译期做了优化,“1”+"2"合并成了“12”);
堆对象:newString(“12”),1个
共2个。
由于s2涉及字符串合并,我们通过命令看下字节码信息:
javacStrTest.java编译源文件得到class文件
java-cStrTest.class查看编译结果得到字节码信息如下:
备注:以上编译结果基于Jdk1.8运行环境我们可以很清晰看到,创建了一个新的String对象和一个字符串常量"12",newString("1"+"2")相当于newString("12"),共创建了2个字符串对象。
Strings3=newString(“12”)+“3”;
字符串常量池对象:“12”、“3”,2个,
堆对象:newStringbuilder().aend(“12”).aend(“3”).toString();转成String对象,1个;
共3个。
我们同样看下编译后的结果:可以看到,包括StringBuilder在内,共创建了4个对象,字符串"12"和字符串"3"是分开创建的,所以共创建了3个字符串对象。
总结:
newString()是在堆内存创建新的字符串对象,其构造参数中可传入字符串,此字符串一般会在常量池中先创建出来,newString()创建的字符串是参数字符串的副本,看下API中关于String构造器的解释:String(Stringoriginal)
初始化新创建的String对象,使其表示与参数相同的字符序列;换句话说,新创建的字符串是参数字符串的副本。所以newString()的方式创建字符串百分百会产生一个新的字符串对象,而类似于"123"这样的字符串对象则需要在创建之前看常量池中有没有,有的话就不创建,没有则创建新的对象。"+"操作符连接字符串常量的时候会在编译期直接生成连接后的字符串,若该字符串在常量池已经存在,则不会创建新的字符串;连接变量的话则涉及StringBuilder等字符串构建器的创建,会在堆内存生成新的字符串对象。以上就是我们给您带来的关于Java字符串的一些知识总结和面试技巧,你学废了吗?
创作不易,如果您喜欢这篇文章的话,请你点赞+评论支持一下作者好吗?您的支持是我创作的源泉哦!喜欢Java,热衷学习的小伙伴可以加我微信:xia_qing2012,私聊我可以获取最新Java基础到进阶的全套学习资料。大家一起学习进步,成为大佬!