面试中必须要掌握的String字符串的知识点
String的基本特性
1.String:字符串,使用一对""引起来表示。
实例化方式有两种:
String s1=“atguigu”;//字面量的定义方式,这种方式会把"atguigu"字符串存放在堆内存中的字符串常量池里面
String s2=new String(“hello”);//创建对象的方式,这种方式会把字符串对象实例化在堆内存中
2.String声明为final的,不可被继承,如下图:
3.String实现了Serializable接口:表示字符串是支持序列化的,实现了Comparable接口:表示String是可以比较大小的,是可以根据大小关系进行排序的。
Comparable接口的使用规则,先给一个类实现Comparable接口,如下图:
然后重写comparaeTo方法,定义排序规则是由小到大排序,还是由大到小排序,如下图:
测试:
4.String在jdk8及以前内部定义了final char[] value用于存储字符串数据,如下图:
但是在jdk9的时候,把char[]数组类型改成了byte[]数组类型,为什么这样改呢?因为如果使用char[]数组太浪费空间了。
5.String代表不可变的字符序列,简称:不可变性。
a.当对字符串重新赋值时,需要重写指定内存区域赋值,不能使用原有的value进行赋值。
注意:字符串常量池里面的字符串一经创建就不能再修改了,这个空间就会一直存在,里面存放的是你创建的字符串比如"abc"字符串。
b.当对现有的字符串进行连接操作时,也需要重新指定内存区域赋值,不能使用原有的value进行赋值。
注意:字符串常量池里面的字符串一经创建就不能再修改了,这个空间就会一直存在,里面存放的是你创建的字符串比如"abc"字符串。
c.当调用String的replace()方法修改指定字符或字符串时,也需要重新指定内存区域赋值,不能使用原有的value进行赋值。
注意:字符串常量池里面的字符串一经创建就不能再修改了,这个空间就会一直存在,里面存放的是你创建的字符串比如"abc"字符串。
6.通过字面量的方式给字符串赋值(区别于new),此时创建的字符串会存放在字符串常量池里面。
String底层Hashtable结构的说明
字符串常量池中是不会存储相同内容的字符串的?
1.String的String Pool是一个固定大小的Hashtable,默认值大小长度是1009。如果放进String Pool的String非常多,就会造成Hash冲突严重,如果Hash冲突严重则会让添加到字符串常量池的字符串存放到哈希表元素对应的链表当中,从而会导致链表会很长,而链表长了后直接会造成的影响就是当调用String.intern时性能会大幅下降。Hashtable哈希表的结构如下图:
2.使用-XX:StringTableSize=长度 可设置StringTable的长度,如下图:
3.在jdk6中StringTable是固定的,就是1009的长度,所以如果常量池中的字符串过多就会导致效率下降很快。StringTableSize设置没有要求
4.在jdk7及以后的版本中,StringTable的长度默认值是60013,并且对长度的设置是有要求的,设置的最小值是1009,查看默认的StringTable字符串常量池的长度如下图:
String的内存分配
1.在Java语言中有8种基本数据类型和一种比较特殊的类型String。这些类型为了使它们在运行过程中速度更快,更节省内存,都提供了一种常量池的概念。
2.常量池就类似一个Java系统级别提供的缓存。8种基本数据类型的常量池都是系统协调的,String类型的常量池比较特殊。它的主要使用方法有两种:
a.直接使用双引号声明出来的String对象会直接存储在常量池中
b.如果不是用双引号声明的String对象,可以使用String提供的intern()方法把字符串存储到字符串常量池中
3.jdk6及以前,字符串常量池存放在永久代中;
jdk7的时候,正在去永久代的工程中,虽然有永久代,但是字符串常量池这个时候已经被放到了堆内存中,这个时候如果我们相对字符串常量池进行调优,我们只需要调整堆内存的大小就可以了,因为所有的字符串都保存在堆内存中,和其它普通对象一样;
jdk8的时候,用元空间代替了永久代,但是字符串常量池仍然是放在堆内存中的。
StringTable字符串常量池的位置为什么会调整?
1.永久代的空间默认比较小,字符串常量池如果放在永久代里面容易发生OOM(OutOfMemoryError)内存溢出。
2.永久代的垃圾回收的频率比较低,这样的话字符串常量池中的没有被引用的字符串就会得不到及时的垃圾回收,浪费内存,并且如果创建的字符串很多的话,由于没有及时的进行垃圾回收,很可能会导致OOM(OutOfMemoryError)内存溢出。
String字符串拼接操作
1.字符串常量与字符串常量的拼接结果在常量池,原理是编译期优化,如下图:
2.常量池中不会存在相同内容的常量。
3.字符串拼接的时候,只要其中有一个字符串是变量,结果就在存放在除字符串常量池的堆中,如下图:
注意如果字符串变量使用final修饰符进行了修饰,那么此字符串变量就相当于字符串常量,所以两个字符串相加相当于直接把这两个字符串合在一起,而不是两个字符串重新进行拼接,如下图:
4.如果拼接的结果调用intern()方法,则主动将常量池中还没有的字符串对象放入池中,并返回此对象地址。
字符串拼接的时候(只要有一方是字符串变量)—它的操作的底层原理
如果是两个字符串常量直接相加,则不会进行字符串拼接,而是会直接把这两个字符串常量合在一起,比如String s2=“abc”+“edf”;其实也就相当于是String s2=“abcedf”;什么是字符串拼接?就是创建一个StringBuilder对象然后调用它的append方法,最后调用toString方法,后面有详细说明。
两个字符串相加的时候,只有其中一方是字符串变量的时候,才会进行字符串拼接,如下:
String s1="abc";
String s2="def";
String s3=s1+s2;
上述代码中s1+s2字符串拼接的时候两个都是字符串变量,操作底层原理:
会先创建一个StringBuilder对象,StringBuilder sb=new StringBuilder();
然后会调用StringBuilder的append方法,sb.append(“abc”);sb.append(“def”);
最后会调用toString()方法创建一个String对象,toString()方法里面有一个new String()创建对象语句,String s3=sb.toString();
所以含有字符串变量的字符串拼接的时候,其实底层是创建了两个对象,一个是StringBuilder对象,另外一个是String对象。
补充:在jdk5.0之后使用的是StringBuilder进行字符串拼接,在jdk5.0之前使用的是StringBuffer进行字符串拼接
String字符串拼接操作与StringBuilder的append方法追加字符串操作的效率对比
结论:
通过StringBuilder的append()的方式添加字符串的效率要远高于使用String拼接字符串的方式的效率!
使用StringBuilder的append()方式好处:自始至终只创建爱你过一个StringBuilder的对象,而使用String字符串拼接的方式,创建了多个StringBuilder和String对象。
使用String字符串拼接方式的一个坏处:内存中由于创建了较多的StringBuilder和String的对象,内存占用更大;如果进行GC,需要花费额外的时间。
那通过上面的String拼接字符串的例子,和使用StringBuilder的append()方法追加字符串的方式,我们已经体会到使用StringBuilder的优点了,那么这样就可以了吗?StringBuilder还有没有什么值得优化的地方呢?有的。StringBuilder需要改进的点:
在使用StringBuilder的append(s1)方法追加字符串的时候,会先判断StringBuilder的字符数组的所剩空间是否还能够容乃要添加的字符串s1,如果不能容纳的话,StringBuilder会自动扩容,即会重新创建一个空间大的字符数组,会把原数组中的所有的内容全部复制到这个新创建的字符数组里面,这样做有两个缺点:第一,原来的字符数组需要进行GC垃圾回收,比较占内存;第二,把原来数组里面的字符全部赋值到新的数组里面,这个过程是比较费时间的,影响程序运行的效率。
扩容的流程图,如下:
首先查看StringBuilder最底层的append方法的源码,如下图:
点击ensureCapacityInternal方法查看源码,如下图:
因此我们需要怎么改进StringBuilder呢?
如果我们在创建StringBuilder字符串的时候,可以先看一下字符串的长度,然后根据字符串的长度来给StringBuilder字符串指定一个数组长度,这样我们在程序运行的时候就不需要进行扩容了,那么怎么指定呢?可以通过StringBuilder的构造器直接指定StringBuilder需要的字符数组的长度,
StringBuilder sb=new StringBuilder(指定长度);如下图:
点击super(capacity)方法如下图:
StringBuilder字符串的默认的字符数组的长度是16,如下图:
new String()到底创建了几个对象
需要注意的两点:
1.单独使用new String(“abc”)实例化对象的时候会创建两个对象,一个创建在字符串常量池外的堆内存,另外一个创建在字符串常量池中
2.如果用到了字符串拼接,那么最后会调用StringBuilder的toString方法,这个时候只会在字符串常量池外的堆内存创建一个对象,但是不会在字符串常量池中创建对象
intern()方法的使用
比如new String(“abc”).intern():
1.当字符串常量池中存在"abc"这个字符串时,intern()方法会返回字符串常量池中的"abc"这个字符串所在空间的地址。
2.当字符串常量池中不存在"abc"这个字符串时,会把堆内存中的new String(“abc”)实例化对象的地址在字符串常量池中创建一份,就相当于在字符串常量池中创建了"abc"字符串,以后再出现String s=“abc”;这种情况的时候,会直接引用字符串常量池中存放的地址,也就是s和堆内存中new String(“abc”)实例化的地址是相同的。并且intern方法会返回new String(“abc”)实例化对象的地址。如下图:
intern的使用如下图:
对于程序中大量存在的字符串,尤其其中存在很多重复字符串时,使用intern()方法可以节省内存空间,也即是在创建字符串的时候后面跟上intern方法,即new String(“abc”).intern()
大的网站平台,需要内存中存储大量的字符串。比如社交网站,很多人都存储:北京市,海淀区等信息。这时候如果字符串都调用intern()方法,就会明显降低内存的大小。
转载:https://blog.csdn.net/qq_45950109/article/details/116992408