関連する学習の推奨事項: java 基本チュートリアル
前回の記事では、 String のメモリとそのいくつかの機能。この記事では、String に関連する他の 2 つのクラス、StringBuilder と StringBuffer を詳しく分析します。これら 2 つのクラスと String の間にはどのような関係があるのでしょうか?まず、以下のクラス図を見てみましょう。
#図から、StringBuilder と StringBuffer の両方が AbstractStringBuilder と AbstractStringBuilder を継承していることがわかります。および String は共通インターフェイス CharSequence を実装します。 文字列は一連の文字で構成されていることがわかりますが、文字列の内部は char 配列 (jdk9 以降はバイト配列) をベースに実装されており、通常、配列は連続したメモリ領域になります。配列が初期化される 次に、配列のサイズを指定する必要があります。前回の記事で、String は内部配列が Final 宣言されているため不変であることがわかりましたが、同時に String 文字の結合、挿入、削除などの操作はすべて新しいオブジェクトをインスタンス化することによって実装されます。今日説明する StringBuilder と StringBuffer は、String よりも動的です。次に、これら 2 つのカテゴリを一緒に理解しましょう。 1. StringBuilderStringBuilder の親クラスである 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 は動的に変更できることになります。次に、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 は、デフォルトで char[] 配列を容量 16 で初期化します。パラメーターなしの構築に加えて、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 を初期化します。他の 2 つのコンストラクターは、String と CharSequence をそれぞれ渡して StringBuilder を初期化できます。これら 2 つのコンストラクターの容量は、渡された文字列の長さに 16 だけ追加されます。 1.StringBuilder の追加操作と拡張前回の記事では、StringBuilder の append メソッドを使用して効率的な文字列の結合を実行できることをすでに知っていましたが、append メソッドはどのように実装されているのでしょうか。 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 メソッドの 1 行目では、まず 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 メソッドは、最初に ensureCapacityInternal を呼び出して、文字列配列の容量が再充電されることを確認します。ensureCapacityInternal メソッドについては、以下で詳しく分析します。次に、「null」文字が char[] 配列値に追加されていることがわかります。 StringBuilder の内部配列のデフォルトの容量は 16 であると上で述べました。したがって、文字列を結合するときは、まず char[] 配列に十分な容量があることを確認する必要があります。したがって、appendNull メソッドと append メソッドの両方で ensureCapacityInternal メソッドが呼び出され、char[] 配列に十分な容量があるかどうかが確認されます。容量が不足している場合は、配列が拡張されます。ensureCapacityInternal のソース コードは次のとおりです。 #
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 が minimumCapacity より小さい場合、minimumCapacity の値が newCapacity に代入されると判断されます。 ExpandCapacity メソッドが呼び出される場所が複数あるため、安全性を確保するためにこのコードが追加されています。
コードの次の文は非常に興味深いものです。newCapacity と minimumCapacity が 0 未満になる可能性はありますか? minimumCapacity が 0 未満の場合、OutOfMemoryError 例外がスローされます。実際には、範囲外であるため、0 未満になります。コンピューターに保存されているものはすべてバイナリであり、2 を掛けることは 1 ビットを左にシフトすることと同じであることがわかっています。バイトを例にとると、1 バイトは 8 ビットで構成されます。符号付き数値の左端のビットは符号ビットです。正の数値の符号ビットは 0、負の数値の符号ビットは 1 です。この場合、バイトが表現できるサイズ範囲は [-128~127] であり、数値が 127 より大きい場合は範囲外になります。つまり、左端の符号ビットが 2 番目のビットの 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方法将原数组复制到了新的数组中。
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'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!
StringBuilder除了提供了append方法、subString方法以及toString方法外还提供了还提供了插入(insert)、删除(delete、deleteCharAt)、替换(replace)、查找(indexOf)以及反转(reverse)等一些列的字符串操作的方法。但由于实现都非常简单,这里就不再赘述了。
在第一节已经知道,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トレーニングのコラムに注目してください!
以上が温故知新を学ぶ (2) Javaの文字列を深く理解するの詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。