String Concatenation vs VarArgs in Java

vorhergehende Artikel in: Java
24.02.2023

Neulich stolperte ich in meinem $dayjob im Java-Quelltext wieder einmal über ein logging-Statement, das die Meldung per String-Concatenation zusammensetzte. Bevor ich daraufhin wünschte, dass alle diese Statements auf VarArgs umgestellt werden, wollte ich mir eine solide Argumentationsgrundlage schaffen und stürzte dadurch in einen Kaninchenbau...

Es handelte sich bei dem Statement genasuer gesagt um ein solches hier:

LOGGER.trace("this "+some_obj+" is "+some_other_obj+" some concatenated text");

Zunächst einmal nochmals kurz zusammengefasst die (früher) gängige Lehrmeinung was Java betrifft: Man sollte statt des oben angegebenen Beispiels immer besser folgendes schreiben:

LOGGER.trace("this {} is a {} string made with varargs!",some_object,some_other_object);

Der Grund: die Methode trace im Beispiel stellt vielleicht fest, dass die Ausgabe gar nicht sein müsste. In der ersten Variante wird aber der String bereits zusammengesetzt und - falls some_obj gar keine Referenz ist, sondern ein Methodenaufruf - unter Umständen sehr teure Methodenaufrufe unnütz ausgeführt.

In der zweiten Methode wird der String nicht zusammengesetzt, sondern die Methode mit den Argumenten ausgeführt und erst nachdem die Methode festgestellt hat, dass es tatsächlich notwendig ist, den String zusammenzusetzen tut sie das dann auch. Falls - wie oben beschrieben - die Objekte keine zum Aufruf bekannte Referenzen, sondern inline-Methodenaufrufe sind, werden diese jedoch immer noch vor dem Aufruf der Methode trace ausgeführt - in diesen Fällen führt varargs also nicht zu Einsparungen. Nur was die Concatenation des Strings und die Kosten dafür angeht scheint die zweite Variante Vorteile zu haben.

Warum schrieb ich "scheint"? Nun - in frühen Versionen von Java war die Operation String-Concatenation mittels Additionsoperator wirklich furchtbar: sie erzeugte neue Instanzen, belastete den Garbage Collector und war ganz generell die Performance betreffend ein Alptraum.

Das war natürlich den Entwicklern nicht verborgen geblieben und der Java-Compiler begann, Optimierungen auszuführen, sobald er im Quelltext so etwas erkannte. Bei Java 8 zum Beispiel wurde implizit ein StringBuilder im Bytecode erzeugt, der dann die Aufgabe hatte, das Ergebnis zusammenzubauen - wie man im ByteCode für die folgende Beispielmethode sehen kann:

private void logStringConcat()
{
	CLASS_LOGGER.trace("this "+java.lang.Integer.valueOf(rand.nextInt())+" is "+java.lang.Integer.valueOf(rand.nextInt())+" a "+java.lang.Integer.valueOf(rand.nextInt())+" concatenated "+java.lang.Integer.valueOf(rand.nextInt())+" string ");
}
  private void logStringConcat();
    Code:
       0: getstatic     #7                  // Field CLASS_LOGGER:Lorg/slf4j/Logger;
       3: new           #8                  // class java/lang/StringBuilder
       6: dup
       7: invokespecial #9                  // Method java/lang/StringBuilder."<init>":()V
      10: ldc           #10                 // String this
      12: invokevirtual #11                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      15: aload_0
      16: getfield      #6                  // Field rand:Ljava/util/Random;
      19: invokevirtual #12                 // Method java/util/Random.nextInt:()I
      22: invokestatic  #13                 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
      25: invokevirtual #14                 // Method java/lang/StringBuilder.append:(Ljava/lang/Object;)Ljava/lang/StringBuilder;
      28: ldc           #15                 // String  is
      30: invokevirtual #11                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      33: aload_0
      34: getfield      #6                  // Field rand:Ljava/util/Random;
      37: invokevirtual #12                 // Method java/util/Random.nextInt:()I
      40: invokestatic  #13                 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
      43: invokevirtual #14                 // Method java/lang/StringBuilder.append:(Ljava/lang/Object;)Ljava/lang/StringBuilder;
      46: ldc           #16                 // String  a
      48: invokevirtual #11                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      51: aload_0
      52: getfield      #6                  // Field rand:Ljava/util/Random;
      55: invokevirtual #12                 // Method java/util/Random.nextInt:()I
      58: invokestatic  #13                 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
      61: invokevirtual #14                 // Method java/lang/StringBuilder.append:(Ljava/lang/Object;)Ljava/lang/StringBuilder;
      64: ldc           #17                 // String  concatenated
      66: invokevirtual #11                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      69: aload_0
      70: getfield      #6                  // Field rand:Ljava/util/Random;
      73: invokevirtual #12                 // Method java/util/Random.nextInt:()I
      76: invokestatic  #13                 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
      79: invokevirtual #14                 // Method java/lang/StringBuilder.append:(Ljava/lang/Object;)Ljava/lang/StringBuilder;
      82: ldc           #18                 // String  string
      84: invokevirtual #11                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      87: invokevirtual #19                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
      90: invokeinterface #20,  2           // InterfaceMethod org/slf4j/Logger.trace:(Ljava/lang/String;)V
      95: return

Vergleicht man das mit dem Bytecode der äquivalenten VarArgs-Variante, stellt man fest, dass der Aufruf der Methode trace ungefähr denselben Bedarf an ByteCode hat - das kommt daher, dass im Bytecode erst das Array konstruiert und bestückt werden muss, das intern den VarArgs-Aufruf ergibt:

private void logVarArgs()
{
	CLASS_LOGGER.trace("this {} is {} a {} string {} made with varargs!",rand.nextInt(),rand.nextInt(),rand.nextInt(),rand.nextInt());
}
 private void logVarArgs();
    Code:
       0: getstatic     #7                  // Field CLASS_LOGGER:Lorg/slf4j/Logger;
       3: ldc           #21                 // String this {} is {} a {} string {} made with varargs!
       5: iconst_4
       6: anewarray     #22                 // class java/lang/Object
       9: dup
      10: iconst_0
      11: aload_0
      12: getfield      #6                  // Field rand:Ljava/util/Random;
      15: invokevirtual #12                 // Method java/util/Random.nextInt:()I
      18: invokestatic  #13                 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
      21: aastore
      22: dup
      23: iconst_1
      24: aload_0
      25: getfield      #6                  // Field rand:Ljava/util/Random;
      28: invokevirtual #12                 // Method java/util/Random.nextInt:()I
      31: invokestatic  #13                 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
      34: aastore
      35: dup
      36: iconst_2
      37: aload_0
      38: getfield      #6                  // Field rand:Ljava/util/Random;
      41: invokevirtual #12                 // Method java/util/Random.nextInt:()I
      44: invokestatic  #13                 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
      47: aastore
      48: dup
      49: iconst_3
      50: aload_0
      51: getfield      #6                  // Field rand:Ljava/util/Random;
      54: invokevirtual #12                 // Method java/util/Random.nextInt:()I
      57: invokestatic  #13                 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
      60: aastore
      61: invokeinterface #23,  3           // InterfaceMethod org/slf4j/Logger.trace:(Ljava/lang/String;[Ljava/lang/Object;)V
      66: return

Was den Gesamtaufwand angeht, muss man den irgendwann ja noch erfolgenden Aufwand für die eigentliche String-Concatenierung ebenfalls noch mit einrechnen. Das könnte das Gefühl aufkommen lassen, dass mit Java 8 die Methode der String-Concatenierung vorzuziehen sein könnte.

Wie ist es aber mit moderneren Versionen von Java? Irgendwann wurde invokeVirtual eingeführt. Damit wird dann plötzlich der Bytecode der ersten Variante mit String-Concatenation sehr viel kürzer wie in diesem Beispiel zu erkennen ist.

  private void logStringConcat();
    Code:
       0: getstatic     #28                 // Field CLASS_LOGGER:Lorg/slf4j/Logger;
       3: aload_0
       4: getfield      #22                 // Field rand:Ljava/util/Random;
       7: invokevirtual #32                 // Method java/util/Random.nextInt:()I
      10: invokestatic  #36                 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
      13: aload_0
      14: getfield      #22                 // Field rand:Ljava/util/Random;
      17: invokevirtual #32                 // Method java/util/Random.nextInt:()I
      20: invokestatic  #36                 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
      23: aload_0
      24: getfield      #22                 // Field rand:Ljava/util/Random;
      27: invokevirtual #32                 // Method java/util/Random.nextInt:()I
      30: invokestatic  #36                 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
      33: aload_0
      34: getfield      #22                 // Field rand:Ljava/util/Random;
      37: invokevirtual #32                 // Method java/util/Random.nextInt:()I
      40: invokestatic  #36                 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
      43: invokedynamic #42,  0             // InvokeDynamic #0:makeConcatWithConstants:(Ljava/lang/Integer;Ljava/lang/Integer;Ljava/lang/Integer;Ljava/lang/Integer;)Ljava/lang/String;
      48: invokeinterface #46,  2           // InterfaceMethod org/slf4j/Logger.trace:(Ljava/lang/String;)V
      53: return

Folgerichtig fragt man sich, wie das dann noch funktionieren kann - die Antwort ist zu finden, wenn man nach der Methode, die hier aufgerufen wird recherchiert: Neuere Java-versionen erzeugen beim Startup dafür passenden ByteCode, der die String-Concatenation ausführt und die entsprechende Callsite wird anschließend dort eingetragen, wo eigentlich eine String-Concatenation benötigt wird. Spätestens dann ist es nicht mehr möglich, am Code zu entscheiden welches von beiden die bessere weil ressourcenschonendere Variante ist. Klar ist nur noch, dass Variante eins die Startupzeit einer Anwendung verlängert, da für jede Stringconcatenation eine passende Bytecode-Implementierung erstellt werden muss bevor die Anwendung startet. Es kann sogar so sein, dass dieselbe Anwendung abhängig von der benutzten Runtime und Architektur mal in der einen und mal in der anderen Version performanter ist und man bei der eigentlichen Programmierung gar keinen Gedanken mehr daran verschwenden muss, weil die Laufzeitumgehbung so einen großen Einfluss hat und man diese während der Entwicklung nicht beeinflussen kann.

Alle Artikel rss Wochenübersicht Monatsübersicht Codeberg Repositories Mastodon Über mich home xmpp


Vor 5 Jahren hier im Blog

  • Multi-User-WebDAV, Docker, GitHub

    17.11.2019

    Nachdem ich mich in letzter Zeit verstärkt mit Docker und dem zugehörigen Ökosystem beschäftige, habe ich begonnen, verschiedenste Dienste in Containern zu testen um zu sehen, ob in manchen Fällen LXC oder KVM nicht doch die bessere Wahl wäre...

    Weiterlesen...

Neueste Artikel

  • Migration der Webseite und aller OpenSource Projekte

    In eigener Sache...

    Weiterlesen...
  • AutoHideToolbar für Java Swing

    Ich habe eine neue Java Swing Komponente erstellt: Es handelt sich um einen Wrapper für von JToolBar abgeleitete Klassen, die die Werkzeugleiste minimieren und sie nur dann einblenden, wenn der Mauszeiger über ihnen schwebt.

    Weiterlesen...
  • Integration von EBMap4D in die sQLshell

    Ich habe bereits in einem früheren Artikel über meine ersten Erfolge berichtet, der sQLshell auf Basis des bestehenden Codes aus dem Projekt EBMap4D eine bessere Integration für Geo-Daten zu spendieren und entsprechende Abfragen, bzw. deren Ergebnisse auf einer Kartenansicht zu visualisieren.

    Weiterlesen...

Manche nennen es Blog, manche Web-Seite - ich schreibe hier hin und wieder über meine Erlebnisse, Rückschläge und Erleuchtungen bei meinen Hobbies.

Wer daran teilhaben und eventuell sogar davon profitieren möchte, muss damit leben, daß ich hin und wieder kleine Ausflüge in Bereiche mache, die nichts mit IT, Administration oder Softwareentwicklung zu tun haben.

Ich wünsche allen Lesern viel Spaß und hin und wieder einen kleinen AHA!-Effekt...

PS: Meine öffentlichen Codeberg-Repositories findet man hier.