一. String概念
String 是java中对字符串的一种表达方式,这是一个示例对象,并不属于常见的8中基本类型,和char[]也有一定的区别。
1.String特性
String有三个特性。
不可变性:从JDK文档中我们可以看到,String是常量的,这就意味着当我们对String重新赋值的时候,需要重写指定内存区域进行赋值,不能对原有的内存地址中的value进行修改。当对现有的字符串进行拼接的时候,也需要重新指定内存区域赋值,不能使用原有的value进行赋值。当我们调用String的replace()方法进行修改的时候,同样的也不能直接修改
final 修饰:这意味这String是不可以被继承的,这也增加了String的安全性。实现Serializable接口:表示字符串支持序列化,实现了ComparaBle接口:表示String可以比较大小;
在JVM中维护了一个字符串常量池,用于存放字符串常量,这个对于我们深入理解String是非常重要的,至于字符串常量池的版本变化,我在之前的方法区中有提到过,大家可以去看一下。通过字面量的方式(区别与new)给一个字符串赋值,此时的字符串值声明在字符串常量池中。当然根据字符串常量池的特性,常量池中是不会存放相同的值,当我们创建两个具有相同字面量的字符串时,比如
1 | String a = "abc"; |
因为字符串a和字符串b被同一个字面量创建,当a被创建的时候,会先检查字符串常量池中是否有“abc”,如果没有则开辟一个空间并把“abc”存放到这个地址上,而当b被创建的时候,检查常量池上已经有“abc”了,所以直接把指针指向该地址。实际上a和b指向的是同一个地址。
2.String内存分配
在Java语言中,有8种基本类型,和一种比较特殊的类型String,这些类型为了使他们在运行过程中速度更快,更节省内存,都提供了一种常量池的概念。
常量池就是类似一个Java系统级别的提供的缓存。8中基本类型的常量池都是系统协调的,String类型的常量池比较特殊。它主要使用的方法有两种:
- 直接使用双引号声明出来的String对象会直接存储在常量池中。
- 如果不是用双引号声明的String对象,可以使用String提供的intern()方法。
String操作
String操作有:字符串的创建、拼接、比较等几个比较常用的方法,这些方法的一些用法因为String的特殊性,经常被当作笔试的题目,我们接下来就重点讲解这几个操作。
1.String创建
String的创建有两种方法,一种是使用字面量赋值,比如
1 | String a = "abc"; |
这种方式创建字符串,会直接在常量池中创建对象,栈对象a和b分别用指针指向该字符串常量池中的常量。
另一种方式是使用常用的new关键字创建,如下所示:
1 | String c = new String("abc"); |
这种方式创建的字符串会先在堆中开辟一个空间,并创建字符串对象c,这个对象的value为“abc”,同时会检查常量池是否含有“abc”字符串,如果没有的话会重新创建一个字符串常量“abc”。
总结来说第一种创建方法指针会直接指向字符串常量池相当于a->”abc”,b->”abc”,第二种方式首先会在堆中创建一个c的String对象,它的value是“abc”。 同时如果这个字符串在常量池中不存在,会在常量池中创建这个String对象“abc”;
我们用下图表示,两者的区别:
所以我们可以看到这样的面试题:
1 | public class stringTest { |
返回的结果是
1 | true |
2.String拼接
常量池的拼接遵守以下规则:
- 1.常量和常量的拼接结果在常量池中,原理是编译期优化。
- 2.常量池中不会存在相同内容的常量。
- 只要其中有一个是变量,结果就在堆中。变量拼接的原理是StringBuilder。
- 4.如果拼接的结果调用intern()方法,则主动将常量池中还没有的字符串对象放在常量池中,并返回此对象地址。
下面我们根据一些笔试的题目看一下这些规则:
面试题一:
1 |
|
这里a是由三个字符串拼接的结果,我们根据规则一可以得到拼接结果“abc”会放在常量池中,所以结果阿返回的是true,并且我们根据编译的class文件可以看到,String a = “a” + “b” + “c”;直接会被优化成String a = “abc”;所以最后执行的代码是String a = “abc”;这样我们可以和我们之间的知识对应。
面试题二:
1 |
|
由上面的规则得到,只要其中有一个是变量,得到的结果就存放在堆中,而不是在常量池中,所以除了当一个返回的是true,其他的返回的都是false。
当我们使用变量进行拼接的时候,我们使用的底层是StringBilder,比如上面的String s7 = s1+s2;就相当于
1 | StringBuilder sb = new StringBuilder(); |
StringBuilder的toString操作实际上是一个new String的操作。
当然不是所有的变量拼接操作都是使用StringBuilder操作的,当我们对变量声明为final的时候,变量的拼接就会编程常量的拼接,这样底层就不会用到SringBuilder,而是由编译器优化直接使用常量池进行赋值。
这里同时也变相说明了String的拼接操作会比StringBuilder的拼接操作效率低,因为String在进行拼接操作的过程中会new StringBuilder对象,然后再进行拼接操作,而StringBuilder直接进行拼接,节省了空间和时间从而提高效率。
讲完上面两个String的创建和拼接,我们来看一个综合的笔试面试题目。
1 |
|
我们来计算一下这里到底创建了多少个对象,我们先说答案,再说为什么,这里一共生成了6个对象。
首先是会生成一个StringBuilder对象用于拼接,接着对于new String(a)会在堆中以及常量分别生成String对象,以及对于new String(“b”)同样的会生成两个对象,最后调用StringBuilder的toString方法,会生成一个String对象。这里要注意,toString方法并不会在常量池中生成对象,所以常量池中并没有“ab”的字符串对象。
3.intern()
String字符串还有一个比较特殊的API:intern(),当一个字符串s调用这个函数的时候,会从字符串常量池中寻找是否有与s值相等的字符串,如果找到了,就返回常量池中的字符串。否则,将该字符串加入到常量池中,并且返回对该常量池中这个字符串的引用。
比如说:
1 | String info = new String("1111").intern(); |
也就是说,如果在任意字符串上调用String.intern方法,那么返回结果所指向的那个类实例,必须和直接以常量形式出现的字符串实例完全相同,因此,下列表达式的值必定是true
1 | {"a"+"b"+"c"}.intern()== “abc”; |
通俗来讲,Intern就是确保相同值的字符串在内存中只用一份拷贝,这样可以节约内存空间,加快字符串操作任务的执行速度。注意这个值会被存放在字符串内部池。
这里我们仍然用一个面试题来看一下intern()方法使用:
1 |
|
这题的答案其实根据不同的jdk版本是不一样的,在jdk1.6以前,因为之前说过String a = new String(“a”) + new String(“b”);并没有在常量池中创建“ab”的字符串,且a指向的是堆空间地址,所以返回的是false,但是在jdk7及以后,intern同样会在常量池中寻找“ab”对象,但是因为在堆中已经创建了“ab”的字符串对象,所以当b创建对象的时候,常量池不需要在常量池中重新创建“ab”对象了,可以直接存储堆中的引用,这个引用指向s3引用的对象,也就是说引用地址相同。所以结果最终返回的是true。
当我们将a.intern();和String b = “ab”;调换一下顺序之后,结果又会发生变化,如下:
1 |
|
因为String b = “ab”;并不会像a.intern()一样智能会选择直接引用堆中相同的对象,所以结果返回的就是false;
4.String 比较
String的比较主要有两种,一种是==一种是equals。
- 使用==号:用于比较对象引用的内存地址是否相同。
- 使用equals方法:在Object类中和==号相同,但在自定义类中,建议覆盖equals方法去实现比较自己内容的细节;由于String类覆盖已经覆盖了equals方法,所以其比较的是字符内容。
三. StringBuffer 和 StringBuilder
StringBuffer和StringBuilder常常用于解决字符串拼接的问题,他们都比String直接凭借效率高很多。
先来分别使用String/StringBuilder/StringBuffer来拼接30000次字符串,对比各自损耗的时间,经过测试发现:
String做字符串拼接的时候,耗时最高,性能极低,原因是String内容是不可变的,每次内容改变都会在内存中创建新的对象。
性能最好的是StringBuilder,其次是StringBuffer,最后是String。StringBuilder和StringBuffer区别并不是很大,也有可能是测试次数还不够吧。感兴趣的小伙伴可以增加拼接次数来看看。代码很简单,就不展示出来了。
所以在开发中拼接字符串时,优先使用StringBuffer/StringBuilder,不到万不得已,不要轻易使用String。
StringBuilder以及StringBuffer的区别
StringBuffer和StringBuilder都表示可变的字符串,两种’的功能方法都是相同的。但唯一的区别:
- StringBuffer:StringBuffer中的方法都使用了synchronized修饰符,表示同步操作,在多线程并发的时候可以保证线程安全,但在保证线程安全的时候,对其性能有一定影响,会降低其性能。
- StringBuilder:StringBuilder中的方法都没有使用了synchronized修饰符,线程不安全,正因为如此,其性能较高。
对并发安全没有很高要求的情况下,建议使用StringBuilder,因为其性能很高。像这样的情况会较多些。使用StringBuilder无参数的构造器,在底层创建了一个长度为16的char数组:
此时该数组只能存储16个字符,如果超过了16个字符,会自动扩容(创建长度更大的数组,再把之前的数组拷贝到新数组),此时性能极低;如果事先知道大概需要存储多少字符,可以通过构造器来设置字符的初始值:
四. 参考资料
尚硅谷2020最新版宋红康JVM教程持续更新中(java虚拟机详解,jvm从入门到精通)