【Java基本功】一文读懂String及其包装类的实现原理

7次阅读

共计 7222 个字符,预计需要花费 19 分钟才能阅读完成。

String 作为 Java 中最常用的引用类型,相对来说基本上都比较熟悉,无论在平时的编码过程中还是在笔试面试中,String 都很受到青睐,然而,在使用 String 过程中,又有较多需要注意的细节之处。
String 的连接
<pre code-lang=””>@Testpublic void contact () { // 1 连接方式 String s1 = “a”; String s2 = “a”; String s3 = “a” + s2; String s4 = “a” + “a”; String s5 = s1 + s2; // 表达式只有常量时,编译期完成计算 // 表达式有变量时,运行期才计算,所以地址不一样 System.out.println(s3 == s4); //f System.out.println(s3 == s5); //f System.out.println(s4 == “aa”); //t}</pre>
String 类型的 intern
<pre code-lang=””>public void intern () { //2:string 的 intern 使用 //s1 是基本类型,比较值。s2 是 string 实例,比较实例地址 // 字符串类型用 equals 方法比较时只会比较值 String s1 = “a”; String s2 = new String(“a”); // 调用 intern 时, 如果 s2 中的字符不在常量池,则加入常量池并返回常量的引用 String s3 = s2.intern(); System.out.println(s1 == s2); System.out.println(s1 == s3);}</pre>
String 类型的 equals
<pre code-lang=””>// 字符串的 equals 方法 // public boolean equals(Object anObject) {// if (this == anObject) {// return true;//}// if (anObject instanceof String) {// String anotherString = (String)anObject;// int n = value.length;// if (n == anotherString.value.length) {// char v1[] = value;// char v2[] = anotherString.value;// int i = 0;// while (n– != 0) {// if (v1[i] != v2[i])// return false;// i++;// }// return true;// }// }// return false;// }</pre>
StringBuffer 和 Stringbuilder
底层是继承父类的可变字符数组 value
<pre code-lang=””>/* The value is used for character storage. */char[] value; 初始化容量为 16/* Constructs a string builder with no characters in it and an initial capacity of 16 characters. /public StringBuilder() {super(16);}这两个类的 append 方法都是来自父类 AbstractStringBuilder 的方法 public AbstractStringBuilder append(String str) {if (str == null) return appendNull(); int len = str.length(); ensureCapacityInternal(count + len); str.getChars(0, len, value, count); count += len; return this;}@Overridepublic StringBuilder append(String str) {super.append(str); return this;}@Overridepublic synchronized StringBuffer append(String str) {toStringCache = null; super.append(str); return this;}</pre>
append
<pre code-lang=””>Stringbuffer 在大部分涉及字符串修改的操作上加了 synchronized 关键字来保证线程安全,效率较低。String 类型在使用 + 运算符例如 String a = “a”a = a + a; 时,实际上先把 a 封装成 stringbuilder,调用 append 方法后再用 tostring 返回,所以当大量使用字符串加法时,会大量地生成 stringbuilder 实例,这是十分浪费的,这种时候应该用 stringbuilder 来代替 string。</pre>
扩容
<pre code-lang=””># 注意在 append 方法中调用到了一个函数 ensureCapacityInternal(count + len); 该方法是计算 append 之后的空间是否足够,不足的话需要进行扩容 public void ensureCapacity(int minimumCapacity) {if (minimumCapacity > 0) ensureCapacityInternal(minimumCapacity);}private void ensureCapacityInternal(int minimumCapacity) {// overflow-conscious code if (minimumCapacity – value.length > 0) {value = Arrays.copyOf(value, newCapacity(minimumCapacity)); }}如果新字符串长度大于 value 数组长度则进行扩容扩容后的长度一般为原来的两倍 + 2;假如扩容后的长度超过了 jvm 支持的最大数组长度 MAX_ARRAY_SIZE。考虑两种情况如果新的字符串长度超过 int 最大值,则抛出异常,否则直接使用数组最大长度作为新数组的长度。private int hugeCapacity(int minCapacity) {if (Integer.MAX_VALUE – minCapacity < 0) {// overflow throw new OutOfMemoryError(); } return (minCapacity > MAX_ARRAY_SIZE) ? minCapacity : MAX_ARRAY_SIZE;}</pre>
删除
<pre code-lang=””> 这两个类型的删除操作:都是调用父类的 delete 方法进行删除 public AbstractStringBuilder delete(int start, int end) {if (start < 0) throw new StringIndexOutOfBoundsException(start); if (end > count) end = count; if (start > end) throw new StringIndexOutOfBoundsException(); int len = end – start; if (len > 0) {System.arraycopy(value, start+len, value, start, count-end); count -= len; } return this;}事实上是将剩余的字符重新拷贝到字符数组 value。</pre>
这里用到了 system.arraycopy 来拷贝数组,速度是比较快的
system.arraycopy 方法
<pre code-lang=””> 转自知乎:在主流高性能的 JVM 上(HotSpot VM 系、IBM J9 VM 系、JRockit 系等等),可以认为 System.arraycopy()在拷贝数组时是可靠高效的——如果发现不够高效的情况,请报告 performance bug,肯定很快就会得到改进。java.lang.System.arraycopy()方法在 Java 代码里声明为一个 native 方法。所以最 naïve 的实现方式就是通过 JNI 调用 JVM 里的 native 代码来实现。</pre>
String 的不可变性
关于 String 的不可变性,这里转一个不错的回答
什么是不可变?
String 不可变很简单,如下图,给一个已有字符串 ”abcd” 第二次赋值成 ”abcedl”,不是在原内存地址上修改数据,而是重新指向一个新对象,新地址。
String 为什么不可变?
翻开 JDK 源码,java.lang.String 类起手前三行,是这样写的:
<pre code-lang=””>public final class String implements java.io.Serializable, Comparable<String>, CharSequence {/* String 本质是个 char 数组. 而且用 final 关键字修饰./ private final char value[]; … … } </pre>
首先 String 类是用 final 关键字修饰,这说明 String 不可继承。再看下面,String 类的主力成员字段 value 是个 char[]数组,而且是用 final 修饰的。
final 修饰的字段创建以后就不可改变。有的人以为故事就这样完了,其实没有。因为虽然 value 是不可变,也只是 value 这个引用地址不可变。挡不住 Array 数组是可变的事实。
Array 的数据结构看下图。
也就是说 Array 变量只是 stack 上的一个引用,数组的本体结构在 heap 堆。
String 类里的 value 用 final 修饰,只是说 stack 里的这个叫 value 的引用地址不可变。没有说堆里 array 本身数据不可变。看下面这个例子,
<pre code-lang=””>final int[] value={1,2,3};int[] another={4,5,6}; value=another; // 编译器报错,final 不可变 value 用 final 修饰,编译器不允许我把 value 指向堆区另一个地址。但如果我直接对数组元素动手,分分钟搞定。final int[] value={1,2,3}; value[2]=100; // 这时候数组里已经是 {1,2,100} 所以 String 是不可变,关键是因为 SUN 公司的工程师。在后面所有 String 的方法里很小心的没有去动 Array 里的元素,没有暴露内部成员字段。private final char value[] 这一句里,private 的私有访问权限的作用都比 final 大。而且设计师还很小心地把整个 String 设成 final 禁止继承,避免被其他人继承后破坏。所以 String 是不可变的关键都在底层的实现,而不是一个 final。考验的是工程师构造数据类型,封装数据的功力。</pre>
不可变有什么好处?
这个最简单地原因,就是为了安全。看下面这个场景(有评论反应例子不够清楚,现在完整地写出来),一个函数 appendStr()在不可变的 String 参数后面加上一段“bbb”后返回。appendSb()负责在可变的 StringBuilder 后面加“bbb”。
总结以下 String 的不可变性。
1 首先 final 修饰的类只保证不能被继承,并且该类的对象在堆内存中的地址不会被改变。2 但是持有 String 对象的引用本身是可以改变的,比如他可以指向其他的对象。
3 final 修饰的 char 数组保证了 char 数组的引用不可变。但是可以通过 char[0] = ‘a’ 来修改值。不过 String 内部并不提供方法来完成这一操作,所以 String 的不可变也是基于代码封装和访问控制的。

举个例子
<pre code-lang=””>final class Fi {int a; final int b = 0; Integer s;}final char[]a = {‘a’};final int[]b = {1};@Testpublic void final 修饰类() { // 引用没有被 final 修饰,所以是可变的。//final 只修饰了 Fi 类型,即 Fi 实例化的对象在堆中内存地址是不可变的。// 虽然内存地址不可变,但是可以对内部的数据做改变。Fi f = new Fi(); f.a = 1; System.out.println(f); f.a = 2; System.out.println(f); // 改变实例中的值并不改变内存地址。
Fi ff = f; // 让引用指向新的 Fi 对象,原来的 f 对象由新的引用 ff 持有。// 引用的指向改变也不会改变原来对象的地址 f = new Fi(); System.out.println(f); System.out.println(ff);}这里的对 f.a 的修改可以理解为 char[0] = ‘a’ 这样的操作。只改变数据值,不改变内存值。</pre>

要理解 String 里的 intern 方法,就要注意基本数据类型的拆箱装箱,以及对常量池的理解。
常量池和自动拆箱装箱
<pre code-lang=””> 自动拆箱和装箱的原理其实与常量池有关。3.1 存在栈中:public void(int a){int i = 1;int j = 1;}方法中的 i 存在虚拟机栈的局部变量表里,i 是一个引用,j 也是一个引用,它们都指向局部变量表里的整型值 1.int a 是传值引用,所以 a 也会存在局部变量表。
3.2 存在堆里:class A{int i = 1;A a = new A();}i 是类的成员变量。类实例化的对象存在堆中,所以成员变量也存在堆中,引用 a 存的是对象的地址,引用 i 存的是值,这个值 1 也会存在堆中。可以理解为引用 i 指向了这个值 1。也可以理解为 i 就是 1.
3.3 包装类对象怎么存其实我们说的常量池也可以叫对象池。比如 String a= new String(“a”).intern()时会先在常量池找是否有“a” 对象如果有的话直接返回“a” 对象在常量池的地址,即让引用 a 指向常量”a” 对象的内存地址。public native String intern();Integer 也是同理。</pre>
下图是 Integer 类型在常量池中查找同值对象的方法。
<pre code-lang=””>public static Integer valueOf(int i) {if (i >= IntegerCache.low && i <= IntegerCache.high) return IntegerCache.cache[i + (-IntegerCache.low)]; return new Integer(i);}private static class IntegerCache {static final int low = -128; static final int high; static final Integer cache[];
static {// high value may be configured by property int h = 127; String integerCacheHighPropValue = sun.misc.VM.getSavedProperty(“java.lang.Integer.IntegerCache.high”); if (integerCacheHighPropValue != null) {try { int i = parseInt(integerCacheHighPropValue); i = Math.max(i, 127); // Maximum array size is Integer.MAX_VALUE h = Math.min(i, Integer.MAX_VALUE – (-low) -1); } catch(NumberFormatException nfe) {// If the property cannot be parsed into an int, ignore it.} } high = h;
cache = new Integer[(high – low) + 1]; int j = low; for(int k = 0; k < cache.length; k++) cache[k] = new Integer(j++);
// range [-128, 127] must be interned (JLS7 5.1.7) assert IntegerCache.high >= 127; }
private IntegerCache() {}}</pre>

所以基本数据类型的包装类型可以在常量池查找对应值的对象,找不到就会自动在常量池创建该值的对象。
而 String 类型可以通过 intern 来完成这个操作。
JDK1.7 后,常量池被放入到堆空间中,这导致 intern()函数的功能不同,具体怎么个不同法,且看看下面代码,这个例子是网上流传较广的一个例子,分析图也是直接粘贴过来的,这里我会用自己的理解去解释这个例子:
<pre code-lang=””> view plain copyString s = new String(“1”); s.intern(); String s2 = “1”; System.out.println(s == s2); String s3 = new String(“1”) + new String(“1”); s3.intern(); String s4 = “11”; System.out.println(s3 == s4); 输出结果为:view plain copyJDK1.6 以及以下:false false JDK1.7 以及以上:false true</pre>

JDK1.6 查找到常量池存在相同值的对象时会直接返回该对象的地址。
JDK 1.7 后,intern 方法还是会先去查询常量池中是否有已经存在,如果存在,则返回常量池中的引用,这一点与之前没有区别,区别在于,如果在常量池找不到对应的字符串,则不会再将字符串拷贝到常量池,而只是在常量池中生成一个对原字符串的引用。
那么其他字符串在常量池找值时就会返回另一个堆中对象的地址。

正文完
 0