Java

Java 10 dni – Lekcja 1, Co to jest programowanie, program komputerowy i kod źródłowy?

  • Java 10 dni – Lekcja 1, Co to jest programowanie, program komputerowy i kod źródłowy?
  • Pełny materiał dostępny na:

No comments
Share:

Java 10 dni – Start! 🔥🚀

https://www.youtube.com/watch?v=RnKYjTdcIaw

No comments
Share:
odwrotna notacja polska

Odwrotna Notacja Polska (ONP) – rewolucyjne uporządkowanie obliczeń matematycznych➕➖✖️➗

Odwrotna Notacja Polska (ONP) – Jest to sposób zapisu wyrażeń matematycznych, który między innymi eliminuje potrzebę nawiasów do określania kolejności operacji, co sprawia, że jest idealny do użycia w programowaniu i analizie danych. Ta metoda, choć może wydawać się nieintuicyjna na pierwszy rzut oka, oferuje szereg zalet w obliczeniach i implementacji algorytmów.

Odwrotna Notacja Polska – wprowadzenie

Z tego materiału dowiesz się:

  • Czym jest Odwrotna Notacja Polska?
  • Jak się oblicza Odwrotną Notację Polską?
  • Jakie ma zastosowanie Odwrotna Notacja Polska?
  • Jak w Javie wygląda implementacja ONP?

Odwrotna Notacja Polska – historia

Odwrotna Notacja Polska (ONP), znana także jako notacja postfiksowa, została wprowadzona przez australijskiego naukowca Charlesa Leonarda Hamblina w latach 50. XX wieku. Metoda ta jest odmianą notacji polskiej, wynalezionej przez polskiego logika Jana Łukasiewicza w 1924 roku, która używa formatu prefiksowego, gdzie operator jest umieszczany przed operandami (np. + 3 4). W przeciwieństwie do tego ONP umieszcza operator po operandach (np. 3 4 +).

Odwrotna Notacja Polska – jak to obliczyć?

Aby obliczyć wyrażenie zapisane w notacji postfiksowej, wykonujesz kroki w kolejności, używając stosu:

  • Umieść Operand na Stosie: Każdą liczbę umieszczasz na stosie, gdy ją napotkasz.
  • Wykonaj Operację: Gdy napotkasz operator, zdejmujesz ze stosu odpowiednią liczbę operandów (zwykle dwa dla binarnych operatorów jak +, , *, /), wykonujesz operację, a wynik z powrotem umieszczasz na stosie.
  • Wynik Końcowy: Po przetworzeniu całego wyrażenia, na stosie pozostaje jedna wartość — wynik końcowy operacji.

odwrotna notacja polska

➡ ZOBACZ 👉: Stos (Stack)

Odwrotna Notacja Polska – rozwiązanie

Aby rozwiązać wyrażenie w Odwrotnej Notacji Polskiej (ONP) 2 3 * 4 5 * +, postępujemy zgodnie z regułami obliczania, używając stosu do przechowywania tymczasowych wyników. Oto kroki:

  1. Umieść 2 na stosie:
    • Stos: [2]
  2. Umieść 3 na stosie:
    • Stos: [2, 3]
  3. Napotykasz operator * (mnożenie), więc zdejmujesz dwa ostatnie liczby ze stosu (3 i 2), mnożysz je i wynik umieszczasz z powrotem na stosie:
    • Operacja: 2 * 3 = 6
    • Stos: [6]
  4. Umieść 4 na stosie:
    • Stos: [6, 4]
  5. Umieść 5 na stosie:
    • Stos: [6, 4, 5]
  6. Napotykasz kolejny operator * (mnożenie), więc znowu zdejmujesz dwa ostatnie liczby ze stosu (5 i 4), mnożysz je i wynik umieszczasz z powrotem na stosie:
    • Operacja: 4 * 5 = 20
    • Stos: [6, 20]
  7. Napotykasz operator + (dodawanie), więc zdejmujesz dwa ostatnie liczby ze stosu (20 i 6), sumujesz je i wynik umieszczasz z powrotem na stosie:
    • Operacja: 6 + 20 = 26
    • Stos: [26]

Wynik Końcowy

Na stosie pozostaje jedna liczba, 26, która jest wynikiem końcowym wyrażenia 2 3 * 4 5 * +.

To samo działanie zapisane w zwyczajnej notacji, jaką znamy ze szkoły (infiksowej) wygląda następująco: (2*3) + (4*5).

Odwrotna Notacja Polska – przykłady

Oto kilka przykładów zapisu wyrażeń.

Dodawanie i odejmowanie

notacja infiksowa: 3 + 4 − 53 + 4 − 5

⇒🇵🇱 notacja postfiksowa: 3 4 − 53 + 4 − 5 +

Mnożenie i dzielenie

notacja infiksowa: 3 * 7 * 2 / 3

⇒🇵🇱 notacja postfiksowa: 3 7 * 2 * 3 /

Wyrażenie złożone

notacja infiksowa: (3 + 4) * 5(3 + 4) * 5 / 5 – 25

⇒🇵🇱 notacja postfiksowa: 3 4 + 5 * 3 4 + * 5 * 5 / 25 –

Odwrotna Notacja Polska – zastosowanie

Odwrotna Notacja Polska (ONP), choć wydaje się być abstrakcyjnym pojęciem matematycznym, znajduje szerokie zastosowanie w praktyce i w biznesie, przede wszystkim dzięki swojej efektywności i prostocie implementacji w systemach komputerowych.

Kalkulatory i narzędzia obliczeniowe

Wiele kalkulatorów naukowych wykorzystuje ONP, ponieważ pozwala to na szybsze przetwarzanie skomplikowanych wyrażeń matematycznych bez błędów wynikających z niewłaściwego użycia nawiasów. Przykładem może być popularny kalkulator HP-12C, używany przez inżynierów i finansistów.

hp 12c

Programowanie i rozwój oprogramowania

  • Parsowanie Wyrażeń: W programowaniu często potrzebne jest przetwarzanie i ocena wyrażeń wprowadzanych przez użytkownika. Implementacja parserów wykorzystujących ONP jest prostsza i mniej podatna na błędy, co jest szczególnie przydatne w aplikacjach wymagających dynamicznego obliczania wyrażeń matematycznych.
  • Kompilatory i Interpretery: ONP jest używana w kompilatorach i interpreterach języków programowania do oceny wyrażeń. Ponieważ operacje są wykonywane zgodnie z kolejnością występowania, bez potrzeby analizowania nawiasów, proces ten jest bardziej wydajny.

Nauka algorytmów

ONP jest także cennym narzędziem dydaktycznym w nauczaniu algorytmiki i struktur danych, szczególnie w kontekście stosów i kolejek, które są fundamentem dla notacji postfiksowej.

Odwrotna Notacja Polska – implementacja

Implementacja Odwrotnej Notacji Polskiej (ONP) w Javie wymaga użycia stosu do przechowywania operandów i efektywnego przetwarzania operatorów. Poniżej znajdziesz przykładowy kod, który przetwarza wyrażenie w notacji postfiksowej i oblicza jego wynik. Przykład koncentruje się na podstawowych operacjach arytmetycznych: dodawaniu, odejmowaniu, mnożeniu i dzieleniu.

class ReversePolishNotation {
    public static int evaluatePostfix(String expression) {
        Stack<Integer> stack = new Stack<>();
        // Przetwarzanie każdego elementu w ciągu wejściowym rozdzielonego spacjami
        String[] tokens = expression.split(" ");

        for (String token : tokens) {
            // Jeśli element jest liczbą, zapisz go na stosie
            if (token.matches("\\d+")) { // Regex do sprawdzania, czy token jest liczbą
                stack.push(Integer.parseInt(token));
            } else { // Token jest operatorem
                // Operator wymaga pobrania dwóch ostatnich liczb ze stosu
                int num2 = stack.pop();
                int num1 = stack.pop();

                switch(token.charAt(0)) { // Zakładamy, że operator to pojedynczy znak
                    case '+':
                        stack.push(num1 + num2);
                        break;
                    case '-':
                        stack.push(num1 - num2);
                        break;
                    case '*':
                        stack.push(num1 * num2);
                        break;
                    case '/':
                        if (num2 != 0)
                            stack.push(num1 / num2);
                        else
                            throw new UnsupportedOperationException("Division by zero.");
                        break;
                }
            }
        }
        return stack.pop(); // Wynik końcowy znajduje się na szczycie stosu
    }

    public static void main(String[] args) {
        String expression = "2 3 * 4 5 * +"; // operandy należy odzielić od siebie spacją aby były poprawnie odczytane przez program

        int result = evaluatePostfix(expression);
        System.out.println("The result of the expression is: " + result);
    }
}

Stos jest idealny do tego zadania, ponieważ pozwala łatwo dodać i usunąć elementy w odpowiedniej kolejności. Przechodzimy przez każdy znak w wyrażeniu. Jeżeli znak jest cyfrą, przekształcamy go z char na int (odejmując wartość ‘0’ od kodu znaku) i umieszczamy na stosie. Jeśli napotkamy operator, zdejmujemy dwie ostatnie liczby ze stosu, wykonujemy operację i wynik wrzucamy z powrotem na stos. Obsługiwane operacje to dodawanie (+), odejmowanie (-), mnożenie (*) i dzielenie (/). Dzielenie przez zero jest tu wyraźnie obsłużone jako wyjątek.

Odwrotna Notacja Polska – konwerter

Konwerter z Notacji Infiksowej na Postfiksową

class InfixToPostfix {

    // Metoda zwracająca priorytet operatora
    private static int getPriority(char operator) {
        if (operator == '+' || operator == '-') {
            return 1;
        }
        if (operator == '*' || operator == '/') {
            return 2;
        }
        return 0;
    }

    // Metoda konwertująca wyrażenie infiksowe na postfiksowe
    public static String convertToPostfix(String infix) {
        StringBuilder postfix = new StringBuilder();
        Stack<Character> stack = new Stack<>();
        boolean expectOperand = true;  // Dodane do sprawdzania, czy oczekujemy na operand

        for (int i = 0; i < infix.length(); i++) {
            char c = infix.charAt(i);

            if (Character.isDigit(c)) {
                postfix.append(c);
                if (i + 1 < infix.length() && Character.isDigit(infix.charAt(i + 1))) {
                    continue;
                } else {
                    postfix.append(' ');
                }
                expectOperand = false;  // Po dodaniu liczby oczekujemy operatora
            } else if (c == '(') {
                stack.push(c);
                expectOperand = true;  // Po '(' oczekujemy kolejnego operandu
            } else if (c == ')') {
                while (!stack.isEmpty() && stack.peek() != '(') {
                    postfix.append(stack.pop());
                    postfix.append(' ');
                }
                if (stack.isEmpty()) {
                    throw new IllegalArgumentException("Mismatched parentheses in the expression.");
                }
                stack.pop();
                expectOperand = false;  // Po ')' oczekujemy operatora
            } else if (c == '+' || c == '-' || c == '*' || c == '/') {
                if (expectOperand) {
                    throw new IllegalArgumentException("Missing operand before operator " + c);
                }
                while (!stack.isEmpty() && getPriority(c) <= getPriority(stack.peek())) {
                    postfix.append(stack.pop());
                    postfix.append(' ');
                }
                stack.push(c);
                expectOperand = true;  // Po operatorze oczekujemy kolejnego operandu
            }
        }

        while (!stack.isEmpty()) {
            if (stack.peek() == '(') {
                throw new IllegalArgumentException("Mismatched parentheses in the expression.");
            }
            postfix.append(stack.pop());
            postfix.append(' ');
        }
        if (expectOperand && !postfix.toString().trim().isEmpty()) {
            throw new IllegalArgumentException("Missing operand at the end of expression.");
        }

        return postfix.toString().trim();
    }
}

Metoda pomocnicza getPriority() określa priorytet operatorów matematycznych, co jest kluczowe do zarządzania kolejnością operacji podczas konwersji. Każdy znak w wyrażeniu infiksowym jest przetwarzany w pętli. Cyfry są dodawane bezpośrednio do postfix. Aby obsłużyć liczby wielocyfrowe, kod sprawdza, czy następny znak to również cyfra, zanim doda spację.

Nawiasy Otwierające ‘(’: Są umieszczane na stosie, aby wskazać początek subwyrażenia. Nawiasy Zamykające ‘)’: Powodują opróżnienie stosu z operatorów aż do napotkania nawiasu otwierającego, który jest usuwany. Operatory: Jeśli operator ma niższy lub równy priorytet do operatora na wierzchu stosu, operatory są ściągane ze stosu do wyniku. Następnie bieżący operator jest umieszczany na stosie.

Po przejściu przez wszystkie znaki, wszystkie pozostałe operatory na stosie są ściągane do wynikowego wyrażenia postfiksowego. Sprawdzanie Nawiasów: Rzuca wyjątek IllegalArgumentException, gdy nawiasy są niezbalansowane (np. brakujący ‘(’ lub ‘)’). Sprawdzanie Oczekiwanych Operandów: Rzuca wyjątek, gdy przed operatorem nie ma odpowiedniego operandu lub gdy oczekiwano na operand na końcu wyrażenia.

Konwerter z Notacji Postfiksowej na Infiksową

class PostfixToInfix {
    // Klasa pomocnicza do przechowywania wyrażeń wraz z ich priorytetem operacyjnym
    static class Expression {
        String expr;
        int precedence;

        public Expression(String expr, int precedence) {
            this.expr = expr;
            this.precedence = precedence;
        }
    }

    // Metoda zwracająca priorytet operatora
    private static int precedenceOf(char operator) {
        if (operator == '+' || operator == '-') return 1;
        if (operator == '*' || operator == '/') return 2;
        return -1;
    }

    // Metoda konwertująca wyrażenie postfiksowe na infiksowe
    public static String convertToInfix(String postfix) throws IllegalArgumentException {
        Stack<Expression> stack = new Stack<>();
        String[] tokens = postfix.split("\\s+");

        for (String token : tokens) {
            if (token.matches("\\d+")) { // Sprawdza, czy token jest liczbą
                stack.push(new Expression(token, 3));
            } else if (token.matches("[+\\-*/]")) { // Sprawdza, czy token jest operatorem
                if (stack.size() < 2) { // Sprawdza, czy na stosie są co najmniej dwa elementy
                    throw new IllegalArgumentException("Invalid Expression: Not enough operands for " + token);
                }
                Expression right = stack.pop();
                Expression left = stack.pop();

                int opPrecedence = precedenceOf(token.charAt(0));
                String leftExpr = left.expr;
                if (left.precedence < opPrecedence) {
                    leftExpr = "(" + left.expr + ")";
                }

                String rightExpr = right.expr;
                if (right.precedence < opPrecedence) {
                    rightExpr = "(" + right.expr + ")";
                }

                stack.push(new Expression(leftExpr + " " + token + " " + rightExpr, opPrecedence));
            }
        }

        if (stack.size() != 1) {
            throw new IllegalArgumentException("Invalid Expression: Mismatched operands and operators");
        }
        return stack.pop().expr; // Zwrócenie ostatniego wyrażenia ze stosu jako wynik
    }
}

Metoda convertToInfix() konwertuje wyrażenie postfiksowe na infiksowe. Wyrażenie postfiksowe jest dzielone na tokeny (liczby i operatory), które są przetwarzane jeden po drugim. Wyrażenie postfiksowe jest rozdzielane na tokeny (elementy), które są następnie przetwarzane jeden po drugim.

Dla każdego tokenu sprawdzane jest, czy jest to operand (liczba) czy operator (+, -, *, /). Gdy napotykany jest operator, zdejmowane są dwa ostatnie wyrażenia ze stosu, a następnie łączone zgodnie z priorytetem operacji. Jeśli priorytet operacji jest wyższy niż priorytet wyrażeń, dodawane są nawiasy do tych wyrażeń, aby zachować właściwą kolejność operacji matematycznych. Po przetworzeniu wszystkich tokenów, na stosie powinno zostać tylko jedno wyrażenie, które jest wynikowym wyrażeniem infiksowym.

Jeśli na stosie jest więcej lub mniej niż jedno wyrażenie, rzucony zostaje wyjątek, informujący o błędzie w podanym wyrażeniu postfiksowym.

Odwrotna Notacja Polska – podsumowanie

Odwrotna Notacja Polska oferuje wyjątkową kombinację prostoty, efektywności i szerokiego zakresu zastosowań, czyniąc ją atrakcyjnym rozwiązaniem dla wielu problemów obliczeniowych zarówno w nauce, technologii, jak i biznesie. Jej zdolność do uproszczenia procesów obliczeniowych i minimalizacji potencjalnych błędów sprawia, że jest ceniona w wielu dziedzinach, od edukacji po wysokiej stawki finansowe i techniczne aplikacje.

 

➡ ZOBACZ 👉: Rekurencja ➿ rekursja ➿ rekurencja

No comments
Share:
hanoi tower

Wieża Hanoi🗼: Klasyczne wyzwanie logiczne – przewodnik początkującego programisty💡

Wieża Hanoi (ang. Hanoi Tower). To zadanie, choć proste w swojej idei, stanowi doskonałe pole do ćwiczenia umiejętności programistycznych i logicznego myślenia. Zapraszam do zgłębienia tajemnic tej zagadki, która od ponad wieku fascynuje zarówno matematyków, jak i programistów na całym świecie.

Wieża Hanoi – wprowadzenie

Z tego materiału dowiesz się:

  • Czym jest Wieża Hanoi?
  • Jaką naturę ma Wieża Hanoi?
  • Jak rozwiązać Wieżę Hanoi?
  • Jak Wieżę Hanoi wykorzystuje się w programowaniu?

Wieża Hanoi – jaki jest problem?

Wieża Hanoi to łamigłówka, która najprawdopodobniej powstała w Azji. Natomiast w Europie rozpropagował ją francuski matematyk Édouard Lucas. Zadanie polega na przeniesieniu stosu różnej wielkości dysków z jednego słupka na inny, z wykorzystaniem trzeciego słupka jako pomocniczego, z zachowaniem zasady, że na mniejszym dysku nie może spoczywać dysk większy.

Wieża Hanoi – hierarchiczna natura

Główna zasada Wież Hanoi mówi, że żaden większy dysk nie może spoczywać na mniejszym dysku. Ta reguła wprowadza zależność między dyskami, ustalając hierarchię, w której mniejsze dyski zawsze muszą leżeć na większych, niezależnie od słupka, na którym są umieszczone. Taka organizacja wymaga od gracza strategicznego planowania i przewidywania, jak przeniesienie jednego dysku wpłynie na możliwości przeniesienia innych dysków w przyszłości

Wieża Hanoi – rozwiązanie – algorytm

Rozważmy rozwiązanie problemu dla 3 dysków.

Proces rozwiązywania Wież Hanoi może być opisany następująco:

  • Przenieś n-1 dysków na słupek pomocniczy, co pozwala na odsłonięcie największego dysku.

wieża hanoi

 

  • Przenieś najmniejszy dysk na słupek pomocniczy, a największy dysk na docelowy słupek.

wieża hanoi

  • Teraz największy dysk jest już na swoim miejscu i nie będzie już więcej przenoszony.

wieża hanoi

  • Przenieś n-1 dysków z pomocniczego słupka na docelowy, co kończy proces.

wieża hanoi

Każdy z tych kroków jest zależny od poprzedniego, tworząc złożoną sieć zależności, która musi być przestrzegana, aby osiągnąć cel.

wieża hanoi

Możemy obliczyć minimalną liczbę ruchów potrzebnych do rozwiązania zadania za pomocą wzoru 2n-1.

gdzie n to ilość dysków.

Wieża Hanoi – rekurencyjne podejście

W praktyce rozwiązanie Wież Hanoi opiera się na rekurencyjnym podziale problemu na mniejsze części. Rekurencyjna natura rozwiązania wiąże się bezpośrednio z zależnościami między dyskami. Na przykład, przeniesienie największego dysku (podstawy) na celowy słupek jest możliwe tylko wtedy, gdy wszystkie mniejsze dyski są już na innym, pomocniczym słupku. To z kolei wymaga rozwiązania mniejszego problemu Wież Hanoi dla tych mniejszych dysków na pomocniczym słupku.

➡ ZOBACZ 👉: Rekurencja ➿ rekursja ➿ rekurencja

Wieża Hanoi – programowanie

Gra w Wieże Hanoi nie tylko rozwija zdolności rekurencyjnego myślenia, ale także uczy strategii i planowania. Jest również świetnym wprowadzeniem do stosów w informatyce, ponieważ dyski można traktować jako elementy stosu, co dodatkowo ilustruje zasady LIFO (Last In, First Out).

➡ ZOBACZ 👉: Stos (Stack) – 7+ tajników implementacji LIFO

Wieża Hanoi – Java

Algorytm, który rozwiązuje łamigłówkę i prezentuje każdy krok rozwiązania.

class HanoiTowers {
    public static void main(String[] args) {
        int numberOfDisks = 8;
        solveHanoi(numberOfDisks, 'A', 'C', 'B');
    }
    public static void solveHanoi(int disk, char start, char end, char auxiliary) {
        if (disk == 1) {
            System.out.println("Przenieś dysk 1 z " + start + " na " + end);
            return;
        }
        solveHanoi(disk - 1, start, auxiliary, end);
        System.out.println("Przenieś dysk " + disk + " z " + start + " na " + end);
        solveHanoi(disk - 1, auxiliary, end, start);
    }
}

Funkcja solveHanoi() Jest to rekurencyjna funkcja, która rozwiązuje problem Wież Hanoi. Przyjmuje cztery parametry: liczbę dysków (disk), oraz oznaczenia słupków: startowego (start), docelowego (end) i pomocniczego (auxiliary). Jeśli jest tylko jeden dysk, funkcja po prostu przenosi dysk bezpośrednio z słupka startowego na docelowy i kończy działanie. W przeciwnym razie przenosi dyski zgodnie z wcześniej omówionymi zasadami.

Kiedy uruchomisz ten program, wyświetlą się instrukcje na konsoli, które pokazują dokładną kolejność ruchów potrzebną do rozwiązania łamigłówki Wież Hanoi dla określonej liczby dysków. Możesz łatwo zmieniać liczbę dysków, modyfikując wartość zmiennej numberOfDisks w funkcji main().

Wieża Hanoi, logi

Wieża Hanoi – wizualizacja

Jeśli kod powyżej jest dla Ciebie niewystarczający, to sprawdź rozwiązanie, które dodatkowo wizualizuje w konsoli położenie dysków w trakcie rozwiązywania zadania.

class VisualHanoi {
    private static Stack<Integer>[] towers = new Stack[3];

    public static void main(String[] args) {
        int numberOfDisks = 3;  // Ustaw liczbę dysków tutaj
        setupTowers(numberOfDisks);
        printTowers();
        solveHanoi(numberOfDisks, 0, 2, 1);
    }

    private static void setupTowers(int disks) {
        for (int i = 0; i < 3; i++) {
            towers[i] = new Stack<>();
        }
        for (int disk = disks; disk > 0; disk--) {
            towers[0].push(disk);
        }
    }

    private static void solveHanoi(int disk, int start, int end, int auxiliary) {
        if (disk == 1) {
            towers[end].push(towers[start].pop());
            System.out.println("Przenieś dysk 1 z " + (char) ('A' + start) + " na " + (char) ('A' + end));
            printTowers();
            return;
        }
        solveHanoi(disk - 1, start, auxiliary, end);
        towers[end].push(towers[start].pop());
        System.out.println("Przenieś dysk " + disk + " z " + (char) ('A' + start) + " na " + (char) ('A' + end));
        printTowers();
        solveHanoi(disk - 1, auxiliary, end, start);
    }

    private static void printTowers() {
        System.out.println("A Tower: " + towers[0] + " B Tower: " + towers[1] + " C Tower: " + towers[2]);
        System.out.println("-------------------");
    }
}

UWAGA! Ostrożnie z ilością dysków, no chyba że masz całą wieczność. 🙃

Wieża Hanoi – zastosowanie

Problem Wież Hanoi znajduje zastosowanie w nauce o algorytmach, teorii gier, a nawet w psychologii, badając sposób, w jaki ludzie i maszyny podejmują sekwencyjne decyzje. Powiązany jest również z takimi koncepcjami, jak algorytmy sortowania, przeszukiwanie przestrzeni stanów oraz algorytmy planowania.

Wieża Hanoi – ciekawostka

Rozwiązanie łamigłówki staje się coraz trudniejsze wraz ze wzrostem ilości dysków. W zasadzie nie tyle trudniejsze ponieważ sekwencja ruchów się nie zmienia, co bardziej czasochłonne. Przełożenie wieży z 8 dyskami, zajmuje około 7 minut. Gdy zwiększymy ilość dysków do 30, i poświęcilibyśmy tylko jedną sekundę na przełożenie każdego dysku, zajęłoby nam to 33 lata.

Wieża Hanoi – podsumowanie

Wieża Hanoi to znakomity przykład na to, jak prosty problem matematyczny może być użyty do nauczania kluczowych pojęć informatycznych i programistycznych. Przez próbę rozwiązania tej zagadki, programiści mogą nauczyć się nie tylko rekurencji i stosów, ale również sposobów optymalizacji i efektywnego rozwiązywania problemów. Uczy metody podziału problemów na mniejsze, co jest fundamentalną umiejętnością w programowaniu i inżynierii. Uczy również zarządzania zasobami i przestrzegania określonych ograniczeń, co ma zastosowanie w projektowaniu algorytmów, optymalizacji systemów i wielu innych dziedzinach technicznych.

 

No comments
Share:
sudoku

Sudoku💡

Sudoku – to gra logiczna, która polega na wypełnieniu siatki 9×9 cyframi tak, aby każda kolumna, każdy wiersz i każdy z dziewięciu kwadratów 3×3 (które razem tworzą większą siatkę 9×9) zawierały wszystkie cyfry od 1 do 9.

Oto podstawowe reguły:

  • Podstawowa siatka: Cała gra odbywa się na siatce 9×9, która jest podzielona na 9 mniejszych kwadratów 3×3.
  • Cyfry od 1 do 9: Każda cyfra od 1 do 9 musi pojawić się dokładnie raz w każdym wierszu, każdej kolumnie i każdym kwadracie 3×3.
  • Jedna cyfra na komórkę: Każda komórka w siatce może zawierać tylko jedną cyfrę.
  • Początkowe wskazówki: Gra zaczyna się z pewną liczbą już wypełnionych cyfr (wskazówek), które gracze muszą użyć jako punkt wyjścia.
  • Logika, nie, zgadywanie: Rozwiązanie sudoku opiera się wyłącznie na dedukcji i logice. Nie ma potrzeby zgadywania.
  • Brak powtórzeń: Cyfra może pojawić się tylko raz w każdym wierszu, kolumnie i kwadracie 3×3.
  • Unikalne rozwiązanie: Każda prawidłowo zaprojektowana łamigłówka sudoku ma tylko jedno możliwe rozwiązanie.

Sudoku – przygotowanie planszy

Rozpoczynamy od zdefiniowania planszy jako dwuwymiarowej tablicy liczb całkowitych, gdzie wartość 0 reprezentuje pustą komórkę:

	public static void main(String[] args) {
		int[][] board = {
				{8, 0, 0, 0, 0, 0, 0, 0, 0},
				{0, 0, 3, 6, 0, 0, 0, 0, 0},
				{0, 7, 0, 0, 9, 0, 2, 0, 0},
				{0, 5, 0, 0, 0, 7, 0, 0, 0},
				{0, 0, 0, 0, 4, 5, 7, 0, 0},
				{0, 0, 0, 1, 0, 0, 0, 3, 0},
				{0, 0, 1, 0, 0, 0, 0, 6, 8},
				{0, 0, 8, 5, 0, 0, 0, 1, 0},
				{0, 9, 0, 0, 0, 0, 4, 0, 0}
		};

		printBoard(board);
	}

	private static void printBoard(int[][] board) {
		for (int i = 0; i < SIZE; i++) {
			for (int j = 0; j < SIZE; j++) {
				System.out.print(board[i][j] + " ");
			}
			System.out.print("\n");
		}
		System.out.println();
	}

Sudoku java przykład

Sudoku – rozwiązanie

Kluczową częścią algorytmu jest metoda solve(), która rekurencyjnie przeszukuje możliwości wypełnienia planszy:

    private boolean solve(int[][] board) {
        for (int i = 0; i < SIZE; i++) {
            for (int j = 0; j < SIZE; j++) {
                if (board[i][j] == NO_VALUE) {
                    for (int k = 1; k <= 9; k++) {
                        board[i][j] = k;
                        if (isValid(board, i, j) && solve(board)) {
                            return true;
                        }
                        board[i][j] = NO_VALUE;
                    }
                    return false;
                }
            }
        }
        return true;
    }

➡ ZOBACZ 👉: Rekurencja ➿ rekursja ➿ rekurencja

Dla każdej pustej komórki (NO_VALUE), metoda próbuje wstawić każdą możliwą wartość od 1 do 9, sprawdzając za każdym razem, czy nie narusza to zasad sudoku przy użyciu metody isValid().

Sudoku – walidacja

    private boolean isValid(int[][] board, int row, int column) {
        return (rowConstraint(board, row)
                && columnConstraint(board, column)
                && subsectionConstraint(board, row, column));
    }

Korzystając z pomocniczych metod, algorytm weryfikuje, czy dane umieszczenie liczby nie powoduje konfliktów.

Sudoku – pomocnicze metody walidacyjne

    private boolean rowConstraint(int[][] board, int i) {
        boolean[] constraint = new boolean[SIZE];
        return IntStream.range(1, 9)
                .allMatch(column -> checkConstraint(board, i, constraint, column));
    }
    private boolean columnConstraint(int[][] board, int j) {
        boolean[] constraint = new boolean[SIZE];
        return IntStream.range(1, 9)
                .allMatch(row -> checkConstraint(board, row, constraint, j));
    }
    private boolean subsectionConstraint(int[][] board, int i, int j) {
        boolean[] constraint = new boolean[SIZE];
        int subsectionRowStart = (i / 3) * 3;
        int subsectionRowEnd = subsectionRowStart + 3;

        int subsectionColumnStart = (j / 3) * 3;
        int subsectionColumnEnd = subsectionColumnStart + 3;

        for (int r = subsectionRowStart; r < subsectionRowEnd; r++) {
            for (int c = subsectionColumnStart; c < subsectionColumnEnd; c++) {
                if (!checkConstraint(board, r, constraint, c)) return false;
            }
        }
        return true;
    }

Metody takie jak rowConstraint() i columnConstraint() używają strumieni IntStream do sprawdzania, czy każda liczba w danym wierszu lub kolumnie jest unikalna. Z kolei metoda subsectionConstraint() odpowiada za weryfikację małych kwadratów 3×3.

Sudoku – optymalizacja sprawdzania

    boolean checkConstraint(int[][] board, int i, boolean[] constraint, int j) {
        if (board[i][j] != NO_VALUE) {
            if (!constraint[board[i][j] - 1]) {
                constraint[board[i][j] - 1] = true;
            } else {
                return false;
            }
        }
        return true;
    }

Metoda checkConstraint() zapewnia, że wstawiona liczba nie powtarza się już w danym kontekście (wiersz, kolumna, kwadrat 3×3).

Sudoku – wizualizacja rozwiązania

    private void printBoard(int[][] board) {
        for (int i = 0; i < SIZE; i++) {
            for (int j = 0; j < SIZE; j++) {
                System.out.print(board[i][j]);
                System.out.print(" ");
            }
            System.out.println();
        }
        System.out.println();
    }

Choć algorytm zwraca wartość logiczną wskazującą, czy znaleziono rozwiązanie, dla lepszego zrozumienia procesu warto również wyświetlić końcową konfigurację planszy.

Sudoku – podsumowanie

Wyżej przedstawiony algorytm, jest potężnym narzędziem, które przez systematyczne próbowanie i eliminowanie możliwości, pozwala znaleźć prawidłowe rozwiązanie łamigłówki. Tutaj istotna jest dokładna walidacja każdego ruchu zgodnie z zasadami sudoku oraz rekurencyjne szukanie rozwiązania, które spełnia wszystkie ograniczenia. Mimo że algorytm ten jest skuteczny, istnieje wiele możliwości jego optymalizacji, na przykład poprzez wprowadzenie bardziej zaawansowanych technik eliminacji i sprawdzania możliwości, co może znacząco przyspieszyć proces znajdowania rozwiązania.

➡ ZOBACZ 👉: StormIT | Oferta

No comments
Share:
Exception, wyjątek

Exception, Wyjątek – Od Buga do Rozwiązania🪲⚙️

Witajcie w podróży, gdzie każdy bug🪲 staje się szansą na naukę, a wyjątki w Java przestają być zagadką. Czy zastanawialiście się kiedyś, co dokładnie dzieje się, gdy wasz kod nagle przestaje działać?🔧 Albo co oznacza tajemniczy komunikat o błędzie, który wydaje się mówić w obcym języku? Zapraszam do odkrycia, jak wyjątki mogą stać się waszym najlepszym narzędziem w walce z bugami.

Exception, wyjątki – wprowadzenie

Z tego materiału dowiesz się:

  • Czym są wyjątki  (ang. exceptions)?
  • Jakie mamy rodzaje wyjątków?
  • Jak rzucić wyjątek?
  • Jak złapać wyjątek?
  • Kiedy poleci stacktrace?
  • Jakie są najczęstsze błędy?

Exception, wyjątki – cóż to takiego?

Wyjątki (ang. exceptions) – są mechanizmem w programowaniu, używanym do sygnalizowania wystąpienia błędu lub nietypowej sytuacji, która może zakłócić normalne działanie programu. W wielu językach programowania, w tym w Javie, wyjątki oferują strukturalny sposób na obsługę takich sytuacji, umożliwiając programom radzenie sobie z błędami w kontrolowany i przewidywalny sposób. Możemy wyróżnić następujące rodzaje wyjątków:

  • Checked Exceptions – Są to wyjątki, które muszą być obsłużone (złapane) lub zadeklarowane przez metodę. Java wymaga od programisty, aby jawnie zajął się tymi wyjątkami, co ma na celu zwiększenie niezawodności programu poprzez zmuszenie do przemyślenia sposobów radzenia sobie z potencjalnymi problemami.

Przykłady IOException, SQLException.

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;

public class IOExceptionHandlingExample {
    public static void main(String[] args) {
        String filePath = "example.txt";

        try (BufferedReader reader = new BufferedReader(new FileReader(filePath))) {
            String line;
            while ((line = reader.readLine()) != null) {
                System.out.println(line);
            }
        } catch (IOException e) {
            System.err.println("Wystąpił problem podczas odczytu pliku: " + e.getMessage());
        }
    }
}

 

  • Unchecked Exceptions – Są to wyjątki, które nie muszą być jawnie obsłużone lub zadeklarowane przez metodę. Te wyjątki zazwyczaj wskazują na błędy w logice programu, które mogą być trudne do przewidzenia i obsłużenia. Programista ma wolność w decydowaniu, czy i jak je obsłużyć.

Przykłady NullPointerException, ArrayIndexOutOfBoundsException.

public class ArrayIndexOutOfBoundsExceptionHandlingExample {
    public static void main(String[] args) {
        int[] numbers = {1, 2, 3, 4, 5};
        
        int index = 5; // Przykładowy indeks, który przekracza rozmiar tablicy

        try {
            System.out.println("Wartość na indeksie " + index + ": " + numbers[index]);
        } catch (ArrayIndexOutOfBoundsException e) {
            System.err.println("Błąd: Próba dostępu do indeksu poza zakresem tablicy.");
        }
    }
}
  • Runtime Exceptions – Jest to podkategoria unchecked exceptions. Są to wyjątki, które mogą pojawić się podczas wykonania programu i zazwyczaj są wynikiem błędów w logice programu, takich jak nieprawidłowe odwołanie do obiektu lub próba dostępu do elementu poza zakresem tablicy.

Przykłady IndexOutOfBoundsException, ClassCastException.

public class ClassCastExceptionHandling {
    public static void main(String[] args) {
        Object stringObject = "To jest łańcuch znaków";

        try {
            // Próba rzutowania obiektu na typ, z którym nie jest kompatybilny
            Integer integerObject = (Integer) stringObject;
        } catch (ClassCastException e) {
            System.err.println("Błąd rzutowania: " + e.getMessage());
        }

        System.out.println("Program kontynuuje działanie po obsłudze wyjątku.");
    }
}
  • Errors – Chociaż nie są one technicznie wyjątkami, są traktowane podobnie i mają własną kategorię w hierarchii dziedziczenia Java. Errors wskazują na poważne problemy, których aplikacja nie powinna próbować obsługiwać, ponieważ zazwyczaj są one poza kontrolą programisty.

Przykłady OutOfMemoryError, StackOverflowError.

exceptions

Jak rzucić wyjątek (ang. exception)?

Rzucenie wyjątku to sposób na sygnalizowanie, że coś poszło nie tak. Możemy to zrobić za pomocą słowa kluczowego throw, a następnie utworzyć instancję wyjątku.

if (userInput < 0) {
    throw new IllegalArgumentException("Liczba musi być dodatnia!");
}

Jak złapać wyjątek (ang. exception)?

Aby złapać wyjątek, używamy bloku try-catch, co pozwala nam na eleganckie obsłużenie problemu bez przerywania pracy programu.

try {
    // Tutaj umieszczamy kod, który może spowodować wyjątek.
    int wynik = 10 / 0; // To wyrażenie spowoduje ArithmeticException (dzielenie przez zero).
} catch (ArithmeticException e) {
    // Tutaj obsługujemy wyjątek, który może zostać rzucony w bloku try.
    System.out.println("Wystąpił błąd: Nie można dzielić przez zero." + e.getMessage());
}

W niektórych sytuacjach, lepszym sposobem obsługi wyjątków jest konstrukcja try-with-resources.

➡ ZOBACZ 👉: Try with resources, Java try-with-resources ❤‍🔥

Kiedy poleci stacktrace?

Stacktrace, czyli ślad stosu, to nasz drogowskaz w świecie debugowania. Pojawia się, gdy program napotka wyjątek, który nie został obsłużony. Dostarcza on szczegółowych informacji o błędzie, wskazując, gdzie dokładnie w kodzie wystąpił problem.

➡ ZOBACZ 👉: Stacktrace – jak rozszyfrować Stacktrace w Twoim kodzie?

Excetpion, wyjątki – najczęstsze błędy i jak ich unikać?

Praca z wyjątkami może być niekiedy myląca. Oto kilka wskazówek, jak unikać typowych błędów:

  • Nie ignoruj wyjątków. Zawsze obsługuj je w sposób, który ma sens dla twojego programu.
  • Unikaj ogólnych wyjątków, takich jak Exception w blokach catch. Staraj się łapać tylko te wyjątki, które faktycznie mogą wystąpić.
  • Pamiętaj, aby nie nadużywać wyjątków. Nie wszystkie błędy w programie wymagają wyjątków do ich obsługi.

Exception, wyjątki – podsumowanie

Wyjątki w Java to potężne narzędzie, które jeśli jest używane mądrze, może znacząco poprawić jakość Twojego kodu, uczynić go bardziej odpornym na błędy i łatwiejszym do debugowania. Pamiętaj, że kluczem jest zrozumienie różnicy między checked i unchecked exceptions, a także kiedy i jak je efektywnie stosować. Przyjmij wyjątki jako nieodłączną część procesu programowania, która nie tylko pomaga w radzeniu sobie z błędami, ale również uczy, jak tworzyć lepsze oprogramowanie.

➡ ZOBACZ 👉: Debugowanie, jakiego jeszcze nie znałeś

No comments
Share:
Stacktrace java

Stacktrace – jak rozszyfrować Stacktrace w Twoim kodzie?

Stacktrace na pierwszy rzut oka, może przypominać zapis starożytnego manuskryptu – długi, zawiły i na pozór nie do rozszyfrowania. To zbiór linijek pełnych technicznego żargonu, który dla wielu jest jak labirynt bez wyjścia. Jednak, choć może wydawać się nieprzeniknioną ścianą tekstu, kryje w sobie klucze do najgłębiej skrywanych tajemnic Twojego kodu. Jest jak mapa, która prowadzi przez mroczne zaułki błędów i wyjątków, ostatecznie wskazując drogę do ich pokonania.

Stacktrace – wprowadzenie

Z tego materiału dowiesz się:

  • Co to jest stacktrace?
  • Kiedy poleci stacktrace?
  • Jak czytać stacktrace?

Stacktrace – co to?

Stacktrace, czyli ślad stosu, to zestawienie metod wywoływanych w trakcie działania programu aż do momentu wystąpienia wyjątku. Jest to swoista „ścieżka błędu”, która wskazuje, gdzie i dlaczego coś poszło nie tak. To jak fotograficzny zrzut miejsca „zbrodni” – pokazuje, gdzie dokładnie w Twoim kodzie pojawił się problem.

stacktrace

Kiedy dostaniemy stacktrace?

Stacktrace – pojawia się, gdy w Twoim programie Java zostanie rzucony wyjątek, a nie zostanie on obsłużony. Może to być wynik błędu logicznego, problemu z dostępem do zasobu lub każdej innej sytuacji, która zakłóca normalne działanie programu.

➡ ZOBACZ 👉: Exception, Wyjątek – Od Buga do Rozwiązania🪲⚙️

Jak czytać stacktrace?

  • Znajdź przyczynę wyjątku: Na początku stacktrace zawsze znajdziesz nazwę wyjątku oraz opcjonalnie komunikat, który opisuje problem. Jest to punkt wyjścia do zrozumienia, co poszło nie tak.
  • Zidentyfikuj miejsce wystąpienia wyjątku: Bezpośrednio po nazwie wyjątku znajduje się ślad (ang. stack trace), który zawiera listę metod wywołanych do momentu wystąpienia wyjątku. Pierwsza linijka po nazwie wyjątku wskazuje bezpośrednio na miejsce, gdzie wyjątek został rzucony lub gdzie wystąpił błąd. Zawiera nazwę klasy, metodę oraz numer linii w pliku źródłowym.
  • Przeanalizuj ścieżkę wywołań: Śledź ścieżkę wywołań od góry do dołu, aby zrozumieć, jak wykonanie programu prowadziło do błędu. Każda kolejna linijka wskazuje na poprzednie miejsce w kodzie, które wywołało metodę z linijki wyżej.
  • Szukaj kodu, który został napisany: W stacktrace mogą pojawiać się metody z bibliotek zewnętrznych lub frameworków. Skoncentruj się na tych częściach śladu, które odnoszą się do kodu aplikacji, który piszesz.

➡ ZOBACZ 👉: Debugowanie, jakiego jeszcze nie znałeś

Przykłady

👉NullPointerException | IOException | FileNotFoundException 

Kod

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;

public class ComplexExceptionExample {

    public static void main(String[] args) {
        try {
            initiateProcess();
        } catch (NullPointerException | IOException e) {
            // Rzucanie NullPointerException z wyjątkiem IOException jako przyczyną
            NullPointerException npe = new NullPointerException("Próba odwołania się do pola na obiekcie null");
            npe.initCause(e);
            throw npe;
        }
    }

    private static void initiateProcess() throws IOException {
        readFile();
    }

    private static void readFile() throws IOException {
        try {
            FileReader reader = new FileReader(new File("nieistniejacy_plik.txt"));
        } catch (FileNotFoundException e) {
            IOException ioException = new IOException("Błąd podczas odczytu pliku.");
            ioException.initCause(e);
            throw ioException;
        }
        processFile();
    }

    private static void processFile() {
        // Symulacja dalszej części procesu, która nie jest istotna dla wyjątku
        furtherProcessing();
    }

    private static void furtherProcessing() {
        // Tutaj mogłaby pojawić się dodatkowa logika...
        // Symulacja elipsy w stacktrace przez wywołanie metody, która rzuci wyjątek
        throw new RuntimeException("Dodatkowy wyjątek w procesie.");
    }
}

Stos

NullPointerException

 

Opis błędu👇

  • Exception in thread "main" ⇒ Informacja, że wyjątek wystąpił w wątku o nazwie „main”.
  • java.lang.NullPointerException ⇒ Typ wyjątku, który został rzucony.
  • Próba odwołania się do pola na obiekcie null ⇒ Opcjonalny komunikat wyjątku, który opisuje problem.

Miejsce wystąpienia👇

  • at ComplexExceptionExample.main(ComplexExceptionExample.java:13) ⇒ Wyjątek został rzucony lub zarejestrowany w metodzie main klasy ComplexExceptionExample.

Pierwsza przyczyna(Caused by): java.io.IOException👇

  • java.io.IOException: Błąd podczas odczytu pliku. ⇒ Informuje, że podczas operacji wejścia/wyjścia (I/O) wystąpił problem, konkretnie „Błąd podczas odczytu pliku”.

Ślad stosu dla IOException

  • at ComplexExceptionExample.readFile(ComplexExceptionExample.java:27) ⇒ Wskazuje, że IOException został rzucony w metodzie readFile klasy ComplexExceptionExample, w linii 27.
  • at ComplexExceptionExample.initiateProcess(ComplexExceptionExample.java:20) ⇒ Metoda initiateProcess wywołała readFile, co doprowadziło do błędu.
  • at ComplexExceptionExample.main(ComplexExceptionExample.java:10) ⇒ Metoda main wywołała initiateProcess, kontynuując ślad wywołań wstecz.

Druga przyczyna (Caused by): java.io.FileNotFoundException👇

  • java.io.FileNotFoundException: nieistniejacy_plik.txt (The system cannot find the file specified) ⇒ Ten wyjątek jest bardziej szczegółowy i wskazuje na próbę otwarcia pliku, który nie istnieje w systemie (nieistniejacy_plik.txt).

Ślad stosu dla FileNotFoundException

  • at java.base/java.io.FileInputStream.open0(Native Method) ⇒ Wskazuje, że błąd wystąpił w metodzie natywnej (nienapisanej w Javie, lecz w innym języku, np. C++).
  • at ComplexExceptionExample.readFile(ComplexExceptionExample.java:25) ⇒ Dokładne miejsce w kodzie użytkownika, gdzie próbowano otworzyć plik, co prowadzi do FileNotFoundException.

 

Powyższy stacktrace prezentuje złożoną sytuację, w której próba odczytu nieistniejącego pliku (FileNotFoundException) prowadzi do ogólnego błędu I/O (IOException), który z kolei jest przyczyną ostatecznego wyjątku (NullPointerException).

Fragment ... 2 more na końcu stacktrace wskazuje, że kolejne elementy śladu stosu są kontynuacją tych już przedstawionych wyżej, co jest standardowym sposobem skracania i unikania redundancji w prezentacji stacktrace.

Całość stacktrace pokazuje nie tylko bezpośrednią przyczynę błędu (NullPointerException), ale również łańcuch zdarzeń, które do niego doprowadziły, oferując pełniejszy kontekst problemu i ułatwiając debugowanie.

👉ArithmeticException

Kod

class ArithmeticExceptionExample {

    public static void main(String[] args) {
        divideNumbers(10, 0);
    }

    private static void divideNumbers(int numerator, int denominator) {
        // Próba dzielenia przez zero
        int result = numerator / denominator;
        System.out.println("Wynik dzielenia: " + result);
    }
}

Stos

ArithmeticException

 

Wyjątek👇

  • java.lang.ArithmeticException: / by zero ⇒ Ten komunikat informuje, że wystąpił błąd dzielenia przez zero. Jest to bezpośrednia przyczyna wyjątku.

Lokalizacja👇

  • at ArithmeticExceptionExample.divideNumbers(ArithmeticExceptionExample.java:8) ⇒ Wskazuje na dokładną linijkę kodu, gdzie wystąpił błąd – próba wykonania operacji dzielenia przez zero w metodzie divideNumbers.
  • at ArithmeticExceptionExample.main(ArithmeticExceptionExample.java:4) ⇒ Wskazuje, że metoda main wywołała metodę divideNumbers, która jest źródłem wyjątku.

ArithmeticException jest wyjątkiem czasu wykonania, który wskazuje na błędy w operacjach arytmetycznych, takie jak dzielenie przez zero. Jest to przykład, jak błędy logiczne w kodzie są sygnalizowane przez Javę.

Stacktrace – podsumowanie

Analiza stacktrace wymaga zwrócenia uwagi na typ wyjątku i komunikat błędu, a następnie prześledzenia ścieżki wywołań, aby zidentyfikować miejsce i przyczynę problemu. Pamiętaj, że błędy są nieodłączną częścią procesu programowania, a stacktrace to Twój przewodnik, jak z nich wyciągać wartościowe lekcje.

➡ ZOBACZ 👉:  Kurs Java | Darmowy Kurs Programowania w Javie

No comments
Share:
REST API vs GraphQL

GraphQL vs REST API | Czy to koniec REST API? Czas na GraphQL!

Czy kiedykolwiek zastanawialiście się, jak aplikacje mobilne i strony internetowe pobierają dokładnie te dane, których potrzebują, nie więcej i nie mniej? W epoce cyfrowej, gdzie efektywność i szybkość są na wagę złota, narzędzie, które pozwala na precyzyjne zapytania do bazy danych, staje się nieocenione. Takim narzędziem jest GraphQL. Jego unikalne podejście do zarządzania danymi sprawia, że jest on wyborem w wielu nowoczesnych projektach.

W świecie tworzenia nowoczesnych aplikacji internetowych i mobilnych, wybór między GraphQL a REST API to więcej niż tylko decyzja technologiczna; to wybór między dwoma różnymi filozofiami zarządzania danymi.

GraphQL vs REST API – wprowadzenie

Z tego materiału dowiesz się:

  • Czym jest REST API?
  • Czym jest GraphQL?
  • Jakie są plusy REST API?
  • Jakie są potencjalne trudności REST API?
  • Jakie są plusy GraphQL?
  • Jakie są potencjalne trudności GraphQL?
  • Jakie są różnice między REST API i GraphQL?
  • Kiedy wybrać REST API?
  • Kiedy wybrać GraphQL?

REST API – Stabilność i Prostota

REST (ang. Representational State Transfer) – to architektura API, która korzysta z metod HTTP do komunikacji między klientem a serwerem. REST wykorzystuje standardowe metody HTTP, takie jak GET, POST, PUT i DELETE, do operacji CRUD (Create, Read, Update, Delete) na danych.

➡ ZOBACZ 👉: CRUD, Create | Read | Update | Delete [CRUD] 🛠️📖✍️❌

REST API, zorientowane na zasoby i oparte na standardowych metodach HTTP, jest dobrze znane w świecie programistów. Każdy zasób jest dostępny pod osobnym endpointem, co ułatwia zrozumienie i nawigację po API.rest-api-client

Ocena prezentacji, tech3camp

REST API – Plusy

  • Proste i wygodne w użyciu, z doskonałym wsparciem z licznych narzędzi i integracji.
  • Dobrze sprawdza się w większości przypadków, zarówno prostych, jak i nieco bardziej złożonych.
  • Większość logiki związanej z pobieraniem danych znajduje się po stronie backendu.

➡ ZOBACZ 👉: Backend – czy nadajesz się na backend developera?

Powyżej przedstawione są dwa zapytania.
Pierwsze zwraca miasta – endpoint cities, a drugie zwraca atrakcje – endpoint attractions.

REST API – Potencjalne trudności

  • Zarządzanie wieloma endpointami – REST API wymaga tworzenia osobnych endpointów dla różnych zasobów i operacji. W skomplikowanych systemach może to prowadzić do eksplozji liczby endpointów, co utrudnia zarządzanie i utrzymanie API.
  • Niedobór danych (ang. Under-fetching) – Brak potrzebnych danych, co skutkuje koniecznością np. wykonania dodatkowych wywołań API.
  • Nadmiar danych (ang. Over-fetching) – Pobieranie zbyt wielu niepotrzebnych danych może wpływać na wydajność.

GraphQL – Elastyczność i precyzja

GraphQL – to język zapytań dla API, opracowany przez Facebook w 2015 roku, który pozwala klientom definiować strukturę danych potrzebnych w zapytaniach. To oznacza, że można pobierać wiele zasobów jednym zapytaniem, a nie polegać na wielu oddzielnych endpointach API. Dzięki temu, że GraphQL pozwala na dokładne określenie, jakie dane są potrzebne, aplikacje mogą być szybsze i bardziej wydajne.

graphql

GraphQL – nie jest związany z żadną konkretną bazą danych ani środowiskiem backendowym, co czyni go niesamowicie uniwersalnym. Rozwijany przez Facebooka, szybko zyskał popularność dzięki swojej elastyczności i skuteczności.

GraphQL jest silnie typowany, co oznacza, że każde zapytanie jest weryfikowane względem określonego schematu, zanim zostanie wykonane.

GraphQL – charakterystyka

  • Jeden endpoint do wszystkiego – klient określa w zapytaniu (query), czego dokładnie potrzebuje, serwer musi obsłużyć query.
  • Elastyczność – możliwość definiowania schematu danych i dynamicznych zapytań.
  • Optymalizacja danych – idealne do aplikacji mobilnych i rozbudowanych części frontendowych.

graphql-klient-serwer

Tutaj mamy tylko jedno zapytanie, którym wyciągamy te same dane co wcześnie za pomocą dwóch zapytań korzystając z REST API.

GraphQL – potencjalne trudności

  • Większa złożoność – bardziej wymagające i skomplikowane w implementacji niż REST API.
  • Mniejsze wsparcie – mniej doświadczonych deweloperów i gorsze wsparcie ze strony narzędzi.
  • Trudności w keszowaniu i bezpieczeństwie – dynamiczne zapytania utrudniają prostą obsługę keszowania i mogą wprowadzać problemy z bezpieczeństwem. Zapomnij o prostym keszowaniu HTTP, koszmar zagnieżdżonych zapytań itp.
  • Przeciętna funkcjonalność – zazwyczaj wymaga od nas więcej wysiłku
  • Obsługa błędów – brak możliwości prostego wykorzystania nagłówków HTTP, zawsze otrzymujemy HTTP 200.

GraphQL vs. REST API

Elastyczność i wydajność

GraphQL REST API
Pozwala na pobranie dokładnie tych danych, których potrzebujesz, w jednym zapytaniu, co zmniejsza nadmiarowość i zwiększa wydajność. Może prowadzić do nadmiarowych danych, gdyż każdy endpoint zwraca ustaloną strukturę danych.

Typowanie i walidacja

GraphQL REST API
Silnie typowany system zapewnia automatyczną walidację zapytań, co ułatwia debugowanie i rozwój aplikacji. Walidacja zależy od implementacji serwera i może być mniej ścisła.

Rozwój i skalowalność

GraphQL REST API
Ułatwia iteracyjny rozwój, pozwalając dodawać nowe pola i typy bez zakłócania istniejących zapytań. Wymaga planowania i wersjonowania przy wprowadzaniu zmian, co może być bardziej czasochłonne.

GraphQL dla zaawansowanych

GraphQL oferuje również możliwości dla bardziej zaawansowanych użytkowników, takie jak subskrypcje, które pozwalają na otrzymywanie danych w czasie rzeczywistym, czy mutacje, umożliwiające modyfikację danych. Te zaawansowane funkcje otwierają nowe możliwości dla twórców aplikacji, czyniąc GraphQL jeszcze bardziej potężnym narzędziem.

Kiedy wybrać REST API?

Idealne dla prostych aplikacji z jednoznacznymi, dobrze zdefiniowanymi zapytaniami, gdzie większość logiki opiera się na standardowych operacjach CRUD. Powinno być domyślnym wyborem dla projektów niewymagających złożonych lub dynamicznych zapytań danych.

Kiedy wybrać GraphQL?

Najlepsze dla bardziej złożonych aplikacji z dynamicznie zmieniającymi się wymaganiami, gdzie szybki rozwój iteracyjny i prototypowanie są kluczowe. Idealne, gdy optymalizacja ilości przesyłanych danych jest ważna, np. w aplikacjach mobilnych, lub gdy potrzebne są dynamiczne zapytania częściowo definiowane przez użytkownika.

GraphQL vs. REST API – podsumowanie

GraphQL to potężne narzędzie, które rewolucjonizuje sposób, w jaki pracujemy z API. Jego elastyczność, wydajność i łatwość użytkowania sprawiają, że jest idealnym wyborem dla nowoczesnych projektów internetowych i aplikacji mobilnych.

Wybór między GraphQL a REST API zależy od wielu czynników, takich jak specyfika projektu, wymagania dotyczące wydajności, a także preferencje i doświadczenie zespołu deweloperskiego. GraphQL oferuje większą elastyczność i efektywność dla złożonych zapytań, podczas gdy REST API może być prostszym rozwiązaniem do szybkiego wdrożenia dla mniej skomplikowanych aplikacji.

Decyzja między GraphQL a REST API zależy od specyfiki Twojego projektu, wymagań dotyczących wydajności i elastyczności, a także od zespołu deweloperskiego. GraphQL oferuje niezrównaną elastyczność i efektywność w pobieraniu danych, ale wiąże się z większą złożonością. REST API z kolei jest bardziej dostępne i łatwiejsze w implementacji, co czyni je solidnym wyborem dla wielu aplikacji. Ważne jest, aby dokładnie rozważyć zalety i wady obu rozwiązań, zanim podejmiesz decyzję.

No comments
Share:
Try with resources

Try with resources, Java try-with-resources ❤‍🔥

Jako przedstawiciel tej grupy zawodowej
– z pełną odpowiedzialnością muszę przyznać  – że programiści z natury są raczej zapominalscy…

Zaowocowało to powstaniem wielu automatycznych rozwiązań, które mają odciążyć nas od pamiętania przynajmniej o niektórych sprawach (np. konstrukcja try with resources). Co jest jak najbardziej wskazaną praktyką, ponieważ możemy się wtedy skupić na ciekawszych i bardziej rozwijających zadaniach.

Przyjrzymy się dzisiaj mechanizmowi automatycznego zamykania zasobów z wykorzystaniem wyrażenia: try with resources.

Try with resources

Zacznijmy od małego wprowadzenia. Na początku były jakieś zasoby, np. strumień danych lub uchwyt do pliku. Programiści chętnie z nich korzystali. Wielu z nich jednak zapominało, że po skończonej pracy wypadałoby po sobie zwyczajnie posprzątać i zamknąć taki zasób, tak by inni mogli z niego później skorzystać. W efekcie takiego niechlujstwa wiele aplikacji cierpiało na różnego rodzaju wycieki.

Try catch finally

Standardowa procedura z wykorzystaniem konstrukcji try catch finally wygląda mniej więcej tak:

  • w bloku try otwieramy zasób oraz korzystamy z niego;
  • w bloku catch obsługujemy wyjątki lub przekazujemy je dalej. Blok catch nie jest obowiązkowy i możemy go pominąć przy założeniu, że wyjątki zostaną obsłużone w dalszych warstwach aplikacji;
  • w bloku finally sprzątamy po sobie, czyli zamykamy wykorzystywany zasób.
BufferedWriter fileWriter = null;
String fileName = "/TryCatchFinally.txt";

try {
	fileWriter = new BufferedWriter(new FileWriter(fileName));
	fileWriter.write("Try Catch Finally.");

} catch (IOException e) {
	e.printStackTrace();

} finally {
	if (fileWriter != null) {
		try {
			fileWriter.close();
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
}

I tu tak naprawdę pojawia się problem. ⚠️🚨

Ponieważ, zamknięcie zasobów nie jest w żaden sposób wymagane z punktu widzenia kompilatora Javy i wiele osób zapomina o tym lub robi to źle. Na pierwszy rzut oka zazwyczaj taka aplikacja działa poprawnie, jednak po pewnym czasie dochodzi do dziwnych i trudnych do zdiagnozowania błędów.

Idąc za zasadą, że lepiej zapobiegać niż leczyć, zastanówmy się, jak można zaradzić na takie zapominalstwo.

Try-with-resources

Tutaj całe szczęście przyszli nam z pomocą sami twórcy Javy i od Java 7 mamy natywne wsparcie dla nowej konstrukcji try with resources. Jest to rozbudowana wersja standardowego try catch finally o możliwość automatycznego zarządzania zasobami. Wystarczy, że sami utworzymy taki zasób, następnie Java automatycznie za nas będzie pamiętała, żeby go zamknąć, gdy nie będzie już potrzebny.

try (BufferedWriter fileWriter = new BufferedWriter(new FileWriter(fileName));) {
	fileWriter.write("Try with resources.");

} catch (IOException e) {
	e.printStackTrace();
}

Prawda, że prościej i czytelniej? I bezpieczniej 🙂

A w skrajnym wypadku, jeżeli zrezygnujemy z obsługi wyjątków możemy ograniczyć się do samej klauzuli try.

try (BufferedWriter fileWriter = new BufferedWriter(new FileWriter(fileName));) {
	fileWriter.write("Try with resources.");
}

➡ ZOBACZ 👉: Dlaczego boilerplate code zabija nasze aplikacje

try with resources – Jak to działa?

Podczas kompilacji try with resources zostaje zamieniony na bytecode, który wygląda bardzo podobnie do tego, jaki byłby wygenerowany na podstawie kodu z pierwszego przykładu (działa tu podobny mechanizm jak np. przy generowaniu domyślnego konstruktora dla klas). Czyli pod spodem nie ma żadnej dodatkowej kosztownej magii, a jedynie zamiana fragmentów kodu podczas kompilacji. Z tego powodu nie ma się co obawiać, że to dodatkowo obciąży naszą aplikację.

Własny manager kontekstu z wykorzystaniem interfejsu AutoCloseable

Matrioszka

Matrioszka
(źródło: wikipedia.org)

Żeby móc automatycznie zarządzać zasobami, poza umieszczeniem ich w instrukcji try, muszą one jeszcze implementować interfejs: java.lang.AutoCloseable. Wszystkie klasy obsługujące strumienie wyjścia i wyjścia ze standardowej biblioteki Javy spełniają ten warunek. Dlatego właśnie w poprzednim przykładzie mogliśmy skorzystać z klasy: BufferedReader – implementuje ona pośrednio ten interfejs, klasa BufferedReader dziedziczy po klasie Reader, która implementuje interfejs Closeable, który rozszerza właśnie interfejs AutoCloseable.

Skoro to takie proste, to spróbujmy samodzielnie napisać aplikację z wykorzystaniem konstrukcji try with resources oraz interfejsu AutoCloseable.

Jako przykład niech posłuży nam Matrioszka – to taka rosyjska zabawka, złożona z drewnianych, wydrążonych w środku lalek, włożonych jedna w drugą. Na pierwszy rzut oka niewiele ma to wspólnego z programowaniem, jednak jest to świetny przykład na zagnieżdżenie elementów. Przykładowa struktura lalek zaprezentowana w XML’u mogłaby wyglądać np. tak:

<Big> 
    <Medium> 
        <Small>CoreDoll</Small> 
    </Medium> 
</Big>

Spróbujmy teraz wygenerować taką strukturę dynamicznie korzystając z automatycznego zarządzania kontekstem.

class MatryoshkaDoll implements AutoCloseable {
	private final String name;

	public MatryoshkaDoll(String name) {
		this.name = name;
		System.out.println("<" + name + ">");
	}

	@Override
	public void close() {
		System.out.println("</" + name + ">");
	}
}

try (MatryoshkaDoll big = new MatryoshkaDoll("Big");
     MatryoshkaDoll medium = new MatryoshkaDoll("Medium");
     MatryoshkaDoll small = new MatryoshkaDoll("Small")) {
	System.out.println("CoreDoll");
}
  • tworzymy klasę MatryoshkaDoll, która implementuje AutoCloseable;
  • w instrukcji try() otwieramy trzy kolejne laleczki (zasoby): big, medium, small;
  • w ciele bloku try wywołujemy główną logikę – wyświetlamy laleczkę CoreDoll;
  • zasoby zostaną automatycznie zamknięte (wywołanie metody close) w kolejności odwrotnej, do tej, w której były otwierane.
Matrioszka politycy

Matrioszka politycy
(źródło: wikipedia.org)

Na tym etapie można by właściwie zakończyć rozważania na temat try with resources i w zdecydowanej większości przypadków to, o czym sobie do tej pory powiedzieliśmy będzie dla Ciebie wystarczające. Niestety czasem sytuacje się komplikuje i nie zawsze wszystko idzie tak, jak powinno, dlatego jeżeli będziesz mieć bardzo skomplikowany przypadek warto wiedzieć, jak sobie z nim radzić.

try with resources – Poprawna obsługa zagnieżdżonych zasobów

Niestety poprawna obsługa zagnieżdżonych zasobów nie jest aż taka oczywista i nawet korzystając z konstrukcji try with resources można zrobić to źle. W efekcie czego w przypadku kłopotów możemy zostać z niezamkniętymi zasobami.

Poniżej jest spreparowany przykład dwóch zagnieżdżonych zasobów: InnerResource, który jest wewnętrznym zasobem oraz OuterResource, który wewnętrznie korzysta z InnerResource. W tym przykładzie każda próba utworzenia zewnętrznego zasobu zakończy się niepowodzeniem.

Nie jest to przykład oderwany od rzeczywistości – dość często bowiem zdarzają się przypadki gdzie trzeba wykorzystać dwa strumienie jednocześnie, np. BufferedWriter oraz BufferedReader.

class InnerResource implements AutoCloseable {
	public InnerResource() {
		System.out.println("InnerResource created.");
	}

	@Override
	public void close() {
		System.out.println("InnerResource closed.");
	}
}

class OuterResource implements AutoCloseable {

	private final InnerResource innerResource;

	public OuterResource(final InnerResource innerResource) {
		this.innerResource = innerResource;
		throw new RuntimeException("Error on create OuterResource.");
	}

	@Override
	public void close() {
		System.out.println("OuterResource closed.");
	}
}

Nieprawidłowa obsługa zagnieżdżonych zasobów w try with resources

Ponieważ konstruktor OuterResource jako argument przyjmuje obiekt typu InnerResource, bardzo kusząca wydaje się poniższa konstrukcja, jednak…

UWAGA: poniżej jest błędny przykład!

try (OuterResource outer = new OuterResource(new InnerResource())) {
	System.out.println(outer);

} catch (Exception exception) {
	System.out.println("ERROR: " + exception);
}

Dla przypomnienia podczas  otwierania zewnętrznego zasobu coś pójdzie nie tak i zostanie zgłoszony wyjątek. Zobacz jak wtedy zachowa się nasz kod:

InnerResource created.
ERROR: java.lang.RuntimeException: Error on create OuterResource.

Mimo skorzystania z try with resources wewnętrzny zasób nie został zamknięty!

Jak poprawnie zamykać zasoby?

Prawidłowa obsługa zagnieżdźonych zasóbów w try with resources

Wystarczy, że lekko zmodyfikujemy nasz przykład – rozbijając tworzenie zasobów na dwie niezależne instrukcje, by try with resources było w stanie poprawnie je zamknąć.

try (InnerResource inner = new InnerResource();
     OuterResource outer = new OuterResource(inner)) {
	System.out.println(outer);
} catch (Exception exception) {
	System.out.println("ERROR: " + exception);
}

Logi z aplikacji to potwierdzają. Mimo wystąpienia wyjątku podczas tworzenia zewnętrznego zasobu InnerResource zostało poprawnie zamknięte.

InnerResource created.
InnerResource closed.
ERROR: java.lang.RuntimeException: Error on create OuterResource.

Suppressed Exceptions – zduszone wyjątki

Wprowadzenie bloku try with resources spowodowało konieczność wprowadzenia jeszcze jednego mechanizmu: tłumienia wyjątków (ang. suppressed exceptions).

class Resource implements AutoCloseable {
	public Resource() {
		System.out.println("Resource created.");
	}

	@Override
	public void close() {
		System.out.println("Resource closed.");
		// throw new RuntimeException("Error on close resource.");
	}
}

try (Resource resource = new Resource()) {
	// throw new RuntimeException("Error");

} catch (Exception e) {
	System.out.println(String.format("ERROR: %s, \nsuppressed: %s ", e, Arrays.toString(e.getSuppressed())));
}

W tym przykładzie jeżeli wszystko pójdzie dobrze powinniście zobaczyć komunikat o utworzeniu, a następnie zamknięciu zasobu.

Resource created.
Resource closed.

Ciekawe rzeczy zaczynają się jednak dziać, gdy coś pójdzie nie tak.

Jeżeli wystąpi wyjątek podczas zamykania zasobu, zostanie on normalnie zgłoszony i złapany.

Resource created.
ERROR: java.lang.RuntimeException: Error on close resource., 
suppressed: []

Podobnie w przypadku gdy poleci wyjątek z ciała bloku try.

Resource created.
Resource closed.
ERROR: java.lang.RuntimeException: Error, 
suppressed: []

UWAGA: Jeżeli jednak wystąpią oba wyjątki jednocześnie, to wyjątek z metody close, zostanie stłumiony i będzie dostępny tylko przez metodę: Exception.getSuppressed().

Resource created.
ERROR: java.lang.RuntimeException: Error, 
suppressed: [java.lang.RuntimeException: Error on close resource.]

Podsumowanie – Try With Resources oraz ZADANIE

Automatyczne zarządzanie zasobami przy wykorzystaniu try with resources nie jest niezbędne, jednak jest jedną z tych rzeczy, która tak ułatwia pracę, że zdecydowanie warto dodać ją do swojego warsztatu programisty. Dzięki jej wykorzystaniu nie tylko nasz kod będzie bardziej przejrzysty, ale dodatkowo zmniejszymy prawdopodobieństwo wystąpienia trudnych do zdiagnozowania problemów z niezamkniętymi zasobami.

Na dzisiaj to już wszystko. Spróbuj przećwiczyć tę wiedzę w praktyce, implementując przepisywanie danych z jednego pliku do drugiego z wykorzystaniem try with resources oraz dwóch klas obsługujących zasoby: BufferedReader i BufferedWriter.

Wynikami możesz podzielić się w komentarzu poniżej lub na naszej grupie.

No comments
Share:
Java file, obsługa plików

[Java File] Obsługa plików, zapis, odczyt💾📜 Java IO, Java NIO, FileWriter, BufferedWriter

Obsługa plików w Javie nie musi być skomplikowana. Niezależnie od tego, czy piszesz swoją pierwszą aplikację, czy też szlifujesz umiejętności w zaawansowanych projektach, dzisiejsza podróż pozwoli Ci odkryć, jak w prosty sposób zapisać swoje myśli lub wyniki pracy do pliku i jak z niecierpliwością odzyskać je, kiedy tylko zamarzysz.

Obsługa plików – wprowadzenie

Z tego materiału dowiesz się:

  • Czym jest obsługa plików?
  • Jakie są biblioteki do obsługi plików?
  • Jak zapisywać dane do pliku?
  • Jak odczytywać dane z pliku?
  • Jakie są dobre praktyki podczas obsługi plików?

Obsługa plików – Pierwszy krok do panowania nad danymi

Zanim zagłębimy się w konkretne techniki, ważne jest, aby zrozumieć, czym jest plik z perspektywy programisty. Plik to po prostu zbiór danych przechowywanych na dysku, z którymi można „kontaktować się” za pomocą programu. W Javie mamy do dyspozycji kilka API, które umożliwiają te interakcje, zarówno w sposób prosty, jak i zaawansowany.

Pliki są wszędzie – przechowują nasze zdjęcia, dokumenty, muzykę i niezliczone inne rodzaje danych. W programowaniu, zarządzanie plikami pozwala nam na zapisywanie wyników, czytanie danych wejściowych, konfigurację aplikacji i wiele więcej. Java oferuje bogaty zestaw narzędzi do efektywnej pracy z plikami, pozwalając na łatwe odczytywanie, zapisywanie i manipulowanie danymi.

Obsługa plików – Java IO

Java IO (Input/Output) to tradycyjny sposób obsługi plików w Javie, oferujący klasy takie jak File, FileReader, FileWriter, BufferedReader, i BufferedWriter. Umożliwiają one operacje na plikach takie jak tworzenie, odczyt, zapis i modyfikacja.

IO

Nowoczesne podejście – Java NIO

Java New IO (NIO) to nowsza biblioteka zaprojektowana dla bardziej efektywnej pracy. Klasy Files wraz z Paths stanowią potężne narzędzie do operacji na plikach.

NIO

Zapis do pliku z użyciem FileWriter

Rozpoczniemy od klasy FileWriter, która jest jak pierwszy przyjaciel każdego programisty w Javie, kiedy przychodzi do zapisu danych.

    static void fileWriterMethod() {
        String data = "Witaj, świecie plików!";
        try (FileWriter fileWriter = new FileWriter("plik.txt")) {
            fileWriter.write(data);
            System.out.println("Pomyślnie zapisano dane.");
        } catch (IOException e) {
            System.out.println("Wystąpił błąd.");
            e.printStackTrace();
        }
    }

Zapis do pliku z użyciem PrintWriter

Dla prostych zadań, klasa PrintWriter jest Twoim najlepszym przyjacielem. Pozwala na zapisywanie tekstu do pliku w łatwy sposób.

PrintWriter jest specjalizowaną klasą do zapisu tekstu w sposób wygodny i elastyczny.

    static void printWriterMethod() {
        String data = "Witaj, to jest wygodny PrintWriter!";
        try (PrintWriter out = new PrintWriter("plikPrinter.txt")) {
            out.println(data);
        } catch (FileNotFoundException e) {
            throw new RuntimeException(e);
        }
    }

Zapis do pliku z użyciem BufferedWriter

Klasa FileWriter służy do zapisywania znaków w pliku. Aby zapewnić większą wydajność, warto użyć BufferedWriter, który dodaje buforowanie, minimalizując liczbę operacji wejścia/wyjścia.

Jeśli zależy Ci na wydajności, BufferedWriter będzie Twoim wyborem. Dzięki buforowaniu operacji, zapis przebiega szybciej, co ma znaczenie przy dużych ilościach danych.

    static void bufferedWriterMethod() {
        String data = "To jest test BufferedWriter.\nKolejna linia.";
        try (BufferedWriter writer = new BufferedWriter(new FileWriter("plikBuffer.txt"))) {
            writer.write(data);
            System.out.println("Pomyślnie zapisano dane.");
        } catch (IOException e) {
            System.out.println("Wystąpił błąd podczas zapisu.");
            e.printStackTrace();
        }
    }

Zapis plików z użyciem NIO

To podejście umożliwia np. odczyt i zapis plików za pomocą jednej linijki kodu!

    static void writeFromNIO() {
        String data = "Witaj, NIO!";
        try {
            Files.write(Paths.get("plikNIO.txt"), data.getBytes());
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

Zapisywanie plików binarnych

Aby zapisać plik binarny w Javie, możesz użyć FileOutputStream, który jest przeznaczony do zapisywania bajtów do pliku. Poniżej znajdziesz przykład, jak użyć FileOutputStream do zapisania danych binarnych:

    static void filesOutputStreamWriterMethod() {
        String filePath = "plik.bin";
        byte[] data = {65, 66, 67, 68}; // Przykładowe dane w formacie binarnym

        try (FileOutputStream fos = new FileOutputStream(filePath)) {
            fos.write(data);
            System.out.println("Plik binarny został zapisany.");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

➡ ZOBACZ 👉: System dwójkowy 0️⃣1️⃣ Binarna reprezentacja liczb | System Binarny 🆚 Dwójkowy

Zapisywanie obiektów (Serializacja)

Java umożliwia zapisywanie obiektów do plików za pomocą mechanizmu serializacji. Aby obiekt mógł być serializowany, klasa musi implementować interfejs Serializable. Następnie, możesz użyć ObjectOutputStream w połączeniu z FileOutputStream do zapisania obiektu do pliku.

    static void objectOutputStreamMethod() {
        User user = new User("Jan Kowalski", "tajneHaslo123");
        String filePath = "uzytkownik.ser";

        try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(filePath))) {
            oos.writeObject(user);
            System.out.println("Obiekt został zapisany.");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

 

W powyższym przykładzie, pole password jest oznaczone jako transient, co oznacza, że nie zostanie zapisane podczas serializacji. Jest to ważne ze względów bezpieczeństwa, gdyż nie chcemy, aby wrażliwe dane były łatwo dostępne.

Aby ten kod zadziałał, klasa User musi implementować interfejs Serializable.

import java.io.Serial;
import java.io.Serializable;


class User implements Serializable {

    @Serial
    private static final long serialVersionUID = 122344L;

    private final String name;
    private final transient String password;

    public User(String name, String password) {
        this.name = name;
        this.password = password;
    }

    public String getName() {
        return name;
    }
}

Pamiętaj, że serializacja obiektów Java jest użyteczna, ale wymaga ostrożności, zwłaszcza w kontekście bezpieczeństwa i zarządzania wersjami obiektów. Warto również rozważyć alternatywne formaty zapisu, takie jak JSON czy XML, które mogą oferować większą elastyczność i interoperacyjność.

Odczyt z pliku z użyciem FileReader

FileReader jest tak samo prosty w obsłudze, jak jego kolega do zapisu.

    static String fileReaderMethod() {
        StringBuilder content = new StringBuilder();
        try (FileReader fileReader = new FileReader("plik.txt")) {
            int i;
            while ((i = fileReader.read()) != -1) {
                content.append((char) i);
            }
        } catch (IOException e) {
            System.out.println("Wystąpił błąd podczas odczytu.");
            e.printStackTrace();
        }
        return content.toString();
    }

Odczyt z pliku z użyciem BufferedReader

Czytanie plików w Javie jest prostsze, niż mogłoby się wydawać. Używając klasy FileReader w połączeniu z BufferedReader, możesz łatwo przeczytać zawartość pliku tekstowego linia po linii. Dlaczego BufferedReader? Ponieważ zapewnia on efektywne czytanie znaków, tablic i linii.

    static List<String> bufferedReader() {
        List<String> lines = new ArrayList<>();
        try (BufferedReader br = new BufferedReader(new FileReader("plikBuffer.txt"))) {
            String line;
            while ((line = br.readLine()) != null) {
                lines.add(line);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        return lines;
    }

Odczyt z pliku z użyciem NIO

Bliźniacze rozwiązanie z wykorzystaniem NIO jak przy zapisie plików.

    static List<String> readFromNIO() {
        try {
            return Files.readAllLines(Paths.get("plikNIO.txt"));
        } catch (IOException e) {
            throw new RuntimeException("Wystąpił błąd podczas odczytu pliku NIO.", e);
        }
    }

Odczytywanie plików binarnych

Aby odczytać plik binarny w Javie, możesz użyć FileInputStream, który pozwala na odczytywanie danych z pliku jako strumienia bajtów. Oto przykład, jak użyć FileInputStream do odczytu danych binarnych:

    static byte[] fileInputStreamReadMethod() {
        ByteArrayOutputStream buffer = new ByteArrayOutputStream();
        String filePath = "plik.bin";

        try (FileInputStream fis = new FileInputStream(filePath)) {
            int singleByte;
            while ((singleByte = fis.read()) != -1) {
                buffer.write(singleByte);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        return buffer.toByteArray();
    }

W tym przykładzie, metoda read() klasy FileInputStream zwraca kolejne bajty danych aż do osiągnięcia końca pliku, oznaczonego przez wartość -1.

Odczytywanie obiektów z pliku (Deserializacja)

Aby odczytać zapisane wcześniej obiekty, użyj ObjectInputStream w połączeniu z FileInputStream. To pozwoli zrekonstruować obiekty z ich binarnej postaci z powrotem do życia w Twoim programie.

Deserializacja to proces odwrotny do serializacji, który pozwala na odtworzenie obiektu Java z jego zapisanej formy binarnej. Aby deserializować obiekt, musisz użyć ObjectInputStream w połączeniu z FileInputStream. Poniżej znajduje się przykład, jak deserializować obiekt:

    static User objectInputStreamReadMethod() {
        String filePath = "uzytkownik.ser";

        try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(filePath))) {
            return (User) ois.readObject();
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
            return null;
        }
    }

Używamy tutaj tej samej klasy User implementującej interfejs Serializable. Ponadto, należy pamiętać o obsłudze wyjątku ClassNotFoundException, który może zostać rzucony, jeśli klasa obiektu, który próbujemy deserializować, nie jest dostępna.

Deserializacja obiektów wymaga ostrożności, szczególnie jeśli źródło pliku nie jest zaufane, ponieważ może to prowadzić do ataków i innych zagrożeń bezpieczeństwa. Zawsze upewnij się, że deserializujesz dane z zaufanych źródeł i odpowiednio obsługujesz wyjątki oraz błędy.

W obu przypadkach odczytywanie plików binarnych i deserializacja obiektów kluczowe jest prawidłowe zarządzanie zasobami i obsługa wyjątków, aby zapewnić bezpieczne i efektywne działanie aplikacji.

Obsługa plików – praktyczne zastosowania

Obsługa plików to nie tylko teoria. Wyobraź sobie, że tworzysz aplikację dziennika, która zapisuje Twoje myśli każdego dnia w osobnym pliku. Lub aplikację do zarządzania budżetem, która odczytuje transakcje z zapisanych plików CSV. Możliwości są nieograniczone!

Obsługa plików – bezpieczeństwo i wydajność

Podczas pracy z plikami, ważne jest, aby pamiętać o obsłudze wyjątków i zasobach systemowych. Zawsze używaj bloków try-with-resources do automatycznego zamykania strumieni i unikaj wycieków pamięci. Ponadto pamiętaj o sprawdzaniu uprawnień do plików i zastanów się nad zastosowaniem szyfrowania dla wrażliwych danych.

Obsługa plików – najlepsze praktyki i pułapki

Pamiętaj o odpowiednim zarządzaniu zasobami – zawsze zamykaj strumienie po ich użyciu. Używaj bloków try-with-resources dla automatycznego zarządzania zasobami. Bądź świadomy wydajności – dla dużych plików rozważ użycie buforowanego odczytu i zapisu.

Asynchroniczna obsługa plików

Java NIO (New Input/Output) wprowadza AsynchronousFileChannel, pozwalając na nieblokujący odczyt i zapis do pliku. To idealne rozwiązanie, gdy chcesz zachować responsywność Twojej aplikacji.

Obsługa plików – podsumowanie

Przez naszą podróż po obsłudze plików w Javie, zagłębiliśmy się w różnorodne aspekty zapisu, odczytu oraz zarządzania danymi plikowymi – fundamentalne umiejętności każdego programisty Javy. Zaczynając od podstawowych operacji I/O, przez zastosowanie strumieni, aż po zaawansowane techniki serializacji, naszym celem było zbudowanie solidnych podstaw oraz zrozumienie najlepszych praktyk. Opanowanie tych umiejętności z pewnością otwiera nowe możliwości.

No comments
Share: