Koji je najeleganntniji način spajanja Java stringova (String)?
"Erge, u 'Java razvojnom priručniku' koji si mi dao da pročitam piše sledeće: u telu petlje, za spajanje stringova je najbolje koristiti StringBuilder-ov append() metod, a ne operator +. Zašto je to tako?" pitala je Sanmej zbunjeno.
"O ovom pitanju smo već pričali kada smo obradili StringBuilder," odgovorio sam polako. "Ali, Sanmej, danas ću ti to objasniti detaljnije."
Napomena: jako me raduje što Sanmej tokom učenja neprestano postavlja pitanja i uočava probleme. Zapravo, često ne bi trebalo samo da naučene činjenice zapamtimo napamet, već da se zapitamo zašto je to tako — tek kada pređemo taj korak, možemo zaista da napredujemo.
Suština operatora +
"Operator + Java zapravo drugačije tumači u trenutku kompajliranja. Drugim rečima, operator + je sintaksni šećer koji znatno olakšava spajanje stringova." Dok sam objašnjavao Sanmej, u IntelliJ IDEA-i sam otkucao sledeći kod.
class Demo {
public static void main(String[] args) {
String chenmo = "Chenmo";
String wanger = "Wanger";
System.out.println(chenmo + wanger);
}
}U okruženju Java 8, nakon dekompajliranja bajtkoda pomoću javap -c Demo.class (bajtkod i javap ćemo detaljno obraditi u delu o JVM-u, ovde možete samo letimično baciti pogled), možemo videti sledeći sadržaj:
Compiled from "Demo.java"
class Demo {
Demo();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: ldc #2 // String Chenmo
2: astore_1
3: ldc #3 // String Wanger
5: astore_2
6: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream;
9: new #5 // class java/lang/StringBuilder
12: dup
13: invokespecial #6 // Method java/lang/StringBuilder."<init>":()V
16: aload_1
17: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
20: aload_2
21: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
24: invokevirtual #8 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
27: invokevirtual #9 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
30: return
}(Ako ranije niste upoznati sa instrukcijama bajtkoda, to vam može delovati pomalo zastrašujuće, ali ne brinite — posle kratkog objašnjenja biće vam jasno.)
"Pogledaj, Sanmej, ovde postoji ključna reč new, a tip klase je java/lang/StringBuilder," rekao sam Sanmej pokazujući na red označen brojem 9. "To znači da je kreiran novi StringBuilder objekat."
"Zatim pogledaj red sa oznakom 17 — to je instrukcija invokevirtual, koja služi za poziv metoda nad objektom, odnosno metod append() StringBuilder objekta."
"Što znači da je string chenmo ("Chenmo") dodat u StringBuilder objekat."
"Ako pogledamo dalje, red sa oznakom 21 ponovo poziva metod append(), što znači da je i string wanger ("Wanger") dodat u StringBuilder objekat."
Ako bismo to predstavili Java kodom, izgledalo bi otprilike ovako:
class Demo {
public static void main(String[] args) {
String chenmo = "Chenmo";
String wanger = "Wanger";
System.out.println((new StringBuilder(chenmo)).append(wanger).toString());
}
}"O, dakle u trenutku kompajliranja operator '+' je zamenjen metodom append() klase StringBuilder," shvatila je Sanmej.
"Da, ali od verzije Java 9 (pošto to nije verzija sa dugoročnom podrškom, za demonstraciju ću koristiti Javu 11) situacija se donekle promenila — za isti kod, instrukcije bajtkoda su potpuno drugačije," rekao sam.
Za isti kod, u okruženju Java 11, instrukcije bajtkoda izgledaju ovako:
Compiled from "Demo.java"
public class com.itwanger.thirtyseven.Demo {
public com.itwanger.thirtyseven.Demo();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: ldc #2 // String
2: astore_1
3: iconst_0
4: istore_2
5: iload_2
6: bipush 10
8: if_icmpge 41
11: new #3 // class java/lang/String
14: dup
15: ldc #4 // String Chenmo
17: invokespecial #5 // Method java/lang/String."<init>":(Ljava/lang/String;)V
20: astore_3
21: ldc #6 // String Wanger
23: astore 4
25: aload_1
26: aload_3
27: aload 4
29: invokedynamic #7, 0 // InvokeDynamic #0:makeConcatWithConstants:(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;
34: astore_1
35: iinc 2, 1
38: goto 5
41: return
}Pogledajte red sa oznakom 29 — instrukcija bajtkoda je invokedynamic. Ova instrukcija dozvoljava da sam kod aplikacije odluči o razrešavanju metoda. Takozvani kod aplikacije zapravo je metod poznat kao Bootstrap Method (BSM). BSM vraća CallSite (tačku poziva) objekat koji se povezuje sa invokedynamic instrukcijom. Prilikom svakog narednog izvršavanja te invokedynamic instrukcije neće se kreirati novi CallSite objekat. CallSite je zapravo samo holder za MethodHandle (ručku metoda) koji ukazuje na metod koji se na tom mestu poziva stvarno izvršava — u ovom slučaju to je metod StringConcatFactory.makeConcatWithConstants().
"Erge, nemoj više, ako nastaviš neću moći da pratim," prekinula me je Sanmej.
"Dobro, ukratko — od verzije Java 9, JDK je počeo da koristi drugačiji pristup za dinamičko tumačenje operatora +, pa se konkretna implementacija više ne vidi na nivou instrukcija bajtkoda. Zato ću nastaviti objašnjavanje koristeći Javu 8."
Ovde ćemo reći još nešto. Šta se dešava sa sledećim kodom:
class StringConcat {
public static void main(String[] args) {
int i = 11;
String s = i + "";
System.out.println(s);
}
}Kako ovde operator + obavlja spajanje?
I ovde možemo pogledati instrukcije bajtkoda pomoću javap -c StringConcat:

Sa gornje slike se vidi da je operator + kompajliran u metod append() klase StringBuilder.
A šta se dešava sa ovakvim kodom:
class StringConcat1 {
public static void main(String[] args) {
String s = 11 + "";
System.out.println(s);
}
}Kako ovde operator + obavlja spajanje?
I ovde možemo pogledati instrukcije bajtkoda pomoću javap -c StringConcat1:

StringBuilder je nestao? Zašto je to tako?
To je zato što su oba operanda koje operator + povezuje konstante poznate u trenutku kompajliranja (jedna je celobrojna literalna vrednost 11, a druga je prazan string ""), pa kompajler može ovo spajanje stringova da izvrši već u fazi kompajliranja.
Drugim rečima, spajanje 11 + "" optimizovano je od strane kompajlera — u fazi kompajliranja on ga je razrešio kao string konstantu "11".
Zašto se kompajlira u StringBuilder.append
"Vratimo se na onaj pasus iz 'Java razvojnog priručnika':
U telu petlje je za spajanje stringova najbolje koristiti StringBuilder-ov append() metod, a ne operator +. Razlog je taj što bi upotreba operatora + unutar petlje dovela do kreiranja velikog broja StringBuilder objekata, što ne samo da zauzima više memorijskog prostora, već i primorava Java virtuelnu mašinu da neprestano vrši garbage collection, čime se smanjuje performansa programa."
Bolji pristup je da se izvan petlje kreira jedan StringBuilder objekat, a zatim da se unutar tela petlje metod append() koristi za dodavanje stringova:
class Demo {
public static void main(String[] args) {
StringBuilder sb = new StringBuilder();
for (int i = 1; i < 10; i++) {
String chenmo = "Chenmo";
String wanger = "Wanger";
sb.append(chenmo);
sb.append(wanger);
}
System.out.println(sb);
}
}Hajde da uradimo mali test.
Prvi: unutar for petlje koristi se operator "+".
String result = "";
for (int i = 0; i < 100000; i++) {
result += "tekst";
}Drugi: izvan for petlje kreira se novi StringBuilder, a unutar tela petlje koristi se metod append().
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 100000; i++) {
sb.append("tekst");
}"Koliko vremena će trajati svaki od ova dva testa? Sanmej, pokreni ti."
"O, prvi test je trajao 6212 milisekundi, a drugi je završen za manje od 1 milisekundu — razlika je ogromna!" rekla je Sanmej.
"Da, sada ti je jasan razlog?" rekao sam.
"Jeste, Erge, sada mi je jasno."
Analiza izvornog koda append metoda
"U redu, Sanmej, hajde da pogledamo izvorni kod append() metoda klase StringBuilder!"
public StringBuilder append(String str) {
super.append(str);
return this;
}Ove tri linije koda nemaju šta posebno da se vidi. Pogledajmo append() metod nadklase 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;
}- Proverava se da li je string koji se dodaje
null; ako jeste, tretira se kao string "null". Izvorni kod metodaappendNull()izgleda ovako:
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;
}Uzima se dužina stringa.
Izvorni kod metoda
ensureCapacityInternal()izgleda ovako:
private void ensureCapacityInternal(int minimumCapacity) {
// overflow-conscious code
if (minimumCapacity - value.length > 0) {
value = Arrays.copyOf(value,
newCapacity(minimumCapacity));
}
}Pošto je string interno implementiran preko niza, prvo je potrebno proveriti da li dužina niza znakova nakon spajanja premašuje dužinu trenutnog niza; ako premašuje, niz se prvo proširuje, a zatim se originalne vrednosti kopiraju u novi niz.
- String koji se dodaje,
str, kopira se u ciljni nizvalue.
str.getChars(0, len, value, count)- Ažurira se dužina niza
count.
Spajanje stringova pomoću String.concat
"Pored operatora + i StringBuilder-ovog append() metoda, da li postoje još neki drugi načini za spajanje stringova?" pitala je Sanmej.
"Ima ih, na primer concat() metod klase String, koji pomalo podseća na append() metod klase StringBuilder."
String chenmo = "Chenmo";
String wanger = "Wanger";
System.out.println(chenmo.concat(wanger));Možemo pogledati i izvorni kod concat() metoda.
public String concat(String str) {
int otherLen = str.length();
if (otherLen == 0) {
return this;
}
int len = value.length;
char buf[] = Arrays.copyOf(value, len + otherLen);
str.getChars(buf, len);
return new String(buf, true);
}Ako je dužina stringa koji se dodaje 0, vraća se string kakav je bio pre spajanja.
Niz znakova
valueoriginalnog stringa kopira se u promenljivu nizabuf.String koji se dodaje,
str, kopira se u niz znakovabufi vraća se novi string objekat.
Objašnjavao sam Sanmej liniju po liniju.
"U poređenju sa operatorom +, kada concat() metod naiđe na string čija je vrednost null, baciće NullPointerException, dok operator '+' tretira null kao string "null"."
Ako je string koji se dodaje prazan string (""), concat je nešto efikasniji, jer nema potrebe za kreiranjem new StringBuilder objekta.
Ako se spaja veoma mnogo stringova, efikasnost concat() opada, jer se kreira sve više string objekata.
"Ima još?" čini se da je Sanmej veoma zainteresovana za spajanje stringova.
"Ima, naravno."
Spajanje stringova pomoću String.join
Klasa String ima statički metod join() koji se može koristiti na sledeći način.
String chenmo = "Chenmo";
String wanger = "Wanger";
String cmower = String.join("", chenmo, wanger);
System.out.println(cmower);Prvi argument je znak za spajanje (razdvojnik), na primer:
String message = String.join("-", "Wanger", "jako", "zabavno");Rezultat je: Wanger-jako-zabavno.
Pogledajmo izvorni kod join metoda:
public static String join(CharSequence delimiter, CharSequence... elements) {
Objects.requireNonNull(delimiter);
Objects.requireNonNull(elements);
// Number of elements not likely worth Arrays.stream overhead.
StringJoiner joiner = new StringJoiner(delimiter);
for (CharSequence cs: elements) {
joiner.add(cs);
}
return joiner.toString();
}U njemu se kreira objekat koji se zove StringJoiner, zatim se kroz for-each petlju dodaju varijabilni argumenti, a na kraju se pozivom metoda toString() vraća String.
Spajanje stringova pomoću StringUtils.join
"U praksi se za spajanje stringova često koristi i join() metod iz org.apache.commons.lang3.StringUtils."
String chenmo = "Chenmo";
String wanger = "Wanger";
StringUtils.join(chenmo, wanger);Ovaj metod ne treba da brine o NullPointerException.
StringUtils.join(null) = null
StringUtils.join([]) = ""
StringUtils.join([null]) = ""
StringUtils.join(["a", "b", "c"]) = "abc"
StringUtils.join([null, "", "a"]) = "a"Pogledajmo izvorni kod:
public static String join(final Object[] array, String separator, final int startIndex, final int endIndex) {
if (array == null) {
return null;
}
if (separator == null) {
separator = EMPTY;
}
final StringBuilder buf = new StringBuilder(noOfItems * 16);
for (int i = startIndex; i < endIndex; i++) {
if (i > startIndex) {
buf.append(separator);
}
if (array[i] != null) {
buf.append(array[i]);
}
}
return buf.toString();
}I interno se i dalje koristi StringBuilder.
"U redu, Sanmej, time smo završili današnja znanja o spajanju stringova. Imaj na umu da se od verzije Java 9 tumačenje operatora + promenilo u odnosu na ranije — instrukcije bajtkoda su drugačije. Kada kasnije naučiš instrukcije bajtkoda, obradićemo to još jednom detaljno," rekao sam.
"Dobro, Erge, odmori se, ja ću ponovo provući ove primere," rekla je Sanmej.
