胆小鬼连幸福都害怕,碰到棉花也会受伤
——太宰治《人间失格》
♥ 努力成为更好的自己
字符串你真的了解吗?
首先分享一道面试题(滴滴)
面试官:在Java中字符串的大量拼接为什么会比较耗时,以及它的替代方案是什么?
我:因为String底层采用final修饰的数组来存储,jdk8采用char[],jdk9采用byte[],一旦创建内存大小是不会变的,所以每次使用"+"拼接都会创建一个新的StringBuilder对象,使用该对象的append()方法进行拼接,增加了创建新对象和GC的频率,而GC时用户线程暂停(stop the world),GC线程运行。
替代方案就是采用StringBuffer和StringBuild进行字符串的拼接,还说线程安全的问题,感觉很完美的回答。
面试官又问:那你知道字符串的内存时怎么分配的吗?
我:。。。。。。。。。。。。。。
面试官接着问:inter
n()方法使用过吗,什么情况下使用?
我:。。。。。。。。。。
♥ 努力成为更好的自己
字符串基本特性
字符串的基本特性:
- String:字符串,使用一对 ”” 引起来表示
(1).String s1 = "mogublog" ; // 字面量的定义方式
(2).String s2 = new String("moxi");
string声明为final的,不可被继承
String实现了Serializable接口:表示字符串是支持序列化的。实现了Comparable接口:表示string可以比较大小
string在jdk8及以前内部定义了final char[] value用于存储字符串数据。JDK9时改为byte[]
为什么JDK1.9将char[]改为了byte[]数组:
- Jdk1.8一个char栈两个字节(16位),String主要存储在堆中,大部分的String包含的都是拉丁字符(26个字母)使用一个字节就可以存的下,导致一半的空间被浪费掉。
- Jdk9使用byte[]和字符集的标识来存储,如果字符集的标识是中文使用两个字节来存其他的使用一个字节来存。这样节省了将近一半的空间。
String的不可变性:
导致String字符串不可变性的原因,第一,前面提到过String底层采用char[]和byte[]数组进行存储字符序列,而数组的的特点是一旦创建内存空间大小是不可变的。第二,String str = “abc”属于字面量,存放在运行时常量池中的,而运行时常量池中的字面量都是唯一不可重复的。
/** * String的基本使用:体现String的不可变性 */public class StringTest { @Test public void test1() { String s1 = "abc";//字面量定义的方式,"abc"存储在字符串常量池中 String s2 = "abc"; s1 = "hello"; System.out.println(s1 == s2);//判断地址:true --> false System.out.println(s1);// System.out.println(s2);//abc } @Test public void test2() { String s1 = "abc"; String s2 = "abc"; s2 += "def"; System.out.println(s2);//abcdef System.out.println(s1);//abc } @Test public void test3() { String s1 = "abc"; String s2 = s1.replace('a', 'm'); System.out.println(s1);//abc System.out.println(s2);//mbc }}
字符串常量池不会存储相同的字符串:
- String的string Pool是一个固定大小的Hashtable(数组+链表),默认值大小长度是1009。如果放进string Pool的string非常多,就会造成Hash冲突严重,从而导致链表会很长,而链表长了后直接会造成的影响就是当调string.intern时性能会大幅下降。
- 使用-XX:StringTablesize可设置stringTable的长度
- 在jdk6中stringTable是固定的,就是1009的长度,所以如果常量池中的字符串过多就会导致效率下降很快。stringTablesize设置没有要求
- 在jdk7中,stringTable的长度默认值是60013,
- 在JDK8中,StringTable可以设置的最小值为1009,默认是65536
♥ 努力成为更好的自己
String的内存分配
String的内存分配:
- 在Java语言中有8种基本数据类型和一种比较特殊的类型string。这些类型为了使它们在运行过程中速度更快、更节省内存,都提供了一种常量池的概念。
- 常量池就类似一个Java系统级别提供的缓存。8种基本数据类型的常量池都是系统协调的,string类型的常量池比较特殊。它的主要使用方法有两种。
(1).类似与String str = "hello world” 这样用“”引起来的字符串存放在常量池中。
(2).另外一种是同String的inter() 方法进行使用。 - JDK1.6时字符串常量池是放在永久代的。
- JDK1.7将字符串常量池放在Java堆中
- JDK1.8元空间替代了永久代,字符串常量池还是放在堆中。
- StringTable为什么会从永久代调整到堆空间中?
(1).在应用程序成字符串是最常被使用到的数据类型,会占用大量的空间,而永久代的默认大小比较小,避免因字符串存储过多而导致omm
(2).永久代的垃圾回收频率很低,会导致大量字符串无法被回收
♥ 努力成为更好的自己
String的基本操作
字符串的拼接:
- 常量与常量的拼接结果在常量池,原理是编译期优化
- 常量池中不会存在相同内容的变量
- 只要其中有一个是变量,结果就在堆中。变量拼接的原理是StringBuilder
- 如果拼接的结果调用intern()方法,则主动将常量池中还没有的字符串对象放入池中,并返回此对象地址
import org.junit.Test;/** * 字符串拼接操作 * @author shkstart shkstart@126.com * @create 2020 0:59 */public class StringTest5 { @Test public void test1(){ String s1 = "a" + "b" + "c";//编译期优化:等同于"abc" String s2 = "abc"; //"abc"一定是放在字符串常量池中,将此地址赋给s2 /* * 最终.java编译成.class,再执行.class * String s1 = "abc"; * String s2 = "abc" */ System.out.println(s1 == s2); //true System.out.println(s1.equals(s2)); //true } Test public void test2() { String s1 = "javaEE"; String s2 = "hadoop"; String s3 = "javaEEhadoop"; String s4 = "javaEE"+ "hadoop"; //常量与常量拼接是在编译期进行优化 //如果连接符左右出现了变量,都相当于在堆空间new String() String s5 = s1 + "hadoop"; String s6 = "javaEE" + s2; String s7 = s1 + s2; System.out.println(s3 == s4); System.out.println(s3 == s5); System.out.println(s3 == s6); System.out.println(s3 == s7); System.out.println(s5 == s6); System.out.println(s5 == s7); System.out.println(s6 == s7); //判断字符串常量池是否有”javaEEhadoop“字符串,如有返回其地址,如没有创建一个字符串没返回其地址 String s8 = s6.intern(); System.out.println(s3 == s8); }
对于Test2方法来说,输出结果如下:
字符串的拼接的原理(从字节码角度):
原理:字符串的拼接底层new StringBuilder(), 调用StringBuilder对象的append()方法进行拼接。下面从字节码角度进行验证。
@Test public void test3(){ String s1 = "a"; String s2 = "b"; String s3 = "ab"; /* 如下的s1 + s2 的执行细节:(变量s是我临时定义的) ① StringBuilder s = new StringBuilder(); ② s.append("a") ③ s.append("b") ④ s.toString() --> 约等于 new String("ab") 补充:在jdk5.0之后使用的是StringBuilder,在jdk5.0之前使用的是StringBuffer */ String s4 = s1 + s2;// System.out.println(s3 == s4);//false }
说明:s1 + s2的执行细节
- StringBuilder s = new StringBuilder();
- s.append(s1);
- s.append(s2);
- s.toString(); -> 类似于new String("ab");
注意:(1).在JDK5之后,使用的是StringBuilder,在JDK5之前使用的是StringBuffer
String | StringBuilder | StringBuffer | |
可变性 | 不可变序列 | 可变序列 | 可变序列 |
安全性 | 线程不安全 | 线程安全 | |
效率 | 效率低 | 效率最高 | 次之 |
(2).注意,我们左右两边如果是变量的话,就是需要new StringBuilder进行拼接,但是如果使用的是final修饰,则是从常量池中获取。所以说拼接符号左右两边都是字符串常量或常量引用 则仍然使用编译器优化。也就是说被final修饰的变量,将会变成常量,类和方法将不能被继承。
所以在开发中建议能使用final尽量使用,以提高效率。
@Test public void test4(){ final String s1 = "a"; final String s2 = "b"; String s3 = "ab"; String s4 = s1 + s2; System.out.println(s3 == s4);//true }
字符串的拼接与append方法效率对比:
@Test public void test6(){ long start = System.currentTimeMillis(); method1(100000);//3601// method2(100000);//8 long end = System.currentTimeMillis(); System.out.println("花费的时间为:" + (end - start)); } public void method1(int highLevel){ String src = ""; for(int i = 0;i < highLevel;i++){ src = src + "abc";//每次循环都会创建一个StringBuilder、String }// System.out.println(src); } public void method2(int highLevel){ //只需要创建一个StringBuilder StringBuilder src = new StringBuilder(); for (int i = 0; i < highLevel; i++) { src.append("abc"); }// System.out.println(src); }
根据上述代码跑出来的结果:方法1:3601ms,方法2:8ms
结论:
- 通过StringBuilder的append()方式添加字符串的效率,要远远高于String的字符串拼接方法
- StringBuilder的append的方式,自始至终只创建一个StringBuilder的对象
- 对于字符串拼接的方式,还需要创建很多StringBuilder对象和 调用toString时候创建的String对象。
- 内存中由于创建了较多的StringBuilder和String对象,内存占用过大,如果进行GC那么将会耗费更多的时间
♥ 努力成为更好的自己
intern()方法的使用
intern()方法是一个native方法
原理:官方介绍:
|
String s = "abc".intern(); 如果字符串常量池中包含abc,则将池中的"abc"的地址,如果没有则在字符串常量池创建一个"abc"再将引用地址返回。
如果不是用双引号声明的string对象,可以使用string提供的intern方法:intern方法会从字符串常量池中查询当前字符串是否存在,若不存在就会将当前字符串放入常量池中。
比如:String myInfo = new string("I love atguigu").intern();
也就是说,如果在任意字符串上调用string.intern方法,那么其返回结果所指向的那个类实例,必须和直接以常量形式出现的字符串实例完全相同。因此,下列表达式的值必定是true
("a"+"b"+"c").intern()=="abc"
通俗点讲,Interned string就是确保字符串在内存里只有一份拷贝,这样可以节约内存空间,加快字符串操作任务的执行速度。注意,这个值会被存放在字符串内部池(String Intern Pool)
♥ 努力成为更好的自己
String的面试题
new String()创建几个对象?
- 一个对象是:new关键字在堆空间中创建
- 另一个对象:字符串常量池中的对象
new String("a") + new String("b") 会创建几个对象
- 对象1:new StringBuilder()
- 对象2:new String("a")
- 对象3:常量池的 a
- 对象4:new String("b")
- 对象5:常量池的 b
- 对象6:toString中会创建一个 new String("ab")
- 调用toString方法,不会在常量池中生成ab
实际上toString()也会创建一个对象
String的intern()的面试题
public static void main(String[] args) { String s = new String("1"); s.intern();//调用此方法之前,字符串常量池中已经存在了"1" String s2 = "1"; System.out.println(s == s2);//jdk6:false jdk7/8:false String s3 = new String("1") + new String("1");//s3变量记录的地址为:new String("11") //执行完上一行代码以后,字符串常量池中,是否存在"11"呢?答案:不存在!! s3.intern();//在字符串常量池中生成"11"。如何理解:jdk6:创建了一个新的对象"11",也就有新的地址。 // jdk7:此时常量中并没有创建"11",而是创建一个指向堆空间中new String("11")的地址 String s4 = "11";//s4变量记录的地址:使用的是上一行代码代码执行时,在常量池中生成的"11"的地址 System.out.println(s3 == s4);//jdk6:false jdk7/8:true }
String s = new String("1"); s.intern();//调用此方法之前,字符串常量池中已经存在了"1" String s2 = "1"; System.out.println(s == s2);//jdk6:false jdk7/8:false
这段代码在jdk1.6和jdk1.7/jdk1.8中执行结果都为false
String s3 = new String("1") + new String("1");//s3变量记录的地址为:new String("11") //执行完上一行代码以后,字符串常量池中,是否存在"11"呢?答案:不存在!! s3.intern();//在字符串常量池中生成"11"。如何理解:jdk6:创建了一个新的对象"11",也就有新的地址。 // jdk7:此时常量中并没有创建"11",而是创建一个指向堆空间中new String("11")的地址 String s4 = "11";//s4变量记录的地址:使用的是上一行代码代码执行时,在常量池中生成的"11"的地址 System.out.println(s3 == s4);//jdk6:false jdk7/8:true
jdk1.6:执行结果为true,因为String s3 = new String("1") + new String("1");
相当于在堆中创建11",s3指向"11"的地址,s3.intern();前面提到会在字符串常量池中创建"11",但是并没有变量区接受字符串常量池"11"的地址,s3还是指向堆空间的地址,所以 System.out.println(s3 == s4);为false jdk1.7:执行结果为true,因为String s3 = new String("1") + new String("1");相当于在堆中创建11",s3指向"11"的地址,不同的是执行s3.intern();之后,常量池并没有创建"11",而是创建一个指向堆空间中new String("11")的地址,这是因为jdk1.7之后将字符串常量池从永久代,移到了堆空间中,而new的对象也在堆空间,这就没有必要再次在堆空间(字符串常量池)创建"11"对象,可以将字符常量池新创建的"11"执向堆空间的地址。所System.out.println(s3 == s4);运行结果为true.
String的intern()方法总结
JDK1.6中,将这个字符串对象尝试放入串池。
- 如果串池中有,则并不会放入。返回已有的串池中的对象的地址
- 如果没有,会把此对象复制一份,放入串池,并返回串池中的对象地址
JDK1.7起,将这个字符串对象尝试放入串池。
- 如果串池中有,则并不会放入。返回已有的串池中的对象的地址
- 如果没有,则会把对象的引用地址复制一份,放入串池,并返回串池中的引用地址