Verwandte Lernempfehlungen: Java-Grundlagen-Tutorial
Im letzten Artikel haben wir eine eingehende Analyse des String-Gedächtnisses und einiger seiner Eigenschaften durchgeführt. In diesem Artikel werden wir die beiden anderen Klassen im Zusammenhang mit String, nämlich StringBuilder und StringBuffer, eingehend analysieren. Welche Beziehung besteht zwischen diesen beiden Klassen und String? Schauen wir uns zunächst das Klassendiagramm unten an:
Auf dem Bild können wir sehen, dass sowohl StringBuilder als auch StringBuffer AbstractStringBuilder erben und AbstractStringBuilder und String die gemeinsame Schnittstelle CharSequence implementieren.
Wir wissen, dass eine Zeichenfolge aus einer Reihe von Zeichen besteht. Die interne Implementierung von String basiert auf einem char-Array (basierend auf einem Byte-Array nach jdk9), und das Array ist normalerweise ein kontinuierlicher Speicherbereich, der wann angegeben werden muss Initialisieren des Arrays. Die Größe des Arrays. Im vorherigen Artikel wussten wir bereits, dass String unveränderlich ist, da sein internes Array als endgültig deklariert ist. Gleichzeitig werden das Zusammenfügen, Einfügen, Löschen und andere Vorgänge von String-Zeichen durch Instanziieren neuer Objekte implementiert. Der StringBuilder und der StringBuffer, die wir heute kennenlernen werden, sind dynamischer als String. Lassen Sie uns als Nächstes diese beiden Kategorien gemeinsam kennenlernen.
Sie können den folgenden Code in der übergeordneten Klasse von StringBuilder, AbstractStringBuilder, sehen:
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 wird wie String basierend auf char-Arrays implementiert. Der Unterschied besteht darin, dass StringBuilder keine endgültige Änderung hat, was bedeutet, dass StringBuilder dies kann dynamisch verändert. Schauen wir uns als nächstes die Parameterlose Konstruktionsmethode von StringBuilder an:
/** * Constructs a string builder with no characters in it and an * initial capacity of 16 characters. */ public StringBuilder() { super(16); }复制代码
In dieser Methode wird die Konstruktionsmethode der übergeordneten Klasse aufgerufen. Sehen Sie sich an, dass die Konstruktionsmethode wie folgt lautet:
/** * Creates an AbstractStringBuilder of the specified capacity. */ AbstractStringBuilder(int capacity) { value = new char[capacity]; }复制代码
Die Konstruktionsmethode von AbstractStringBuilder initialisiert intern eine Kapazität als Array von Kapazitäten. Mit anderen Worten: StringBuilder initialisiert standardmäßig ein char[]-Array mit einer Kapazität von 16. Zusätzlich zur parameterlosen Konstruktion bietet StringBuilder auch mehrere Konstruktionsmethoden. Der Quellcode lautet wie folgt:
/** * 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); }复制代码
Die erste Methode dieses Codes initialisiert einen StringBuilder mit einer angegebenen Kapazität. Die anderen beiden Konstruktoren können String und CharSequence übergeben, um StringBuilder zu initialisieren. Die Kapazität dieser beiden Konstruktoren wird zur Länge der übergebenen Zeichenfolge um 16 addiert.
Im vorherigen Artikel wissen wir bereits, dass ein effizientes String-Splicing über die Append-Methode von StringBuilder durchgeführt werden kann. Am Beispiel von append (String) können Sie sehen, dass das append von StringBuilder die append-Methode der übergeordneten Klasse aufruft. Tatsächlich werden nicht nur append, sondern fast alle Methoden zum Betreiben von Zeichenfolgen in der StringBuilder-Klasse über die übergeordnete Klasse implementiert. Der Quellcode der Append-Methode lautet wie folgt:
// 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; }复制代码
In der ersten Zeile der Append-Methode wird zunächst eine Nullprüfung durchgeführt. Wenn diese gleich Null ist, wird die AppendNull-Methode aufgerufen. Der Quellcode lautet wie folgt:
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; }复制代码
In der appendNull-Methode wird zuerst secureCapacityInternal aufgerufen, um sicherzustellen, dass die String-Array-Kapazität neu aufgeladen wird. Die secureCapacityInternal-Methode wird im Folgenden detailliert analysiert. Als Nächstes können Sie sehen, dass das Zeichen „Null“ zum Array-Wert char[] hinzugefügt wird.
Wir haben oben erwähnt, dass die Standardkapazität des internen Arrays von StringBuilder 16 beträgt. Daher müssen Sie beim Zusammenfügen von Zeichenfolgen zunächst sicherstellen, dass das Array char[] über ausreichende Kapazität verfügt. Daher wird die Methode „sichsureCapacityInternal“ sowohl in der Methode „appendNull“ als auch in der Methode „append“ aufgerufen, um zu prüfen, ob das Array char[] ausreichend ist. Wenn die Kapazität nicht ausreicht, wird das Array wie folgt erweitert:
private void ensureCapacityInternal(int minimumCapacity) { // overflow-conscious code if (minimumCapacity - value.length > 0) expandCapacity(minimumCapacity); }复制代码
Hier werden die gespleißten Zeichen interpretiert. Wenn die Zeichenfolgenlänge größer als die Länge des Zeichenfolgenarrays ist, wird expandCapacity aufgerufen, um die Kapazität zu erweitern.
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); }复制代码
Die Logik von expandCapacity ist ebenfalls sehr einfach. Multiplizieren Sie zunächst die Länge des ursprünglichen Arrays mit 2 und addieren Sie 2, um die Länge des erweiterten Arrays zu berechnen. Als nächstes wird beurteilt, dass, wenn newCapacity kleiner als MinimumCapacity ist, der MinimumCapacity-Wert newCapacity zugewiesen wird. Da es mehr als eine Stelle gibt, an der die Methode expandCapacity aufgerufen wird, wird dieser Code hinzugefügt, um die Sicherheit zu gewährleisten.
Der nächste Codesatz ist sehr interessant. Ist es möglich, dass newCapacity und MinimumCapacity kleiner als 0 sind? Wenn MinimumCapacity kleiner als 0 ist, wird eine OutOfMemoryError-Ausnahme ausgelöst. Tatsächlich ist es kleiner als 0, weil es außerhalb der Grenzen liegt. Wir wissen, dass alles, was im Computer gespeichert ist, binär ist und die Multiplikation mit 2 einer Verschiebung um ein Bit nach links entspricht. Am Beispiel eines Bytes hat ein Byte 8 Bits. In einer Zahl mit Vorzeichen ist das Bit ganz links das Vorzeichenbit. Das Vorzeichenbit positiver Zahlen ist 1. Dann beträgt der Größenbereich, den ein Byte darstellen kann, [-128 ~ 127], und wenn eine Zahl größer als 127 ist, liegt sie außerhalb der Grenzen, dh das Vorzeichenbit ganz links wird im zweiten Bit durch 1 ersetzt links und es erscheint eine negative Zahl. Natürlich ist es nicht Byte, sondern Int, aber das Prinzip ist dasselbe.
另外在这个方法的最后一句通过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三个字符串相关类。这块内容其实非常简单,只要花一点时间去读一下源码就很容易理解。当然,如果你没看过此部分源码相信这篇文章能够帮助到你。不管怎样,相信大家通过阅读本文还是能有一些收获。解了这些知识后可以帮助我们在开发中对字符串的选用做出更好的选择。同时,这块内容也是面试常客,相信大家读完本文去应对面试官的问题也会绰绰有余。
Wenn Sie mehr über das Programmieren erfahren möchten, achten Sie bitte auf die Rubrik PHP-Schulung!
Das obige ist der detaillierte Inhalt vonSchauen Sie in die Vergangenheit zurück und lernen Sie Neues kennen (2) Vertiefendes Verständnis von Zeichenfolgen in Java. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!