Elementarz Java #9 – Wybrane klasy z API Java
Wstęp
Ostatni artykuł z cyklu #ElementarzJava, postanowiłem poświęcić najciekawszemu tematowi. Na tapetę wziąłem bowiem to co w tej technologii jest najlepsze czyli kilka dość popularnych klas, obiektów i wyrażeń lambda. Java słynie z tego, że większość rzeczy dostarcza „za darmo”, a programista może je po prostu wykorzystać. Aby dobrze rozumieć swój kod, warto więc wiedzieć jak to wszystko działa. Tego właśnie dowiesz się z dzisiejszego materiału.
Seria #ElementarzJava składa się z następujących artykułów:
- Podstawy języka Java (kompilacja, zmienne, struktura klasy, pakiety),
- Typy danych w języku Java (deklaracja i inicjalizacja zmiennych, różnica między typami, garbage collection, typy opakowujące),
- Operatory i konstrukcje warunkowe w Java (użycie operatorów, porównywanie obiektów, instrukcje:
if
,if/else
,switch
), - Tablice (charakterystyka tablic, tablice jedno i wielowymiarowe),
- Pętle (tworzenie pętli, instrukcje
break
icontinue
, etykiety), - Metody oraz hermetyzacja (metody statyczne, przeciążanie metod, konstruktory, modyfikatory widoczności, hermetyzacja elementów klasy, parametry),
- Dziedziczenie (implementacja, przysłanianie metod, polimorfizm, rzutowanie),
- Obsługa wyjątków (kategorie wyjątków, łapanie wyjątków, klasy wyjątków),
- API Java (
String
,StringBuilder
, data i czas, kolekcje, wyrażenia lambda).
Jesteś tu pierwszy raz? Polecam rozpocząć lekturę od materiału: Elementarz Java #0 – wprowadzenie.
Użycie klasy String oraz StringBuilder
String
String w Java jest obiektem typu immutable. Nie możemy go modyfikować po utworzeniu, ale może on zostać usunięty przez garbage collection. Jeśli jest on immutable to nie może zmienić swojej długości po utworzeniu instancji. Dodatkowo wszystkie literały ciągów są automatycznie tworzone jako obiekt typu String. Stringa można inicjalizować na dwa sposoby.
String myVariable1 = "New String"; String myVariable2 = new String("New String");
Metoda length
wywołana na obiekcie typu String
zwróci ilość znaków licząc od jedynki.
String result = "abcd"; System.out.print(result.length());
Wynikiem działania powyższego kodu, będzie wypisanie cyfry 4
. Metoda charAt(int arg)
wywołana na obiekcie typu String
zwróci nam znak o podanym indeksie (licząc od zera).
String result = "abcd"; System.out.print(result.charAt(2));
Po uruchomieniu przykładu, wypiszemy na ekranie literę „c
”. Gdy podamy nieprawidłowy indeks dostaniemy w trakcie działania aplikacji wyjątek – StringIndexOutOfBoundsException
. Metoda substring(int arg1, int arg2)
, jak sama nazwa wskazuje zwraca podciąg danego Stringa. O tym jak ona działa możemy przekonać się analizując poniższy przykład.
String result = "0123456789"; System.out.println(result.substring(1, 4)); // wypisze: 123 (końcowy indeks jest wyłączony (ang. exclusive)) System.out.println(result.substring(8, 8)); // nic nie wypisze System.out.println(result.substring(8)); // wypisze: 89 (czyli wszystko od indeksu 8 do końca) System.out.println(result.substring(5, 2)); // Exception: StringIndexOutOfBoundsException
Jak pisałem na samym początku String
jest obiektem typu immutable. Nie można go modyfikować, ale jest pewien „haczyk”.
String result = " Java "; result.toUpperCase(); result.trim(); // metoda trim() usuwa białe znaki, na końcach Stringa. Zwraca nowy String. result += "!"; System.out.print(result);
Powyższy kod, wypisze „ Java !”. Dlaczego? String
jest immutable, więc wszelkie jego modyfikacje nie będą działały. Jedyne co się zmieni to „doklejenie” nowego Stringa, przez operator +=
lub +
. Stąd też przedostatnia linijka nieco zmienia nam końcowy rezultat. Co ciekawe do Stringa możemy również przypisać słówko kluczowe false
lub true
, które jest traktowane jako ciąg znaków.
String result = "Java"; result += false; System.out.print(result);
Rezultatem będzie wypisanie “Javafalse
”.
String arg = "Java"; System.out.println(arg.indexOf("v")); System.out.println(arg.indexOf("va")); System.out.println(arg.indexOf("j"));
Metoda indexOf(String arg)
, którą widać na powyższym przykładzie, zwraca indeks (licząc od 0
) podanego znaku, bądź ciągu znaków (zwracany jest indeks pierwszej pozycji). W przypadku, kiedy dany argument nie zostanie odnaleziony zwracane jest -1
. Rezultatem uruchomienia kodu z przykładu będzie: 2
, 2
, -1
.
String numbers = "0123456789"; System.out.println(numbers.startsWith("0")); // true System.out.println(numbers.startsWith("01")); // true System.out.println(numbers.startsWith("12")); // false System.out.println(numbers.endsWith("8")); // false System.out.println(numbers.endsWith("89")); // true System.out.println(numbers.endsWith("9")); // true
Mamy również metodę startsWith(String arg)
oraz endsWith(String args)
, która zwraca wartość boolowską jeśli dany ciąg znaków zaczyna się bądź kończy od podanego argumentu.
StringBuilder
Klasa StringBuilder
jest zdefiniowana w pakiecie java.lang
i reprezentuje ona sekwencję ciągu znaków typu mutable. Może więc w przeciwieństwie do Stringa zostać dowolnie modyfikowana. Metody takie jak charAt
, indexOf
, substring
i length
działają w takim sam sposób jak w przypadku obiektu String
. StringBuilder
można utworzyć w następujący sposób.
StringBuilder sb1 = new StringBuilder(); StringBuilder sb2 = new StringBuilder(1); StringBuilder sb3 = new StringBuilder("a");
Co ciekawe w drugim przykładzie, gdzie jako argument podaliśmy cyfrę jeden, tak naprawdę zdefiniowaliśmy parametr capacity
(pl. pojemność). sb2
jest cały czas pusty.
StringBuilder sb = new StringBuilder(); sb.append("aaa").insert(1, "bb").insert(4, "ccc"); System.out.println(sb);
Metoda append(String arg)
, dodaje przekazany argument na koniec istniejącego w obiekcie StringBuilder
ciągu znaków. Metoda insert(int arg, String arg2)
, wstawia podany ciąg znaków w miejsce wyspecyfikowanej pozycji (licząc od zera). Przenalizujmy powyższy przykład.
aaa
– zaczynamy od takiego ciągu znaków,abbaa
– po wykonaniu pierwszej instrukcjiinsert
,abbaccca
– po wykonaniu drugiej instrukcjiinsert
.
Metoda delete(int arg1, int arg2)
, działa tak samo jak substring
. Końcowy index jest „wyłączony”.
StringBuilder result = new StringBuilder("0123456789"); result.delete(2, 9);
Rezultatem uruchomienia powyższego kodu będzie: 019
.
public class Main { public void change(String s1, StringBuilder sb1) { s1.concat("Java"); sb1.append("Java"); } public static void main(String[] args) { String s1 = "Hello "; StringBuilder sb1 = "Hello "; change(s1, sb1); System.out.println(s1); System.out.println(sb1); } }
Mamy teraz trochę ciekawszy przykład. Do metody change
, przekazujemy dwie zmienne. Jedną typu String
, a drugą typu StringBuilder
. Pytanie jakie się pojawia to co zostanie zmienione na poziome metody wywołującej? Można by powiedzieć, że obydwa obiekty. Wywoływana jest wewnętrzna metoda concat na String
oraz wewnętrzna metoda append
na StringBuilder
. Nie zapominajmy jednak, że String
jest immutable. Zmieniony zostanie jedynie StringBuilder
. Nasz program wypisze.
Hello Hello Java
Wywołanie metody change
powoduje zwrócenie nowego Stringa, ale nie zmienia oryginalnego. Tego nowego Stringa nigdzie nie zapisujemy więc nie możemy go też wypisać. Inaczej sprawa wygląda ze StringBuilderem który jest mutable. Do metody change
przekazujemy referencję na obiekt StringBuildera
, który stworzyliśmy w metodzie main
. Cały czas, więc operujemy na tym samym obiekcie i w efekcie go zmieniamy. Przejdźmy do kolejnego przykładu.
public class Main { public static void main(String[] args) { StringBuilder sb1 = new StringBuilder("1"); StringBuilder sb2 = sb1.append("2"); // Tutaj jest przypisana referencja do sb1 sb1.append("3"); sb2.deleteCharAt(1); System.out.println(sb1==sb2); System.out.println(sb1); System.out.println(sb2); } }
Co się tutaj dzieje? Mamy stworzony obiekt typu StringBuilder
o nazwie sb1
. Dalej na tym obiekcie wywołujemy metodę append
, która zwraca referencję instancji obiektu, na której została uruchamiana. W efekcie obiekt sb2
to ten sam obiekt co sb1
. Skoro tak jest to jakiekolwiek zmiany z poziomu zmiennej sb1
albo sb2
w efekcie modyfikują ten sam obiekt. Po uruchomieniu kodu otrzymamy następujące wyjście: true
, 13
, 13
.
Na zakończenie dodam, że StringBuilder
i StringBuffer
definiują te same publiczne metody.
Obsługa daty i czasu
Od lat sen z powiek spędzały programistą wszystkie operacje związane z datami i czasem. Od wersji ósmej, twórcy Javy postanowili zrobić z tym swoisty porządek i dostarczyć natywne API. Zobaczmy jak w Java za pomocą obiektu LocalDate
możemy przechowywać informacje odnośnie daty. W poniższym przykładzie zapiszemy datę: 10 lipca 2019
.
LocalDate date1 = LocalDate.of(2019, 7, 10); LocalDate date2 = LocalDate.of(2019, Calendar.JULY, 10); // To jest niepoprawne LocalDate date3 = LocalDate.of(2019, Month.JULY, 10);
Myślę, że ten kod wymaga kilku zdań komentarza. Po pierwsze obiekt LocalDate
nie ma zdefiniowanego publicznego konstruktora. Stąd używamy metody of
. Jego instancja jest typu immutable. Nie może więc być modyfikowana. Kod w drugiej linijce jest niepoprawny, ponieważ używa starego sposobu liczenia miesięcy. Od wersji ósmej, miesiące przy użyciu klasy Calendar
numerowane są od 0
. W naszym przypadku sugeruje, że data to 10 czerwca 2019
.
LocalDate date1 = LocalDate.of(2019, 7, 10); date1.plusDays(10); date1.plusYears(4); Ssystem.out.print(date1);
Powyższy kod „udowadnia” nam, że obiekt LocalDate
jest immutable. Metoda plusDays
wywołana na obiekcie LocalDate
zwraca nowy obiekt LocalDate
, który nie jest nigdzie zapisywany. Koniec końców nic się nie zmieni.
LocalDateTime date1 = LocalDateTime.of(2019, 7, 10, 12, 19, 02); Period period = Period.of(1, 2, 3); date1 = date1.minus(period); System.out.print(date1);
Rezultatem uruchomienia powyższego kodu będzie: 2018-05-07T12:19:02
. Przekazując do metody minus()
obiekt typu Period
, odjęliśmy od daty 1
– rok, 2
– miesiące, 3
– dni). Metoda minus()
zwróciła nowy obiekt LocalDateTime
, który przypisany został do zmiennej date1
.
LocalDate date = LocalDate.of(2019, Month.SEPTEMBER, 1).plusMonths(1).plusYears(2); System.out.print(date);
Po uruchomieniu takiego przykładu jak wyżej zobaczymy, że w dacie została zmieniona liczba miesięcy oraz liczba lat. Metoda plusMonths
zwróciła nowy obiekt, a następnie na tym samym obiekcie została wywołana metoda plusYears
i powstał kolejny obiekt, który został przypisany do zmiennej date
.
LocalDateTime date1 = LocalDateTime.of(2019, 7, 10, 12, 19, 02); Period period = Period.ofDays(1).ofYears(2); date1 = date1.minus(period); System.out.print(date1);
Tutaj pokusiłem się o „mały” trik. W drugiej linijce statycznie wywołujemy metodę ofDays
obiektu Period
. Metoda ta zwraca nam nowy obiekt Period
, który przypisywany jest do zmiennej period
. Cała zabawa polega jednak na tym, że operacja ta jest powtarzana chwilę później. Tym razem uruchamiamy statycznie metodę ofYears
obiektu Period
. Metoda ta ponownie zwraca nam nowy obiekt Period
, który nadpisuje ten poprzedni. Finalnie w zmiennej period
, mamy obiekt Period
, który ma ustawioną liczbę lat na 2
. Po wykonaniu operacji z linii trzeciej, od daty odejmowane są dwa lata. Finalnie otrzymamy więc następujący rezultat: 2017-07-10T12:19:02
.
LocalDateTime date1 = LocalDateTime.of(2019, 7, 10, 12, 19, 02); DateTimeFormatter f = DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT); System.out.println(date1.format(f));
Java udostępnią takie klasy jak DateTimeFormatter
oraz DateFormatter
, które umożliwiają łatwiejsze prezentowanie danych czasowych. Uruchamiając kod z powyższego przykładu, otrzymamy następujący rezultat na ekranie komputera.
12:19 PM
Aby korzystać z obiektów umożliwiających operowanie czasem, należy wykonać wcześniej odpowiedni import. Wszystkie opisane wyżej klasy są częścią pakietu java.time
.
import java.time.*; import java.time.LocalDate;
Java oferuje znacznie więcej metod i operacji na danych czasowych. Nie opisywałem ich wszystkich w tym materiale, ale zachęcam do własnych ćwiczeń i przeglądnięcia dokumentacji.
Użycie ArrayList z określonym typem
ArrayList implementuje interfejs List
w tym wszystkie operacje na liście add
, modify
i delete
. Przykład deklaracji i inicjalizacji ArrayList
.
ArrayList list = new ArrayList(); list.add(1);
Przy próbie wypisania tak zdefiniowanej listy (System.out.print(list)
) otrzymamy rezultat: [1]
. Co ciekawe, podanie wiele różnych danych do takiej listy nie powoduje błędu.
ArrayList list = new ArrayList(); list.add(1); list.add("Hello"); list.add(false);
Powyższy kod zadziała poprawnie. Przy próbie wypisania, otrzymamy wszystkie wartości jakie zostały umieszczone na liście. ArrayList
udostępnia metodę size
. Zwraca ona aktualny rozmiar kolekcji.
ArrayList list = new ArrayList(); list.add(1); System.out.println(list.size());
Wynikiem działania powyższego kodu, będzie wypisanie cyfry 1
. ArrayList
można też zdefiniować z określonym typem.
ArrayList<String> list = new ArrayList<>(); list.add("Hello");
W takim wypadku instrukcja list.add(1)
, która była dopuszczalna bez definiowanego typu, spowoduje błąd kompilacji.
Użycie Collections
Java umożliwia znacznie bardziej zaawansowane operacje na abstrakcyjnych typach danych. Aby je przeprowadzić używamy klasy Collections
.
List<Integer> list = Arrays.asList(4, 2, 1, 3); Collections.sort(list); int x = Collections.binarySearch(list, 2); int y = Collections.binarySearch(list, 8); System.out.print(x + ", " + y);
Statyczna metoda binarySearch
, jak sama nazwa wskazuje odpowiada za uruchomienie wyszukiwania binarnego na podanej kolekcji. Zwraca ona indeks elementu, jeśli został on znaleziony lub ujemną wartość rozmiaru danej kolekcji powiększoną o 1 w przeciwnym przypadku. Dla zadanego przykładu otrzymamy rezultat: 1
, -5
. W drugiej linijce naszego kodu użyłem statycznej metody sort
(kolekcja powinna być posortowana przed implementacją wyszukiwania binarnego). Można to oczywiście uprościć stosując odpowiedni import (pisałem o tym w materiale na temat podstaw Javy).
import static java.util.Collections.*; // statyczny import metod z Collections public class Test { public void method(ArrayList<Integer> list) { sort(list); // dzięki statycznemu importowi teraz możemy tak wywoływać metodę sort() z Collections } }
Poniżej zamieszczam przykład z wykorzystaniem metody contains
, która sprawdza czy dany element znajduje się w kolekcji czy też nie. Jak widzisz, użyłem tutaj kolekcję bez zdefiniowanego typu. Mogłem więc dodawać różnego rodzaju elementy.
public class Main { public static void main(String[] args) { List myList = new ArrayList(); myList.add(“Hello”); myList.add(true); myList.add(0); System.out.println(myList.contains("Hello")); System.out.println(myList.contains(new Boolean(true))); System.out.println(myList.contains(0)); } }
Rezultatem uruchomienia takiego kodu będzie oczywiście: true
, true
, true
. Ciekawa jest jeszcze kwestia metody remove
. Czy wiesz co się stanie, jeśli będziemy chcieli usunąć element, którego w kolekcji już nie ma?
List<String> myList = new ArrayList<>(); myList.add("Kraków"); myList.add("Warszawa"); myList.remove("Gdańsk"); // zwróci false i nic się nie stanie System.out.print(myList.size());
W takim wypadku metoda remove zwróci nam false
. Nic się innego nie stanie. Kod będzie działał poprawnie. Nie będzie też żadnego wyjątku. Na zakończenie tego podpunktu, przenalizujmy jeszcze taki kod.
public class MyClass { public static void main(String... args) { ArrayList<String> list = new ArrayList<>(); list.add(1, "First element"); } }
Mamy tutaj listę ArrayList
oraz metodę add
, która dla indeksu 1
wstawia nowy element. Czy taki kod zadziała poprawnie? Niestety nie. W tym przypadku otrzymamy wyjątek IndexOutOfBoundsException
. Dzieje się tak z prostej przyczyny – nie mamy elementu o indeksie 0
, który byłby zapisany na liście. Nie możemy więc „nagle” wstawić elementu o indeksie 1
.
Proste wyrażenia lambda i predykaty
Wyrażenia lambda działają z interfejsami funkcyjnymi. Interfejs funkcyjny to taki interfejs, który definiuje tylko jedną metodę abstrakcyjną. Każde wyrażenie lambda ma kilka opcjonalnych i obowiązkowych sekcji:
- typ parametru (opcjonalne),
- nazwa parametru (obowiązkowe),
- „strzałka” (obowiązkowe),
- nawiasy klamrowe (opcjonalne),
- słówko kluczowe return (opcjonalne),
- lambda body (obowiązkowe).
Struktura wyrażenia lambda.
(int arg1, String arg2 -> { return myLambdaBody(arg1, arg2); } // argumenty -> ciało
Jeśli ciało funkcji umieszczamy w nawiasach klamrowych (są one opcjonalne) to musimy w nich umieścić wyrażenie return …;
. Nie jest dopuszczalna taka forma.
myArg -> { myArg.name.equals("Hello") } // brak wyrażenia return – błąd kompilacji
Kilka przykładowych wyrażeń lambada.
myArg -> myArg.isEmpty();
myArg -> { return myArg.isEmpty(); }
( ) -> 1;
(String myArg) -> myArg.isEmpty();
Nieco wcześniej napisałem, że wyrażenia lambda działają z interfejsami funkcyjnymi. Jednym z takich interfejsów jest interfejs Predicate
. Wykonuje on operacje logiczne i zwraca true
albo false
. Popatrzmy na przykład.
import java.util.function.*; public class World { String name; public static void main(String[] args) { World world = new World(); world.name = "Earth"; check(world, myArg -> myArg.name.equals("Moon")); } private static void check(World world, Predicate<World> pred) { System.out.print(pred.test(world) ? "Landlord" : "Foreign"); } }
Mamy obiekt World
, który ma zdefiniowane pole name
. Kolejno zdefiniowaliśmy metodę check
, która przyjmuje obiekt World
oraz interfejs Predicate<T>
z obiektem typu World
jako argumentem. Nasza metoda check
, wywołuje zdefiniowaną w interfejsie Predicate
metodę test
na przekazanym obiekcie typu World
i w zależności od wyniku wypisuje słówko „Landlord
” lub „Foreign
”. Wywołując metodę check
z poziomu metody main
, jako argument podajemy obiekt typu World
oraz wyrażenie lambda, które w ciele zdefiniowane ma wyrażenie logiczne, sprawdzające czy pole name
obiektu World
zawiera napis „Moon
”.
Interfejs funkcyjny to taki, który zawiera jedną metodę abstrakcyjną. W poprzednim przykładzie skorzystaliśmy z interfejsu Predicate
, który wbudowany jest w Javie (jest ich całkiem sporo. Zachęcam do zapoznania się ze wszystkimi w dokumentacji). Nic jednak nie szkodzi, aby napisać swój własny.
interface Requirement { boolean IsTooHigh(int height, int limit); } public class Chair { public static void main(String[] args) { check( (height, limit) -> height > limit, 1); } private static void check(Requirement requirement, int height) { if(requirement.isTooHigh(height, 10)) { System.out.print("Ups… we have problem"); } else { System.out.print("It is ok"); } } }
Mamy tutaj metodę check
, która przyjmuje nasz własny interfejs funkcyjny nazwany Requirement
oraz zmienną typu int
. W metodzie check w instrukcji warunkowej if
, „wywołujemy” metodę isTooHigh
zdefiniowaną w interfejsie, podając dwa argumenty. Przekazaną wcześniej wartość height
, oraz cyfrę 10
. W zależności od wyniku wyrażenia logicznego (z ciała wyrażenia lambda) podejmujemy odpowiednią akcję. Metodę checkwywołujemy podając w miejsce pierwszego argumentu (czyli naszego interfejsu) wyrażenie lambda, gdzie mamy dwa argumenty (są one przyjmowane przez metodę isTooHeight
z interfejsu Requirement
). Następnie w ciele lambdy definiujemy wyrażenie logiczne (height > limit
) – zwraca ono true
albo false
. Jako drugi argument (height
) podajemy cyfrę 1
. Jak to zadziała? Zostanie wywołana metoda check
z wysokością o wartości 1
i limitem 10
. Po sprawdzeniu wyrażenia logicznego (z lambdy) zostanie wypisany napis „It is ok
”.
interface Requirement { int count(); } class Main implements Requirement { public int count() { return 1; } }
Jak wywołać metodę count
nie implementując interfejsu? Użyjemy do tego celu wyrażenie lambda (interfejs Requirement
jest interfejsem funkcyjnym).
interface Requirement { int count(); } class Main { private static int myMethod (Requirement req) { return req.count(); } public static void main(String[] args) { myMethod( () -> 1 ); myMethod( () -> { return 2; } ); } }
Tworzymy klasę Main
ale zamiast implementacji interfejsu, definiujemy metodę, która jako argument przyjmuje zmienną o typie interfejsu Requirement
. Na przekazanym w ten sposób obiekcie wywołujemy metodę count
i zwracamy jej wynik (będzie to int
bo metoda count
zwraca int’a). Teraz przy wywołaniu utworzonej wcześniej metody (myMethod
) z poziomu metody main
, jako argument podajemy wyrażenie lambda. W samej lambdzie nie definiujemy, żadnych argumentów (bo metoda count
ich nie przyjmuje), a w ciele podajemy to co chcemy zwrócić. Możemy użyć słówka kluczowego return
, lub obejść się bez niego. W moim przykładzie, pierwsza lambda zwróci 1
, a druga 2
.
Podsumowanie
I tym miłym akcentem oficjalnie kończę serię #ElementarzJava. Zapraszam do lektury pozostałych wpisów (linki do nich na początku artykułu). Oczywiście wszystkie te materiały zostają do poczytania i nigdzie nie znikają. Mam nadzieję, że jeszcze się spotkamy może w kolejnej serii o Java?
Przykład nie jest widoczny w Twoim tekście
„Metoda indexOf(String arg), która widać na powyższym przykładzie zwraca indeks (licząc od 0) podanego znaku bądź ciągu znaków (zwracany jest indeks pierwszej pozycji). W przypadku, kiedy dany argument nie zostanie odnaleziony zwracane jest -1. Rezultatem uruchomienia kodu z przykładu będzie: 2, 2, -1”
Cześć Tomek,
Wielkie dzięki za informację. Brakujący kod już dodany :)
Pozdrawiam,
Łukasz