1、String
String 是 Java 定义的一个字符串类型类,源码(JDK11,本篇所有源码环境都是 11 )如下:
这里说明一点,Java 在不同版本对 String 源码做了点修改,具体改动如下图。
改动最大的莫过于将存储字符串的 char 类型数组改成了 byte 类型。那这是为什么呢!
J3:节省 String 占用的内存。
Java 程序语言是按照 Unicode 编码标准存储字符串的,而我们都知道 UTF - 8 编码占用两个及以上的字节个数、ISO-8859-1 编码则是单字节编码只占一个字节。在大部分的时候计算机任然使用的是 ISO-8859-1 编码,所以在存储像字母时,则会白白浪费一个字节的空间,也正是这个原因,Java 才会将 char 改成 byte。
那多了的一个属性 code 是干啥?
J3:标识字符串编码方式
源码:
coder 属性默认有 0 和 1 两个值。如果 String 判断字符串只包含了 Latin-1,则 coder 属性值为 0 ,反之则为 1
0 代表Latin-1(单字节编码)。
1 代表 UTF-16 编码。
另外 String 类是被 final 修饰的,表示最终类即不可被继承。而且内部存储字符串值的数组属性也是被 final 修饰表明 String 类型变量一旦被定义赋值,则值不可修改(下面会解释不可修改这个点)。
以上介绍了 String 的基本情况,那再来说说它在 JVM 中的内存布局。
JVM 内部划分为两个组件和两个系统(《Java 虚拟机运行时数据区》):
两个子系统为:
- Class Loader(类装载子系统)
- Execution Engine(执行引擎)
两个组件为:
- Runtime Data Area(运行时数据区)
- Native Interface(本地接口)
String 所涉及的区为 Runtime Data Area(运行时数据区) ,在该区中 String 类型的字
符串常量存放区域倒是因为 JDK 版本的不一样而略有不同。
- JDK1.6 及以前字符串常量都存放在方法区的字符串常量池中。
- JDK1.7 及以后字符串常量池被移到了堆中,所以字符串常量自然就存放在堆中了。
那下面来看看几行代码:
public class StringTest { public static void main(String[] args) { // 直接赋值一个字符串常量值 String name = "J3"; String name1 = "J3"; // 创建一个 String 对象赋值 String name2 = new String("J3"); System.out.println("name 重新赋值前:" + name); // name 和 name1 是否相等 System.out.println("name 和 name1 是否相等:" + (name == name1)); // 给 name 重新赋值 name = "刘亦菲"; System.out.println("name 重新赋值后:" + name); System.out.println("name2 赋值:" + name2); // name 和 name1 是否相等 System.out.println("name 和 name1 是否相等:" + (name == name1)); } }
上面代码的 5,6,8 行代码都是给变量赋值,体现在 JVM 中的效果如图:
紧接着 13 行代码体现图如下:
结合上图,当字符串常量池中出现相同的字符串时,JVM 不会再生成对应的字符串而时将已经存在的字符串地址赋给变量,从而在字符串常量池中相同的字符串只会存在一份。当栈中变量重新赋值字符串时,则会将变量引用指向新创建的常量池中字符串地址,而常量池原先的值是不会改变的,所以 String 类型变量重新赋值只是变量指向的地址变化,不是值变化。
2、StringBuffer 与 StringBuilder
类继承图:
StringBuffer 是一个字符串可变的序列,通过其提供的方法可以改变这个字符串对象的字符串序列。
StringBuilder 是从 JDK1.5 开始出现的,功能和 StringBuffer 类似,不同点是 StringBuffer 线程安全,StringBuilder 线程不安全。
这里有两个点,字符串可变和线程安全。
1、字符串可变
String 类型字符串不可变是因为内部存储值的属性是被 final 修饰,所以其值不可变。如果在进行字符串拼接的时候,字符串常量值不会在原来的字符串后面添加字符串,而是重新生成一个拼接后的字符串放到字符串常量池中。
看如下代码:
String name3 = "J3" + "-西行";
继续结合上图,效果如下:
由图可发现,原来的字符串其实是不会改变,而是重新在字符串常量池中生一个新字符串,这就是 String 字符串不可变的真正地方。
而 StringBuffer 字符串可变是体现在什么地方,咱上源码。
StringBuffer # append
@Override public synchronized StringBuffer append(String str) { toStringCache = null; // 调用父类(AbstractStringBuilder)拼接字符串方法 super.append(str); return this; }
既然调用了父类方法,那点进去瞧瞧。
AbstractStringBuilder # append
public AbstractStringBuilder append(String str) { // 拼接字符串为空,那就直接拼接一个空字符串 if (str == null) return appendNull(); // 获取拼接的字符串长度 int len = str.length(); // (重点代码)扩容!!!将原始 char 数组扩容到可以容纳拼接后字符串的长度 ensureCapacityInternal(count + len); // 真正开始字符串拼接 str.getChars(0, len, value, count); // 重新计算字符串长度 count += len; // 返回字符串对象 return this; }
在 StringBuffer 和 StringBuilder 中,存储字符串的数组还是 char 类型,并且没有被 final 修饰,所以其指向的 char 类型数组引用可以重新赋值。
那现在来关注一下重点代码,扩容。
AbstractStringBuilder # ensureCapacityInternal
private void ensureCapacityInternal(int minimumCapacity) { // overflow-conscious code // 如果合并后的字符串长度,大于原始字符串长度,才开始扩容 if (minimumCapacity - value.length > 0) { // 数组扩容,底层调用 System.arraycopy 方法,原理是生成一个新的数组,将原始数组中的内容移动到新数组中,最终赋值给 value value = Arrays.copyOf(value, newCapacity(minimumCapacity)); } }
扩容代码仅仅只是一个开始,保证最后字符串拼接的时候不会导致 char 类型数组溢出,那最后只剩下字符串拼接了,上代码。
AbstractStringBuilder # getChars
public void getChars(int srcBegin, int srcEnd, char dst[], int dstBegin) { // 各种字符串长度校验 if (srcBegin < 0) { throw new StringIndexOutOfBoundsException(srcBegin); } if (srcEnd > value.length) { throw new StringIndexOutOfBoundsException(srcEnd); } if (srcBegin > srcEnd) { throw new StringIndexOutOfBoundsException(srcEnd - srcBegin); } // 最终走到这里,将 待拼接字符串:value,赋值到 目标数组:dst 中,完成拼接。 System.arraycopy(value, srcBegin, dst, dstBegin, srcEnd - srcBegin); }
以上就是 Java 提供的可变字符串内部原理,总结一下可变原因。
内部存放字符串值的 char 类型数组没有被修饰成 final。
实现了一套可扩容的数组机制。
2、线程安全问题
在字符串可变问题上,我已经贴出了可变字符串类型中的一个重要字符串拼接方法(StringBuffer # append)源代码,其中方法上被 synchronized 修饰了,这就是其是一个线程安全的字符串拼接类。
大家可以仔细留意一下,只要是涉及改变字符串内容的方法,都被 synchronized 修饰了,以此来保证线程安全。
why?为什么,这是为什么?
说一下我的理解:保证字符串拼接出的结果和我们预期的一样,系统中可变字符串对象引用可以被多个方法所执行,而他们都想进行字符串拼接,那同一时刻多个方法调用同一个可变字符串对象进行字符串拼接,我们能得到预期的结果嘛,显然是不能的,所以 Java 就在方法的开头加了一把锁(synchronized)谁能第一个锁住这个对象,那就谁先来执行字符串拼接。
而对资源进行加锁与解锁毕竟是要有点开销的,所以 StringBuffer 在字符串拼接的时候效率就会有点损耗 StringBuilder 则不会,因为它内部拼接方法没有加锁,但这也是它线程不安全的原因。
3、我的面试答案
面试官你好,对于这个问题我说一下我的理解:
Java 中常用的字符串类型就莫过于 String 类型了,它是一个被 final 修饰的类,表示类不可被继承。其中存储字符串的数组属性同样也被 final 修饰这也是其字符串不可变的原因,并在不同的 JDK 版本中存储字符串的数组类型也是不一样。
在 JDK1.8 及以前存储字符串的数组类型为 char 之后则是改成了 byte 类型,究其原因则是为了节省字符串占用空间。我们都知道 Java 的字符串编码规则是按 Unicode 编码,Unicode 只是一个规范(其实现有 ISO-8859-1、UTF-8 等),如果 char 类型在 ISO-8859-1 字符编码中字母类型只占 1 个字节,而 UTF-8 则会占用 2 个字节,这就造成了空间浪费。
而 StringBuffer 和 StringBuilder 是属于字符串可变类,内部存储字符的是一个 char 类型数组并没有被 final 修饰,且其内部实现了一套可变的数组代码,这就使得其可以在 char 数组中进行扩容添加字符。
对于可变字符串类 StringBuffer 和 StringBuilder 两者功能基本一样,只不过两者在拼接字符串的时候考虑的使用环境不同。StringBuffer 类在线程安全和不安全环境都可以使用,因为其内部拼接方法都被 synchronized 修饰了,使其变成了一个线程安全方法,但效率有点损耗;StringBuilder 类内部拼接方法则没有保证线程安全未被 synchronized 修饰,所以其只能在线程安全环境下使用,也正是其未被 synchronized 修饰,所以在字符串拼接的时候效率比 StringBuffer 高一点。
到这里,内心窃喜,没有被难倒。
今天的内容到这里就结束了,关注我,我们下期见。