Java substring Methode

Java substring()-Methode

Autor-Bild
von Sven Woltmann – 17. Januar 2022

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):

Java substring Beispiel
Java substring Beispiel

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.