Ein unveränderliches Objekt ist ein Objekt, bei dem die internen Felder (oder zumindest alle internen Felder, die sich auf das externe Verhalten auswirken) nicht geändert werden können.
Unveränderliche Saiten bieten viele Vorteile:
Leistung: Führen Sie die folgenden Schritte aus:
String substring = fullstring.substring(x,y);
Das zugrunde liegende C für die Methode substring () ist wahrscheinlich ungefähr so:
// Assume string is stored like this:
struct String { char* characters; unsigned int length; };
// Passing pointers because Java is pass-by-reference
struct String* substring(struct String* in, unsigned int begin, unsigned int end)
{
struct String* out = malloc(sizeof(struct String));
out->characters = in->characters + begin;
out->length = end - begin;
return out;
}
Beachten Sie, dass keines der Zeichen kopiert werden muss! Wenn das String-Objekt veränderbar wäre (die Zeichen könnten sich später ändern), müssten Sie alle Zeichen kopieren, andernfalls würden Änderungen an Zeichen in der Teilzeichenfolge später in der anderen Zeichenfolge wiedergegeben.
Parallelität: Wenn die interne Struktur eines unveränderlichen Objekts gültig ist, ist sie immer gültig. Es besteht keine Möglichkeit, dass verschiedene Threads einen ungültigen Status innerhalb dieses Objekts erstellen. Daher sind unveränderliche Objekte threadsicher .
Speicherbereinigung: Für den Speicherbereiniger ist es viel einfacher, logische Entscheidungen über unveränderliche Objekte zu treffen.
Die Unveränderlichkeit hat jedoch auch Nachteile:
Leistung: Warten Sie, ich dachte, Sie sagten, Leistung sei ein Vorteil der Unveränderlichkeit! Nun, manchmal, aber nicht immer. Nehmen Sie den folgenden Code:
foo = foo.substring(0,4) + "a" + foo.substring(5); // foo is a String
bar.replace(4,5,"a"); // bar is a StringBuilder
Die beiden Zeilen ersetzen das vierte Zeichen durch den Buchstaben "a". Der zweite Code ist nicht nur besser lesbar, sondern auch schneller. Schauen Sie sich an, wie Sie den zugrunde liegenden Code für foo erstellen müssten. Die Teilzeichenfolgen sind einfach, aber jetzt, da sich bereits ein Zeichen im fünften Feld befindet und etwas anderes möglicherweise auf foo verweist, können Sie es nicht einfach ändern. Sie müssen die gesamte Zeichenfolge kopieren (natürlich wird ein Teil dieser Funktionalität in Funktionen im realen zugrunde liegenden C abstrahiert, aber hier geht es darum, den Code anzuzeigen, der an einer Stelle ausgeführt wird).
struct String* concatenate(struct String* first, struct String* second)
{
struct String* new = malloc(sizeof(struct String));
new->length = first->length + second->length;
new->characters = malloc(new->length);
int i;
for(i = 0; i < first->length; i++)
new->characters[i] = first->characters[i];
for(; i - first->length < second->length; i++)
new->characters[i] = second->characters[i - first->length];
return new;
}
// The code that executes
struct String* astring;
char a = 'a';
astring->characters = &a;
astring->length = 1;
foo = concatenate(concatenate(slice(foo,0,4),astring),slice(foo,5,foo->length));
Beachten Sie, dass die Verkettung zweimal aufgerufen wird, was bedeutet, dass die gesamte Zeichenfolge durchlaufen werden muss! Vergleichen Sie dies mit dem C-Code für die bar
Operation:
bar->characters[4] = 'a';
Die veränderbare String-Operation ist offensichtlich viel schneller.
Fazit: In den meisten Fällen möchten Sie eine unveränderliche Zeichenfolge. Wenn Sie jedoch viel anhängen und in eine Zeichenfolge einfügen müssen, benötigen Sie die Veränderbarkeit für die Geschwindigkeit. Wenn Sie die Vorteile der Parallelitätssicherheit und der Speicherbereinigung nutzen möchten, müssen Sie Ihre veränderlichen Objekte für eine Methode lokal halten:
// This will have awful performance if you don't use mutable strings
String join(String[] strings, String separator)
{
StringBuilder mutable;
boolean first = true;
for(int i = 0; i < strings.length; i++)
{
if(!first) first = false;
else mutable.append(separator);
mutable.append(strings[i]);
}
return mutable.toString();
}
Da es sich bei dem mutable
Objekt um eine lokale Referenz handelt, müssen Sie sich keine Gedanken über die Sicherheit der Parallelität machen (nur ein Thread berührt es jemals). Und da es nirgendwo anders referenziert wird, wird es nur auf dem Stapel zugewiesen, sodass die Zuordnung aufgehoben wird, sobald der Funktionsaufruf abgeschlossen ist (Sie müssen sich nicht um die Speicherbereinigung kümmern). Und Sie erhalten alle Leistungsvorteile von Veränderlichkeit und Unveränderlichkeit.