1、String的两种形式
String x = "a";
String y = new String("ab");
变量x指向String Pool中的"a"的引用,即String Pool中存在"a";
变量y指向堆中的String 对象,堆中的String对象指向String Pool中的"ab"
2、不可变性
在JDK8及其之前,final char value[];
在JDK9及其之后,final byte[] value + encoding-flag;
-
更改底层存储结构的动机:研究发现大部分字符串都是拉丁字符,这些字符只要1个字节即可满足存储空间需求,JDK8及其之前使用的char[]数组,每个字符使用2个字节存储,浪费空间;
-
优化方式:将char[]变成byte[],同时为了兼容中文等这种每个字符占用2个字节的情况,加入一个编码标识;当使用ISO8859-1/Latin-1编码时,采用byte[];当使用其他编码方式,如UTF-16编码时,采用char[];
-
不变性的深入理解:当对一个已经存在字符串常量池中的字符串进行修改(拼接/截取/部分替换…)时,都是会在字符串常量池中新建一个字符串,原有的字符串不变;
3、String拼接
某些情况下,较多的字符串进行拼接时,使用StringBuffer(线程安全)和StringBuilder(非线程安全)的appen()方法,效率比直接"+"高很多;
-
原因:"+“底层实际会new StringBuiler(),以及new String(),每一个”+"都会进行这些操作;直接使用StirngBuilder()的append()则只会new 一次StringBuilder();
-
对使用StringBuilder拼接的性能优化:基本思路,减少数组copy次数,
可以在new StringBuilder()的时候,在确定拼接的字符串的大概长度时,直接指定初始的char[]容量,减少扩容的次数,StringBuilder底层采用的是char[]存放元素;
3.1、“+”进行拼接的底层原理
(1)String s1 = "a" + "b"; // 等同于"ab"
String s2 = "ab";
- 常量之间的拼接,通过编译期优化,导致s1和s2指向的都是Stirng Pool中"ab"的引用
(2)String s3 = "a";
String s4 = s3 + "b";
String s5 = "ab";
-
存在变量的拼接,s4指向的引用生成的底层原理:
首先new StringBuilder对象,通过该对象调用append()方法,最后通过该对象调用toString()方法;
toString()方法,底层调用了new String(“ab”)方法, 将new的对象返回; -
结果:s4指向堆中new 的String对象,但该String对象不指向String Pool中的"ab";s5是直接指向String Pool中的"ab";
-
StringBuilder的toString()分析:底层类似new String(“xxx”),但是和一般的new String(“xxx”)会在String Pool中加入对应的"xxx"不同,该操作不会如此;
3.2、append()拼接
底层原理:通过char[]数组存放元素,将需要拼接的元素直接放在数组的空闲末尾,若数组越界则进行数组扩容;
4、字符串常量池
4.1、字符串常量池中不会存储相同内存的字符串;
实现方式:字符串常量池的底层实际是一个固定大小的Hashtable,所以当String Pool中的String比较多的时候,容易产生hash冲突,导致桶数组中的链表比较长,影响性能;
优化:不断的将Hashtable的默认容量变大;
4.2、String Pool存放的位置
JDK6及其之前,和静态变量一起存放在永久代(方法区的具体实现)
JDK7及其之后,和静态变量一起存放在Java堆中
JDK8及其之后,永久代该为元空间
-
逻辑上字符串常量池和静态变量一直都在方法区中
-
变化的动机:永久代的默认空间比较小,存放大量的字符串容易OOM;永久代的回收频率比较低;
5、intern()
判断调用该方法的字符串在String Pool中是否存在:
-
若存在,返回字符串常量池中的引用地址;
-
若不存在,Jdk6在String Pool中新建对应的字符串,返回该引用地址,jdk7以后在String Pool中不创建字符串对象,而是创建一个指向堆中某个存放该字符串的对象的地址;
原因分析:在Jdk7及其之后,String Pool已经被存放在了Java堆中,虽然String Pool中没有目标字符串,但是若堆中有对象存放这对应字符串,则直接返回堆中该对象的引用即可;
用于理解的示意图:
6、new String()的对象个数分析
new String("ab");
上面的方式创建了2个对象:
- new关键字在堆中创建了一个对象;
- 字符串常量池中有对象"ab"
String s = new String("ab") + new String("cd");
上面的方式创建了6个对象:
- new StringBuilder()
- new String(“ab”)
- 字符串常量池中的"ab"
- new String(“cd”)
- 字符串常量池中的"cd"
- toString()中的new String(“abcd”),注意该方法不会在字符串常量池中生成"abcd";
注意点:即通过上述方式可知,在字符串常量池中没有字符串"abcd",变量s指向toString()方法返回的堆中的String对象,但是String该对象不指向Stirng Pool;
7、String常用方法
- 获取字符串指定下标处的字符:charAt(index);
- 判断是否包含指定字符串:contains(string);
- 判断是否以指定字符串作为后缀:endsWith(string);
- 忽略大小写比较两个字符串是否相等:equalsIgnoreCase(string string);
- 转换成字节数组:getBytes();
- 从左到右将指定字符串替换成目标字符串:replace(target, replace);
- 以指定正则表达式分割字符串:split(string);
- 将字符串转换成字符数组:toCharArray();
- 将其他数据类型转换成字符串:valueOf(string);底层调用了toString();
- 获取指定字符串中第一次出现的下标:indexOf(string);
- 从指定开始下标截取字符串的部分:subString(beginIndex);
参考:
- 尚硅谷康师傅JVM视频
- JAVA编程思想