Pętle i przepływ sterowania w Javie

Poprzednio omówiliśmy ważne tematy, którymi są tablice i klasa String.

W tym wpisie przyjrzymy się mechanizmom, które pozwalają na przetwarzanie dużych ilości danych oraz instrukcjom sterującym. Kluczowym pytaniem, na które będziemy musieli odpowiedzieć brzmi – czym jest prawda?

To pytanie z pewnością bardzo trudne z punktu widzenia filozofii i życia codziennego. Podczas różnych dyskusji i debat każdy pokazuje, że to on ma rację próbując przekonać do niej swojego oponenta. Na szczęście w przypadku komputera prawda ściśle opiera się na algebrze Boole’a. Poniżej znajdują się jej podstawowe właściwości oraz definicja formalna.

Algebra Boole’a

Niech B = {B, ∩, ∪, 0, 1, ¬} wtedy ∀ a, b, c ∈ B, wtedy:

  • Łączność: a ∪ (b ∪ c) = (a ∪ b) ∪ c  oraz a ∩ (b ∩ c) = (a ∩ b) ∩ c
  • Przemienność: a ∪ b = b ∪ a oraz a ∩ b = b ∩ a
  • Absorpcja: a ∪ (a ∩ b) = a oraz a ∩ (a ∪ b) = a
  • Rozdzielność: a ∪ (a ∩ c) = (a ∪ b) ∩ (a ∪ c)
  • Rozdzielność: a ∩ (a ∪ c) = (a ∩ b) ∪ (a ∩ c)
  • Pochłanianie: – a ∪ ¬a = 1 oraz a ∩ ¬a = 0

Każda z właściwości algebry Boole`a pomaga budować wyrażenia logiczne i optymalizować je pod względem wydajności oraz przejrzystości kodu.

Łączność oraz przemienność działań w powyższej strukturze pozwala na zachowanie kolejności, która wynika z logiki programu.

Absorpcja pozwala na zmniejszenie liczby porównań gdy tylko zauważymy powyższą zależność.

Działanie są rozdzielne zarówno względem iloczynu i sumy (co różni się od powszechnie znanej zależności z arytmetyki liczb rzeczywistych, w których istnieje jedynie rozdzielność mnożenia względem dodawania).

Pochłanianie pozwala zrozumieć podstawowe właściwości logiki czyli koniunkcją prawdy i fałszu jest fałsz, natomiast alternatywą prawy i fałszu jest prawda.

Własności

Poniżej przedstawimy właściwości, które wynikają z powyższej definicji.

  • Suma:
    • a ∪ a = a
    • a ∪ 1 = 1
  • Przecięcie:
    • a ∩ a = a
  • Przecięcie ze stałymi:
    • a ∩ 1 = a
    • a ∩ 0 = 0
  • Negacja:
    • ¬0 = 1
    • ¬1 = 0
    • ¬(¬a) = a

Z powyższych własności wynika, że:

  • jeśli ∩ jest działaniem dwuargumentowym, to wartość wyrażenia jest prawdą, wtedy i tylko wtedy, gdy oba operandy iloczynu logicznego (koniunkcji) są prawdą.
  • jeśli ∪ jest działaniem dwuargumentowym, to wartość wyrażenia jest prawdą, jeżeli, co najmniej jeden operand sumy logicznej (alternatywy) jest prawdą.
  • Zaprzeczenie wartości logicznej jest zawsze wartością przeciwną.

W Javie wartości logiczne zawsze są typu boolean w związku z tym nie istnieje problem znany z języków z rodziny C, w którym operator przypisania zawsze zwraca true.

Operatory logiczne

Przy okazji omawiania podstawowych typów danych Poznaliśmy niezbędne operatory do budowania wyrażeń relacyjnych. Poniżej przypomnimy te, które okażą się przydatne w tym celu.

  1. () – Nawiasy – powodują zwiększenie priorytetu wykonania instrukcji wewnątrz
  2. !  Operator negacji – zmienia wartość logiczną wyrażenia na przeciwną
  3. Relacje – operatory porównania
    • (mniejszy)
    • <= (mniejszy lub równy)
    • > (większy)
    • >= (większy lub równy)
    • ==
    • !=
  4. Funktory logiczne
    • && iloczyn logiczny
    • || suma logiczna
  5. ? operator warunkowy

Powyższa kolejność jest pokazana zgodnie z priorytetem ustalonym przez maszynę wirtualną Javy. Dlatego budując wyrażenie należy uważać, gdyż wynik operacji może różnić się od naszych oczekiwań.

Tabelki prawdy

Poniższe tabele pokazują absolutne podstawy, które należy zapamiętać, aby móc tworzyć wyrażenia logiczne.

Tabela prawdy - Koniunkcja (And)

AND01
000
101

Tabela prawdy - Alternatywa (OR)

OR01
001
111

Pętle

Z powyższym warsztatem możemy rozpocząć właściwą część wpisu. Wszystkie rodzaje pętli powodują wykonanie danego kodu wielokrotnie – dopóki przy sprawdzeniu warunków uruchomienia pętli zostaje zwrócona prawda. W Javie wyróżniamy kilka rodzajów pętli, które wymienimy i opiszemy ich zastosowania.

Pętla while

Postać ogólna pętli wygląda następująco.

Instrukcje znajdujące się w pętli wykonują się tak długo jak warunek jest spełniony (jego wartość to true). Co jednak gdy przy pierwszym sprawdzeniu warunek wejścia do pętli jest fałszywy? Wtedy pętla nie wykona się ani razu. Poniżej znajduje się rysunek, który pokazuje przepływ sterowania.

Pętla while Java
Przepływ sterowania w pętli while Java

Pętla do while

Jeżeli chcemy mieć gwarancję, że pętla wykona się co najmniej raz należy skorzystać z konstrukcji tej pętli.

Pętla ta w zasadzie nie różni się od poprzedniej, natomiast najpierw wykonywana jest instrukcja, a dopiero potem sprawdzany jest warunek czy wykonać ją ponownie. Poniżej przepływ danych w pętli do while.

do while Java
Przepływ pętli do while w Javie

Pętla for

Pętla ta wykonuje się określoną liczbę razy. Jej ogólna postać wygląda następująco.

Pętla for w Javie
Przepływ pętli for w Javie

Pętla for (each) – dla każdego elementu w kolekcji

Pętla, która jest opracowana specjalnie do pracy na tablicach i kolekcjach. Nie potrzebuje do swojego działania indeksów, w związku z tym zdejmuje programistę z obowiązku pilnowania czy indeks nie przekroczył rozmiaru kolekcji lub tablicy.

Kolekcja znajdująca się w powyższym kodzie musi być tablicą lub obiektem klasy implementującą interfejs Iterable. W porównaniu do poprzednich pętli for posiada jasno zdefiniowane działania w swojej składni, z których należy korzystać.

Ogólnie o pętlach

W każdym rodzaju pętli (poza specjalnym typem do pracy z kolekcjami) posiadają warunek, który sprawdza czy instrukcje w pętli mają zostać wykonane. Przy jego konstruowaniu należy uważać, aby przez przypadek nie utworzyć pętli nieskończonej, czyli takiej, w której warunek zawsze zwraca wartość true. Warunek może być złożony z kilku instrukcji – byle zwracały wartość logiczną (typ boolean).

Warunki te można konstruować przy pomocy operatorów relacyjnych (&& w przypadku iloczynu logicznego – koniunkcji, || w przypadku alternatywy czyli sumy logicznej). Należy również uważać na zasięg działania pętli. Pokażemy to na przykładzie pętli while.

W tym przypadku pętla nigdy się nie skończy gdyż instrukcja i++ jest poza zasięgiem pętli while. Aby poprawić ten defekt należy umieścić instrukcje w bloku klamrowym.

Instrukcje warunkowe

W Javie podobnie jak w wielu innych językach programowania mamy do dyspozycji dwie instrukcje warunkowe – if i switch. Ze względu na różne zastosowania warto poznać je obie.

Instrukcja if

Z if powinniśmy korzystać gdy:

  • jest mało warunków do sprawdzenia np. jeden lub dwa,
  • zbiór danych porównawczych nie jest dyskretny np. rozważamy zbiór liczb rzeczywistych, a nie naturalnych,
  • porównanie nie jest relacją „równa się” (np. i >= 5).

Warianty if

Instrukcja warunkowa if występuje w kilku wariantach. Każda instrukcja może znajdować się w bloku. W przeciwnym wypadku tylko pierwsza zostanie wykonana gdy zostanie spełniony warunek, a następna zawsze. Poniżej przedstawimy przepływy sterowania dla różnych kombinacji powyższych instrukcji.

Prosta instrukcja if

Jeżeli do wykonania jest tylko jedna instrukcja można użyć instrukcji if bez bloku. Jednakże ze względu na czytelność kodu polecamy stosować ten zapis tylko wtedy, gdy cały kod zmieści się w jednej linii. Poniżej znajduje się schemat blokowy instrukcji if.

Instrukcję if można rozumieć w następujący sposób: „jeśli warunek zostanie spełniony, to wykonaj kod instrukcja, a jeśli nie, to go pomiń.

instrukcja if Java
Przepływ sterowania instrukcji if

Kilka instrukcji w bloku if

W tym przypadku nie mamy wyboru i musimy zawrzeć wszystkie instrukcje w bloku. Poniżej pokazujemy przepływ sterowania.

if blok instrukcji
Przepływ sterowania w bloku if

Konstrukcja if-else

W sytuacji, gdy w przypadku zwrócenia false należy podjąć inne działanie niezbędne jest skorzystanie z instrukcji else. Ponownie zachęcamy do stosowania składni blokowej. Poniżej znajduje się przepływ sterowania.

if else
Przepływ sterowania if-else

Konstrukcję if-else należy odczytywać następująco: „jeżeli warunek zostanie spełniony, to wykonaj kod instrukcja 1, a jeśli nie, to wykonaj kod instrukcja 2.

Konstrukcja if-else-if

Powyższe warunki będą sprawdzane po kolei. Dlatego należy zachować odpowiednią kolejność przy budowaniu kolejnych warunków. Dlatego ogólnie powinno budować warunki od najbardziej szczegółowego. Poniżej znajduje się schemat blokowy instrukcji if z wieloma warunkami.

if elseif Java
Przepływ sterowania if-else-if

Tę konstrukcję if-else-if należy rozumieć w następujący sposób: „Jeżeli warunek 1 zostanie spełniony to wykonaj instrukcję 1. W przeciwnym wypadku sprawdź warunek 2. Jeżeli jest on prawdziwy, to wykonaj instrukcję 2. W przeciwnym wypadku wykonaj instrukcję 3„.

Niektóre sytuacje mogą wymagać użycia oddzielnej instrukcji if.

Zagnieżdżony if

Instrukcje warunkowe mogą zostać zagnieżdżone w sobie. Niektóre sytuacje wymagają takiego rozwiązania. Poniżej znajduje się przykładowy kod.

W tym przypadku najpierw sprawdzany jest warunek 1. Dopiero gdy jego wynikiem jest true, sprawdzany jest warunek 2.

Instrukcja switch

W przypadku gdy:

  • mamy do sprawdzenia wiele warunków,
  • argumenty do sprawdzenia są dyskretne (np. liczby naturalne, łańcuch znaków, typ wyliczeniowy enum),

można rozważyć użycie instrukcji switch. Jej użycie bywa kłopotliwe, gdyż trzeba pamiętać o użyciu słowa kluczowego break po wpisaniu każdej z opcji. W przypadku gdy chcemy obsłużyć przypadek, że żadna z etykiet nie została dopasowana należy napisać default. Ogólna postać instrukcji switch wygląda następująco.

Powyższy przykład korzysta ze zmienne wyliczeniowej, która nie wymaga używania wywołania Pizza.Mala – wystarczy napisać samą etykietę Mala. Pozostałe możliwości to porównywanie łańcuchów lub liczb.

Poniżej diagram przepływu na przykładzie zmiennej wyliczeniowej pizza.

switch Java
Przepływ sterowania switch w Javie

Przy każdej etykiecie case i default powinna znaleźć się instrukcja break – w przeciwnym wypadku zamiast wybrać jedną możliwość program przejdzie do wykonania następnej etykiety (tak jakby w ogóle tej instrukcji nie było).

W następnej części przyjrzymy się paradygmatowi programowania obiektowego.