« poprzedni punkt  następny punkt »

4. Trochę więcej o operatorach i wyrażeniach

Ze zmiennych, stałych, literałów oraz wywołań metod (funkcji), posługując się operatorami języka i nawiasami możemy konstruować wyrażenia.

Jeśli a i b są nazwami zmiennych typu numerycznego, to wyrażeniami będą np.:
a
1
b + 1
a * (b + 10)
'a'
"ala ma kota"

Wyrażenia są opracowywane (wyliczane), a ich wyniki mogą być w różny sposób wykorzystane (np. w przypisaniu, jako argumenty innych operatorów, w instrukcjach sterujących wykonaniem programu, w wywołaniach metod).
Np. jeśli zmienna a ma wartość 1, zmienna b - wartośc 2, to:

Wyrażenie
Wynik
a
wartość zmiennej a ( =1 )
1
wartość literału 1 ( = 1)
a * (b + 10)
12
'a'
znak a (dziesiętnie liczba 91, która jest kodem znaku a)

Kolejność wyliczeń zależy od priorytetów i wiązań operatorów użytych w wyrażeniach.

Priorytety mówią o tym, w jakiej kolejności będą wykonywane różne operacje zawarte w tym samym wyrażeniu (np. czy w wyrażeniu a + b * c najpierw będzie wykonywane mnożenie czy dodawanie).

Wiązania określają kolejność wykonywania operacji o tym samym priorytecie - czy są wykonywane od lewej strony wyrażenia czy od prawej (np. czy wyrażenie a + b - c będzie traktowane jako (a+b)-c czy jako (b-c)+a - co nie zawsze - chociaż nie w tym przypadku - jest kwestią obojętną !).

Jak pamiętamy, operatory mają argumenty, inaczej też zwane operandami.
W Javie są operatory jedno- i dwu- argumentowe oraz jeden operator trzyargumentowy.
Argumentami operatorów zawsze są wyrażenia. Np. w wyrażeniu
a + (b - 1)
argumentami dwuargumentowego operatora + są wyrażenia a i (b-1),
natomiast w wyrażeniu b-1 argumentami dwuargumentowego operatora - (minus) są wyrażenia b i 1.

W Javie zdefiniowano następujące operatory.

Priorytet,Wiązanie
Operator Nazwa
1, prawe ! Negacja logiczna
~ Bitowe uzupełnienie do 1
+ Jednoargumentowy + (liczba dodatnia)
- Jednoargumentowy - (liczba ujemna)
++ Operator zwiększania
-- Operator zmniejszania
(typ) konwersja (rzutowanie)
2, lewe * mnożenie
/ dzielenie
% reszta z dzielenia
3, lewe + Dodawanie
- Odejmowanie
4, lewe << Przesunięcie bitowe w lewo
>> Przesunęcie bitowe w prawo
>>> Przesunęcie bitowe w prawo bez znaku
5, lewe <

operatory relacyjne

<=
>=
>
instanceof stwierdzenie typu
6, lewe == Operatory równości - nierówności
!=
7. & Bitowa koniunkcja 
8. ^ Bitowe wyłączające ALBO
9. | Bitowa alternatywa
10. && Logiczna koniunkcja
11. || Logiczna alternatywa
12, prawe ?: Operator warunku (trójargumentowy)
13, prawe =
=*
=/
=%
=+
=-
=&
=^
=|
=<<
=>>

Operatory przypisania

Uwagi:  
mniejsza liczba w kolumnie priorytetu oznacza wyższy priorytet
niebieski kolor tła - operatory jednoargumentowe, biały - dwu

Nie będziemy teraz omawiać wszystkich tych operatorów, po części uwagi na temat niektórych z nich były już wypowiedziane, o innych będziemy się dowiadywać sukcesywnie.

Kilka uwag na tle przedstawionej tablicy warto jednak sformułować.

Zauważmy najpierw, że operatory przypisania jako jedyne wśród operatorów dwuargumentowych mają wiązanie prawostronne. Oznacza to, że wyrażenie:

x = a + b

opracowywane jest od prawej do lewej. W takim wyrażeniu nie ma to istotnego znaczenia, ale jeśli uwzględnimy to, że samo przypisanie jest wyrażeniem o wartości równej jego lewej stronie (ktora zawsze musi być zmienną) po wykonaniu przypisania, to np. w takiej sekwencji:

int x, a = 3, b = 4;
int c = 1;
x = a = b = c + 7

po pierwsze, priorytet operatora + jest wyższy od priorytetu operatora =, wobec tego
najpierw zostanie wyliczone wyrażenie c + 7, jego wartość zostanie podstawiona na zmienną b, wartością wyrażenia b = c + 7 będzie teraz ta właśnie wartość (8) i kolejno - przesuwając się do lewej strony całego wyrażenia będzie ona podstawiona na zmienne a i x. W rezultacie zmienne x, a, b będą mialy taką samą wartość 8.

Jak widzimy w tablicy operatorów, występuje cała grupa tzw. złożonych operatorów przypisania w postaci:

                op=

gdzie op - to jeden z operatorów

 *        /        %        +        -        <<    >>    >>>        &        ^        |

Złożone operatory przypisania, stosowane w następujący sposób:

            x op= wyrażenie

są wygodną formą skrócenia zapisu:

            x = x op ( wyrażenie )

gdzie:
x - dowolna zmienna
wyrażenie - dowolne wyrażenie
op - symbol operatora

Na przykład, zamiast:

numOfChildren = numOfChildren + 1

możemy napisac:

numOfChildren += 1

Wśród operatorów arytmetycznych szczególną rolę odgrywają jednoargumentowe  operatory zwiększania (++) i zmniejszania (--). Oba występują w dwóch postaciach:

  • przyrostkowej (operator po argumencie - zmiennej)
  • przedrostkowej (operator przed argumentem - zmienną)

Przy czym :

  • ++ zwiększa, a -- zmniejsza o jeden wartość argumentu (zmiennej),
  • przyrostkowa forma operatorów (znak operatora po argumencie) modyfikuje wartość argumentu po jej wykorzystaniu w wyrażeniu, przedrostkowa (znak operatora przed argumentem) - przed wykorzystaniem tej wartości.

Na przykład zapis:

int n, i = 1;
n = i++;      // przyrostkowa forma operatora ++

interpretowany jest w następujący sposób: zmienna i zostanie zwiększona o 1, ale zmiennej n zostanie nadana wartość zmiennej i sprzed zwiększenia, czyli po wykonaniu obu instrukcji: n będzie równe 1 , a  i  będzie równe 2.

Natomiast:
int n, i = 1;
n = ++i;      // przedrostkowa forma operatora ++

interpretowany jest w następujący sposób: zmienna i zostanie zwięksozna o 1i ta nowa jej wartość zostanie przypisana zmiennej n,  czyli po wykonaniu obu instrukcji: n = 2 , i = 2.

Oczywiście, jeśli jedynym wyrażeniem w instrukcji jest wyrażenie zwiększania (lub zmniejszania), to rozróżnienie przyrostkowej i przedrostkowej form tych operatorów nie ma znaczenia:

i++;
znaczy to samo, co
++i;
oraz
i = i + 1

Gdy nie jesteśmy absolutnie pewni kolejności opracowania wyrażeń, należy unikać stosowania operatorów ++ oraz -- w wyrażeniach, w których występują inne wyrażenia.

Zamiast pisać:

if (n++ > 2) ...;

lepiej jest zawsze napisać:

n++;
if (n > 2) ...

Na pewno zaś nigdy nie należy pisać (choć jest to dopuszczalne), czegoś w rodzaju:

x = i++*++j;

Pełniejszemu zrozumieniu przedstawionych uwag może posłużyć analiza działania poniższego programu.

public class Express1 {

  public static void main(String[] args) {

    int a = 1, b = 2, c = 3;
    a = b = c * 1 + 2;
    System.out.println("a=" + a + " b=" + b + " c=" + c);
    a = b = c * (1 + 2);
    System.out.println("a=" + a + " b=" + b + " c=" + c);
    a = b++;
    System.out.println("a=" + a + " b=" + b + " c=" + c);
    c = --b;
    System.out.println("a=" + a + " b=" + b + " c=" + c);
    a++;
    b++;
    c++;
    System.out.println("a=" + a + " b=" + b + " c=" + c);

    a = b++*++c;
    System.out.println("a=" + a + " b=" + b + " c=" + c);

    int DlugaNazwaZmiennej = 20;
    DlugaNazwaZmiennej = DlugaNazwaZmiennej * 10;
    DlugaNazwaZmiennej *= 10;
    System.out.println( DlugaNazwaZmiennej );

  }
}

Wydruk
a=5 b=5 c=3
a=9 b=9 c=3
a=9 b=10 c=3
a=9 b=9 c=9
a=10 b=10 c=10
a=110 b=11 c=11
2000

W programie tym stosujemy operator +  nie tylko do dodawania liczb. Służy on także konkatenacji (łączeniu łańcuchów znakowych). O szczegółach dowiemy się w następnym wykładzie.

Inne operatory arytmetyczne były już po części omówione. Jeszcze raz warto jednak podkreślić, że dzielenie dla liczb całkowitych odrzuca ułamkową część wyniku.

Np.
int a,b,c;
b = 1; c = 10; a = b/c;   // a = 0, bo "prawdziwy" wynik dzielenia jest 0 i 1/10
a = 19/10;                    // a = 1, bo "prawdziwy" wynik dzielenia jest 1 i 9/10

Ogólnie więc, gdy b i c są zmiennymi typu int nie spelniona jest intuicyjna zależność matematyczna:

b  równa się c razy b dzielone przez c

Będzie ona zachodzić na pewno wtedy, gdy b dzieli się bez reszty przez c
Jeśli b nie dzieli się bez reszty przez c, to możemy otrzymać "prawdziwy" wynik gdy iloczyn c*b dzieli się bez reszty przez c.

Np.

b = 30; c = 20; b = c*b/c;    //  b = 30 

ale:

b = 30; c = 20; b = c*(b/c);  // b = 20 !

Tutaj widzimy jak nawiasy zmieniają kolejność obliczania wartości wyrażenia.
Wiązanie operatorów * i / jest lewostronne i w pierwszym przypadku cała operacja
wykonuje się w następujący sposób:
c*b daje 600 co dzielone przez c = 20 daje równo 30.

W drugim przypadku najpierw wykonane zostanie dzielenie w nawiasach (30/20 da 1), a wynik pomnożony przez wartość c (20) da 20.

Wbrew pozorom "dziwne" dzielenie całkowitoliczbowe może być użyteczne, szczególnie wraz z zastosowaniem operatora reszty z dzielenia.
Rozważmy przykład.
Wyobraźmy sobie, że musimy wydrukować szablon jakiejś numerowanej listy zawierającej posNum pozycji. Na każdej stronie wydruku ma być umieszczone maksymalnie posPP pozycji, a nasz program ma umieścić na niej kolejne numery pozycji, zwiększające się w sposób ciągły od 1 do numPos. Każda nowa strona wydruku (oprócz pierwszej) winna zawierać nagłówek w postaci "Strona i z n", gdzie i - numer bieżącej strony, a n - liczba wszystkich stron.

Zadanie to można łatwo oprogramować stosując operatory dzielenia całkowitoliczbowego oraz reszty z dzielenia.
Liczba pełnych (czyli zawierających określoną maksymalną liczbę pozycji na stronie - posPP) stron wydruku będzie równa  posNum/posPP. Jeżeli reszta z dzielenia posNum przez posPP nie jest równa 0, znaczy to, że sumaryczna liczba stron będzie o 1 większa od liczby pełnych stron (na tej dodatkowej stronie wyprowadzone zostaną ostatnie numery listy, ich liczba będzie oczywiście mniejsza o posPP). W ten sposób określimy informację o liczbie wszystkich stron niezbędną w nagłówku każdej (oprócz pierwszej) strony.
Wypisując kolejne pozycje będziemy je numerować od 1 i przy każdym wyprowadzeniu zwiększać numer bieżącej wyprowadzanej pozycji o 1. Łatwo można określić moment rozpoczęcia kolejnej strony. Będzie on następował zawsze wtedy, gdy wyprowadzono kolejną porcję posPP pozycji, a zatem zawsze wtedy, gdy numer wyprowadzonej pozycji dzieli się bez reszty przez posPP.

public class Pages {

  public static void main(String[] args) {

    int posNum = 17;                       // liczba pozycji do wyprowadzenia
    int posPP = 5;                         // liczba pozycji na stronie

    int pageNum = posNum/posPP;            // liczba pelnych stron
    if (posNum % posPP != 0) pageNum++;    // i ew. dodatkowa

    int pageNr = 1;                        // bieżący numer strony
    int pos = 1;                           // bieżący numer pozycji

    while (pos <= posNum) {

      System.out.println(pos);

      if (pos % posPP == 0) {              // zaczyna się nowa strona
         pageNr++;
         System.out.println('\f' + "Strona " + pageNr + " z " + pageNum);
      }
      pos++;
    }

  }

}

Przypomnienie: znak '\f' jest znakiem sterującym "nowa strona". Dzięki jego wyprowadzeniu w nagłówku stron - po przekierowaniu standardowego strumienia wyjściowego na drukarkę - otrzymamy rzeczywiście podział listy na fizyczne strony.

W programie pojawia się kilka nowych elementów języka. Będą one szczegółowo omawiane dalej, teraz dla zrozumienia działanie programu wystarczy odnieść się do znanych nam instrukcji uproszczonego REXXa z wykładu 3:
  • operator "czy nie równe" zapisujemy w Javie jako !=
  • operator "czy równe" zapisujemy jako ==
  • zuważmy, że priorytet operatora % jest wyższy niż operatoró == i !=, dlatego w if nie musieliśmy stosować dodatkowych nawiasów
  • instrukcja while { ... } działa tak samo jak znana nam instrukcja do while ... end, w instrukcji if pomijamy słówko then, a instrukcja grupująca do ... end zapisywana jest jako { }


Zwróćmy uwagę na styl programowania. Stosujemy wcięcia dla wyróżnienia bloków instrukcji grupujących. Otwierający nawias klamrowy instrukcji grupującej w if i while umieszczamy w tym samym wierszu co instrukcja sterująca (if lub while). Jest to tzw.wierszooszczędny ("linesaver") styl programowania, alternatywą dla niego jest umieszczenie otwierającego nawiasu klamrowego dokładnie pod if lub while. Zamykające nawiasy klamrowe umieszczamy zawsze w nowym wierszu dokładnie pod if lub while.
Zatem zamiast:

        do while (...) {
            ...
        end

piszemy:

        while (..) {
            ...
        }

a zamiast:

         if (...) then do
            ....
         end

piszemy
          
         if (...) {
            ....
        }


« poprzedni punkt  następny punkt »