String Javában

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.

Unless otherwise stated, the content of this page is licensed under Creative Commons Attribution-ShareAlike 3.0 License