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.

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.

Metoda length wywołana na obiekcie typu String zwróci ilość znaków licząc od jedynki.

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).

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.

Jak pisałem na samym początku String jest obiektem typu immutable. Nie można go modyfikować, ale jest pewien „haczyk”. 

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.

Rezultatem będzie wypisanie “Javafalse”.

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.

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 charAtindexOfsubstring i length działają w takim sam sposób jak w przypadku obiektu StringStringBuilder można utworzyć w następujący sposób.

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.

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.

  1. aaa – zaczynamy od takiego ciągu znaków,
  2. abbaa – po wykonaniu pierwszej instrukcji insert,
  3. abbaccca – po wykonaniu drugiej instrukcji insert.

Metoda delete(int arg1, int arg2), działa tak samo jak substring. Końcowy index jest „wyłączony”.

Rezultatem uruchomienia powyższego kodu będzie: 019.

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.

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.

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.

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.

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.

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.

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.

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.

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.

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.

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 addmodify i delete. Przykład deklaracji i inicjalizacji ArrayList.

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.

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.

Wynikiem działania powyższego kodu, będzie wypisanie cyfry 1. ArrayList można też zdefiniować z określonym typem.

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.

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).

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.

Rezultatem uruchomienia takiego kodu będzie oczywiście: truetruetrue. 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?

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. 

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:

  1. typ parametru (opcjonalne),
  2. nazwa parametru (obowiązkowe), 
  3. „strzałka” (obowiązkowe), 
  4. nawiasy klamrowe (opcjonalne),
  5. słówko kluczowe return (opcjonalne),
  6. lambda body (obowiązkowe).

Struktura wyrażenia lambda.

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.

Kilka przykładowych wyrażeń lambada.

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.

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.

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”. 

Jak wywołać metodę count nie implementując interfejsu? Użyjemy do tego celu wyrażenie lambda (interfejs Requirement jest interfejsem funkcyjnym).

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 na temat Java (linki do nich na początku tego 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?

PS: Na blogu też są inne ciekawe materiały, które będą pojawiały się regularnie :-)

PS 2: Tradycyjnie, jeśli masz jakieś pytania, zapraszam do sekcji komentarzy…

Przeczytaj również

, , , , , , , , , , , , , , , , ,

Dodaj komentarz

guest
0 komentarzy
Inline Feedbacks
View all comments

Pin It on Pinterest