小护士青铜上分系列之《Java源码阅读》第二篇String-StringBuffer-StringBuilder

小护士青铜上分系列之《Java源码阅读》第二篇String-StringBuffer-StringBuilder

Hello,我是小护士。今天为大家带来StringStringBufferStringBuilder的源码阅读,虽然这三个类都是面试必问的,但其实内容很简单,也不用死记硬背。开门见山,首先有请String登场。

1. String 源码阅读

String的内部结构比较简单,只有四个字段和几个重要方法:

public final class String implements java.io.Serializable, Comparable<String>, CharSequence {

    private final char value[];
    private int hash;

    private static final long serialVersionUID = -6849794470754667710L;
    private static final ObjectStreamField[] serialPersistentFields = new ObjectStreamField[0];

...
}

1.1 String 字段解读

  • char value[]:Java模仿C语言的字符串实现,使用字节数组做存储;使用final意味着数组一旦创建就不能变更。
  • int hash:哈希值并非是初始化时就计算好,而是调用hashCode()才开始计算并缓存到该字段中。
  • serialVersionUID:Java序列化机制需要用的,这里暂时不展开序列化相关内容。
  • serialPersistentFields:Java序列化机制需要用的,也是不展开讲,直到nio包才讲。

1.2 String length() 方法

public int length() {
    return value.length;
}

别看上面length()方法这么简单,有没有思考过为什么char[]数组会自带length字段?为什么Java的字符串不需要'\0'作为结束符,而C/C++就需要?

真正原因在于JVM是这样定义数组结构的:

template <typename T>
class Array: public MetaspaceObj {
...  
protected:
  int _length;                                 // the number of array elements
  T   _data[1];                                // the array memory

  void initialize(int length) {
    _length = length;
  }
...
}

JVM数组源码地址:
https://github.com/unofficial-openjdk/openjdk/blob/jdk/jdk/src/hotspot/share/oops/array.hpp

虽然是C++代码,但并不妨碍Java开发者阅读Array类的定义,在protected那里就声明了int _lengthT _data[1]两个重要字段。其中,_length就是用来标记该字符串的长度,通过这个字段可以计算出字符串在内存中的结束位置。因此,Java字符串就不需要\0作为字符串结束符来标记结束位置。而C/C++的数组就真的是纯数组了,例如_data[1]字段。

1.3 String toString() & toCharArray() 方法

public String toString() {
    return this;
}

public char[] toCharArray() {
    char result[] = new char[value.length];
    System.arraycopy(value, 0, result, 0, value.length);
    return result;
}

如果想要把字符串深拷贝一份出来,使用toString()方法是不行的,因为它只是做浅拷贝处理,直接返回对象引用。所以,要使用toCharArray()方法,通过System.arraycopy()这个本地方法实现深拷贝。

关于这个本地方法,小护士在这里稍微跟踪一下代码,路径如下:

1. https://github.com/unofficial-openjdk/openjdk/blob/jdk/jdk/src/java.base/share/native/libjava/System.c
找到 JVM_ArrayCopy 关键词 。往下走:

2. https://github.com/unofficial-openjdk/openjdk/blob/jdk/jdk/src/hotspot/share/include/jvm.h
在h文件找到 JVM_ArrayCopy(...); 函数声明,追踪cpp文件:

3. https://github.com/unofficial-openjdk/openjdk/blob/jdk/jdk/src/hotspot/share/prims/jvm.cpp
在cpp文件中找到 JVM_ENTRY(void, JVM_ArrayCopy(...))...JVM_END
在方法定义中找到真正做copy的函数s->klass()->copy_array(s, src_pos, d, dst_pos, length, thread);
往上几行找到 arrayOop s = arrayOop(JNIHandles::resolve_non_null(src));
这里的 arrayOop 是类型的别名。

4. https://github.com/unofficial-openjdk/openjdk/blob/jdk/jdk/src/hotspot/share/oops/access.inline.hpp
看头文件 access.inline.hpp ,找到 access.hpp 。

5. https://github.com/unofficial-openjdk/openjdk/blob/jdk/jdk/src/hotspot/share/oops/access.hpp
寻找 arrayOop 声明,从access.hpp找到 include "oops/oopsHierrachy.hpp"

6. https://github.com/unofficial-openjdk/openjdk/blob/jdk/jdk/src/hotspot/share/oops/oopsHierarchy.hpp
在 oopsHierarchy.hpp 中找到以下类型别名定义 typedef class arrayOopDesc* arrayOop;
可知类型真名 arrayOopDesc

7. https://github.com/unofficial-openjdk/openjdk/blob/jdk/jdk/src/hotspot/share/oops/arrayOop.hpp
根据 oopsHierarchy.hpp 注释描述,由于 arrayOopsDesc 是视为抽象类,在编译的时候C++编译器会根据头文件选择性地找到实现类。因此,回看 jvm.cpp 就会发现只仅仅包含了 objArrayOop.inline.hpp 。

8. https://github.com/unofficial-openjdk/openjdk/blob/jdk/jdk/src/hotspot/share/oops/objArrayOop.hpp
通过 objArrayOop.inline.hpp 直接找到对应的 objArrayOop.hpp。发现klass字段正是使用 objArrayKlass

9. https://github.com/unofficial-openjdk/openjdk/blob/jdk/jdk/src/hotspot/share/oops/objArrayKlass.cpp
最后经过 klass() 返回的 objArrayKlass 指针来到 void ObjArrayKlass::copy_array(...){...}
该方法在底部做内部函数调用 do_copy(...)
这里面调用了 ArrayAccess<>::oop_arraycopy(...) 函数做拷贝。

10. https://github.com/unofficial-openjdk/openjdk/blob/jdk/jdk/src/hotspot/share/oops/access.hpp
ArrayAccess 是在 access.hpp 里面声明的。调用了它的内部类 AccessInternal

11. https://github.com/unofficial-openjdk/openjdk/blob/jdk/jdk/src/hotspot/share/oops/accessBackend.cpp
AccessInternal 在 accessBackend.cpp 中声明。里面的方法大部分都是使用 Copy 工具类。

12. https://github.com/unofficial-openjdk/openjdk/blob/jdk/jdk/src/hotspot/share/utilities/copy.cpp
最后来到了这里。嗯,追踪结束。小护士表示很满意。

1.4 String intern() 方法

public native String intern();

又是一个本地方法,听说是可以把字符串变量转为常量池的常量。小护士再次跟踪JVM源码看个究竟:

1. https://github.com/unofficial-openjdk/openjdk/blob/jdk/jdk/src/java.base/share/native/libjava/String.c
先找到String.c文件,找到函数定义。

JNIEXPORT jobject JNICALL
Java_java_lang_String_intern(JNIEnv *env, jobject this)
{
    return JVM_InternString(env, this);
}

2. https://github.com/unofficial-openjdk/openjdk/blob/jdk/jdk/src/hotspot/share/prims/jvm.cpp
再次来到jvm.cpp核心文件,找到函数定义。

JVM_ENTRY(jstring, JVM_InternString(JNIEnv *env, jstring str))
  JVMWrapper("JVM_InternString");
  JvmtiVMObjectAllocEventCollector oam;
  if (str == NULL) return NULL;
  oop string = JNIHandles::resolve_non_null(str);
  oop result = StringTable::intern(string, CHECK_NULL);
  return (jstring) JNIHandles::make_local(env, result);
JVM_END 

其中,StringTable::intern(string, CHECK_NULL);才是真正做intern()处理的。而StringTable是字符串表,Java常量池的实现。

3. https://github.com/unofficial-openjdk/openjdk/blob/jdk/jdk/src/hotspot/share/classfile/stringTable.cpp
追查到stringTable.cpp文件,找到intern()函数定义。

oop StringTable::intern(Handle string_or_null_h, jchar* name, int len, TRAPS) {
  // shared table always uses java_lang_String::hash_code
  unsigned int hash = java_lang_String::hash_code(name, len);
  oop found_string = StringTable::the_table()->lookup_shared(name, len, hash);
  if (found_string != NULL) {
    return found_string;
  }
  if (StringTable::_alt_hash) {
    hash = hash_string(name, len, true);
  }
  return StringTable::the_table()->do_intern(string_or_null_h, name, len, hash, CHECK_NULL);
}

可以看到有两个函数主导intern()逻辑。look_shared()是查找目标字符串是否已经在字符串表中,有就可以复用。如果没有,则执行do_intern(),把目标字符串加入到字符串表中。这里说字符串表(StringTable)就是字符串常量池。

到了这里就不需要再深入往下看look_shared()do_intern()的具体代码细节了。已经超纲了。

1.5 String indexOf() 方法

static int indexOf(
    char[] source, 
    int sourceCount,
    char[] target,
    int targetOffset,
    int targetCount,
    int fromIndex) {
...
    char first = target[targetOffset];
    int max = sourceOffset + (sourceCount - targetCount);

    for (int i = sourceOffset + fromIndex; i <= max; i++) {
    /* Look for first character. */
        if (source[i] != first) {
            while (++i <= max && source[i] != first);
        }

        /* Found first character, now look at the rest of v2 */
        if (i <= max) {
            int j = i + 1;
            int end = j + targetCount - 1;
            for (int k = targetOffset + 1; j < end && source[j] == target[k]; j++, k++);

            if (j == end) {
                /* Found whole string. */
                return i - sourceOffset;
            }
        }
    }
    return -1;
}

如果看到这么大段代码,千万别怂。其实很多谣言说Java String 的查找方法indexOf(String str)用了KMP算法,其实不是。仍然是一个一个字符去匹配,找到第一个匹配的就往下继续匹配剩余字符。这是一个简单的两层循环处理,并不是很高深算法。

号外,这个方法还会被JVM做深度优化。直接替换掉字节码,换成JVM根据具体硬件环境优化过的函数。
关键词:HotSpot JVM intrinsics

1.6 String hashCode() & equal() 方法

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;
}

public int hashCode() {
    int h = hash;
    if (h == 0 && value.length > 0) {
        char val[] = value;

        for (int i = 0; i < value.length; i++) {
            h = 31 * h + val[i];
        }
        hash = h;
    }
    return h;
}   

equal()方法的逻辑比较朴素,先对比是不是同一个对象,若不是,则把两个字符串的char[] value进行数组长度比较,若长度相同,则对它们的数组元素进行逐一比较。

hashCode()方法的逻辑稍微偏向数学化,因为哈希码的类型是int,也就是32位字长。众所周知,对于哈希码来说,最好是通过%求余算法得出大范围的数对应小范围的数。但字符串是由连续的字符组成的字符数组,而又因为ascii码和unicode码可以把字符与整数进行一一对应,因此,hashCode()方法直接遍历字符数组元素,通过h = 31 * h + val[i]的累加方式进行哈希码的计算。

那么问题来了,为什么hashCode() ×31 哈 希 码 × 31 ,目前没有明确答案。小护士只知道如果是乘以32的话,就可以转化为左移运算, h×32=h<<5 h × 32 = h << 5 。有部分观点说因为31是素数、质数,具有某种数学意义,小护士表示保留意见。当然,由于31 * hval[i]相加,而不是val[i]纯粹叠加,因此大大减少了哈希碰撞概率。

哈希码是int的有符号数,其算法是纯粹的累加,该值会出现正溢出,也就是会出现负数,而且哈希碰撞的概率还是有的。

2. StringBuffer 源码阅读

StringBuffer是“线程安全”的字符串拼接类,但并非所有方法都是线程安全的,很大部分insert()方法都直接调用父类的方法。StringBuffer的所有append()方法都加了synchronized关键字修饰,以表示这是线程安全的同步方法。

小护士拿其中一个append()方法举例子:

...
private transient char[] toStringCache;
...
@Override
public synchronized StringBuffer append(String str) {
    toStringCache = null;
    super.append(str);
    return this;
}
...
@Override
public synchronized String toString() {
    if (toStringCache == null) {
        toStringCache = Arrays.copyOfRange(value, 0, count);
    }
    return new String(toStringCache, true);
}
...

toStringCache是一个缓存字段,每次StringBufferchar[] value被修改的时候都会清空缓存。而该缓存值会在toString()被调用的时候才会深拷贝字符数组,刷新缓存,这里return new String(toStringCache, true);的用法属于历史遗留问题,里面再次深拷贝了一次字符数组。

深度思考:为什么需要toStringCache
简单回答:因为StringBuffer部分insert()方法不是线程安全的。

3. StringBuilder 源码阅读

StringBuilder则是StringBuffer的非线程安全版本,用于局部变量处理的场景。

来看看它的其中一个append()方法:

...
@Override
public StringBuilder append(String str) {
    super.append(str);
    return this;
}
...
@Override
public String toString() {
    // Create a copy, don't share the array
    return new String(value, 0, count);
}
...

StringBuilder没有toStringCache字段做缓存,它的toString()方法也没有出现两次深拷贝。

4. 三者异同对比

相同点:

  • 都以char[] value作为内部存储的数据结构。
  • 都是字符串相关的类。
  • StringBufferStringBuilder的抽象类都是AbstractStringBuilder

不同点:

  • String是Java内置的特殊数据类型,StringBufferStringBuilder则是字符串拼接类
  • Stringchar[] value加了final,不可变;AbstractStringBuilderchar[] value则可变。
  • StringBuilder线程不安全,StringBuffer线程安全但部分方法非线程安全,比如insert()方法。

实际面试过程中,面试官不会让你说得太详细,大概说出上面六个点也差不多了。

5. 故障问题猜想

5.1 数组长度无上限问题

由于AbstractStringBuilderchar[] value数组长度没有上限,如果无限添加字符串,直到超出int所表示的最大值,导致发生正溢出变为负数,就会抛出OutOfMemoryError错误。

void expandCapacity(int minimumCapacity) {
    int newCapacity = value.length * 2 + 2;
    if (newCapacity - minimumCapacity < 0)
        newCapacity = minimumCapacity;
    if (newCapacity < 0) {
        if (minimumCapacity < 0) // overflow
            throw new OutOfMemoryError();
        newCapacity = Integer.MAX_VALUE;
    }
    value = Arrays.copyOf(value, newCapacity);
}

5.2 AbstractStringBuilder扩容缩容过程中,count字段一致性问题

因为AbstractStringBuilderchar[] value是可变的,可以通过append()等扩容,也可以通过delete()等缩容。问题来了,因为这些方法存在线程安全问题,count字段作为char[] value的数组长度标记在执行过程就会容易出现数据不一致问题。

abstract class AbstractStringBuilder implements Appendable, CharSequence {
    /**
     * The value is used for character storage.
     */
    char[] value;

    /**
     * The count is the number of characters used.
     */
    int count;
...
}

学习交流

为了方便大家日后技术交流,小护士在这里特别推荐如下交流方式:

QQ群:JAVA高级交流(329019348)
QQ群:大宽宽的技术交流群(317060090)

如果有对博文内容有任何技术问题,欢迎评论留言或加群讨论,谢谢大家。

你可能感兴趣的:(青铜上分,Java源码)