Praca z plikami - czytanie pliku
Kontynuacja pracy z plikami – w poprzednim wpisie omówiliśmy jak zrobić zapis do pliku, dziś zajmiemy się odczytem. Podobnie jak przy zapisie mamy wiele opcji do wyboru 😉
01. BufferedReader
Podobnie jak w przypadku zapisu do pliku przy użyciu BufferedWriter, BufferedReder oferuje nam bardzo przejrzyste API do odczytania zawartości pliku:
public static void bufferedReader() throws IOException { try (BufferedReader reader = new BufferedReader(new FileReader("test.txt"))) { System.out.println(reader.readLine()); } }
02. Scanner
Scanner jest kolejną klasą która może nam umożliwić odczytanie zawartości pliku:
public static void scannerLineReader() throws FileNotFoundException { try (Scanner scanner = new Scanner(new File("test.txt"))) { System.out.println(scanner.nextLine()); } }
Klasy scanner możemy również używać do czytania pliku z użyciem “ogranicznika”, ogranicznikiem może być wybrany przez nas znak. Scanner będzie czytał do pierwszego wystąpienia danego znaku, kolejne części są czytane z wykorzystaniem metody next().
public static void scannerReader() throws FileNotFoundException { try (Scanner scanner = new Scanner(new File("test.txt"))) { scanner.useDelimiter(" "); while (scanner.hasNext()) { System.out.println(scanner.next()); } } }
03. Files
Od Javy 7 pojawiła się klasa Pomocnicza: Files, z jej pomocą możemy ułatwić sobie nieco czytanie plików.
public static void filesReader() throws IOException { System.out.println(Files.readAllLines(Path.of("test.txt")).get(0)); }
Podsumowanie
Tylko żeby ktoś nie pomyślał że temat jest wyczerpany! W tym krótkim wpisie przedstawiłem jedynie podstawowe metody do czytania plików. Jeśli ktoś potrzebuje wgłębić się w zaawansowane operacje pracy z plikami, serdecznie zachęcam do zajrzenia do dokumentacji 🙂 Dokumentacja Javy jest na prawdę na dość dobrym poziomie! 🙂
Praca z plikami - zapis do pliku
Dziś popracujemy sobie z plikami! Temat wcale nie jest taki oczywisty – Java udostępnia nam wiele możliwości aby zapisać dane do pliku. Postaram się omówić najczęściej używane opcje oraz różnice między nimi. Zaczynamy!
01. BufferedWriter
Pierwszą sposób zapisu do pliku jaki przedstawię, jest użycie klasy BufferWriter:
public static void createAndWriteToNewFile(String fileName, String content) throws IOException { try (BufferedWriter writer = new BufferedWriter(new FileWriter(fileName))) { writer.write(content); } }
Jest to bardzo podstawowa metoda która utworzy nam plik, oraz wypisze w nim to co podaliśmy w metodzie write. W tym momencie należy wspomnieć że wielokrotne uruchomienie tej metody nie spowoduje dodanie do pliku niczego nowego – za każdym razem plik będzie nadpisywany.
Warto również zaznaczyć że BufferWriter jest tworzony w bloku “try with resources”, dzięki temu nie musimy się martwić że jakieś zasoby nie zostaną zamknięte w momencie gdy kończymy pracę.
Żeby dodać do pliku kolejne informacje musimy użyć metody append.
public static void appendFileWithBufferedWriter(String fileName, String content) throws IOException { try (BufferedWriter writer = new BufferedWriter(new FileWriter(fileName, true))) { writer.append(' '); writer.append(content); } }
02. PrintWriter
Kolejną metodą którą możemy użyć aby zapisać informacje do pliku jest PrintWriter
public static void createAndWriteToNewFilePrintWriter(String fileName, String content) throws IOException { try (PrintWriter writer = new PrintWriter(new FileWriter(fileName))) { writer.println(content); } }
Istnieją pewne różnice pomiędzy BufferedWriter a PrintWriter – PrintWriter posiada dodatkowe metody które mogą nam nieco ułatwić pracę z plikami, właściwie z PrintWriterem możemy pracować tak jak byśmy chcieli wypisać informacje na konsolę.
Jednak PrintWriter “połyka” błędy, zauważ że podczas pracy z PrintWriterem, metody zapisu nie deklarują wyrzucanego wyjątku:
public void println(String x) {...}
Może być to naprawdę problematyczne – w momencie gdy pójdzie coś nie tak, nie dowiemy się o tym, aby sprawdzić czy pojawił się jakiś problem musimy użyć metody checkError.
03. FileOutputStream oraz DataOutputStream
FileOutputStream jest klasą która pozwoli nam pracować z danymi w postaci binarnej
public void howToUseFOS(String fileName, String content) throws IOException { try (FileOutputStream outputStream = new FileOutputStream(fileName)) { outputStream.write(content.getBytes()); } }
DataOutputStream jest bardzo podobny do klasy opisanej wyżej – jednak pozwala na dodatkowe operacje umożliwiając nam zapis do pliku również typów prostych
try (DataOutputStream dataOutputStream = new DataOutputStream(new FileOutputStream(fileName))) { dataOutputStream.write(1); }
04. RandomAccessFile
RandomAccessFile jest klasą która pozwala nam edytować plik w wybranym miejscu.
public static void writeToPosition(String filename, String content, long position) throws IOException { try (RandomAccessFile writer = new RandomAccessFile(filename, "rw")) { writer.seek(position); writer.write(content.getBytes()); } }
05. Files
Od Javy 7 pojawiła się klasa która nieco ułatwia nam podstawową pracę z pikami. Zamiast zastanawiać się co wybrać – możemy użyć klasy Files! 🙂
public static void java7FilesMethod(String fileName) throws IOException { List<String> lines = Arrays.asList("The first line", "The second line"); Files.write(Paths.get(fileName), lines); }
Podsumowanie
Warto wspomnieć że nie są to wszystkie możliwe metody zapisu do pliku, istnieje np. klasa FileChannel która przeznaczona jest do pracy z dużymi plikami. Istnieje również szereg klas które ułatwiają nam pracę z plikami – np. pozawalają na założenie “locka” – powoduje to zablokowanie pliku na inne operacje do momentu w którym nie skończymy swoich operacji.
Bardzo ważną rzeczą jest zamknięcie strumienia danych po zakończonej pracy, najlepiej użyć bloku try-with-resources jak było to pokazane w powyższych przykładach.
Najczęstsze użycia:
PrintWriter – jest używany do zapisu “formatowanego” tekstu z użyciem printf();
FileOutputStream – do zapisu binarnych danych;
DataOutputStream – do zapisu typów prymitywnych (int, long, double etc.);
RandomAccessFile – do edycji pliku w konkretnym miejscu;
FileChannel – do pracy z dużymi plikami.
Wpis jest częścią kursu programowania w Javie.
QuickSort Implementacja
Koniec wakacyjnej przerwy! Ten artykuł miał zostać opublikowany ponad tydzień temu – ale wiecie jak to jest z wakacjami 😉 czasem po prostu trzeb a zluzować troszkę! Ale wracam z naładowanymi bateriami! Dziś zajmiemy się implementacją QuickSortu!
01. QuickSort - Użycie wbudowanej implementacji / Bibloteki
Pod poprzednim artykułem, czytelnik zadał pytanie czy istnieje już biblioteka która posiad implementację quicksortu? – jak się można spodziewać, odpowiedź na to pytanie brzmi – oczywiście 😊 żeby było lepiej, implementacja QuickSortu znajduje się w samej Javie! 😊 (od wersji 1.2)
import java.util.Arrays; ... Arrays.sort(array);
I w zasadzie to wystarczy 😊
02. QuickSort- Własna implementacja
Nasza własna implementacja nie jest najbardziej optymalną wersją – jest wersją przedstawionej wcześniej teorii. Opisana implementacja pokrywa się w 100% z opisaną teorią z poprzedniego wpisu, dzięki czemu można porównać co się dokładnie dzieje.
Po pierwsze mamy jeden punkt wejściowy do którego przesyłamy naszą nieposortowaną tablicę:
public static void sort(int[] array) { quickSort(array, 0, array.length-1); }
W ciele metody robimy odesłanie do naszej głównej metody quickSort:
private static void quickSort(int[] array, int firstIndex, int lastIndex) { if(firstIndex < lastIndex) { int partitionIndex = partition(array, firstIndex, lastIndex); quickSort(array, firstIndex, partitionIndex-1); quickSort(array, partitionIndex+1, lastIndex); } }
Pierwszym krokiem jest sprawdzenie na jakich pozycjach są ustawione indexy, index początkowy musi być mniejszy od ostatniego indexu – w przeciwnym wypadku nie podejmujemy żadnych działań.
Główną metodą jest metoda partition która dokonuje sortowania przesłanej tablicy względem pivotu, oraz zwraca również index podziału – dzięki czemu mamy podział na lewą i prawą część tablicy.
Jak możesz zauważyć metoda quickSort, jest metodą rekurencyjną (wywołuje sama siebie), pokrywa się to z sortowaniem lewej oraz prawej części tablicy 😊
Metoda partition wygląda następująco:
private static int partition(int[] array, int firstIndex, int lastIndex) { int pivot = array[lastIndex]; int pIndex = firstIndex; for (int i = firstIndex; i < lastIndex; i++) { if(array[i] <= pivot) { swap(array, i ,pIndex); pIndex++; } } swap(array, pIndex, lastIndex); return pIndex; }
Jest to metoda która sortuje względem wybranego pivotu, oraz zwraca index podziału. Metoda swap jest metodą pomocniczą która zamienia dwa wskazane elementy:
private static void swap(int[] array, int index1, int index2) { int tmp = array[index1]; array[index1] = array[index2]; array[index2] = tmp; }
Podsumowanie
Metody sortowania mogą być kłopotliwe do zrozumienia, pamiętaj że cały kod dostępny jest na GitHubie! Ściągnij kod – poeksperymentuj z nim, pomoże Ci to w zrozumieniu tego co się dzieje!
QuickSort - Teoria
Kolejny wpis z cyklu JavaByExample. W poprzednim wpisie rozpisałem jak wygląda rekurencja – jeśli potrzebujesz to wróć do tego wpisu, dziś po raz kolejny użyjemy tej techniki!
01. QuickSort
Dzisiejsze zadanie jest kolejnym klasykiem – algorytmami sortującymi wykładowcy meczą studentów od zawsze, i pewnie już nigdy nie przestaną 😉
Zaimplementuj algorytm sortujący QuickSort
02. QuickSort - Teoria
Okej, jak wygląda ten algorytm? Powiem szczerze – jest naprawdę wiele wersji tego algorytmu. Jest ich tak wiele ze względu na proces optymalizacji jaką przechodził ten algorytm. My zajmiemy się dziś jedną z podstawowych wersji algorytmu QuickSort 😊
Ogólna założenia algorytmu: Wybierany jest element według którego sortujemy (pivot), wszystkie elementy mniejsze, umieszczamy po jego lewej stronie a elementy większe – po jego prawej stronie.
Następnie powtarzamy tą czynność dla lewej i prawej strony, do momentu aż wszystkie elementy będą posortowane.
Daną wejściową jest nieposortowana tablica np.:

Pierwszym krokiem jest wybór elementu według którego będziemy sortować, potocznie ten element jest nazywany pivotem – dla uproszczenie można wybrać ostatni index tablicy wejściowej.

Zaczynamy nasze sortowanie. Iterujemy w pętli po elementach na pozycjach mniejszych niż nasz pivot. Będziemy potrzebować dodatkowego wskaźnika który będzie nam wskazywać na element z którym będziemy przestawiać elementy, nazwijmy sobie go pIndex.

Mamy nasze początkowe ustawienie. Sprawdzamy czy element na który wskazuje index pętli jest mniejszy od naszego pivot’u (4), jeśli tak zamieniamy element na który wskazuje index pętli z elementem na który wskazuje pIndex. Po zamianie zwiększamy pIndex.
W naszym przypadku 7 jest większy od wybranego pivotu (4) – nie podejmujemy żadnej akcji, przechodzimy w pętli do kolejnego elementu:

Czy element na który wskazuje index pętli (2) jest mniejszy od naszego pivotu (4) jest mniejszy? Tak! Zamieniamy zatem elementy znajdujące się na miejscach na który wskazuje index pętli oraz wskaźnik pIndex


Po zamianie zwiększamy pIndex:

Przestawiliśmy jeden element. Jedziemy dalej według tego samego schematu 😉


Przestawiamy elementy -> zwiększamy pIndex -> przchodzimy do kolejnego elementu w pętli




Przedstawiamy elementy -> zwiększamy pIndex -> index pętli nie zostanie zwiększony, doszliśmy do ostatniego sprawdzanego elementu:

W momencie gdy doszliśmy do ostatniego sprawdzanego elementu, dokonujemy dodatkowej zamiany, pIndexu z naszym pivotem:


Ufffff! Właśnie ustawiliśmy pierwszy element! Wszystkie elementy mniejsze od naszego wybranego pivotu znajdują się po jego lewej stronie, a wszystkie większe elementy po jego prawej stronie! Teraz to samo robimy z lewą stroną oraz prawą stroną 🙂
Podsumowanie
Ze względu że artykuł wyszedł dość obszerny, został podzielony na dwie części – teoria, dostępna w tym wpisie, oraz implementację – która zostanie omówiona w kolejnym wpisie. Jeśli chcesz sprawdzić implementację już teraz, jest ona dostępna na GitHubie!
Ciąg Fibonacciego
Dziś kolejne zadanie z cyklu JavaByExample. Dziś będziemy omawiać ciąg Fibonacciego. W poprzedniej lekcji z serii omawialiśmy przykład zliczania poszczególnych znaków z daniu. Dostałem na ten temat komentarz, iż zadania da się rozwiązać znacznie łatwiej niż zostało to przeze mnie przedstawione, używając Stremów. Bardzo dziękuję za ten komentarz! Poprzednie zadanie zostało zaktualizowane o rozwiązanie oparte na Streamach! Update znajdziecie we wpisie, jak i na GitHubie! 😊
01. Zadanie - Ciąg Fibonacciego
Dzisiejsze zadanie jest naprawdę klasykiem! 😊 Mała ciekawostka, kiedyś dostałem zadanie związane z ciągiem Fibonacciego na rozmowie rekrutacyjnej, jako zadnie na rozgrzewkę 😊 Warto zapoznać się o co chodzi 😊
Napisz program który policzy podany przez użytkownika element ciągu Fibonacciego.
Jak wygląda ten cały ciąg? Pierwsze dwie liczby to 1,1 a kolejna to suma dwóch poprzednich, czyli kolejnym elementem jest liczba 2! Mamy zatem 1,1,2 – kolejnym elementem jest 1+2 = 3 mamy teraz 1,1,2,3 – kolejny element 2+3=5 itd. Tak przedstawiają się kolejne elementy tego ciągu:
1,1,2,3,5,8,13,21,34,55,89… i tak w nieskończoność 😉
Przykład 1:
Użytkownik podaje liczbę: 6
Odpowiedź programu: 8
Przykład 2:
Użytkownik podaje liczbę: 10
Odpowiedź programu: 55
02. Uwaga!
Przede wszystkim – postaraj się rozwiązać to zadanie samodzielnie. Poniżej znajdziesz moje rozwiązanie, opis wraz z całym kodem źródłowym!
Przed rozpoczęciem mojego rozwiązania chciałbym przypomnieć o jednej bardzo ważnej sprawie. Pamiętaj o tym że nie ma jednego konkretnego sposobu na napisanie programu który rozwiązuje dany problem. Jak to powtarzał jeden z moich profesorów, konkretny program możemy napisać na milion sposobów i każdy z nich może być poprawny!
STOP -Poniżej znajdziesz moje rozwiązanie tego zadania – postaraj się rozwiązać najpierw samemu! W razie problemów możesz przeczytać moje rozwiązanie oraz zajrzeć na GitHuba!

03. Moje rozwiązanie
Zadanie można rozwiązać na wiele sposobów 😊 Naprawdę! Zadanie jest naprawdę klasyczne 😊 Wykładowcy lubią męczyć nim adeptów programowania, dziś przedstawię wam 3 różne sposoby rozwiązania, klasyczny z użyciem pętli, z użyciem rekurencji oraz z użyciem Stremów.
04. Fibonacci klasycznie
public static long fibClassic(int n) { long x1 = 1; long x2 = 1; for (int i = 3; i <= n; i++) { long temp = x1 + x2; x1 = x2; x2 = temp; } return x2; }
Opis: Tworzymy dwie zmienne którym nadajemy wartość 1, zgodnie z pierwszymi elementami ciągu. Zaczynamy pętle, początek pętli ustalamy na 3, pierwsze dwa elementy są znane, wiec sens wykonywania jakichkolwiek obliczeń mamy dopiero od trzeciego elementu. W pętli tworzymy tymczasową zmienną do której przypisujemy wynik sumy dwóch poprzednich elementów. Następnie dokonujemy „przesunięcia” nasze x1 dostaje wartość obecnego x2, natomiast x2 dostaje wyliczoną wartość. Użytkownikowi zwracamy x2 😊 będzie on miał przypisaną ostatnią wyliczoną wartość 😊
05. Fibonacci Rekursywnie
public static long fibRecursive(int n) { if(n <= 1) return n; return fibRecursive(n-2)+fibRecursive(n-1); }
Yeah, piękno rozwiązań rekursywnych 😉 Całe ciało metody zamknięte dosłownie w dwóch linijkach! Jeśli nigdy wcześniej nie spotkałeś się z metodami rekursywnymi już tłumaczę o co chodzi. W prostych słowach polega to na tym że metoda wywołuje sama siebie 😊 Metody rekursywne mają wiele pułapek, i łatwo stworzyć nieskończoną pętlę, trzeba ich używać ze szczególną ostrożnością.
Opis: Jeśli metoda dostanie liczbę 1 lub mniejszą, zwróci tą liczbę, jeśli dostanie liczbę większą od jeden zwraca sumę z wykonania metody dla dwóch poprzednich elementów (n-2 oraz n-1). Dla osób początkujących może być to ciężko do wyobrażenia więc spieszę z pomocnym rysunkiem! 😊 Żeby aż tak wiele nie rysować, przyjmiemy że użytkownik poprosił o 6 element ciągu (wynik 8) :

Okej! Rozrysowałem tylko jedną stronę 😉 ale myślę że z tego rysunku możecie sobie wyobrazić jak wygląda pozostała część 😊 Po prostu nie zmieściło mi się na rysunku 😊
Użytkownik podaje jako parametr wejściowy 6, zatem w zaczynamy naszą rekursywę fib(6-2)+fib(6-1). Czyli tak naprawdę fib(4)+fib(5). Rysunek przedstawia pełne drzewo dla fib(6-2) czyli dla fib(4), liczmy ostatnie gałęzie:
fib(4)= 0+1+1+0+1 = 3
Druga gałąź wyglądała by bardzo podobnie: fib(5)= fib(3)+fib(4) zwróć uwagę że fib(3) mamy policzone na rysunku i wynosi fib(3)=2, fib(4) również mamy policzone i wynosi fib(4)= 3, podstawiając liczby fib(5) = 2+3 = 5
Podstawiamy wszystkie policzone liczby fib(6) = fib(4) + fib(5) = 3+5 = 8
Uwaga! Rozwiązanie rekursywne jest bardzo wolne! Jest to spowodowane że niektóre wartości liczone są wielokrotnie.
06. Fibonacci Stream
public static long fibStream(int n) { return Stream.iterate(new int[]{1,1}, x -> new int[]{x[1], x[0]+x[1]}) .limit(n) .map(x->x[0]) .collect(Collectors.toList()) .get(n-1); }
Taka trochę ciekawostka że się po prostu da 😊 Nie uważam żeby to rozwiązanie było specjalnie atrakcyjne taka ciekawostka!
Podsumowanie
Ok! 3 całkowicie różne podejścia do rozwiązania danego problemu 🙂 Pamiętajcie że jeśli wasze może się różnić od mojego i nie ma w tym nic złego! Bonus! Na GitHubie dodałem porównanie wydajności poszczególnych metod, oraz jak Java radzi sobie z optymalizacją podobnych zadań 😉 fajnie to widać na Streamach!
Zliczanie znaków w zdaniu
Dziś oficjalnie rozpoczynamy serię Java By Example. Jestem przekonany że najlepszą nauką programowania jest po prostu praktyka. W serii Java By Example postawimy sobie jakiś problem który będziemy rozwiązywać, kody źródłowe będą dostępne oczywiście na GitHubie. Seria JavaByExample jest częścią kursu programowania w Javie.
01. Zadanie
Dzisiejsze zadanie jest zadaniem które pamiętam z podstaw programowanie:
Napisz program który wypisze liczbę występowania poszczególnych znaków, występujących we wprowadzonym przez użytkownika zdaniu.
Przykład 1:
Użytkownik wprowadza zdanie: ala
Odpowiedź programu: a -2 , l – 1
Przykład 2:
Użytkownik wprowadza zdanie: Ala ma kota
Odpowiedź programu: A – 1, a – 3, t – 1, k – 1, l – 1, m – 1, o – 1
Uwaga: Zwróć uwagę że wielka litera A jest liczona osobno od małej literki a
02. Uwaga!
Przede wszystkim – postaraj się rozwiązać to zadanie samodzielnie. Poniżej znajdziesz moje rozwiązanie, opis wraz z całym kodem źródłowym!
Przed rozpoczęciem mojego rozwiązania chciałbym przypomnieć o jednej bardzo ważnej sprawie. Pamiętaj o tym że nie ma jednego konkretnego sposobu na napisanie programu który rozwiązuje dany problem. Jak to powtarzał jeden z moich profesorów, konkretny program możemy napisać na milion sposobów i każdy z nich może być poprawny!
STOP -Poniżej znajdziesz moje rozwiązanie tego zadania – postaraj się rozwiązać najpierw samemu! W razie problemów możesz przeczytać moje rozwiązanie oraz zajrzeć na GitHuba!

03. Moje rozwiązanie
Ok – Czas pomyśleć nad rozwiązaniem. Zadanie możemy podzielić na 3 oddzielne elementy:
1) Pobranie od użytkownika wartości
2) Policzenie ile razy w zdaniu występuje konkretny znak
3) Wypisanie wyniku na ekranie
Podzielenie programu na osobne odpowiedzialności jest bardzo dobrą praktyką – każda część programu jest odpowiedzialna za tylko jedną funkcjonalność – „Single responsibility principle”.
Ok! Zabieramy się za kodowanie. Pierwsza część programu jest prosta:
System.out.println("Input sentence: "); Scanner scanner = new Scanner(System.in); String userInput = scanner.nextLine();
Pobranie zdania od użytkownika mamy za sobą 🙂
Kolejna część jest tak naprawdę sercem naszego programu, musimy policzyć występowania poszczególnych znaków. Pobrane przez nas zdanie użytkownika jest przypisane do zmiennej typu String, żaden nasza metoda przyjmie String jako parametr. Metoda zwróci nam ma Mapę – jako klucz będzie znakiem, wartość liczbę która będzie reprezentować ile razy dany znak występował we wprowadzonym zdaniu.
public static Map<Character, Integer> countCharacters(String userInput) { }
Cała metoda wygląda następująco:
public static Map<Character, Integer> countCharacters(String userInput) { if(userInput == null || userInput.isEmpty()) return Collections.emptyMap(); Map<Character, Integer> characterCounter = new HashMap<>(); for (char c : userInput.toCharArray()) { Integer value = characterCounter.get(c); if (value != null) { value++; } else { value = 1; } characterCounter.put(c,value); } return characterCounter; }
Opis:
W pierwszym kroku sprawdzam co przyszło do metody, jeśli dostaliśmy nulla, lub pustego Stringa, nie ma sensu nic robić. Po prostu zwracamy pustą mapę. Jeśli jednak coś dostaliśmy od użytkownika do przetworzenia, mamy trochę roboty 😊
Tworzymy pustą mapę, oraz pętlę po wszystkich znakach z wprowadzonego zdania. W pętli staramy się pobrać z mapy wartość dla konkretnego znaku.
Jeśli dostaniemy wartość różną od null -> zwiększamy ją – znak wystąpił w zdani kolejny raz, jeśli dostaniemy null, znaczy to że konkretnego znaku nie ma jeszcze w mapie – wartość ustawiamy na 1. W obu przypadkach dodajemy parę znak – ilość do mapy, w przypadku nowej wartości będziemy mieć kolejny element w mapie, w przypadku istniejącego już takiego klucza, zastąpimy go z nowa wartością.
Powyższa metoda w oparciu o pętlę, choć poprawna, to można ją zdecydowanie uprościć używając Streamów (Bardzo dziękuję czytelnikowi za zwrócenie uwagi!), streamy na blogu nie były jeszcze poruszane, ale warto zapoznać się również z tym rozwiązaniem:
public static Map<Character, Long> countCharactersStream(String userInput) { return userInput.chars().mapToObj(c -> (char) c) .collect(Collectors.groupingBy(Function.identity(), Collectors.counting())); }
Metoda jest teraz niezwykle prosta! Cały wprowone zdanie zamieniamy w Stream, oraz zbieramy wszystkie unikalne elementy, oraz zliczamy ich występowanie używając
Collectors.groupingBy(Function.identity(), Collectors.counting())
Pozostaje nam jeszcze ostatnia część zadania – Wypisanie wyniku:
public static void printCountedInput(Map<Character, ? extends Number> characterCounter) { for (Map.Entry<Character, ? extends Number> entry : characterCounter.entrySet()) { System.out.println("Sign: "+entry.getKey().toString()+" Count: "+entry.getValue()); } }
Cały kod:
System.out.println("Input sentence: "); Scanner scanner = new Scanner(System.in); String userInput = scanner.nextLine(); printCountedInput(countCharacters(userInput));
Podsumowanie
Pamiętaj o punkcie drugim w którym wspominałem że nie ma jednego dobrego rozwiązania 😊 to jest tylko moja propozycja! Cały kod do zadania jest dostępny na Githubie! Możesz ściągnąć cały kod i uruchomić go u siebie 😊
Wyjątki - Podstawa
Kolejna część kursu programowania w Javie! Zanim rozpocznę dodatkową serię, roboczo nazwaną Java By Example, chciałbym omówić jeszcze jeden zdecydowanie ważny element języka jakim są wyjątki (ang. Exceptions). Wyjątki w Javie potrafią zaskoczyć. Powiem szczerze że niektóre decyzje projektowe zaskakują mnie do dziś na tym obszarze 😉
01. Java Exceptions
Wyjątki w Javie standardowo dzielimy na dwie grupy – Checked exceptions oraz Unchecked Exceptions. Różnica polega na tym że Checked exceptions muszą zostać obsłużone przez programistę, natomiast Unchecked Exceotions mogą być „pozostawione” same sobie. Tyle jeśli chodzi o prostą teorię.
Zawsze pomocny przy takich tematach jest mały diagram 😊 zatem proszę bardzo:

Małe wyjaśnienie. Wszystko co dziedziczy po klasie Throwable zaliczamy do wyjątków, mamy początkowe rozgałęzienie na Error oraz Exception.
Error w 99,9% nie powinien nas przejmować, w momencie kiedy nasz program sypnie errorem nie jesteśmy w stanie nic zrobić. Nasz program po prostu „padnie”. Musimy wtedy poszukać błędu w naszej logice może pojawiła się jakaś nieskończona pętla? Może dodajemy bardzo wiele elementów do ArrayListy co spowoduje przepełnienie pamięci? Tak czy siak, jeśli nasz program wyrzuci error, prawdopodobnie się wyłączy.
W drugiej gałęzi mamy klasę Exception, jest to pierwszy z Checked exceptions (pomijając Throwable który jest rodzicem dla wszystkich wyjątków 😉 ) – wyjątek który musi zostać obsłużony przez programistę. Poniżej mamy RuntimeException który jest pierwszym z Unchecked excptions – wyjątków które nie wymagają od programisty żadnych akcji 😊
02. Try - Catch - Finally
Podstawowym mechanizmem jest blok try catch finally, z czego blok finally jest opcjonalny, jest to blok kodu który zawsze się wykona, bez znaczenia czy wystąpi wyjątek czy nie.
try { //code } catch (Exception ex) { //code } finally { //code }
03. Checked Exceptions
Checked Exceptions – na diagramie są zaznaczone kolorem czerwonym. Nie są to wszystkie wyjątki Checked Exception’y! Nie starczyło by mi nocy żeby wyrysować je wszystkie 😉 Tak jak zostało to wcześniej powiedziona, są to wyjątki które muszą zostać obsłużone przez programistę.
Jednym z najbardziej popularnych tego typu wyjątków to IOException:
String pathToFile = "C:\\Users\\abc.txt"; BufferedReader bufferedReader = null; try { bufferedReader = new BufferedReader(new FileReader(pathToFile)); System.out.println(bufferedReader.readLine()); } catch (IOException e) { e.printStackTrace(); System.out.println(e.getMessage()); } finally { if(bufferedReader != null) { try { bufferedReader.close(); } catch (IOException e) { e.printStackTrace(); } } System.out.println("blok finally wykona sie zawsze :)"); }
Co tu się dzieje? Po prostu próbujemy odczytać plik znajdujący się na dysku naszego komputera. Wiele rzeczy przy takiej operacji może pójść nie tak! Chociażby – nie ma takiego pliku w określonej ścieżce.
W momencie kiedy próbujemy użyć metody która w swojej sygnaturze mówi o tym iż może rzucić takim wyjątkiem, nasze IDE podpowie nam, iż musimy taki wyjątek obsłużyć.
Np. metoda readLine()
public String readLine() throws IOException
Aby użyć takiej metody możemy ją zamknąć w bloku try catch tak jak zostało to przedstawione powyżej, lub przesłać wyjątek „dalej do góry”
public static void main(String[] args) throws IOException { String pathToFile = "C:\\Users\\abc.txt"; BufferedReader bufferedReader = null; bufferedReader = new BufferedReader(new FileReader(pathToFile)); System.out.println(bufferedReader.readLine()); }
04. Unchecked Exceptions
Unchecked Exception – na diagramie oznaczone kolorem zielonym, są to wyjątki które nie wymagają od programisty żadnej reakcji. Przykład:
String param = "abc"; Integer number = Integer.valueOf(param);
Jednak oczywiście jeśli mamy takie życzenie, możemy obsłużyć taki wyjątek:
String param = "abc"; try { Integer number = Integer.valueOf(param); System.out.println("Udała się konwersja na liczbę :)"); } catch (NumberFormatException e) { System.out.println("Ejjj '"+param+"' to nie jest liczba!"); }
Nieprzypadkowo wybrałem tego typu przykład 😉 Na StackOverflow jest bardzo fajny temat w którym ludzie dyskutują na temat czy np. NumberFormatException powinien być typu checked (powinna być wymagana obsługa), no ale polecam zajrzeć już do tego wpisu. Naprawdę bardzo ciekawa lektura!
05. Podsumowanie
Temat wyjątków został jedynie zarysowany w tym wpisie 😊 Jednak jest to solidna podstawa która w połączeniu z resztą kursu pozwoli nam pisać podstawowe aplikacje! Koncepcje wyjątków w Javie jeszcze poruszę w bardziej zaawansowanym stopniu, w późniejszej części kursu!
Kolekcje - Mapy
Dotarliśmy do końca drogi kolekcji, dziś omówimy Mapy, które zamkną nam serię dotyczącą podstaw kolekcji. Jak to zostało wspomniane we wstępie serii, mapy nie są ściśle kolekcjami – nie dziedziczą interfejsu Collection, jednak zawsze gdy mówi się o kolekcjach w Javie, mapy dokłada do całego grona 😊
01. Mapy
Mapy są specyficzną strukturą danych, które przechowują elementy w sposób klucz – wartość. Oczywiście Mapy są generyczne – o typie kluczy jak i wartości decyduje programista, jest to kolejna cecha łącząca Mapy z resztą kolekcji.
Mapa jest bardzo przydatną strukturą, pozwala bardzo szybko pobrać z niej elementy, pod warunkiem że znasz odpowiedni klucz 😉
Klucze w Mapie są unikalne, jednak wartości w mapach mogą się powtarzać. W zależności od implementacji (HashMap oraz LinkedHashMap) możemy posiadać nulle jako klucz oraz wartość, natomiast w przypadku TreeMapy nulle są niedozwolone.
02. Implementacje
HashMap – Jest najpopularniejszą implementacją, podobnie jak HashSet, HashMapa nie zachowuje konkretnej kolejności.
Map<Integer, String> map = new HashMap<>(); map.put(10, "Arek"); map.put(2,"Zuza"); for (String value : map.values()) { System.out.println(value); }
Wynik:
Zuza Arek
LinkedHashMap – Jest analogiczna do LinkedHashSetu, zachowuje kolejność elementów w jakiej zostały one dodane.
Map<Integer, String> map = new LinkedHashMap<>(); map.put(10, "Arek"); map.put(2,"Zuza"); for (String value : map.values()) { System.out.println(value); }
Wynik:
Arek Zuza
TreeMap – Jest analogiczna do TreeSetu, wartość są posortowane według comparatora klucza. Możemy podać własny comparator podczas tworzenia mapy. Jeśli tego nie zrobimy, zostanie użyty domyślny comparator klucza.
Map<Integer, String> map = new TreeMap<>(); map.put(10, "Arek"); map.put(2,"Zuza"); map.put(8,"Klaudiusz"); for (String value : map.values()) { System.out.println(value); }
Wynik:
Zuza Klaudiusz Arek
03. Podstawowe operacje
Podstawowymi operacjami na Mapach są operacje put oraz get. Metoda put dodaje element do mapy w postaci klucz – wartość:
Map<Integer, String> map = new HashMap<>(); map.put(10, "Arek"); map.put(2,"Zuza"); map.put(8,"Klaudiusz");
Natomiast metoda get pozwala nam wyciągnąć daną wartość po konkretnym kluczu.
String value = map.get(2);
Jeśli chcemy przeszukać całą mapę, możemy pobrać wszystkie klucze jako Set:
Set<Integer> keySet = map.keySet();
wartości jako Collection:
Collection<String> values = map.values();
lub wszystkie elementy mapy w postaci klucz – wartość jako Set 😊
Set<Map.Entry<Integer, String>> entries = map.entrySet();
Dzięki takim pomocniczym metodą, możemy w łatwy sposób iterować po interesujących nas elementach, korzystamy przecież z dobrze nam znanych kolekcji 😊
04. Podsumowanie
Mapy są specyficzną strukturą danych. Umiejętne posługiwanie się mapami pozwoli oszczędzić nam naprawdę sporo mocy obliczeniowej w momencie gdy musimy wyszukiwać specyficzne elementy, dobrze przemyślana mapa może zdecydowanie przyspieszyć działanie naszego programu 😊
Kolekcje - Kolejki
Kontynuacja tematu kolekcji 😊 Tak ja powiedziałem w pierwszym wpisie z serii, temat kolekcji jest dość złożony. Dziś omówimy kolejki. Muszę przyznać że przez ponad 7 lat aktywnego programowania czystych Javowych kolejek nie używałem zbyt często 😉 Jednak nie zmienia to faktu że kolejki w określonych sytuacjach mogą być naprawdę bardzo przydatne!
01. Kolejki
Kolejki zachowują się mniej więcej jak kolejki w sklepie. Ktoś podchodzi do kasy, zanim ustawiają się kolejne osoby, pierwsza osoba zostaje obsłużona, odchodzi o kasy, kolejna osoba jest z przodu, gotowa aby ją obsłużyć. Większość kolejek w Javie jest typu FIFO – first in first out, dokładnie takich jak w sklepie.
Odstępem od kolejek typu FIFO jest PriorityQueue o której wspomniałem w pierwszym wpisie odnośnie kolekcji. Kolejka ta zachowuje się podobnie do TreeSetu, sortuje elementy według comperatora.
ArrayDeque jest specyficzną implementacją kolejki która pozwala na obsługę elementów z początku jak i końca kolejki.
Ciekawostką jest to że LinkedList jest jednocześnie Listą oraz Kolejką 😊 (Spójrz na rysunek w pierwszym wpisie o kolekcjach), zatem całkowicie poprawnym sposobem na utworzenie kolejki jest taki zapis:
Queue<String> stringQueue = new LinkedList<>();
02. Podstawowe operacje
Podstawowe operacja dla kolejek add, offer, remove, poll, element oraz peek. Wszystkie te metody są zadeklarowane w interfejsie Queue. Jednak tylko trzy z tych metod są najczęściej wykorzystywane, są to:
offer – dodaje element do kolejki, jest to preferowana metoda dodawania elementu do kolejki, metoda add w niektórych implementacjach podczas nieudanego dodania rzuca wyjątkiem, co może zaskoczyć 😉 Gdy pracujemy z kolejkami i chcemy dodać do niej element, stosujmy offer.
poll – pobiera oraz zdejmuje element z początku kolejki.
peek – pobiera element z kolejki bez zdejmowania go.
Należy pamiętać że kolejki należą do kolekcji! Możemy zatem sprawdzić ile elementów znajduje się w kolejce wywołując metodę size, czy przejść przez wszystkie elementy z wykorzystaniem pętli.
Queue<String> stringQueue = new LinkedList<>(); stringQueue.offer("Arek"); stringQueue.offer("Karol"); stringQueue.offer("Robert"); stringQueue.offer("Szymon"); System.out.println(stringQueue.size()); System.out.println(stringQueue.poll()); System.out.println(stringQueue.size()); System.out.println("--------"); for (String hero : stringQueue) { System.out.println(hero); }
Podsumowanie
Kolejki są naprawdę specyficznym rodzajem kolekcji, jest to bardzo przydatna struktura w określonych sytuacjach. Dobrze mieć świadomość tego jak działa i jakie możliwości oferuje. Nigdy nie wiadomo kiedy przyjdzie czas w którym może uratować sytuację 😊
Kolekcje - Listy
Dziś kontynuujemy temat kolekcji. W poprzednim wpisie zostały omówione Sety, dziś natomiast zajmiemy się Listami. Temat będzie nieco krótszy od Setów (uffff!), chociażby ze względu na to że omówimy tylko najbardziej popularne implementacje, ArrayList oraz LinkedList. Zapraszam! 🙂
01. Lista a Set - podobieństwa i różnice
Niezależnie od implementacji, Sety oraz Listy zaliczamy do grona kolekcji, wywodzą się od wspólnego interfejsu Collection. Listy tak samo jak Sety, są generyczne. Czym są generyki zarysowałem w poprzednim wpisie odnośnie Setów.
Główną różnicą jest to iż Lista pozwala przechowywać duplikaty, dokładnie ten sam obiekt, lub taki sam obiekt, możemy dodać wielokrotnie do Listy, gdzie Set nam to uniemożliwi.
Wiele stron oraz kursów w różnicach podaje że Lista jest uporządkowana, natomiast Set nie. Nie jest to prawda! Wszystko zależy od użytej implementacji Setu np. LinkedHashSet zachowuje kolejność dodania 😊
02. Listy
Niezależnie od implementacji z jakiej korzystamy, dysponujemy takim samym podstawowym arsenałem funkcjonalności. Możemy dodawać pojedyncze elementy do listy, dodawać całe kolekcje, usuwać elementy, sortować, iterować po wszystkich elementach, lub pobrać konkretny element z listy.
Nie będę omawiać tutaj każdego elementu z osobna, operacje są tak domyślne że zachęcam was do samodzielnego eksperymentowania! 😊
Główna różnica leży w wydajności konkretnych implementacji dla danej operacji, np. dodawanie wielu elementów do ArrayListy jest niewydajne, do tego zdecydowanie lepiej nadaje się LinkedLista.
List<Hero> heroList = new LinkedList<>(); heroList.add(new Person("Arek")); heroList.add(new Person("Arek")); heroList.add(new Person("Dominika")); heroList.add(new Person("Karolina")); heroList.add(new SuperPerson("Dominika")); for (Hero hero : heroList) { System.out.println(hero); }
//skorzystałem z tych samych klas co w omówieniu Setów
03. ArrayList
ArrayList jest tak naprawdę dynamiczną tablicą. W ramach przypomnienia odsyłam do lekcji o tablicach, zauważ że podczas tworzenia tablicy deklarujemy jej wielkość która jest niezmienna.
ArrayList wewnętrznie używa tablicy do przechowywania danych, a w momencie dodawania sprawdza czy zbliżamy się do końca tablicy, jeśli tak, tworzona jest nowa tablica o większym rozmiarze, i przepisywane są do niej elementy ze starej tablicy.
Jeśli będziemy dodawać bardzo dużo elementów do ArrayListy, możemy mieć problemy związane z wydajnością. ArrayList ma natomiast przewagę jeśli chcemy odczytywać elementy z wybranych indexów. Dostęp do każdego elementu jest stały.
04. LinkedList
LinkedList wewnętrznie przechowuje elementy w podwójnie związanej liście elementów. Każdy z elementów posiada wskaźnik do następnego jak i poprzedniego elementu. Iterowania po wszystkich elementach jest szybkie, natomiast pobranie konkretnego elementu z danego indexu wymaga większego nakładu pracy. Wewnętrznie i tak musimy przejść przez wszystkie poprzedzające elementy aż dojdziemy do konkretnego indexu.
Zaletą LinkedListy w porównaniu z ArrayListą jest szybkość wykonywania operacji dodawania oraz usuwania.
Podsumowanie
LinkedLista powinna być wybierana w przypadku gdy nie posiadamy „losowych” pobrań elementów (z konkretnego indexu), oraz gdy na danej liście stosujemy wiele operacji dodawania lub usuwania elementów.
Jeśli zaś liczba elementów w liście jest stała, oraz potrzebujemy bardzo szybkiego dostępu do elementów znajdujących się na konkretnych pozycjach, wtedy zdecydowanie lepszym wyborem będzie ArrayList.