Klasa String w Java od zawsze była jednym z podstawowych elementów w testach dla kandydatów na Junior Java Developerów. Teoretycznie prosta, praktycznie jednak nie jeden Junior ma z nią problem, kiedy przychodzi do bardziej szczegółowych pytań. Przyjrzyjmy się więc kilku przykładom opisującym ten fundament języka programowania.
W przykładach będę posługiwał się testami napisanymi w JUnit. Dwa słowa wyjaśnienia:
- assertSame() sprawdza, czy referencje są takie same.
- assertNotSame() sprawdza, czy referencje nie są takie same.
- assertEquals() sprawdza, czy obiekty są równe, czyli jak programista zaimplementuje metodę equals() w danej klasie.
- assertTrue() sprawdza, czy argument jest prawdziwy.
No to lecimy.
Podstawy
Bezwględnie zapamiętaj: obiekt klasy String jest obiektem niezmiennym (ang. Immutable Object) w Javie. Utworzony raz nie zmienia się i kropka.
String możemy tworzyć na kilka sposobów.
String text1 = "Java Rocks!";
String text2 = new String("Java Rocks!");
Pierwsza linia tworzy obiekt String za pomocą literału. Literał, czyli napis, który mamy między cudzysłowami.
W drugim przypadki tworzymy nowy obiekt String przy pomocy słowa kluczowego new.
Różnica jest znacząca. W pierwszym przypadku literał, przy założeniu, że wcześniej nie występował, trafi do tzw. String Constant Pool i następnie otrzymamy jego referencję.
String Constant Pool jest to pula stałych literałów. I jest to optymalizacja języka Java. Skoro jest to pula, to obiektów String jest tam wiele. Skoro tworzenie w ten sposób obiektów String sprawdza tę pulę i zwraca z niej obiekt to oznacza, że w wielu miejscach w aplikacji możemy dostać referencję do tego samego obiektu z tej puli. I na pewno nie chcemy, żeby nam przez przypadek ktoś w innym miejscu aplikację nasz obiekt zmienił. Zgadza się?
Pewnie, że się zgadza. Dlatego właśnie patrz na początek akapitu … Stringi są niezmienne.
Drugi przypadek tworzenia, za pomocą słowa kluczowego new zapewnia nam tradycyjne utworzenie nowego obiektu na stercie w pamięci.
A, gdy zaczniemy porównywać napisy utworzone różnymi metodami, to zaczynają się pewne komplikacje.
A co z tą niezmiennością?
@Test
public void stringImmutability() {
String text1 = "Java Rocks!";
String text2 = text1.concat(" YES!");
assertEquals("Java Rocks!", text1);
assertEquals("Java Rocks! YES!", text2);
}
Ok, zobacz. Obiekt text1 reprezentuje nasz napis Java Rocks!. I łączymy go za pomocą metody concat() z literałem ” YES!”. Tutaj zapamiętaj – metody klasy String nie zmieniają obiektu, a tworzą nowy. Stąd, jak później widzimy, text1 się nie zmienił, a nowy jest tylko text2.
Jeszcze jeden HINT dla Ciebie, jeśli chcesz kogoś zaskoczyć. W JDK 15 wejdą na stałe już tzw. Text Blocks, czyli wielowierszowe Stringi. Tak to będzie wyglądało:
String text = """
line 1
line 2
line 3
""";
Nie wszyscy to wiedzą, możesz więc zabłysnąć na spotkaniu lub rozmowie kwalifikacyjnej 👍
Równy, czy nierówny?
Jak porównywać obiekty klasy String w Java? Wróćmy do przykładu:
String text1 = "Java Rocks!";
String text2 = new String("Java Rocks!");
No tak, wydaje się, że raczej są to napisy takie same. Czy na pewno? Seria testów:
@Test
public void stringEquality() {
String text1 = "Java Rocks!";
String text2 = new String("Java Rocks!");
String text3 = "Java Rocks!";
assertNotSame(text1, text2);
assertEquals(text1, text2);
assertSame(text1, text3);
assertTrue(text1.equalsIgnoreCase(text2));
assertTrue(Objects.equals(text1, text2));
}
Tak, jak pisałem wcześniej, referencje do obiektów text1 i text 2 będą inne, na co wskazuje assertNotSame(). Jednak, gdy skorzystamy z assertEquals(), to widać, że napisy pod względem wartości są takie same.
Z kolei text1 i text3 są równe, mają taką samą referencję, co sprawdziliśmy za pomocą assertSame().
I to podstawowa lekcja dotycząca klasy String w Javie. Jeśli chcesz w swoim projekcie porównywać napisy, to korzystaj jednej z metod w klasie String:
- equals(), gdy interesuje Cię, czy napisy są dokładnie takie same.
- equalsIgnoreCase(), gdy chcesz wiedzieć czy napisy są takie same, a wielkość liter jest dla Ciebie bez znaczenia.
A jeszcze bezpieczniej, gdy szukasz dokładnej zgodności, zwróć uwagę również na metodę statyczną w klasie Objects, a konkretnie na Objects.equals(), np. Objects.equals(text1, text2). Ta metoda, choć poza klasą String, zapewnia nam poprawne sprawdzenie, nawet, gdy elementy będą null’ami, co może uchronić nas przed niechcianym wyjątkiem NullPointerException.
Wielkość ma znaczenie
W systemach, które piszemy bardzo często spotykamy się z różnego rodzaju listami słownikowymi. Może to być lista kolorów jakiegoś produktu, kody usług, czy jakieś inne rzeczy.
Dobrą praktyką jest trzymanie na takich listach słownikowych pewnym konwencji. Zwykle stwierdzamy, że będziemy trzymać takie wartości w postaci napisów dużymi, bądź małymi literami.
I tak np. powstaje słownik kolorów produktów w postaci listy RED, GREEN, BLUE.
A skoro jest to lista słownikowa, to ktoś z niej korzysta. Bez problemu możemy sobie wyobrazić aplikację web’ową, która pokazuje różne kolory wybranego produktu. Klient wówczas może któryś wybrać i go na przykład zamówić.
W każdym razie zwykle chcemy się zabezpieczyć przed błędami danych przychodzących np. z UI. W przypadku przyjętej konwencji, dla wybranego czerwonego produktu na froncie, chcielibyśmy, żeby UI przysłał właśnie RED. A nie koniecznie wartość rEd. Zawsze może się tak zdarzyć, że użytkownik będzie chciał trochę namieszać w danych i coś podmieni w request do serwera.
I tutaj przychodzą nam z pomocą dwie metody, czyli toUpperCase() i toLowerCase(). Zobaczmy dwa testy.
@Test
public void stringToUpperCase() {
String expected = "GREEN";
String userInput = "grEen";
assertEquals(expected, userInput.toUpperCase());
}
@Test
public void stringToLowerCase() {
String expected = "green";
String userInput = "GreeN";
assertEquals(expected, userInput.toLowerCase());
}
W testach zmienna expected jest naszym oczekiwanym stringiem. Za pierwszym razem widzimy duże litery, a za drugim małe. W obu testach nasz użytkownik końcowy trochę poszalał i przesłał userInput w dość niekonwencjonalny sposób, czyli ani wszystkie małe litery, ani wszystkie duże. A my i tak chcemy sprawdzić, czy przysłany kod koloru jest poprawny, choć nie zgadza się wielkość znaków.
Dzięki zastosowaniu toUpperCase i toLowerCase możemy zwiększyć odporność naszej aplikacji na taki typ błędów danych.
Kilka pozostałych metod
Klasa String w Java ma parę pomocnych metod. Jedna z najpopularniejszych metod to length(), czyli jak długi jest nasz napis. Prosta w użyciu:
@Test
public void stringLength() {
String text1 = "Java Rocks!";
assertEquals(11, text1.length());
}
Bardzo ważna w kontekście zbierania danych wejściowych jest metoda trim(). Działa tak, że usuwa białe znaki z początku i końca naszego napisu:
@Test
public void stringTrim() {
String text1 = " Java Rocks! ";
assertEquals("Java Rocks!", text1.trim());
}
Gdy chcemy połączyć napisy możemy skorzystać z concat(), ale tam naprawdę wszyscy korzystają z operatora +.
@Test
public void stringConcat() {
String text1 = "Java".concat(" Rocks!");
String text2 = "Java" + " Rocks!";
assertEquals("Java Rocks!", text1);
assertEquals("Java Rocks!", text2);
}
Możemy też coś z napisu wyciąć dzięki substring():
@Test
public void stringSubstring() {
String text1 = "Java Rocks!";
assertEquals("Rocks!", text1.substring(5));
assertEquals("Java", text1.substring(0, 4));
}
Lub coś w napisie zastąpić z udziałem replaceFirst() lub replace():
@Test
public void stringReplace() {
String text1 = "Java Java Java Rocks!";
assertEquals("Groovy Java Java Rocks!", text1.replaceFirst("Java", "Groovy"));
assertEquals("Groovy Groovy Groovy Rocks!", text1.replace("Java", "Groovy"));
}
I w końcu możemy napis również podzielić przez metodę split(). Poniżej dzielimy względem białych znaków.
@Test
public void stringSplit() {
String text1 = "Java Java Java Rocks!";
assertEquals(4, text1.split("\\s").length);
}
Creme de la Creme, czyli podsumowanie dla Ciebie
Moja lista rzeczy do zapamiętania dla Ciebie w kontekście klasy String:
- String jest obiektem niezmiennym (ang. Immutable Object).
- Do porównywania korzystaj z metod equals() i equalsIgnoreCase() i Objects.equals().
- Zapamiętaj metody: toLowerCase(), toUpperCase(), length(), concat(), replace(), replaceFirst(), trim(), split(), substring().
- Zwykle do łączenia napisów używamy operatora +.
- W JDK 15 wchodzą już na stałe do języka Text Blocks, o których możesz przeczytać więcej w artykule Java 15 – co nowego w JDK?
Artykuł jest dla Ciebie pomocny? Klasa String w Java jest teraz bardziej zrozumiała? Świetnie! Daj znać, zostaw komentarz poniżej! ⚽⚽⚽
Fajnie sobie przypomnieć. Dzięki. Ładnie i prosto napisane. Skondensowana wiedza przydatna na rozmowę kwalifikacyjną.
Dzięki za komentarz 🙂
Krótki, ale pomocny i treściwy wpis. Dzięki za wyjaśnienie różnic na początku tekstu, ale również przypomnienie informacji o stosie i stercie 🙂
Miło to słyszeć. Dziękuję za komentarz.
Dzięki, bardzo przydatne informacje !
Miło mi to słyszeć 🙂