【Java】从源码角度了解 String

: Mar 21, 2016

: Java

: onlylemi/notes/tree/master/Java


类图

首先先看下类图(下图)

String类图

类定义

我们再看下每个类具体是怎么定义的(JDK1.8)

// CharSequence 类定义
public interface CharSequence {
...
}

// AbstractStringBuilder 类定义
abstract class AbstractStringBuilder implements Appendable, CharSequence {
...
}

// String 类定义
public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
...
}

// StringBuffer 类定义
public final class StringBuffer
    extends AbstractStringBuilder
    implements java.io.Serializable, CharSequence{
...
}

// StringBuilder 类定义
public final class StringBuilder
    extends AbstractStringBuilder
    implements java.io.Serializable, CharSequence {
...
}

从以上各个类的定义中,我们可以发现 CharSequence 是一个定义字符串操作的接口,AbstractStringBuilder、String、StringBuilder、StringBuffer 都实现了它,而 StringBuilder、StringBuffer 继承了 AbstractStringBuilder 这个抽象类。那他们之间到底有什么不一样呢?

区别

我相信大家应该都知道 String、StringBuilder、StringBuffer 的区别了。

  • String 字符串常量,不可变得对象。对其进行改变的同时会在串池中新生成一个 String 对象,进而指向这个对象。耗内存
  • StringBuffer 字符串变量(线程安全)
  • StringBulder 字符串变量(线程不安全)

可是这是怎么得到的呢(反正我之前就不是很清楚)?下面我就给大家从源码角度说说,如有错误,请批评指导!

String

String 的底层是一个数组,我们的操作都是基于这个数组来进行的

private final char value[];

再看 String 的部分构造函数,当新生成一个实例时,value 的长度就是你传入字符串的长度

// String 构造函数
public String() {
    this.value = new char[0];
}

public String(String original) {
    this.value = original.value;
    this.hash = original.hash;
}

public String(char value[]) {
    this.value = Arrays.copyOf(value, value.length);
}

同时可以再看看 String 的拼接函数 concat(),我们可以很明显的看出返回的是一个新的 String 对象,所以当你改变一个 String 时,它就指向一个新的地址,而不再之前的地址。

// String 拼接函数
public String concat(String str) {
    int otherLen = str.length();
    if (otherLen == 0) {
        return this;
    }
    int len = value.length;
    char buf[] = Arrays.copyOf(value, len + otherLen);
    str.getChars(buf, len);
    return new String(buf, true);
}

接下来我们再看看 StringBulder、StringBuffer,不过在介绍之前我们先看看他们的父类 AbstractStringBuilder。

AbstractStringBuilder

你会发现这个类中定义了两个变量

/**
 * The value is used for character storage.
 * 这个 value 用来存储你的字符串
 */
char[] value;

/**
 * The count is the number of characters used.
 * count 是这个 value 你占用了字符的个数,什么意思呢?比如说,你定义了一个 values[10],而你只存储了 “abcdefg”,你只占用了 7 个字符,所以你的 count 就是 7,而不是 10
 */
int count;

再看看构造函数,需要传入一个容量,也就是 value 数组的大小。

// AbstractStringBuilder 构造函数
AbstractStringBuilder(int capacity) {
    value = new char[capacity];
}
public int length() {
    return count;
}

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

从以上代码,我们会发现我们平常所调用的 length() 方法,其实得到都是本身字符串所占用的长度,而 capacity() 才是数组空间的容量。

那 StringBulder 是如何做拼接的呢,我们先看它的拼接函数

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

private AbstractStringBuilder appendNull() {
    int c = count;
    ensureCapacityInternal(c + 4);
    final char[] value = this.value;
    value[c++] = 'n';
    value[c++] = 'u';
    value[c++] = 'l';
    value[c++] = 'l';
    count = c;
    return this;
}

可以看出

  • 在调用 append() 的时候,不能传入 null,否则会调用 appendNull() 方法,直接给你拼接一个 “null” 的字符串,一定要切记,不能传空。
  • 拼接前都会调用 ensureCapacityInternal() 方法,来确保当前的 value 的容量是否够用,不够就加容量。从下面的方法中我们可以看出,每次增加容量都为原来的 2 倍再 +2
void expandCapacity(int minimumCapacity) {
    // 原来的 2 倍再 +2
    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);
}

同时,如果你觉得这样太浪费空间的话,你可以调用 trimToSize() 方法,这样你的 length() 就和 capacity() 一样了。

public void trimToSize() {
    if (count < value.length) {
        value = Arrays.copyOf(value, count);
    }
}

从以上介绍你会发现,value 会根据你字符串的大小进行扩充,但是始终都是在操作同一个 value,所以指向的始终是同一个地址。

接下来再看 StringBuilder、StringBuffer。

StringBuilder

StringBulder 它继承了 AbstractStringBuilder,看他的源码你会发现,其构造函数中指定了容量的大小,其他地方实现基本都是引用父类方法。

// 默认指定容量大小为 16
public StringBuilder() {
    super(16);
}

public StringBuilder(int capacity) {
    super(capacity);
}

// 如果传入字符串,则初始容量就为 str 的长度+16
public StringBuilder(String str) {
    super(str.length() + 16);
    append(str);
}

StringBuffer

而 StringBuffer 它也继承了 AbstractStringBuilder,看他的源码你会发现,其构造函数中也是指定了容量的大小,其他地方实现基本都是引用父类方法,不过都添加了 synchronized 关键字,所以应该知道,为什么 StringBuffer 是线程安全,而 StringBuilder 不是了吧。但是很明显加上线程控制会拖慢程序运行的速度,所以如果不需要线程控制,那么最好就用 StringBuilder。

// 默认指定容量大小为 16
public StringBuffer() {
    super(16);
}
    
public StringBuffer(int capacity) {
    super(capacity);
}

public StringBuffer(String str) {
    super(str.length() + 16);
    append(str);
}

补充

String 的 equals() 方法

  • 首先是对象判断是否为同一对象
  • 判断是否为 String
  • 判断每个字符是否相等
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;
}

String 的 hashCode() 方法

  • 31 奇素数
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;
}

END

通过以上学习,应该知道 String 基本原理了吧。后续将会从源码角度继续解析其他类,欢迎大家关注学习。


如果我的文章对您有所帮助,就请我喝杯咖啡吧^^


Messages