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 (...) {
....
}
|