Odpowiedzi na pytania i zadania - rozdziały 10-12

Pliki .java z rozwiązaniami do zadań znajdziesz na GitHubie w katalogu rozwiazania_do_zadan:

Rozwiązania do zadań na GitHub

Rozdział 11 – Wyjątki

Pytania

  1. Do czego służą wyjątki?

    Wyjątki służą do sygnalizowania, że coś w programie poszło nie tak. Zawierają także informację o ścieżce wykonania programu, która doprowadziła do zaistnienia problemu.

  2. Co to jest stack trace?

    Stack trace to lista wywołań kolejnych metod, które doprowadziły do sytuacji, w której doszło do rzucenia wyjątku w programie. Stack trace zawiera nie tylko nazwy metod, posortowane (patrząc od dołu) od pierwszej wywołanej do ostatniej, ale także numer linii kodu, które określają miejsce w metodzie, w którym nastąpiło przejście do następnej metody. Dzięki temu możemy prześledzić ścieżkę wykonania programu, która zakończyła się rzuceniem wyjątku.

  3. W której z metod wymienionych w poniższym stack trace rzucony został wyjątek?
    Exception in thread "main" java.lang.IllegalArgumentException at Pytania.innaMetoda(Pytania.java:13) at Pytania.pewnaMetoda(Pytania.java:9) at Pytania.main(Pytania.java:5)

    Wyjątek rzucany jest w ostatniej wykonanej metodzie, która w stack trace znajduje się na szczycie listy metod. W tym przypadku jest to metoda o nazwie innaMetoda i to w niej rzucony został wyjątek.

  4. Jak w języku Java obsługuje się wyjątki?

    Wyjątki obsługuje się umieszczając instrukcje, których wykonanie może zakończyć się rzuceniem wyjątku, w bloku try. Obsługa konkretnego typu wyjątku powinna zostać umieszczona w sekcji catch.

  5. Czy musimy stosować obsługę wyjątków jeżeli metoda, z której chcemy skorzystać, może rzucić wyjątek?

    Nie musimy – jeżeli rzucany jest wyjątek rodzaju Unchecked, to w ogóle nie musimy takiego wyjątku obsługiwać w naszym kodzie, jeżeli nie mamy takiej potrzeby.

    Jeżeli jednak rzucany jest wyjątek rodzaju Checked, a nie chcemy go obsłużyć, to musimy dodać do sygnatury metody, która korzysta z metody rzucającej wyjątek, klauzulę throws i wyszczególnić ten typ rzucanego wyjątku. W ten sposób oddelegowujemy złapanie wyjątku do metody, która będzie korzystała z naszej metody – prędzej czy później, ktoś potencjał rzucenia tego wyjątku będzie musiał obsłużyć.

  6. Do czego służy sekcja finally i czy jest wymagana?

    W tej opcjonalnej sekcji, która może być częścią obsługi wyjątków, zawieramy instrukcje, które mają zostać wykonane niezależnie od tego, czy wyjątek w sekcji try został rzucony, czy nie.

  7. Jak rzuca się wyjątki?

    Wyjątki rzuca się stosując słowo kluczowe throw, po którym podany powinien zostać wyjątek. Możemy albo rzucić już złapany wyjątek, albo utworzyć nowy za pomocą słowa kluczowego new dokładnie w ten sam sposób, jak do tej pory tworzyliśmy obiekty różnych klas.

  8. Do czego służy słowo kluczowe throws?

    To słowo kluczowe nie powinno być mylone z podobnym do niego słowem kluczowym throw. Słowo kluczowe throws stosujemy w sygnaturach metod, aby wskazać, że metoda może rzucić pewny typ wyjątku bądź wyjątków.

  9. Jaką regułę muszą spełniać klasy, aby były klasami wyjątków?

    Aby klasa była uznawana za klasę wyjątku, musi ona rozszerzać (dziedziczyć po) klasę Throwable lub którąś z jej pochodnych. Zazwyczaj jako klasy bazowe dla wyjątków stosuje się klasy Exception lub RuntimeException, które są pochodnymi klasy Throwable.

  10. Co trzeba zrobić, aby pobrać komunikat skojarzony z wyjątkiem?

    Należy na obiekcie wyjątku wywołać metodę getMessage, która ten komunikat zwraca. Metoda ta dziedziczona jest z klasy bazowej.

  11. Czym różnią się wyjątki Checked oraz Unchecked?

    Wyjątki Checked to wyjątki, które nie mają w swojej hierarchii dziedziczenia klasy RuntimeException. Wyjątki pochodzące od RuntimeException to wyjątki Unchecked. Jeżeli w wyniku wykonania metody może zostać rzucony wyjątek typu Checked, to muszą one być obsłużone, a metoda ta musi zadeklarować potencjał rzucenia tych wyjątków w swojej sygnaturze za pomocą słowa kluczowego throws. Wyjątków Unchecked ani nie trzeba obsługiwać, ani deklarować ich za pomocą throws, chociaż można to zrobić.

  12. Jakiego rodzaju (Unchecked / Checked) są poniższe wyjątki (musisz zajrzeć do dokumentacji Biblioteki Standardowej Java – Java Doc)?
    1. EOFException
    2. ClassCastException
    3. DateTimeParseException
    4. SQLException

    Wyjątki EOFException i SQLException to wyjątki rodzaju Checked, ponieważ nie mają w swojej hierarchii dziedziczenia klasy RuntimeException. Pozostałe dwa wyjątki są rodzaju Unchecked.

  13. Czy wyjątek NullPointerException to wyjątek rodzaju Checked czy Unchecked?

    NullPointerException to wyjątek rodzaju Unchecked, ponieważ dziedziczy po klasie RuntimeException.

  14. Co to jest silent catch (połykanie wyjątków)?

    Silent catch to łapanie wyjątków za pomocą try..catch bez jakiejkolwiek obsługi tych wyjątków – blok z instrukcjami dla catch w takich przypadkach jest pusty.

  15. Do czego służy try-with-resources i jak się tego mechanizmu używa?

    Ten dodany w wersji Java 1.7 mechanizm służy do skrótowego zapisu kodu obsługi wyjątków, gdy korzystamy z pewnych zasobów, np. obiektu służącego do czytania bądź zapisywania do pliku. Taki zasób będzie po zakończeniu bloku try..catch automatycznie zamknięty (wykonana zostania na nim jego metoda close). Oznacza to, że my, jako programiści, nie musimy pisać sami fragmentu kodu służącego do zamknięcia tego zasobu. Obiekt, który ma zostać dla nas obsłużony, podajemy w nawiasach po słowie kluczowym try, na przykład:

    try (FileReader fr = new FileReader(f)) {
    

    "Zasoby", z jakich możemy korzystać w try-with-resources, to obiekty klas, które implementują interfejs AutoCloseable lub Closeable.

  16. Czy poniższy kod jest poprawny?
    class MojWyjatekException {
    
    }
    
    public class Pytania {
      public static void main(String[] args) {
        try {
          pewnaMetoda();
        } catch (MojWyjatekException e) {
          System.out.println("Wystapil blad.");
        }
      }
    
      public static void pewnaMetoda() {
        throw new MojWyjatekException();
      }
    }

    Nie, ponieważ próbujemy korzystać z klasy MojWyjatekException jakby była to klasa wyjątku, ale klasa ta nie dziedziczy po żadnej innej klasie wyjątku – kod się nie skompiluje.

  17. Czy poniższe metody skompilują się bez błędów?
    public static void main(String[] args) {
      try {
        int x = getInt();
      } catch (InputMismatchException e) {
        System.out.println("Wystapil blad: " + e.getMessage());
      }
    
      if (x >= 0) {
        System.out.println("Podana liczba jest nieujemna.");
      } else {
        System.out.println("Podana liczba jest ujemna.");
      }
    }
    
    public static int getInt() {
      return new Scanner(System.in).nextInt();
    }

    Nie, ponieważ próbujemy skorzystać ze zmiennej x poza blokiem try. Zmienna ta zdefiniowana jest w bloku try i tylko w nim istnieje. Kompilator zgłosi błąd nieznanego identyfikatora "x" w linii if (x >= 0) {

  18. Czy poniższe metody skompilują się bez błędów?
    public static void pewnaMetoda() {
      try {
        innaMetoda();
      } catch (Exception e) {
        System.out.println(e.getMessage());
      } catch (MojWyjatekException e) {
        System.out.println(e.getMessage());
      }
    }
    
    public static void innaMetoda() throws MojWyjatekException {
      // pewne instrukcje mogace rzucic wyjatek
    }
    gdzie MojWyjatekException to:
    class MojWyjatekException extends Exception {
    
    }

    Nie, ponieważ kolejność obsługi wyjątków w metodzie pewnaMetoda jest nieprawidłowa. Najbardziej ogólne typy wyjątków powinny być zawsze po mniej ogólnych. W tym przypadku wszystkie wyjątki zostaną dopasowane do pierwszego bloku catch, ponieważ wszystkie wyjątki mogą być traktowane jako Exception (polimorfizm i dziedziczenie), więc kompilator wykryje, że drugi blok catch nigdy nie ma szansy się wykonać.

  19. Czy poniższa metoda skompiluje się bez błędów?
    public static void pewnaMetoda() throw IllegalArgumentException {
      // pewne instrukcje moga rzucic wyjatek
    }

    Nie, ponieważ skorzystaliśmy z nieprawidłowego słowa kluczowego. Deklarowanie rzucanych przez metodę wyjątków odbywa się za pomocą słowa throws, a nie throw.

  20. Czy poniższe metody skompilują się bez błędów?
    public static void main(String[] args) {
      int wynik;
    
      try {
        int x = getInt();
        wynik = x * x;
      } catch (InputMismatchException e) {
        System.out.println("Wystapil blad: " + e.getMessage());
      }
    
      System.out.println(wynik);
    }
    
    public static int getInt() {
      return new Scanner(System.in).nextInt();
    }

    W metodzie main kompilator zgłosi błąd kompilacji, ponieważ wykryje, że zmienna wynik może zostać użyta bez nadania jej wpierw wartości. Jeżeli metoda getInt rzuci wyjątek, to instrukcja wynik = x * x; nigdy się nie wykona, a tym samym zmienna wynik pozostanie niezainicjalizowana żadną wartością.

  21. Czy poniższy kod się skompiluje?
    public class Pytania {
      public static void main(String[] args) {
        pewnaMetoda(null);
      }
    
      public static String pewnaMetoda(String str)
          throws NullPointerException {
    
        return str.toUpperCase();
      }
    }

    Tak, ale po uruchomieniu zobaczymy na ekranie informację, że rzucony został wyjątek NullPointerException. Korzystając z pewnaMetoda nie musimy używać z try..catch do złapania potencjalnego wyjątku NullPointerException, ponieważ ten wyjątek jest rodzaju Unchecked. Wyjątków Unchecked nie trzeba koniecznie łapać w try..catch.

  22. Czy poniższy kod się skompiluje?
    class MojWyjatekException extends Exception {
    }
    
    public class Pytania {
      public static void main(String[] args) throws MojWyjatekException {
        throw new MojWyjatekException("Nic nie robie.");
      }
    }

    Ten kod się nie skompiluje, ponieważ próbujemy rzucić wyjątek korzystając z konstruktora, który przyjmuje komunikat błędu. Klasa MojWyjatekException nie udostępnia takiego konstruktora – kompilator zaprotestuje.

  23. Czy poniższe metody skompilują się bez błędów?
    public static void pewnaMetoda() {
      throw new IllegalArgumentException();
    }
    
    public static void innaMetoda() {
      throw new Exception();
    }
    
    public static void kolejnaMetoda() throws IOException {
    
    }

    Metoda pewnaMetoda skompiluje się bez błędów – IllegalArgumentException jest rodzaju Unchecked, więc nie musimy deklarować potencjału jego rzucenia.

    W metodzie innaMetoda kompilator zgłosi błąd – rzucamy wyjątek rodzaju Checked, a nie informujemy o tym w sygnaturze metody za pomocą throws.

    Metoda kolejnaMetoda jest poprawna – co prawda jest pusta, ale to nie problem. Metoda może sygnalizować, że może rzucić wyjątek za pomocą throws nawet, jeżeli tego nie robi.

  24. Czy poniższy kod skompiluje się poprawnie?
    class MojWyjatekException extends Exception {
    
    }
    
    public class Pytania {
      public static void main(String[] args) {
        try {
          pewnaMetoda();
        } catch (MojWyjatekException | Exception e) {
          System.out.println(e.getMessage());
        }
      }
    
      public static void pewnaMetoda() {
        // pewne instrukcje
      }
    }

    Nie, ten kod się nie skompiluje, ponieważ łapiąc wyjątki za pomocą | (ang. pipe), musimy wylistować wykluczające się wyjątki. W powyższym kodzie wyjątek Exception jest klasą bazową dla wyjątku MojWyjatekException, więc wyjątek MojWyjatekException i tak załapałby się do tej sekcji catch – kompilator zgłosi błąd.

  25. Czy poniższe metody skompilują się poprawnie?
    public static void pewnaMetoda() {
      try {
        innaMetoda();
      } catch (Exception e) {
        System.out.println("Wystapil blad: " + e.getMessage());
        throw e;
      }
    }
    
    public static void innaMetoda() throws Exception {
      throw new Exception();
    }

    Druga metoda skompiluje się bez błędów – rzucamy wyjątek typu Exception, deklarujemy w klauzuli throws, że taki wyjątek może być rzucony – wszystko w porządku.

    Pierwsza metoda się nie skompiluje, ponieważ rzuca ona wyjątek typu Exception w sekcji catch (ponownie rzuca już złapany wyjątek), a nie zawiera klauzuli throws w swojej sygnaturze.

  26. Czy poniższy kod skompiluje się bez błędów? Jeżeli tak, to co zobaczymy na ekranie?
    public class Pytania {
      public static void main(String[] args) {
        try {
          pewnaMetoda(0);
        } catch (Exception e) {
          System.out.println(e.getMessage());
        }
      }
    
      public static int pewnaMetoda(int x) {
        if (x == 0) {
          throw new IllegalArgumentException();
        }
    
        return x * x;
      }
    }

    Ten kod kompiluje się bez błędów. Metoda pewnaMetoda nie musi zgłaszać za pomocą throws, że rzuca IllegalArgumentException, ponieważ ten typ wyjątku jest rodzaju Unchecked. W metodzie main wywołujemy metodę pewnaMetoda z argumentem, który spowoduje rzucenie wyjątku. Zostanie on dopasowany do sekcji catch, ponieważ Exception jest jedną z klas, ktore IllegalArgumentException rozszerza.

    Na ekranie zobaczymy napis "null", ponieważ rzucany wyjątek nie zawiera żadnego komunikatu.

Zadania

Silnia z obsługą ujemnych liczb

Napisz metodę, która będzie zwracać silnię podanej jako argument liczby. Metoda powinna rzucać wyjątek rodzaju Checked zdefiniowanego przez Ciebie typu BlednaWartoscDlaSilniException, gdy jej argument będzie ujemny. Skorzystaj z tej metody w main, obsługując potencjalny wyjątek.

Wykonując to zadanie, musimy pamiętać o:

  • utworzeniu klasy wyjątku BlednaWartoscDlaSilniException, która będzie dziedziczyć po klasie Exception,
  • zawarciu w klasie wyjątku konstruktora, który będzie przyjmował jako argument treść komunikatu, ponieważ rzucając wyjątek zawrzemy w nim informację o błędzie,
  • dodaniu do sygnatury metody silni klauzuli throws z informacją o rzucanym wyjątku,
  • sprawdzaniu wartości przekazanej jako argument i rzuceniu wyjątku, jeżeli okaże się nieprawidłowa,
  • opakowaniu wywołania metody liczącej silnię w blok try..catch w metodzie main.

Przykładowa implementacja rozwiązania mogłaby wyglądać następująco:

class BlednaWartoscDlaSilniException extends Exception {
  public BlednaWartoscDlaSilniException(String message) {
    super(message);
  }
}

public class SilniaZObslugaLiczbUjemnych {
  public static void main(String[] args) {
    try {
      System.out.println("Silnia 5 = " + silnia(5));
      System.out.println("Silnia -2 = " + silnia(-2));
    } catch (BlednaWartoscDlaSilniException e) {
      System.out.println("Wystpil blad: " + e.getMessage());
    }
  }

  public static int silnia(int n)
      throws BlednaWartoscDlaSilniException {

    if (n < 0) {
      throw new BlednaWartoscDlaSilniException(
          "Silnia moze byc liczona tylko dla n >= 0"
      );
    }

    int result = 1;

    for (int i = 2; i <= n; i++) {
      result *= i;
    }

    return result;
  }
}

Wynik działania powyższego programu jest następujący:

Silnia 5 = 120 Wystpil blad: Silnia moze byc liczona tylko dla n >= 0

Klasa Adres z walidacją danych

Napisz program z klasą Adres, która będzie miała podane poniżej pola, które będą ustawiane w konstruktorze klasy Adres. Konstruktor powinien sprawdzić wszystkie podane wartości i rzucić wyjątek NieprawidlowyAdresException rodzaju Checked, jeżeli któraś z wartości będzie nieprawidłowa. Uwaga: komunikat rzucanego wyjątku powinien zawierać informację o wszystkich nieprawidłowych wartościach przekazanych do konstruktora – dla przykładu, jeżeli ulica i miasto będą miały wartość null, to komunikat wyjątku powinien być następujący: "Ulica nie może być nullem. Miasto nie może być nullem". Pola klasy:

  1. String ulica – wartość nieprawidłowa to null,
  2. int numerDomu – wartość nieprawidłowa to liczba <= 0,
  3. String kodPocztowy – wartość nieprawidłowa to null,
  4. String miasto – wartość nieprawidłowa to null.

W tym programie musimy utworzyć nowy typ wyjątku udostępniający konstruktor, do którego będziemy mogli przekazać komunikat błędu.

Ponadto, zgodnie z założeniami zadania, w konstruktorze klasy Osoba nie powinniśmy od razu rzucać wyjątku, gdy sprawdzimy, że pewny argument przesłany do konstruktora jest nieprawidłowy – zamiast tego powinniśmy do zmiennej typu String dodawać kolejne informacje o błędnych danych.

Po sprawdzeniu wszystkich argumentów, jeżeli jakikolwiek komunikat o błędzie został dodany do używanej zmiennej, rzucimy wyjątek NieprawidlowyAdresException, którego komunikatem błędu będzie właśnie ten komunikat, zawierający informację o wszystkich błędnych wartościach.

Jeżeli jednak wszystkie pola są poprawne, to po prostu przypiszemy je do pól klasy.

Przykładowa implementacja – w metodzie main tworzymy kilka obiektów typu Adres, aby zobaczyć, jak program się zachowa:

class NieprawidlowyAdresException extends Exception {
  public NieprawidlowyAdresException(String message) {
    super(message);
  }
}

public class Adres {
  private String ulica;
  private int numerDomu;
  private String kodPocztowy;
  private String miasto;

  public Adres(String ulica,
               int numerDomu,
               String kodPocztowy,
               String miasto)
      throws NieprawidlowyAdresException {

    String znalezionBledy = "";

    if (ulica == null) {
      znalezionBledy += "Ulica nie moze byc nullem. ";
    }

    if (numerDomu <= 0) {
      znalezionBledy += "Numer domu musi byc liczba dodatnia. ";
    }

    if (kodPocztowy == null) {
      znalezionBledy += "Kod pocztowy nie moze byc nullem. ";
    }

    if (miasto == null) {
      znalezionBledy += "Miasto nie moze byc nullem.";
    }

    if (!znalezionBledy.equals("")) {
      throw new NieprawidlowyAdresException(znalezionBledy);
    }

    this.ulica = ulica;
    this.numerDomu = numerDomu;
    this.kodPocztowy = kodPocztowy;
    this.miasto = miasto;
  }

  public static void main(String[] args) {
    try {
      Adres adres = new Adres("Jasna", 1, "05-025", "Warszawa");
      System.out.println("Obiekt typu Adres utworzony.");
    } catch (NieprawidlowyAdresException e) {
      System.out.println("Blad tworzenia adresu: " + e.getMessage());
    }

    try {
      Adres adres = new Adres("Koszykowa", -5, null, "Warszawa");
    } catch (NieprawidlowyAdresException e) {
      System.out.println("Blad tworzenia adresu: " + e.getMessage());
    }

    try {
      Adres adres = new Adres(null, 0, null, null);
    } catch (NieprawidlowyAdresException e) {
      System.out.println("Blad tworzenia adresu: " + e.getMessage());
    }
  }
}

Wynik działania tego programu jest następujący:

Obiekt typu Adres utworzony. Blad tworzenia adresu: Numer domu musi byc liczba dodatnia. Kod pocztowy nie moze byc nullem. Blad tworzenia adresu: Ulica nie moze byc nullem. Numer domu musi byc liczba dodatnia. Kod pocztowy nie moze byc nullem. Miasto nie moze byc nullem.

Liczba znaków w pliku

Skorzystaj z try-with-resources w programie, które pobierze od użytkownika lokalizację pliku z rozszerzeniem .txt na dysku, a następnie wypisze na ekran liczbę znaków, z których składa się ten plik. Weź pod uwagę, że podany przez użytkownik plik może nie istnieć lub być plikiem o innym rozszerzeniu. Skorzystaj z klas File oraz FileReader.

Uwaga: wynik liczenia znaków nie będzie się zgadzał z liczbą widocznych w pliku znaków. Na końcu każdej linii, po której następuje kolejna linia, znajdują się (w systemie Windows) dwa dodatkowe znaki końca linii. Weź to pod uwagę testując swój program (w systemie Linux będzie to jeden dodatkowy znak na linię). Pamiętaj także, że spacje i tabulatory to także znaki!

W tym programie możemy skorzystać z kodu przykładu z rozdziału o mechanizmie try-with-resources. W programie nie musimy wypisywać odczytanych z pliku znaków, a jedynie je policzyć – wystarczy więc inkrementować zmienną zliczającą znaki. Będziemy to robić tak długo, aż metoda read obiektu klasy FileReader, z której skorzystamy, nie zwróci liczby -1 świadczącej o odczytaniu już całego pliku.

Program będzie zawierał znaną już metodę getString, pobierającą od użytkownika tekst, a także metodę pobierzLokalizacjePliku, która w pętli będzie próbować pobrać od użytkownika taki tekst, który kończy się na .txt (bo takiego pliku oczekujemy).

Odczytywanie pliku ujmiemy w try-with-resources, a o potencjalnych błędach poinformujemy użytkownika w odpowiednich sekcjach catch:

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

public class LiczbaZnakowWPliku {
  public static void main(String[] args) {
    File f = new File(pobierzLokalizacjePliku());
    int liczbaZnakow = 0;

    try (FileReader fileReader = new FileReader(f)) {
      while (fileReader.read() != -1) {
        liczbaZnakow++;
      }

      System.out.println("Liczba znakow: " + liczbaZnakow);
    } catch(FileNotFoundException e) {
      System.out.println("Podany plik nie istnieje!");
    } catch (IOException e) {
      System.out.println(
          "Wystapil blad podczas odczytywania pliku: " + e.getMessage()
      );
    }
  }

  public static String pobierzLokalizacjePliku() {
    String sciezkaDoPliku;

    while (true) {
      System.out.print("Podaj lokalizacje pliku txt na dysku: ");
      sciezkaDoPliku = getString();

      if (!sciezkaDoPliku.endsWith(".txt")) {
        System.out.println("Zle rozszerzenie pliku!");
      } else {
        return sciezkaDoPliku;
      }
    }
  }

  public static String getString() {
    return new Scanner(System.in).next();
  }
}

Dwa przykładowe uruchomienia tego programu:

Podaj lokalizacje pliku txt na dysku: C:/pewienPlik Zle rozszerzenie pliku! Podaj lokalizacje pliku txt na dysku: C:/pewienPlik.txt Liczba znakow: 36
Podaj lokalizacje pliku txt na dysku: C:/nieistniejacyPlik.txt Podany plik nie istnieje!

Implementacja stosu

Stos to rodzaj kolekcji do przechowywania danych typu FILO – First In, Last Out. Gdy dodajemy na stos kilka elementów, to mamy jedynie dostęp do tego ostatniego. Jeżeli chcemy odnieść się do pierwszego dodanego na stos elementu, musimy najpierw "zdjąć" elementy dodane na stos po nim – stąd określenie First In, Last Out. Stos można porównać do kartek położonych jedna na drugiej – jeżeli chcemy kartkę ze spodu stosu, to musimy najpierw zdjąć kartki leżące na niej.

Napisz klasę Stack (w opisie poniżej nazywaną stosem). Ma ona za zadanie przechowywać liczby typu int. Klasa Stack powinna posiadać:

  1. Konstruktor, który przyjmuje jako argument liczbę typu int – maksymalną liczbę elementów, które ten stos może przechowywać. Jeżeli podamy ujemną liczbę, powinien zostać rzucony wyjątek IllegalArgumentException.
  2. Metody:
    1. push – dodaje przekazaną jako argument liczbę typu int do stosu, jeżeli jest w nim jeszcze miejsce – jeżeli nie, rzuca nowy zdefiniowany wyjątek rodzaju Unchecked o nazwie StackFullException,
    2. pop – usuwa ze stosu ostatnio dodany element i zwraca go – jeżeli stos był pusty, rzuca nowy zdefiniowany wyjątek rodzaju Unchecked o nazwie StackEmptyException,
    3. clear – czyści stos,
    4. top – zwraca ostatnio dodany do stosu element – jeżeli stos był pusty, rzuca wyjątek typu StackEmptyException,
    5. size – zwraca liczbę elementów aktualnie przechowywanych w stosie.

Utwórz kilka obiektów typu Stack i przetestuj ich działanie. Obsłuż w try..catch potencjalne wyjątki.

Klasa Stack to ciekawe zadanie, ponieważ pomimo prostoty tej struktury danych, trzeba wziąć pod uwagę kilka rzeczy. W pierwszej kolejności musimy zastanowić się jak przechowywać dane – w tym celu możemy skorzystać z tablicy liczb typu int, którą nazwiemy values.

W konstruktorze będziemy tworzyć tablicę values o rozmiarze przekazanym jako argument konstruktora. Najpierw jednak sprawdzimy, czy liczba ta nie jest ujemna – w takim przypadku rzucimy wyjątek IllegalArgumentException.

Musimy także w jakiś sposób zapisywać informację o liczbie już przechowywanych na stosie elementów – będzie ona nam potrzebna w każdej z pozostałych metod klasy Stack.

Aktualny rozmiar stosu będziemy zapisywać w polu o nazwie currentSize, które będzie miało wartość (domyślną) zero dla każdego nowego obiektu typu Stack.

Następnie, będziemy odpowiednio operować tym polem w każdej z metod:

  • size – po prostu zwracamy wartość pola currentSize.
  • clear – ustawiamy currentSize na 0 (nie musimy "zerować" tablicy z danymi, bo przechowujemy liczby typu prymitywnego – jeżeli nasz stos przechowywałby obiekty pewnej klasy, to powinniśmy całą tablicę "wynullować", aby Garbage Collector wiedział, że przechowywane tam wcześniej obiekty nie są już potrzebne i powinny zwolnić pamięć).
  • push – dodajemy do tablicy values element przekazany jako argument – indeks, pod którym wstawiamy element, to aktualna wartość pola currentSize. Wstawiając element, dodatkowo zwiększymy wartość przechowywaną w polu currentSize. Dla przykładu, jeżeli stos jest pusty i wywołamy metodę push z argumentem 5, to wstawimy tą wartość do elementu tablicy values[0], ponieważ currentSize wynosi 0. Dodatkowo zwiększymy currentSize z 0 do 1. Na początku metody push musimy jeszcze sprawdzić, czy liczba aktualnie przechowywanych elementów nie jest już równa rozmiarowi tablicy – będzie to oznaczało, że stos jest pełny i nie możemy już dodać do niego nowych elementów – zamiast tego rzucimy wyjątek StackFullException.
  • pop – zwracamy aktualny element i zmniejszamy currentSize o 1. Poprzednio dodany element znajduje się zawsze w elemencie tablicy o indeksie mniejszym o 1 od wartości currentSize, dlatego korzystamy z operatora predekrementacji w metodzie pop, co zobaczysz w kodzie poniżej. Jeżeli currentSize jest równy zero, czyli stos jest pusty, to zamist tego rzucimy wyjątek StackEmptyException.
  • top – zwracamy ostatnio dodaną do stosu wartość (tą na "szczycie" stosu) – ponieważ tablice indeksujemy od 0, od wartości currentSize musimy odjąć 1, ponieważ gdy w stosie będzie przechowywany jeden element, to będzie on w elemencie values[0], a currentSize będzie wtedy miało wartość 1. Jeżeli jednak currentSize wynosi 0, czyli stos jest pusty, to rzucimy wyjątek StackEmptyException.

Implementacja klasy Stack mogłaby wyglądać następująco – w ramach treningu przeanalizuj poniższy kod:

class StackEmptyException extends RuntimeException {}
class StackFullException extends RuntimeException {}

public class Stack {
  private int[] values;
  private int currentSize;

  public Stack(int size) {
    if (size < 0) {
      throw new IllegalArgumentException("size musi byc >= 0");
    }
    this.values = new int[size];
  }

  public void push(int value) {
    if (currentSize == values.length) {
      throw new StackFullException();
    }
    values[currentSize++] = value;
  }

  public int pop() {
    if (currentSize == 0) {
      throw new StackEmptyException();
    }
    return values[--currentSize];
  }

  public int top() {
    if (currentSize == 0) {
      throw new StackEmptyException();
    }
    return values[currentSize - 1];
  }

  public int size() {
    return currentSize;
  }

  public void clear() {
    currentSize = 0;
  }
}

Klasę Stack używamy w osobnej klasie StackUzycie:

public class StackUzycie {
  public static void main(String[] args) {
    try {
      Stack s = new Stack(-1);
    } catch (Exception e) {
      e.printStackTrace();
    }

    try {
      Stack s = new Stack(0);
      s.push(1);
    } catch (Exception e) {
      e.printStackTrace();
    }

    try {
      Stack s = new Stack(1);
      s.push(1);
      System.out.println("Rozmiar: " + s.size());
      System.out.println("Top: " + s.top());
      System.out.println("Wartosc: " + s.pop());
      System.out.println("Rozmiar: " + s.size());
      System.out.println("Top: " + s.top());
    } catch (Exception e) {
      e.printStackTrace();
    }

    try {
      Stack s = new Stack(3);
      s.push(5);
      s.push(10);
      s.push(15);
      s.clear();
      s.push(20);
      System.out.println("Wartosc: " + s.pop());
      System.out.println("Top: " + s.top());
    } catch (Exception e) {
      e.printStackTrace();
    }

    try {
      Stack s = new Stack(5);
      s.push(10);
      s.push(200);
      s.push(3000);
      s.push(40000);
      s.push(500000);
      System.out.println("Rozmiar: " + s.size());
      System.out.println("Top: " + s.top());
      System.out.println("Wartosc: " + s.pop());
      System.out.println("Top: " + s.top());
      s.push(5);
      System.out.println("Wartosc: " + s.pop());
      System.out.println("Wartosc: " + s.pop());
      System.out.println("Wartosc: " + s.pop());
      System.out.println("Top: " + s.top());
    } catch (Exception e) {
      e.printStackTrace();
    }
  }
}

Wynik działania klasy StackUzycie jest następujący:

java.lang.IllegalArgumentException: size musi byc >= 0 at wyjatki.Stack.(Stack.java:12) at wyjatki.StackUzycie.main(StackUzycie.java:4) wyjatki.StackFullException at wyjatki.Stack.push(Stack.java:19) at wyjatki.StackUzycie.main(StackUzycie.java:11) Rozmiar: 1 Top: 1 Wartosc: 1 Rozmiar: 0 wyjatki.StackEmptyException at wyjatki.Stack.top(Stack.java:33) at wyjatki.StackUzycie.main(StackUzycie.java:23) Wartosc: 20 wyjatki.StackEmptyException at wyjatki.Stack.top(Stack.java:33) at wyjatki.StackUzycie.main(StackUzycie.java:36) Rozmiar: 5 Top: 500000 Wartosc: 500000 Top: 40000 Wartosc: 5 Wartosc: 40000 Wartosc: 3000 Top: 200

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Nie musisz podawać swojego imienia, e-mailu, ani strony www, aby opublikować komentarz. Komentarze muszą zostać zatwierdzone, aby były widoczne na stronie.