Informacje o nowych artykułach oraz akcjach edukacyjnych prosto na Twojej skrzynce e-mail!

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.

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

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.

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 charAtindexOfsubstring i length działają w takim sam sposób jak w przypadku obiektu StringStringBuilder 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.

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

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 addmodify 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: 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?

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:

  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.

(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?

Spodobało się?

Jeśli tak, to zarejestruj się do newslettera aby otrzymywać informacje nowych artykułach oraz akcjach edukacyjnych. Gwarantuję 100% satysfakcji i żadnego spamowania!

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

Dodaj komentarz

Odpowiedz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *

Pin It on Pinterest