Kategória: Java standard könyvtárak.
A Stringről már volt szó: a főprogram paraméterlistája String tömbként kapja meg az átadott paramétereket, így már a legegyszerűbb Java program esetén is megkerülhetetlen, ugyanakkor - noha nem primitív típus - annyira alapvető, hogy elkerülhetetlen a használata már kezdettől fogva. Most egy picit részletesebben megnézzük! Az egyszerűség érdekében mostantól elhagyjuk az osztály- és függvénydefiníciót; a már bemutatott példák segítségével tudunk fordítható és futtatható kódot készíteni.
String létrehozása
A String valójában nem más, mint karakterek (char) egymásutánja. Tehát alapértelmezésben a következőképpen hozzuk azt létre:
char[] szia = {'s', 'z', 'i', 'a'}; String sziaStr = new String(szia);
Ilyen a valóságban a legritkább esetben teszünk, a tipikus inkább a következő:
String hello = "hello";
Már volt róla szó, de ismétlésként álljon itt ismét: a Java-ban kettő vagy több Stringet a + operátorral tudunk egymás után fűzi, pl.:
System.out.println(hello + " world");
Szerintem sajnos elég szerencsétlen döntésnek bizonyult a + jel használata, ugyanis ugyanezt használjuk a "rendes" összeadásnál. Nézzük a következő példát!
System.out.println(2 + 3); System.out.println("" + 2 + 3);
Az első sor eredménye 5 lesz, mert először elvégzi az összeadást, utána pedig kiírja az eredményt. A második viszont 23, mert eleve (üres) stringgel indul, ahhoz hozzáfűzi a 2-t, ami egész, de automatikusan stringgé konvertálja, majd ugyanígy a 3-at is.
Egyenlőség vizsgálat
Az egyenlőség vizsgálat előtt érdemes kicsit kitérni arra, hogy hogyan tárolja a Java a stringet a memóriában. Az egész módszer mögött az a felismerés van, hogy már egy közepes méretű programban is igen sok szöveg képződik, melyeknél nagyon sok az ismétlődés. Ha például az egész számok 1 és 100 között sokszor előfordulnak, akkor érdemes azokat eleve eltárolni, majd csak rá hivatkozni. Így ha kétszázszor előfordul a programban mondjuk az 52, akkor elég mindegyik esetben az "előre gyártottra" hivatkozni. Ez persze azt is jelenti, hogy megváltoztatni nem tudjuk; egészen pontosan ha változtatunk rajta, akkor az valójában a háttérben vagy áthivatkozást jelent (ha már előfordul), vagy új memóriafoglalást (ha még nincs ilyen). Nos, a Java pont ezt a módszert alkalmazza: ha keletkezik egy újabb string, megnézi, hogy van-e már olyan a memóriában és ha talál, akkor rá hivatkozik; ha nem, akkor újat hoz létre. Persze tökéletes megoldást csak úgy lehetne létrehozni, ha minden egyes kis string esetén "végignyálazná" a teljes memóriát, ami nyilván borzasztó hatékonytalan lenne, így elfordulhat, hogy mégis új memóriahelyet foglal le, noha már létezik.
A programozó az összehasonlításhoz ösztönösen a == operátort használja, a Java esetén viszont String összehasonlításnál ez nem jó, és sajnos nagyon nehezen felderíthető hibákhoz vezethet. Ez ugyanis primitív esetben az értékeket hasonlítja össze, osztályok esetén viszont magát a hivatkozást! Az, hogy két egyelnő tartalmú string esetén a hivatkozás ugyanarra a memóriacímre mutat-e, nem egyértelmű. Így az egyik fenti példát folytatva a hello == "hello" eredménye nem megjósolható. Általában igaz, de előfordulhat, hogy nem. Ráadásul feltételezhető, hogy ez Java verzió függő is; remélhetőleg a fenti egyenlőség vizsgálat a Java verziószám növekedésével nagyobb eséllyel ad igazt mint hamist.
Viszont erre a bizonytalanságra nem alapozhatunk! Emiatt a stringek összehasonlítását a Java-ban mindenképpen az equals() függvény segítségével érdemes végrehajtanunk, és a fenti példában a hello.equals("hello") garantáltan igaz értékkel tér vissza.
Itt érdemes megemlíteni egy ún. best practice-t: az említett összehasonlítást valójában nem a fenti formában szokás végrehajtani, hanem fordítva, így: "hello".equals(hello). Ennek az oka az, hogy ha a hello változó értéke null, akkor az első NullPointerException-nel elszáll, a második pedig hamis értékkel tér issza, de a program folytatódik. Személy szerint egyébként ezzel nem értek egyet: egyrészt olvashatóság szempontjából az első megközelítés természetesebb, másrészt az eleve egy elhibázott koncepció, ha a null érték összehasonlításkor ténylegesen előfordulhat; ha a hello értéke null, akkor szerintem inkább szálljon el a program mint adjon vissza egy - valószínűleg - hibás hamis értéket, elnyomva ezzel egy potenciális hibát. Ha a program elszáll, akkor a programozó megkeresi a hibát, ill. átszervezi, hogy azon a ponton már elő se fordulhasson a null érték. Annak a tesztelése ugyanis, hogy az adat hiányzik-e,egész más dimenzió, mint az, hogy az adat rendelkezésre áll és adott értéket tartalmaz-e.
String műveletek
A stringeken számos műveletet hajthatunk végre. Az összefűzést a + operátorral már említettük, most vizsgáljuk meg egy adott stringen végrehajtható függvényeket! A példában a hello egy String.
- hello.length(): visszaadja a string hosszát.
- hello.toUpperCase(): visszaadja a string nagybetűs változatát. (Tehát nem helyben módosítja.)
- hello.charAt(2): visszaadja a 2. karakter, 0-tól sorszámozva; a példában így l lesz az eredmény.
- "apple:peach:banana".split(":"): felbontja a stringet részekre; a példában az eredmény egy 3 elemű string tömb lesz (String[]).
A neten számos stringekkel kapcsolatos tananyag található. A hivatalos API dokumentációt (pl. https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/lang/String.html) kifejezetten jónak tartom, mindegyik művelethez példákkal alátámasztott alapos magyarázatot olvashatunk, így kivételesen akár tananyagként is használhatjuk. De referenciaként mindenképp!
StringBuffer és StringBuilder
Amint azt láttuk az egyenlőség vizsgálatban, a String memóriakezelése igen sajátságos. A stringek nem módosíthatóak, és a + operátorral össze lehet őket fűzni. Ez persze azt is jelenti, hogy a "hello" + " " + "world" utasítás hatására valójában 4 string jön létre: a felsorolt 3, valamint külön az eredmény. Valójában tehát igen pazarló a stringek egymás után fűzése, csak egyszerű esetekben célszerű használni. A stringek összefűzése viszont igen gyakori művelet; erre alkották meg a StringBuffer osztályt, melynek működését egy példán illusztráljuk:
StringBuffer buffer = new StringBuffer(); buffer.append("hello"); buffer.append(" "); buffer.append("world"); System.out.println(buffer.toString());
Ez tehát képes módosítani helyben a stringet, egészen pontosan azt a struktúrát, amiből a végén string keletkezik.
A StringBuffer szálbiztos, ami azt jelenti, hogy többszálú program esetén is biztonsággal használhatjuk, a szálak nem fognak "összeakadni". (A többszálúságról később lesz szó részletesebben.) A szálbiztosságnak viszont ára is van: lassúbb mintha nem lenne szálbiztos. Mivel az esetek döntő többségében a StringBuffer esetén valójában nincs szükség szálbiztosságra, a Java 1.5-ben bevezették a StringBuilder osztályt, ami azon túl, hogy nem szálbiztos, teljes egészében megegyezik a StringBuffer-rel. Ha tehát biztosak vagyunk abban, hogy a StringBuilder-t olyan függvényből használjuk, amit csak egyetlen szál hívhat, akkor érdemes inkább StringBuilder-t használni.
toString()
A toString() függvényt az Object deklarálja, így tetszőleges objektumot stringgé tudunk alakítani. Ez azt is jelenti, hogy a System.out.println(o) függvénynek tetszőleges objektumot átadhatunk, az implicit módon meghívja a toString() metódust, és valamit mindenképp ki fog írni. Az Object osztályban az alapértelmezett megvalósítás szerint az eredmény az objektum referenciája lesz. A legtöbb esetben ezt felüldefiniálják, pl. a StringBuffer esetén a tartalmazott szöveg string formátuma lesz az eredmény. A saját osztályok esetén is célszerű felüldefiniálni ezt a függvényt, különösen akkor, ha az osztályunk valójában egy adatstruktúra, hogy ne (az egyébként semmitmondó) referenciát írja ki, hanem a tartalmat. Erre példát láthattunk a fenti MyStructure példában.
Reguláris kifejezések
A string műveletekhez szorosan kapcsolódik a reguláris kifejezések (regular expression) fogalma. Az ezzel kapcsolatos osztályok a java.util.regex csomagban találhatóak. Például a Pattern.matches("^ab+c$", "abbbbc") első paramétere a reguláris kifejezés, a második pedig a szöveg, amit ellenőrizni szeretnénk, a visszatérési értéke pedig az, hogy a szöveg megfelel-e a reguláris kifejezésnek.
A reguláris kifejezésekről a Szabványok oldalon olvashatunk bővebben.
String tömörítés
A Java a szöveget betűit 16 bites formában (UTF-16) tárolja. A legtöbb esetben ez pazarló, mivel a String az esetek döntő többségében angol szöveget tartalmaz, melyre a 8 bit is elég lenne. A 9-es Java-ban bevezették a String tömörítést, melynek a lényege a következő: ha a String kódolható 8 bites formában, akkor char[] helyett byte[] formában tárolja az adatokat. Ehhez bevezettek egy coder nevű, bájt típusú változót, melynek értéke LATIN1 (0) vagy UTF16 (1) lehet.
Ez a programozó számára transzparens, a kódot ugyanúgy kell megírni. Alapértelmezésben be van kapcsolva; kikapcsolható a +XX:-CompactStrings kapcsolóval.