> Java > Java베이스 > 본문

과거를 복습하고 새로운 것을 배운다. (2) Java의 문자열에 대한 심층적인 이해

coldplay.xixi
풀어 주다: 2020-09-19 09:36:20
앞으로
1664명이 탐색했습니다.

과거를 복습하고 새로운 것을 배운다. (2) Java의 문자열에 대한 심층적인 이해

관련 학습 권장사항: Java 기본 튜토리얼

지난 글에서는 String의 메모리와 일부 기능에 대한 심층 분석을 수행했습니다. 이 기사에서는 String과 관련된 다른 두 클래스인 StringBuilder 및 StringBuffer를 심층적으로 분석합니다. 이 두 클래스와 String의 관계는 무엇입니까? 먼저 아래 클래스 다이어그램을 살펴보겠습니다.

과거를 복습하고 새로운 것을 배운다. (2) Java의 문자열에 대한 심층적인 이해

그림에서 StringBuilder와 StringBuffer는 모두 AbstractStringBuilder를 상속하고 AbstractStringBuilder와 String은 공통 인터페이스 CharSequence를 구현하는 것을 볼 수 있습니다.

우리는 문자열이 일련의 문자로 구성된다는 것을 알고 있습니다. 문자열의 내부 구현은 char 배열(jdk9 이후 바이트 배열 기반)을 기반으로 하며 배열은 일반적으로 연속적인 메모리 영역이므로 다음과 같이 지정해야 합니다. 배열의 크기를 초기화합니다. 이전 기사에서 우리는 String의 내부 배열이 final로 선언되었기 때문에 변경할 수 없다는 것을 이미 알고 있었습니다. 동시에 String 문자 접합, 삽입, 삭제 및 기타 작업은 모두 새 개체를 인스턴스화하여 구현됩니다. 오늘 알아볼 StringBuilder와 StringBuffer는 String보다 더 동적입니다. 다음으로, 이 두 가지 범주를 함께 알아봅시다.

1. StringBuilder

StringBuilder의 상위 클래스인 AbstractStringBuilder에서 다음 코드를 볼 수 있습니다.

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;
}复制代码
로그인 후 복사

StringBuilder는 String과 마찬가지로 char 배열을 기반으로 구현됩니다. 차이점은 StringBuilder에는 최종 수정이 없다는 점입니다. 동적으로 변경되었습니다. 다음으로 StringBuilder의 매개변수 없는 구성 방법을 살펴보겠습니다. 코드는 다음과 같습니다.

 /**
     * Constructs a string builder with no characters in it and an
     * initial capacity of 16 characters.
     */
    public StringBuilder() {        super(16);
    }复制代码
로그인 후 복사

이 방법에서는 AbstractStringBuilder로 이동하여 구성 방법이 다음과 같은지 확인합니다.

    /**
     * Creates an AbstractStringBuilder of the specified capacity.
     */
    AbstractStringBuilder(int capacity) {
        value = new char[capacity];
    }复制代码
로그인 후 복사

AbstractStringBuilder의 구성 방법은 내부적으로 용량을 초기화합니다. 즉, StringBuilder는 기본적으로 용량이 16인 char[] 배열을 초기화합니다. 매개변수 없는 구성 외에도 StringBuilder는 여러 구성 방법을 제공합니다. 소스 코드는 다음과 같습니다.

 /**
     * Constructs a string builder with no characters in it and an
     * initial capacity specified by the {@code capacity} argument.
     *
     * @param      capacity  the initial capacity.
     * @throws     NegativeArraySizeException  if the {@code capacity}
     *               argument is less than {@code 0}.
     */
    public StringBuilder(int capacity) {        super(capacity);
    }    /**
     * Constructs a string builder initialized to the contents of the
     * specified string. The initial capacity of the string builder is
     * {@code 16} plus the length of the string argument.
     *
     * @param   str   the initial contents of the buffer.
     */
    public StringBuilder(String str) {        super(str.length() + 16);
        append(str);
    }    /**
     * Constructs a string builder that contains the same characters
     * as the specified {@code CharSequence}. The initial capacity of
     * the string builder is {@code 16} plus the length of the
     * {@code CharSequence} argument.
     *
     * @param      seq   the sequence to copy.
     */
    public StringBuilder(CharSequence seq) {        this(seq.length() + 16);
        append(seq);
    }复制代码
로그인 후 복사

이 코드의 첫 번째 방법은 지정된 용량으로 StringBuilder를 초기화합니다. 다른 두 생성 메서드는 각각 String 및 CharSequence를 전달하여 StringBuilder를 초기화할 수 있습니다. 이 두 생성 메서드의 용량은 16으로 전달된 문자열 길이에 추가됩니다.

1. StringBuilder의 추가 연산 및 확장

이전 글에서 StringBuilder의 추가 메소드를 통해 효율적인 문자열 접합이 수행될 수 있다는 것을 이미 알고 있었습니다. Append(String)를 예로 들면 StringBuilder의 Append는 부모 클래스의 Append 메소드를 호출하는 것을 알 수 있다. 실제로 Append뿐만 아니라 StringBuilder 클래스의 문자열을 연산하는 거의 모든 메소드가 부모 클래스를 통해 구현된다. Append 메소드의 소스코드는 다음과 같습니다.

    // StringBuilder
    @Override
    public StringBuilder append(String str) {        super.append(str);        return this;
    }    
  // 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;
    }复制代码
로그인 후 복사

Append 메소드의 첫 번째 줄에서는 먼저 Null 검사를 수행하고, Null과 같을 경우 AppendNull 메소드가 호출됩니다. 소스 코드는 다음과 같습니다.

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;
    }复制代码
로그인 후 복사

appendNull 메소드에서는 문자열 배열 용량이 재충전되었는지 확인하기 위해 verifyCapacityInternal이 먼저 호출됩니다. 아래에서 verifyCapacityInternal 메소드를 자세히 분석합니다. 다음으로 char[] 배열 값에 "null" 문자가 추가된 것을 확인할 수 있습니다.

위에서 StringBuilder 내부 배열의 기본 용량은 16이라고 언급했습니다. 따라서 문자열을 이어붙일 때 먼저 char[] 배열의 용량이 충분한지 확인해야 합니다. 따라서 char[] 배열의 용량이 충분한지 확인하기 위해 AppendNull 메서드와 Append 메서드 모두에서 verifyCapacityInternal 메서드를 호출합니다. 용량이 충분하지 않으면 배열이 확장됩니다.

private void ensureCapacityInternal(int minimumCapacity) {        // overflow-conscious code
        if (minimumCapacity - value.length > 0)
            expandCapacity(minimumCapacity);
    }复制代码
로그인 후 복사

접속된 문자를 해석하는 방법은 다음과 같습니다. 문자열 길이가 문자열 배열의 길이보다 길면 ExpandCapacity를 호출하여 용량을 확장합니다.

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);
    }复制代码
로그인 후 복사

expandCapacity의 논리도 매우 간단합니다. 먼저 원래 배열의 길이에 2를 곱하고 2를 더하여 확장된 배열 길이를 계산합니다. 다음으로 newCapacity가 maximumCapacity보다 작을 경우 newCapacity에 maximumCapacity 값을 할당한다고 판단한다. ExpandCapacity 메소드가 호출되는 곳이 2곳 이상이므로 안전성을 확보하기 위해 이 코드를 추가합니다.

다음 코드 문장이 매우 흥미롭습니다. newCapacity와 maximumCapacity가 0보다 작을 수 있나요? maximumCapacity가 0보다 작으면 OutOfMemoryError 예외가 발생합니다. 실제로는 범위를 벗어났기 때문에 0보다 작습니다. 우리는 컴퓨터에 저장된 모든 것이 이진수라는 것을 알고 있으며 2를 곱하는 것은 1비트를 왼쪽으로 이동하는 것과 같습니다. 바이트를 예로 들면, 바이트는 8비트로 구성됩니다. 부호 있는 숫자의 가장 왼쪽 비트는 부호 비트입니다. 양수는 0이고 음수는 1입니다. 그러면 바이트가 나타낼 수 있는 크기 범위는 [-128~127]이고 숫자가 127보다 크면 범위를 벗어납니다. 즉, 가장 왼쪽 부호 비트는 다음 비트에서 1로 대체됩니다. 왼쪽에는 음수가 표시됩니다. 물론 byte가 아니라 int이지만 원리는 동일합니다.

另外在这个方法的最后一句通过Arrays.copyOf进行了一个数组拷贝,其实Arrays.copyOf在上篇文章中就有见到过,在这里不妨来分析一下这个方法,看源码:

 public static char[] copyOf(char[] original, int newLength) {        char[] copy = new char[newLength];
        System.arraycopy(original, 0, copy, 0,
                         Math.min(original.length, newLength));        return copy;
    }复制代码
로그인 후 복사

咦?copyOf方法中竟然也去实例化了一个对象!!那不会影响性能吗?莫慌,看一下这里仅仅是实例化了一个newLength长度的空数组,对于数组的初始化其实仅仅是指针的移动而已,浪费的性能可谓微乎其微。接着这里通过System.arraycopy的native方法将原数组复制到了新的数组中。

2.StringBuilder的subString()方法toString()方法

StringBuilder中其实没有subString方法,subString的实现是在StringBuilder的父类AbstractStringBuilder中的。它的代码非常简单,源码如下:

public String substring(int start, int end) {        if (start < 0)            throw new StringIndexOutOfBoundsException(start);        if (end > count)            throw new StringIndexOutOfBoundsException(end);        if (start > end)            throw new StringIndexOutOfBoundsException(end - start);        return new String(value, start, end - start);
    }复制代码
로그인 후 복사

在进行了合法判断之后,substring直接实例化了一个String对象并返回。这里和String的subString实现其实并没有多大差别。 而StringBuilder的toString方法的实现其实更简单,源码如下:

 @Override
    public String toString() {        // Create a copy, don&#39;t share the array
        return new String(value, 0, count);
    }复制代码
로그인 후 복사

这里直接实例化了一个String对象并将StringBuilder中的value传入,我们来看下String(value, 0, count)这个构造方法:

    public String(char value[], int offset, int count) {        if (offset < 0) {            throw new StringIndexOutOfBoundsException(offset);
        }        if (count < 0) {            throw new StringIndexOutOfBoundsException(count);
        }        // Note: offset or count might be near -1>>>1.
        if (offset > value.length - count) {            throw new StringIndexOutOfBoundsException(offset + count);
        }        this.value = Arrays.copyOfRange(value, offset, offset+count);
    }复制代码
로그인 후 복사

可以看到,在String的这个构造方法中又通过Arrays.copyOfRange方法进行了数组拷贝,Arrays.copyOfRange的源码如下:

   public static char[] copyOfRange(char[] original, int from, int to) {        int newLength = to - from;        if (newLength < 0)            throw new IllegalArgumentException(from + " > " + to);        char[] copy = new char[newLength];
        System.arraycopy(original, from, copy, 0,
                         Math.min(original.length - from, newLength));        return copy;
    }复制代码
로그인 후 복사

Arrays.copyOfRange与Arrays.copyOf类似,内部都是重新实例化了一个char[]数组,所以String构造方法中的this.value与传入进来的value不是同一个对象。意味着StringBuilder在每次调用toString的时候生成的String对象内部的char[]数组并不是同一个!这里立一个Falg

3.StringBuilder的其它方法

StringBuilder除了提供了append方法、subString方法以及toString方法外还提供了还提供了插入(insert)、删除(delete、deleteCharAt)、替换(replace)、查找(indexOf)以及反转(reverse)等一些列的字符串操作的方法。但由于实现都非常简单,这里就不再赘述了。

二、StringBuffer

在第一节已经知道,StringBuilder的方法几乎都是在它的父类AbstractStringBuilder中实现的。而StringBuffer同样继承了AbstractStringBuilder,这就意味着StringBuffer的功能其实跟StringBuilder并无太大差别。我们通过StringBuffer几个方法来看

     /**
     * A cache of the last value returned by toString. Cleared
     * whenever the StringBuffer is modified.
     */
    private transient char[] toStringCache;    @Override
    public synchronized StringBuffer append(String str) {
        toStringCache = null;        super.append(str);        return this;
    }    /**
     * @throws StringIndexOutOfBoundsException {@inheritDoc}
     * @since      1.2
     */
    @Override
    public synchronized StringBuffer delete(int start, int end) {
        toStringCache = null;        super.delete(start, end);        return this;
    }  /**
     * @throws StringIndexOutOfBoundsException {@inheritDoc}
     * @since      1.2
     */
    @Override
    public synchronized StringBuffer insert(int index, char[] str, int offset,                                            int len)
    {
        toStringCache = null;        super.insert(index, str, offset, len);        return this;
    }@Override
    public synchronized String substring(int start) {        return substring(start, count);
    }    
// ...复制代码
로그인 후 복사

可以看到在StringBuffer的方法上都加上了synchronized关键字,也就是说StringBuffer的所有操作都是线程安全的。所以,在多线程操作字符串的情况下应该首选StringBuffer。 另外,我们注意到在StringBuffer的方法中比StringBuilder多了一个toStringCache的成员变量 ,从源码中看到toStringCache是一个char[]数组。它的注释是这样描述的:

toString返回的最后一个值的缓存,当StringBuffer被修改的时候该值都会被清除。

我们再观察一下StringBuffer中的方法,发现只要是操作过操作过StringBuffer中char[]数组的方法,toStringCache都被置空了!而没有操作过字符数组的方法则没有对其做置空操作。另外,注释中还提到了 toString方法,那我们不妨来看一看StringBuffer中的 toString,源码如下:

   @Override
    public synchronized String toString() {        if (toStringCache == null) {
            toStringCache = Arrays.copyOfRange(value, 0, count);
        }        return new String(toStringCache, true);
    }复制代码
로그인 후 복사

这个方法中首先判断当toStringCache 为null时会通过 Arrays.copyOfRange方法对其进行赋值,Arrays.copyOfRange方法上边已经分析过了,他会重新实例化一个char[]数组,并将原数组赋值到新数组中。这样做有什么影响呢?细细思考一下不难发现在不修改StringBuffer的前提下,多次调用StringBuffer的toString方法,生成的String对象都共用了同一个字符数组--toStringCache。这里是StringBuffer和StringBuilder的一点区别。至于StringBuffer中为什么这么做其实并没有很明确的原因,可以参考StackOverRun 《Why StringBuffer has a toStringCache while StringBuilder not?》中的一个回答:

1.因为StringBuffer已经保证了线程安全,所以更容易实现缓存(StringBuilder线程不安全的情况下需要不断同步toStringCache) 2.可能是历史原因

三、 总结

本篇文章到此就结束了。《深入理解Java中的字符串》通过两篇文章深入的分析了String、StringBuilder与StringBuffer三个字符串相关类。这块内容其实非常简单,只要花一点时间去读一下源码就很容易理解。当然,如果你没看过此部分源码相信这篇文章能够帮助到你。不管怎样,相信大家通过阅读本文还是能有一些收获。解了这些知识后可以帮助我们在开发中对字符串的选用做出更好的选择。同时,这块内容也是面试常客,相信大家读完本文去应对面试官的问题也会绰绰有余。

프로그래밍에 대해 더 자세히 알고 싶다면 php training 칼럼을 주목해주세요!

위 내용은 과거를 복습하고 새로운 것을 배운다. (2) Java의 문자열에 대한 심층적인 이해의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

관련 라벨:
원천:juejin.im
본 웹사이트의 성명
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.
인기 튜토리얼
더>
최신 다운로드
더>
웹 효과
웹사이트 소스 코드
웹사이트 자료
프론트엔드 템플릿
회사 소개 부인 성명 Sitemap
PHP 중국어 웹사이트:공공복지 온라인 PHP 교육,PHP 학습자의 빠른 성장을 도와주세요!