#06 – Praca z kolekcjami

By 18 września 2014Kurs Javy

W poprzedniej lekcji utworzyliśmy kolekcję elementów, a następnie dodaliśmy do niej naszego nowo utworzonego kota. Dzisiaj poznamy kilka sposobów, jak odczytać informacje zapisane w kolekcji lub jak z kolekcjami pracować.

Kolekcje to jak już zostało powiedziane jeden z najwazniejszych elementów języka Java. Dlatego tak ważne jest ich dobre poznanie, a także oswojenie się z operacjami które możemy na nich wykonać.

Lekcja

Dzisiejsza lekcja będzie obejmowała dwa główne elementy – pętlę for oraz korzystanie z kolekcji typu lista (będziemy w przykładach uzywać ArrayListy jako przykładu, ale pamiętaj, że istnieją też inne listy – wszystkie je znajdziesz w dokumentacji Oracle, w sekcji ‚all known implementations’ dla interfejsu List). Zaczniemy od samych kolekcji a następnie omówimy pętlę for i jak używać jej z kolekcjami.

Kolekcje typu lista (java.util.List)

Lista to najczęściej spotykana kolekcja w pracy z językiem Java. Nie zgłębiając szczegółów technicznych, listę można opisać jako uporządkowany zbiór elementów, w którym możemy użyć dowolnego z elementów (zarówno tego, który jest pierwszy, jak i ostatni, ale też każdego pomiędzy nimi), możemy dodawać nowe elementy (bez limitu rozmiaru – przynajmniej teoretycznie) i usuwać te już istniejące. Możemy ją porównać do nieskończenie długiej szuflady w jakiejś szafce na dokumenty z amerykańskich filmów, w której mamy teczki z jakimiś dokumentami – te teczki to nasze elementy, a szuflada to kolekcja. Możemy wyciągnąć dowolną z teczek, i wiemy ile ich jest.

Listy w Javie (jak wszystkie kolekcje) są parametryzowane, tzn. musimy wcześniej zadeklarować jakiego typu obiekty będziemy trzymali w tej liście. Możemy oczywiście powiedzieć ‚dowolny’ (jako typ wpisując Object) ale raczej tego nie chcemy. W poprzednim zadaniu używaliśmy listy kotów, w przykładach w dzisiejszej lekcji będziemy używać ciągów znaków – przykłady dzięki temu będą krótsze ;) Parametr kolekcji – oznaczany w dokumentacji jako E – to w naszym przypadku będzie String.

Do tej pory używaliśmy już operacji dodawania na listę, ale usystematyzujemy sobie naszą wiedzę o listach i omówimy najważniejsze metody. Oczywiście metod jest dużo więcej, z niektórych będzie potrzeba skorzystać bardzo szybko, innych nie będziesz używała bardzo długo. Dlatego potraktuj to jedynie jako zachęte do zapoznania się z dokumentacją (link znajdziesz w tytule tego akapitu).

add (E element) – dodaj nowy element do kolekcji

Dodaje nowy element do kolekcji. Po tej operacji długość listy zwiększa się o jeden, nowo dodany element trafia na jej koniec. To najczęstszy sposób dodawania elementów do listy.

List<String> lista = new ArrayList<String>(); //lista pusta
lista.add("pierwszy"); //lista ma jeden element
lista.add("inny"); //lista ma dwa elementy
lista.add("pierwszy"); //lista ma trzy elementy

addAll (Collection<E> elementy) – dodaj wiele elementów do kolekcji

Ta metoda pozwala za jednym razem dodać wiele elementów (nie używając pętli, w zależności od typu listy, operacja ta powinna być co najmniej tak szybka jak dodawanie w pętli, najczęściej jest dużo szybsza, co ma znaczenie w przypadku, kiedy mamy naprawdę dużo elementów). Podobnie jak w poprzedniej metodzie, elementy trafiają na koniec listy, są dodawane w kolejności, w jakiej kolekcja elementy je zwraca – w poniższym przykładzie listy będą po prostu połaczone a kolejność elementów zostanie w nich zachowana.

List<String> listaMoja = ... ; //jakaś lista z 5 elementami
List<String> listaCzyjas = ... ; //jakaś lista z 7 elementami

List<String> lista = new ArrayList<String>(); //lista pusta
lista.addAll(listaCzyjas); //lista ma 7 elementów
lista.addAll(listaMoja); //lista ma 12 elementów

get (int indeks) – pobierz element  kolekcji

Pozwala pobrać element z dowolnej pozycji na liście, przy czym pierwsza pozycja ma indeks 0 (jak wszędzie w Javie). Jeśli wskazana pozycja jest większa niż ilość elementów na liście, otrzymamy wyjątek (IndexOutOfBoundException). Pamiętajmy, że nawet usuwając element ze środka, lista pozostaje ciągła (tzn. element z kolejnej pozycji zostaje umieszony na pozycji z której usuwamy).

List<String> lista = new ArrayList<String>(); //lista pusta
lista.add("pierwszy"); //lista ma jeden element
lista.add("inny"); //lista ma dwa elementy
lista.add("pierwszy"); //lista ma trzy elementy
String poczatek = lista.get(0); // zwraca "pierwszy"
String koniec = lista.get(2); // zwraca "pierwszy"
String srodek = lista.get(1); // zwraca "inny"

remove(int indeks) – usuń element z określonej pozycji

Metoda ta pozwala usunąć dowolny element z dowolnej pozycji na liście. Jeśli element nie istnieje, możemy otrzymać wyjątek taki sam, jak w przypadku pobierania elementu z listy (możemy także w pewnych szczególnych przypadkach otrzymać wyjątek UnsupportedOperationException, jeśli użyty przez nas typ listy nie pozwala usuwać elementów, ale jest to bardzo rzadka sytuacja). Metoda ta poza usunięciem elementu z listy, zwraca go (możemy więc coś z nim dalej robić)

List<String> lista = new ArrayList<String>(); //lista pusta
lista.add("pierwszy"); //lista ma jeden element
lista.add("inny"); //lista ma dwa elementy
lista.add("pierwszy"); //lista ma trzy elementy
String poczatek = lista.get(0); // zwraca "pierwszy"
poczatek = lista.remove(0); //zwraca "pierwszy"
String srodek = lista.get(0); // zwraca "inny"
String koniec = lista.get(1); // zwraca "pierwszy"

isEmpty() – sprawdź, czy na liście są jakies elementy

Metoda zwraca wartość prawda/fałsz, która mówi nam czy lista jest pusta (tzn. czy ma dokładnie zero elementów).

List<String> lista = new ArrayList<String>(); //lista pusta
boolean pusta = lista.isEmpty() //zwraca true
lista.add("pierwszy"); //lista ma jeden element
pusta = lista.isEmpty() //zwraca false
lista.remove(0); //zwraca "pierwszy", lista ma 0 elementów
pusta = lista.isEmpty() //zwraca true

size() – sprawdź, ile elementów jest na liście

Metoda ta pozwala na sprawdzenie ile elementów znajduje się na liście. Jest ona szczególnie użyteczna, jesli wczytujemy od użytkownika np. numer elementu z listy i chcemy się upewnić, że użytkownik podał prawidłową wartość (dzięki czemu unikniemy konieczności obsługiwania wyjątków w naszym kodzie).

List<String> lista = new ArrayList<String>(); //lista pusta
int ilosc = lista.size(); //zwraca 0
lista.add("pierwszy"); //lista ma jeden element
ilosc = lista.size(); //zwraca 1
lista.add("inny"); //lista ma dwa elementy
ilosc = lista.size(); //zwraca 2
lista.add("pierwszy"); //lista ma trzy elementy
ilosc = lista.size(); //zwraca 3
lista.remove(0); //zwraca "pierwszy"
ilosc = lista.size(); //zwraca 2

Pętla for

W Javie istnieją dwa warianty pętli for (nazwy zmyślone, na potrzeby rozróżniania w ramach tej lekcji, nie są to oficjalne nazwy obu wariantów!) – pierwszy, normalny (‚standardowy’) oraz drugi, iteracyjny (w innych językach często pętla ta ma nazwę foreach, za chwilę dowiesz się dlaczego; czasem można spotkać się z nazwą ‚for-each’).

Standardowa pętla for

Standardowa pętla for ma w języku Java następującą składnię:

for (inicjalizacja; warunek_zakończenia; krok) {
//to, co chcemy w pętli robić
}

W pierwszej części mamy następujące elementy (każdy z nich jest opcjonalny):

  • inicjalizacja – ten fragment wykona się tylko raz, przed pierwszą iteracją. Można tutaj np. zainicjować zmienną pomocniczą, której będziemy używali do tego, żeby pętla wykonała się dokładnie X razy
  • warunek_zakończenia – ten fragment jest wykonywany przed każda iteracją. Jeśli warunek tutaj opisany jest spełniony, to następna iteracja się wykonuje, jeśli nie to pętla kończy działanie
  • krok – ten fragment wykonuje się po każdej iteracji, używamy go np. do inkrementowania (powiększenia o jeden) licznika iteracji

Przykładowa pętla for wygląda następująco:

for (int i = 0; i<10; i++) {
System.out.println("Iteracja: " + i);
}

Powyższa pętla wypisze dziesięć linijek. Nasza pętla for:

  • przed iteracjami inicjalizujemy zmienną i i nadajemy jej wartość 0
  • przed każdą iteracją, sprawdzamy czy i jest mniejsze od 10 (zwróć uwagę, że sprawdzamy czy jest mniejsza od 10 – czyli dla i==10, pętla zostanie przerwana; zmienna i jest inicjowana wartością 0, więc zanim pętla będzie przerwana, wykona się 10 iteracji: 0,1,2,3,4,5,6,7,8,9)
  • po każdej iteracji powiększamy i o 1 (i++ oznacza powiększ i o jeden)
  • w każdej iteracji wypisujemy do konsoli tekst „Iteracja X” (gdzie X to kolejno 0, 1, 2 itd)

Możemy oczywiście pominąć wszystkie te elementy, otrzymując tym samym pętle nieskończoną:

for (;;;) {
//tutaj będziemy potrzebować warunku, który przerwie pętlę instrukcją sterującą break, albo zakończy działanie aplikacji
}

Pętle najczęściej używane są w połączeniu z jakąś kolekcją – np. w sytuacji kiedy chcemy wykonać operację na każdym z elementów, albo np. zsumować pola wszystkich elementów w jedną wartość. Wracając bliżej aplikacji, którą piszemy, fragment kodu który sumuje masę kotów w naszej bazie danych mógłby wyglądać następująco:

Float masa = 0.0f;
List<Kot> koty = ... ; //tutaj inicjujemy i wypełniamy listę
for (int i = 0; i<koty.size(); i++) {
Kot kot = koty.get(i);
masa += kot.getMasa();
}
System.out.println("Wszystkie koty razem ważą w sumie: " + masa);

Jak widać jest to mało wygodne (dużo niepotrzebnego pisania), zobaczmy więc, jak możemy to uprościć.

Pętla for-each

Ta konstrukcja jest w mojej subiektywnej opinii jest bardziej użyteczna od przedstawionej powyżej, choć oczywiście zależy to od sytuacji i należy dobrze znać obie. Konstrukcja ta jest ściśle powiązana z kolekcjami, ponieważ przechodzi ona po każdym elemencie kolekcji (jest to skrót od powyższego kodu). Zaletą jest to, że w przypadku kolekcji nieuporządkowanych (np. Set, Map w ogólnym przypadku), nie musimy samodzielnie obsługiwać iteratora (sposób na przejście po wszystkich elementach kolekcji, także nieuporządkowanej). Przedstawiony na końcu poprzedniej sekcji przykład można zapisac jako:

List<Kot> koty = ... ; //tutaj inicjujemy i wypełniamy listę
for (Kot kot : koty) {
masa += kot.getMasa();
}

Jak widać, sam zapis jest bardziej czytelny i łatwiejszy do pracy z nim. Ogólna konstrukcja wygląda następująco:

Collection<E> collection = ... ; //tutaj jakaś kolekcja
for (E element : collection) {
//działania na obiekcie
}

Poza słowem kluczowym for deklarujemy jakiego typu ma być element podbrany w każdej iteracji, jak ma się in nazywać, a także jak nazywa się kolekcja, z której będziemy pobierać elementy.

Materiały dodatkowe / dokumentacja

  1. Opis pętli for na stronie Oracle (EN)

Zadanie

Dzisiejsze zadanie będzie nieco bardziej skomplikowane od poprzednich. Ale w końcu to już 6 lekcja ;) Dzisiaj przerobimy zupełnie nasz program tak, aby był on już funkcjonalny. Dla ułatwienia na końcu zadania znajdziesz diagram aktywności (jak z niego skorzystać, przeczytasz we wpisie Ani).

Zadanie polega na przerobieniu programu tak, aby na początku wyświetlało się menu (najprostsze: najpierw wypisz wszystkie dostępne opcje, a następnie wczytaj od użytkownika jego wybór), w którym będziemy mogli wybrać jedną z opcji: dodaj kota, pokaż koty, zamknij program (wpisując odpowiednio 1, 2 lub x z klawiatury). Po wykonaniu danej czynności, wracamy do początku, czyli pokazujemy menu.

Pierwsza opcja, dodaj kota, powinna wywoływać ten kod, który napisaliśmy do tej pory. Czyli wczytujemy po kolei wszystkie dane kota, po czym dodajemy go do kolekcji poprzez nasz obiekt kotDAO.

Druga opcja najpierw wyświetli wszystkie koty (tylko imiona) oraz ich identyfikatory (numer kolejny). Użytkownik powinien wpisać numer w konsoli, po czym wybrany kot się przedstawia lub wyświetlamy informacje o błędzie, że takiego kota nie ma.

Trzecia opcja od razu zamyka program.

Diagram aktywności do tego zadania znajdziesz poniżej. Krok oznaczony innym kolorem to ten fragment, który już zrobiliśmy w poprzednich lekcjach.

Diagram pomocniczy do zadania.

Diagram pomocniczy do zadania.

Podpowiedzi

More »

  1. Zwróć uwagę, że wszystkie opcje (poza wyjściem z programu) wracają do menu – to wskazuje że warto użyć pętli nieskończonej w tym miejscu lub sprytnego warunku
  2. Przedstaw kota musi pobierać z listy kota na odpowiedniej pozycji – pozycja ta musi być wyrażona liczbą. Pamiętaj, żeby # kota zamienić z ciągu znaków (to, co wczytamy od użytkownika) na liczbę (podobnie jak w poprzednich lekcjach z wagą)
  3. Należy też mieć na uwadze, że użytkownik może wpisać nieprawidłowe dane – nie zapominaj o walidacji w każdym miejscu, w którym pytasz o coś użytkownika

zip Pobierz rozwiązanie tego zadania

Licencja Creative Commons

Jeśli uważasz powyższą lekcję za przydatną, mamy małą prośbę: polub nasz fanpage. Dzięki temu będziesz zawsze na bieżąco z nowymi treściami na blogu ( i oczywiście, z nowymi częściami kursu Javy). Dzięki!

  •  
  •  
  •  
  •  
  •  
  • searlas

    Hej, wkradł się mały błąd przy opisie operacji get :)

    • Dzięki za zwrócenie uwagi! Już poprawione.

  • 1. Byłaby istotna różnica jakby
    zamiast KotDAO.getCats().size() użyć KotDAO.koty.size() ? (Uogólniając: lepiej sięgać po instance variable bezpośrednio czy poprzez metodę?)

    2. Byłaby istotna różnica jakby
    zamiast Integer.parseInt( userInput ) użyć Integer.valueOf( userInput ) ?

    3. Byłaby istotna różnica jakby
    metodę prezentującą wszystkie koty w kolekcji

    public void showAllCatsI()
    {
    for (int c = 0; c < koty.size(); c++)
    {
    Kot cat = koty.get(c);
    System.out.print( c +"." + cat.getImie() );
    System.out.println();
    }
    }

    umieścić w KotDAO zamiast w Interfejs?

    4. W jakim programie można robić takie UML diagramy?

    • 1. Z punktu widzenia działania – nie, przynajmniej w tym wypadku. Natomiast lepszą praktyką jest używanie metod, dzięki czemu możemy korzystać ze wszystkich dobrodziejstw polimorfizmu w przyszłości
      2. Tak, pierwsze zwraca int (prymityw), drugie Integer (obiekt). Przez to, że jest autoboxing i autounboxing prawdopodobnie nei zauwazyłbyś różnicy w samym kodzie, natomiast są to różne metody
      3. Tak, DAO to DataAccessObject – umieszczamy tam logikę, która sie odnosi do obsługi np. bazy danych, a nie logikę biznesową (bo jako taką możemy traktować wyświetlenie wszystkich kotów)
      4. canva.com ;) Nie jest stricte do diagramów czy tym bardziej UML, ale da się jak widać ;)

      • To punktu 3. nie rozumiem.
        Jeśli przelatywanie przez wszystkie koty w liście nie jest związane z obsługa bazy danych, to co jest?!

        • Jest pewna różnica, mając normalne, bazodanowe DAO, żeby wykonać powyższy kod, będziesz używał metod, które sięgają do bazy danych (np. pobierzesz najpierw kolekcję, a dopiero później wykonasz pętlę for na tej kolekcji). To są operacje na już pobranych danych. DAO powinno zawierać tylko operacje związane ściśle z komunikacją z bazą danych i elementarnymi operacjami (utwórz / pobierz / aktualizuj / usuń – CRUD). Wypisywanie w pętli obiektów jest już logiką biznesową – w tej metodzie nie jest ‚zaszyte’ jak pobrac te dane (jest to w innej metodzie, której użyjesz, żeby je pobrać) – pewnym prostym algorytmem jest to, że wypisujesz liczbę porządkową i imię kota w linii. W aplikacji prawie każda operacja pracuje na danych, ale sa one pobierane za pośrednictwem właśnie DAO będącego gdzies ‚niżej’.

  • Karolina

    Bardzo pomocny kurs :) pozwolił mi przekonać się do javy (do tej pory jakoś przed nią broniłam się rękami i nogami). Jeżeli ktoś ma wątpliwości, polecam :)

    • Dzięki za polecenie, bardzo nas to motywuje do dalszego działania!

  • Kasia

    Czy jest jakaś różnica w zapisach:
    1. Integer numerKota = Integer.parseInt(numerWczytany);
    if (kotDao.getKoty().size()>numerKota) {
    …..
    }

    2. if (kotDao.getKoty().size()>Integer.parseInt(numerWczytany)) {
    …..
    }

    • Kasia

      i tak samo:
      1.
      Kot kot;
      for (int i=0; i<kotDao.getKoty().size(); i++) {
      kot = kotDao.getKoty().get(i);
      System.out.println(i + ": " + kot.getImie());
      }

      2.
      for (int i=0; i<kotDao.getKoty().size(); i++) {
      System.out.println(i + ": " + kotDao.getKoty().get(i).getImie());
      }

    • Kasia

      Coś się posypało…

      1.
      Kot kot;
      for(int i=0; i<kotDao.getKoty().size(); i++){
      kot = kotDao.getKoty().get(i);
      System.out.println(i + ": " + kot.getImie());
      }

      2.
      for(int i=0; i<kotDao.getKoty().size(); i++){
      System.out.println(i + ": " + kotDao.getKoty().get(i).getImie());
      }

      • Do wklejania fragmentów kodu polecamy np http://pastebin.com – w komentarzach czasem formatowanie się sypie. Dla czytelności przekleiłem to co wysłałaś: http://pastebin.com/KLeTDVVy
        Podobnie jak powyżej – w tym konkretnym wypadku nie ma różnicy, odwołanie będzie się odbywało tylko raz, różnica jest głównie w czytelności tego kodu. Ogólnie lepiej jest pobrać obiekt do zmiennej i potem odwoływać się poprzez tą zmienną.

        • Kasia

          Dzięki za odpowiedź :-)

    • W tym wypadku nie ma funkcjonalnej różnicy – wywołanie Integer.parseInt() wywoła się tylko raz. Różnic jest jedynie w czytelności kodu – wersja pierwsza jest czytelniejsza w przyszłości

  • Piotr

    Czy miał ktoś problem z hmm… „nadpiswaniem” elementu listy?
    Po dodaniu co najmniej dwóch kotów i wypisaniu ich wyświetla mi całą liste z tym samym imieniem kota, którego dodałem jako ostatniego (petla iteruje prawidlowo, bo liste numeruje poprzez i. Kazdy element listy ma kolejny numer, ale te same imie kota :/

    Pewnie już tutaj nikt nie zagląda, ale jeśli ktoś sie odezwie to wstawie mój beznadziejny kod.

    • Pobierałeś nasze rozwiązanie? Powinno działać prawidłowo, porównaj ze swoim kodem ;)

      • Piotr

        Przepraszam za kłopot. To chyba była wina tego, że miałem static przy utworzonym obiekcie ‚Kot’ :)

  • olekxd

    Odnosnie nazewnictwa to oficjalna nazwa to petla for i enhanced for dla petli for-each, ale rozumiem, ze trzymacie sie konwencji i wszystko dla zrozumienia staracie ujac sie po po polsku. Tak po prostu dla osob co sa poczatkujace jak ja ale podstawy angielskiego jakies tam maja.

    • Cześć,
      faktycznie w Javie pętla ta jest często okreslana jako ‚enhanced for’ – przy czym enhanced ma tutaj znaczenie dosłowne, jako ‚ulepszona’, ‚usprawniona’. Pisząc to załozyliśmy, że przez określenie for-each trochę łatwiej od razu zrozumieć intencję tego typu pętli. Uzupełnimy też o formalną nazwę, ponieważ faktycznie łatwiej tak znaleźć to w dokumentacji. Dzięki za czujność!

  • Michał

    Bardzo pomocny kurs ! Oby tak dalej ;)

    Chciałbym zapytać o lokalizację poszczególnych elementów programu i czy nie złamałem przy tym dobrych praktyk. Wiadomo, lepiej zmienić złe nawyki zanim się zakorzenią.

    Mój kod w skrócie wygląda tak:
    w metodzie main klasy Interfejs zrobiłem dokładnie to co Ty, tylko przy opcji „1” dodałem

    kot.setImie(getUserInput());
    kot.setDataUrodzenia(getUserDate());
    kot.setWeight(getUserWeight());
    kot.setImieOpiekuna(getUserInput());
    kotDAO.dodajKota(kot);

    w klasie Interfejs pod metodą main zawarłem metody precyzujące format wagi i daty czyli:
    getUserDate()
    getUserWeight()

    zaś w klasie KotDAO wpisałem wszystko co się tyczy działań na naszej ArrayList np. ciało metody wypiszKoty() znajdującej się w klasie KotDAO wygląda u mnie tak:

    public void wypiszKoty(){
    System.out.println(„Wybierz kota”);

    for(int i=0; i=0 && wybranyKot<listaKotow.size()){
    System.out.println(listaKotow.get(wybranyKot).przedstawSie());
    }else{
    System.out.println("Nie mamy takiego kota :(");
    }

    Jeszcze raz dzięki za kurs :)

    • Wyłączenie metod getUserDate() i getUserWeight() jest jak najbardziej poprawnym krokiem – można by zmienić ewentualnie nazwę getUserWeight() na getUserFloat() ponieważ metoda ta może pobierać dowolną liczbę zmiennoprzecinkową od użytkownika.

      Co do zmian w kotDao – tutaj nieco łamiemy podział pomiędzy warstwami – z założenia powinniśmy mieć jedną warstwę (u nas klasa ‚Interfejs’), która komunikuje się z użytkownikiem (czyli wypisuje na konsole, wczytuje z konsoli itp), oraz zupełnie oddzielną warstwę (DAO w tym wypadku), która zarządza danymi. Dzięki temu łatwo będzie wykorzystać to samo DAO, jeśli zmienimy aplikację z konsolowej na np. webową (co robimy w kolejnych krokach). To jest główne założenie podziału aplikacji na warstwy – możliwość ponownego wykorzystania jak największej ilości elementów nawet jeśli robimy drastyczne zmiany w jednym z obszarów aplikacji (np. zmieniamy interfejs tekstowy na webowy).

      Powodzenia i trzymamy kciuki za dalszą naukę!

  • Dobry

    Patrząc z punktu widzenia przyszłych rozdziałów bazujących na kodzie już napisanym – czy powinienem poprawiać swój kod nawet jeżeli działa poprawie, tak by nie różnił się od kodu zamieszczonego w lekcji? Mam na myśli np. różnice w metodach.

    Moje rozwiązania zadania:
    – klasa Interfejs: http://wklej.org/id/2884927/
    – klasa kotDAO: http://wklej.org/id/2884955/

    P. S. Fantastyczny kurs, lepszego podejścia nie widziałem. Dopiero teraz czuję, że się naprawdę uczę w praktyce :)

    • Dalsza część kursu bazuje na kodzie z rozwiązań – oczywiście możesz nie modyfikować swojego, ale będziesz musiał odpowiednio dostosowywać wszystkie pozostałe zmiany. To myślę będzie jeszcze lepszą nauką, niż dokładne podążanie ;)
      Dzięki za miłe słowa!