

Javas String.substring()
-Methode ist eine der am häufigsten verwendeten Java-Methoden überhaupt (zumindest nach Google-Suchergebnissen). Grund genug, sich die Methode einmal genauer anzuschauen.
Dieser Artikel beschreibt, wie man substring()
einsetzt, aber auch wie es intern funktioniert. Dabei gab es spannende Änderungen im Laufe der Java-Releases. Erfahrene Java-Entwickler, die mit der Benutzung der Methode vertraut sind, können direkt zum Abschnitt "Wie funktioniert die substring-Methode in Java?" springen.
String.substring()
Die Methode String.substring()
gibt einen Teilstring des ursprünglichen Strings, basierend auf einem Start- und einem End-Index, zurück. Am besten lässt sich das an einem Bild erklären.
Im folgenden Beispiel wird aus dem String "HappyCoders" der Teilstring von Position 5 bis 8 extrahiert (die Zählung beginnt bei 0):

Beim Aufruf der substring()
-Methode geben wir als ersten Parameter die Start-Position an, also 5, und als zweiten Parameter die Position nach der End-Position, also 9:
String string = "HappyCoders";
String substring = string.substring(5,9);
System.out.println("substring = " + substring);
Code-Sprache: Java (java)
Das Programm gibt, wie erwartet, den Teilstring "Code" aus. Die Länge des Teilstrings entspricht End-Position minus Start-Position, also 9-5 = 4.
Substring einer bestimmten Länge
Wie im vorherigen Beispiel gezeigt, müssen wir der substring()
-Methode den Start- und End-Index des Teilstrings übergeben. Manchmal kennen wir allerdings nicht den End-Index, sondern die gewünschte Länge des Teilstrings.
Das ist einfach gelöst: den End-Index können wir als Start-Index plus Länge berechnen. Das können wir direkt in eine Methode, wie die folgende auslagern:
public static String substring(String string, int beginIndex, int length) {
int endIndex = beginIndex + length;
return string.substring(beginIndex, endIndex);
}
Code-Sprache: Java (java)
Die Methode können wir dann wie folgt aufrufen:
String code = substring("HappyCoders", 5, 4);
Code-Sprache: Java (java)
Eine Überprüfung der Parameter auf Gültigkeit brauchen wir nicht vorzunehmen; das erledigt die String.substring()
-Methode.
Substring bis zum Ende
Um einen Teilstring ab einer vorgegebenen Position bis zum Ende des Strings zu erhalten, können wir eine überladene String.substring()
-Methode verwenden, bei der man nur den Start-Index angeben muss.
Das folgende substring
-Beispiel zeigt, wie wir aus dem String "Do or do not. There is no try." den Teilstring von Position 14 bis zum Ende (also den zweiten Satz) extrahieren:
String yodaQuote = "Do or do not. There is no try.";
String thereIsNoTry = yodaQuote.substring(14);
Code-Sprache: Java (java)
Substring vom Ende
Eine weitere Vorgabe könnte sein, einen Teilstring vorgegebener Länge vom Ende des ursprünglichen Strings extrahieren zu müssen. Dazu müssen wir den Start-Index berechnen als Länge des Strings minus Länge des gewünschten Substrings. Auch das sollten wir in eine Methode extrahieren:
public static String substringFromEnd(String string, int length) {
int beginIndex = string.length() - length;
return string.substring(beginIndex);
}
Code-Sprache: Java (java)
Weitere Teilstring-Aufgaben
Dieser Abschnitt zeigt Lösungen für diverse String/Teilstring-Aufgaben, die mit anderen Methoden als String.substring()
gelöst werden müssen.
Teilstring innerhalb eines Strings finden
Um innerhalb eines vorgegebenen Strings einen bestimmten Teilstring zu finden, setzt du in Java die String.indexOf()
-Methode ein. Sagen wir, wir wollen die Positionen von "Happy" und "Code" in "HappyCoders" finden. Das funktioniert wie folgt:
String string = "HappyCoders";
int happyIndex = string.indexOf("Happy");
int codeIndex = string.indexOf("Code");
Code-Sprache: Java (java)
Für "Happy" liefert indexOf()
den Wert 0 zurück und für "Code" den Wert 5.
Wird der angegebene Teilstring nicht gefunden, gibt indexOf()
den Wert -1 zurück.
Die letzte Position eines Teilstring findet man mit lastIndexOf()
:
String string = "The needs of the many outweigh the needs of the few, or the one.";
int lastNeedsIndex = string.lastIndexOf("needs");
Code-Sprache: Java (java)
In diesem Beispiel gibt lastIndexOf()
den Wert 35 zurück.
Prüfen, ob ein String einen Substring enthält
Um zu prüfen, ob ein String einen bestimmten Teilstring enthält, können wir seit Java 5 die Methode String.contains()
verwenden. Der folgende Code prüft beispielsweise, ob der String "foobar" den String "oo" enthält:
String string = "foobar";
boolean containsOo = string.contains("oo");
Code-Sprache: Java (java)
Vor Java 5 müssen wir die indexOf()
-Methode zu Hilfe nehmen:
boolean containsOo = string.indexOf("oo") != -1;
Code-Sprache: Java (java)
Tatsächlich ruft die String.contains()
-Methode intern auch String.indexOf()
auf.
Teilstring innerhalb eines Strings ersetzen
Einen Teilstring ersetzen können wir in Java mit der String.replace()
-Methode. Im folgenden Beispiel wird im angegebenen Satz jedes Vorkommnis des Wortes "the" durch "a" ersetzt:
String string = "the quick brown fox jumps over the lazy dog";
string = string.replace("the", "a");
Code-Sprache: Java (java)
Teilstring innerhalb eines Strings löschen
Um einen Teilstring zu löschen, können wir diesen einfach durch den leeren String ""
ersetzen. Im folgenden Beispiel löschen wir jedes Vorkommnis von "and ":
String string = "When there is no emotion, there is no motive for violence.";
string = string.replace("no ", "");
Code-Sprache: Java (java)
Wie funktioniert die substring-Methode in Java?
String ist eine der am häufigsten verwendeten Java-Klassen und nimmt oft einen großen Teil des Heaps ein. Kein Wunder, dass String im Laufe der Zeit immer wieder optimiert wurde.
So wurde z. B. die Berechnung des Hash-Werts mehrfach geändert, und in Java 9 wurden Compact Strings eingeführt. Seither werden Strings, die ausschließlich Latin-1-Zeichen enthalten, mit nur einem Byte je Zeichen codiert anstatt mit zwei.
Auch die substring
-Funktion wurde grundlegend verändert:
Bis einschließlich Java 6 zeigt ein durch substring()
erzeugter Teilstring auf das gleiche char
-Array wie der ursprüngliche String. In den String-Feldern offset
und count
wird die Startposition und Länge des Teilstrings hinterlegt.
Hier der relevante Teil der substring
-Methode von Java 1 bis 6:
public String substring(int beginIndex, int endIndex) {
// ... parameter validation ...
return ((beginIndex == 0) && (endIndex == count)) ? this :
new String(offset + beginIndex, endIndex - beginIndex, value);
}
Code-Sprache: Java (java)
Wenn der Teilstring den kompletten ursprünglichen String umfasst, wird einfach this
zurückgegben. Ansonsten wird der folgende Konstruktor aufgerufen:
String(int offset, int count, char value[]) {
this.value = value;
this.offset = offset;
this.count = count;
}
Code-Sprache: Java (java)
Der Teilstring und der ursprüngliche String teilen sich also ein char
-Array und unterscheiden sich lediglich durch die offset
- und count
-Werte, die den Ausschnitt des char
-Arrays festlegen. Die JDK-Entwickler versprachen sich dadurch zwei Vorteile:
- Weniger Speicherbelegung auf dem Heap
- Schnellere Ausführung der
substring
-Methode, als wenn das Array kopiert werden würde
Ein wichtiger Aspekt wurde allerdings nicht berücksichtigt:
Wenn der ursprüngliche String nicht mehr benötigt wird, kann der Garbage Collector dessen char
-Array nicht aufräumen, da dieses noch vom Teilstring referenziert wird. Wenn z. B. der ursprüngliche String 10.000 Zeichen enthält und der Teilstring nur 10 Zeichen, dann würden 9.990 Zeichen, also knapp 20 KB (ein char
belegt zwei Bytes) Heap verschwendet werden.
Java-Entwicklerinnen und -Entwickler, die sich dessen bewusst waren, arbeiteten oft mit einem der folgenden zwei Workarounds:
String substring = new String(string.substring(5, 9));
String substring = "" + string.substring(5, 9);
Code-Sprache: Java (java)
Der in der ersten Zeile aufgerufene String-Konstruktor prüft, ob der übergebene String ein Teilstring ist. Wenn ja, erstellt er eine Kopie des gewünschten Ausschnitts. Die in der zweiten Zeile verwendete String-Verkettung führt erst ab Java 5 zum gewünschten Ergebnis (s. u.).
Letztlich wogen die JDK-Entwickler die Vor- und Nachteile der bisherigen Lösung ab und entschieden sich in Java 7 die Implementierung dahingehend zu ändern, dass char
-Arrays nicht mehr von mehreren Strings geteilt werden; die substring
-Funktion (bzw. der String-Konstruktor, den sie aufruft) erstellt stattdessen eine Kopie des angeforderten Ausschnitts des char
-Arrays.
In Java 7 ist substring()
wie folgt implementiert:
public String substring(int beginIndex, int endIndex) {
// ... parameter validation ...
return ((beginIndex == 0) && (endIndex == value.length)) ? this
: new String(value, beginIndex, subLen);
}
Code-Sprache: Java (java)
Das sieht auf den ersten Blick gleich aus. Auf den zweiten Blick fällt auf, dass count
durch value.length
ersetzt wurde, also die Länge des char
-Arrays. Da jeder String sein eigenes char
-Array hat, werden die Felder offset
und count
nicht mehr benötigt.
Außerdem wird ein anderer String-Konstruktor aufgerufen (mit value
am Anfang statt am Ende). Dieser Konstruktor sieht wie folgt aus:
public String(char value[], int offset, int count) {
// ... parameter validation ...
this.value = Arrays.copyOfRange(value, offset, offset+count);
}
Code-Sprache: Java (java)
Es wird also eine Kopie des angefordeten char
-Array-Ausschnitts erstellt.
In Java 9 wurde die substring
-Methode noch dahingehend angepasst, dass sie das verwendete Encoding (1-Byte/Latin 1 bzw. 2-Byte/UTF-16) berücksichtigt; die grundlegende Funktionalität (Aufruf von Arrays.copyOfRange
) wurde aber beibehalten.
String.substring-Internals – Demo
Ich habe ein kleines Programm geschrieben, um die Änderung der substring
-Methode im Laufe der Java-Versionen zu demonstrieren. Du findest den Code auch in diesem GitHub-Repository.
package eu.happycoders.substring;
import java.lang.reflect.Field;
public class SubstringInternalsDemo {
public static void main(String[] args) throws IllegalAccessException {
String string = "HappyCoders.eu";
String substring = string.substring(5, 9);
printDetails("original string", string);
printDetails("substring", substring);
printDetails("substring appended to empty string", "" + substring);
printDetails("substring wrapped with new string", new String(substring));
}
private static void printDetails(String name, String string)
throws IllegalAccessException {
System.out.println(name + ":");
System.out.println(" string identity : " + identity(string));
System.out.println(" string : " + string);
Object value = getPrivateField(string, "value");
System.out.println(" value[] identity : " + identity(value));
System.out.println(" value[] : " + valueToString(value));
// Java 1-6: offset + count
Integer offset = (Integer) getPrivateField(string, "offset");
if (offset != null) {
System.out.println(" offset : " + offset);
}
Integer count = (Integer) getPrivateField(string, "count");
if (count != null) {
System.out.println(" count : " + count);
}
// Java 9+: coder
Byte coder = (Byte) getPrivateField(string, "coder");
if (coder != null) {
System.out.println(" coder : " + coder);
}
System.out.println();
}
private static String identity(Object o) {
return "@" + Integer.toHexString(System.identityHashCode(o));
}
private static String valueToString(Object value) {
if (value instanceof byte[]) {
return Arrays.toString((byte[]) value);
}
if (value instanceof char[]) {
return Arrays.toString((char[]) value);
}
return value.toString();
}
private static Object getPrivateField(String string, String fieldName)
throws IllegalAccessException {
try {
Field field = String.class.getDeclaredField(fieldName);
field.setAccessible(true);
return field.get(string);
} catch (NoSuchFieldException e) {
return null;
}
}
}
Code-Sprache: Java (java)
Das Programm zeigt die Identitäten und Werte der Strings und Teilstrings und deren internen Felder. Um die oben beschriebenen Workarounds zu testen, werden die Teilstrings einmal mit einem leeren String verkettet und einmal durch new String(…)
gewrappt.
Damit das Programm auch mit älteren Versionen als Java 5 läuft, konnte ich java.util.Arrays.toString()
nicht einsetzen. Eine Arrays
-Ersatzimplementierung liegt ebenfalls im GitHub-Repo.
Wenn wir das Programm mit der ältesten noch herunterladbaren Java-Version, Java 1.2, laufen lassen, erhalten wir die folgende Ausgabe:
original string:
string identity : @b450fff4
string : HappyCoders.eu
value[] identity : @b454fff4
value[] : [H, a, p, p, y, C, o, d, e, r, s, ., e, u]
offset : 0
count : 14
substring:
string identity : @b42cfff4
string : Code
value[] identity : @b454fff4
value[] : [H, a, p, p, y, C, o, d, e, r, s, ., e, u]
offset : 5
count : 4
substring appended to empty string:
string identity : @b42cfff4
string : Code
value[] identity : @b454fff4
value[] : [H, a, p, p, y, C, o, d, e, r, s, ., e, u]
offset : 5
count : 4
substring wrapped with new string:
string identity : @bf34fff4
string : Code
value[] identity : @bf30fff4
value[] : [C, o, d, e]
offset : 0
count : 4
Code-Sprache: Klartext (plaintext)
Wir können sehen, dass String, Teilstring und der mit "" verkettete Teilstring alle auf das identische char
-Array @b454fff4 verweisen. Der mit new String(…)
erzeugte String hingegen verwendet ein separates char
-Array, das nur den Text "Code" enthält.
In Java 1.3 und 1.4 führt die String-Verkettung zu einem anderen Ergebnis (die vollständige Ausgabe für alle Java-Versionen findest du im results-Verzeichnis auf GitHub):
...
substring appended to empty string:
string identity : @20c10f
string : Code
value[] identity : @62eec8
value[] : [C, o, d, e, , , , , , , , , , , , ]
offset : 0
count : 4
...
Code-Sprache: Klartext (plaintext)
Das liegt daran, dass in diesen Versionen ein StringBuffer
zur Verkettung genutzt wird, der mit einer initialen Länge von 16 Zeichen erstellt wird und dessen toString()
-Methode dessen char
-Array direkt übernimmt.
In Java 5 ändert sich das Ergebnis der Verkettung des Substrings mit einem leeren String erneut:
...
substring appended to empty string:
string identity : @1004901
string : Code
value[] identity : @1b90b39
value[] : [C, o, d, e]
offset : 0
count : 4
...
Code-Sprache: Klartext (plaintext)
Ab Java 5 rufen StringBuffer.toString()
und StringBuilder.toString()
den oben gezeigten String-Constructor auf, der mit Arrays.copyOfRange()
den tatsächlich benötigten Ausschnitt des char
-Arrays kopiert.
In Java 7 und 8 sieht die Ausgabe dann so aus:
original string:
string identity : @26ffd553
string : HappyCoders.eu
value[] identity : @f74f6ef
value[] : [H, a, p, p, y, C, o, d, e, r, s, ., e, u]
substring:
string identity : @47ffccd6
string : Code
value[] identity : @6ae11a87
value[] : [C, o, d, e]
substring appended to empty string:
string identity : @6094cbe2
string : Code
value[] identity : @48d593f7
value[] : [C, o, d, e]
substring wrapped with new string:
string identity : @3de5627c
string : Code
value[] identity : @6ae11a87
value[] : [C, o, d, e]
Code-Sprache: Klartext (plaintext)
Wie oben erläutert, verweist der durch String.substring()
zurückgegebene Teilstring ab Java 7 auf ein separates char
-Array. Außerdem gibt es die offset
- und count
-Felder nicht mehr.
Die Workarounds durch Konkatenation oder Aufruf des String-Konstruktors sind also nicht mehr erforderlich. Auffällig ist hier, dass die String-Verkettung einen neuen String mit neuem char
-Array erstellt, während der String-Konstruktor das char
-Array übernimmt.
Seit Java 9 enthält der String kein char
-Array mehr, sondern ein byte
-Array:
original string:
string identity : @4c203ea1
string : HappyCoders.eu
value[] identity : @71be98f5
value[] : [72, 97, 112, 112, 121, 67, 111, 100, 101, 114, 115, 46, 101, 117]
coder : 0
substring:
string identity : @96532d6
string : Code
value[] identity : @3796751b
value[] : [67, 111, 100, 101]
coder : 0
substring appended to empty string:
string identity : @3498ed
string : Code
value[] identity : @1a407d53
value[] : [67, 111, 100, 101]
coder : 0
substring wrapped with new string:
string identity : @3d8c7aca
string : Code
value[] identity : @3796751b
value[] : [67, 111, 100, 101]
coder : 0
Code-Sprache: Klartext (plaintext)
Analog zur vorherigen Java-Version wird bei der String-Verkettung ein neues byte
-Array angelegt, während der String-Konstruktor das bestehende byte
-Array wiederverwendet.
Fazit
DIeser Artikel hat gezeigt, wie man String.substring()
einsetzt, wie die Methode intern arbeitet und wie sich die Funktionsweise im Laufe der Zeit geändert hat.
Wenn dir der Artikel gefallen hat, hinterlasse mir gerne einen Kommentar oder teile den Artikel über einen der Share-Buttons. Wenn du über jeden neuen Artikel auf HappyCoders.eu informiert werden möchtest, klicke hier, um dich für den HappyCoders-Newsletter anzumelden.