Spis treści
Operatory arytmetyczne
Jedną z ważniejszych i oczywistych funkcjonalności języków programowania jest możliwość przeprowadzania operacji matematycznych umożliwiających dokonywanie obliczeń w ramach programu. Dla przypomnienia zestawmy sobie poniżej najczęściej używane operatory matematyczne i logiczne włącznie z charakterystyką ich działania.
- operator dodawania: +
- operator odejmowania: –
- operator mnożenia: *
- operator dzielenia: /
- operator reszty z dzielenia (modulo): %
Więcej funkcjonalności dotyczących operacji matematycznych możemy znaleźć w dedykowanej bibliotece Math, którą należy dodać do naszego programu jeśli zamierzamy jej użyć. Na poniższym przykładzie omówione są podstawowe operacje matematyczne.
public class NewMain {
public static void main(String[] args) {
int z, x=10, y=15;
double d;
z = x + y; // wynik 25
z = x - y; // wynik -5
z = x * y; // wynik 150
z = x / y; // wynik 0
z = y % x; // wynik 5
d = x / y; // wynik 0.0
}
}
Poza oczywistościami z użyciem typowych operatorów matematycznych, ciekawą obserwacją jest wynik dzielenia, gdzie obserwujemy w pierwszym dzieleniu wynik równy wartości 0. Jest to spowodowane faktem, że zmienne użyte w tych obliczeniach są typu całkowitego int, stąd wynik jest także sprowadzony (przekonwertowany) do takiego samego typu. Dzielenie rzeczywiste dałoby nam wartość 0,66666666(6)7, a tutaj po odcięciu części ułamkowej pozostaje nam wartość 0. Wydaje się, że aby uzyskać rzeczywisty wynik dzielenia to wystarczy użyć zmiennej rzeczywistej (zmiennoprzecinkowej). Tymczasem użycie zmiennej typu double daje nam wynik również równy 0. Takie zachowanie może być zaskakujące, ale należy pamiętać, że dzielimy dwie liczby całkowite, więc kompilator sugeruje się tym, żeby wynik był tego samego typu, co składowe operacji matematycznej. Aby poinformować go, że jednak oczekujemy wyniku innego niż zgodny z argumentami operacji należy dokonać operacji konwersji na „szerszy” typ. Jedną z możliwości jest tzw. rzutowanie (ang: cast) typów. Stosuje się je wtedy, gdy chcemy przekształcić jeden typ na inny. Tak przekształcony argument będzie już potraktowany jako posiadający inny typ i kompilator dostosuje wynik do „najszerszego” użytego typu (w tym przypadku double). Poniżej jeszcze raz przedstawiono na listingu operację dzielenia tym razem z rzutowaniem typu int na double.
public class NewMain {
public static void main(String[] args) {
int x=10, y=15;
double d;
d = (double)(x / y); // Przykład 1: wynik 0 - nieprawidłowe rzutowanie!
d = (double)x / (double)y; // Przykład 2: wynik 0,66667
d = (double)x / y; // Przykład 3: wynik 0,66667
d = x / (double)y; // Przykład 4: wynik 0,66667
}
}
Przedstawione wyżej sposoby rzutowania są prawidłowe za wyjątkiem pierwszego przykładu, co prawda nastąpiła konwersja na liczbę zmiennoprzecinkową, ale odbyło się to już po wyliczeniu wyniku operacji z użyciem argumentów całkowitych, stąd wynik będzie zmiennoprzecinkowym 0. Pozostałe przykłady są poprawne, z tym, że nie ma potrzeby używania dwu rzutowań jak w przykładzie drugim, gdyż kompilator dostosowuje wynik do „najszerszego” typu argumentu użytego w operacji. Takim typem jest double i wystarczy tylko raz się do niego odwołać rzutując jeden z argumentów na ten typ, tak jak na przykładzie 3 lub 4.
Oczywiście trzeba pamiętać, że w celu dokonania obliczeń nie musimy za każdym razem powoływać do życia zmiennych jeśli to niecelowe. Obliczenia możemy wykonać bezpośrednio, lub stworzyć bardziej złożone równanie. Należy tylko pamiętać zawsze o regule, że przypisujemy do lewej strony równania to co obliczamy po jego stronie prawej.
public class NewMain {
public static void main(String[] args) {
int x=10, y=15;
double d;
System.out.printf("%10d \n\r", 156+17-y/2);
System.out.printf("%10f \n\r", 156 + 17 - y /(double) 2);
d = 156 + 17 - y /(double) 2;
System.out.println(d);
}
}
W językach programowania typu Java możemy stosować także uproszczone operatory działań matematycznych, które znakomicie poprawiają czytelność kodu i upraszczają zapis i tak:
public class NewMain {
public static void main(String[] args) {
int a = 10;
double b = 14.56;
a += b; // jest tożsame: a = a+b
a -= b; // jest tożsame: a = a-b
a *= b; // jest tożsame: a = a*b
a /= b; // jest tożsame: a = a/b
a %= b; // jest tożsame: a = a%b
}
}
Innym rodzajem operatorów matematycznych są tzw. inkrementacja i dekrementacja. Umożliwiają one zmianę wartości zmiennej o 1 na plus lub na minus. W zależności od tego z której strony zmiennej je umieścimy są to tzw. pre-fiksowe lub post-fiksowe. Ich działanie polega na tym, że zmienna jest albo wpierw modyfikowana przez ten operator zanim zostanie użyta w szerszej operacji, albo dopiero po jej użyciu w szerszej operacji. Przykład przedstawiony jest poniżej:
public class NewMain {
public static void main(String[] args) {
int z, x=10;
double d=6.754;
z = x++;
System.out.println(z); //10
System.out.println(x); //11
z = ++x;
System.out.println(z); //12
System.out.println(x); //12
d--;
System.out.println(d); //5.754
}
}
Na powyższym przykładzie widać, że w pierwszej grupie wpierw została wartość zmiennej x przypisana do zmiennej z, a dopiero w drugiej fazie nastąpiła inkrementacja wartości x.
W drugiej grupie natomiast z uwagi na zastosowanie pre-fiksowej inkrementacji zmienna x została wpierw zmodyfikowana, a dopiero potem użyta w operacji przypisania do zmiennej z.
Potwierdzają to wyniki wyświetlone w ramach ćwiczenia.
Dodatkową informacją jest także to, że inkrementacja czy dekrementacja dotyczy również liczb zmiennoprzecinkowych, a także typu wyliczeniowego char. Nie można za to inkrementować czy dekrementować typu boolean i ciągów znakowych.
Istnieją również operatory logiczne, czyli koniunkcja && („i”) i alternatywa || („lub”). Koniunkcja w wyniku daje true tylko wtedy, gdy oba warunki, które nią łączymy są prawdą, natomiast alternatywa daje false tylko wtedy, gdy oba warunki będą false.
Są one przydatne w sytuacji, gdy chcemy żeby kilka warunków było (lub nie) spełnionych w tym samym czasie. Przykładowo jeśli będziemy musieli sprawdzić „czy liczba jest dodatnia i jednocześnie parzysta”. Działanie operatorów:
public class NewMain {
public static void main(String[] args) {
int a = 7;
int b = 2;
boolean result1 = a > b; //true, bo 7 większe od 2
boolean result2 = a < b; //false, bo 7 większe od 2
boolean result3 = a == b; //false, bo a jest różne od b
boolean koniunkcja1 = result1 && result3; //false, bo true&&false daje false
}
}
Operatory bitowe
Osobnym zagadnieniem są operacje bitowe. Java, jak i wiele innych języków programowania operuje przede wszystkim na bajtach. Operacje bitowe są jak najbardziej możliwe, ale wymagają operowania tzw maską bitową.
AND
10011101
&
11000000
———————————
10000000
Logiczna koniunkcja, operacja logiczna „i”. Powyższa operacja bitowa wymaga znajomości tablicy logicznej AND
| Liczba | Maska | Wynik |
| 0 | 0 | 0 |
| 0 | 1 | 0 |
| 1 | 0 | 0 |
| 1 | 1 | 1 |
Jak widać wynik jest równy 1 tylko i wyłącznie w przypadku, gdy na wejściach mamy obie 1. Przykład programowy realizujący operację logiczną AND na bitach może wyglądać tak:
int liczba=0b10011101;
int maska =0b11000000;
liczba &= maska;
System.out.println(Integer.toBinaryString(liczba));
Widać tutaj wprowadzanie do zmiennej typu int wartości w sposób binarny (dla większej czytelności kodu), ale jak najbardziej nic nie stoi na przeszkodzie aby użyć zapisu dziesiętnego, który jest tożsamy z powyższym, binarnym:
int liczba=157; //128+0+0+16+8+4+0+1
int maska =192; //128+64+0+0+0+0+0+0
liczba &= maska;
System.out.println(Integer.toBinaryString(liczba));
Jak widać można wprowadzać wartości w dowolny sposób, to o czym musimy pamiętać, to ich reprezentację w formie binarnej, oraz to, że int ma… 4 bajty, a ustawiliśmy tylko 8 bitów pierwszego, najmniej znaczącego bajtu. Reszta bitów przyjmuje domyślnie wartość 0.
W kodzie programu można wyróżnić operację logiczną AND zapisaną skrótowo:
liczba &= maska;
co jest oczywiście zapisem wyprowadzonym od formy:
liczba = liczba & maska;.
Drugą ciekawostką jest wypisanie liczby w postaci binarnej na konsolę. Metoda standardowa w System.out.printf(); nie ma formatowania do liczby binarnej więc należałoby stworzyć odpowiednią metodę konwertującą wartość zmiennej na jej zapis bitowy. Na szczęście istnieje już taka metoda we wspominanej już klasie osłonowej dla typu prostego int, czyli Integer.
OR
Logiczne „lub”.
10011101
OR
11000000
———————————
11011101
Operacja bitowa przedstawiona powyżej realizowana jest zgodnie z tabelę stanów logicznych umieszczoną poniżej:
| Liczba | Maska | Wynik |
| 0 | 0 | 0 |
| 0 | 1 | 1 |
| 1 | 0 | 1 |
| 1 | 1 | 1 |
Przykładem użycia tej operacji jest poniższy fragment kodu:
int liczba=0b10011101;
int maska =0b11000000;
liczba|= maska;
System.out.println(Integer.toBinaryString(liczba));
NOT
Negacja logiczna jest przykładem odwracania binarnego zawartości rejestru (zmiennej) na przeciwny.
10011101
!
———————————
01100010
Tablica przejść dla funkcji NOT jest przedstawiona poniżej:
| Liczba | Wynik |
| 0 | 1 |
| 1 | 0 |
Przykładem użycia jest poniższy fragment kodu:
int liczba=0b10011101;
int maska =0b11000000;
liczba=~liczba;
System.out.println(Integer.toBinaryString(liczba));
Spodziewamy się wyniku zgodnie z powyższą tablicą przejścia:
01100010
a uzyskaliśmy:
11111111111111111111111101100010
Co się stało?!
Wróćmy zatem do tematu reprezentacji typów zmiennych. Typ prosty int jest zapisany na 4 bajtach. My operujemy na jednym, najmłodszym (najmniej znaczącym) bajcie, angażując w przykładach wszystkie 8 jego bitów. Nie możemy jednak zapomnieć o fakcie, że przecież reszta bajtów i pozostałe 24 bity nadal są częścią zmiennej typu int i zostały uzupełnione 0, aż do wypełnienia całości rejestru (zmiennej). Co powoduje, że przy operacji NOT przeprowadzonej na zmiennej typu int, wszystkie te 0 przechodzą na 1 i w rezultacie otrzymujemy liczbę 32-bitową. Co więc zrobić, aby uzyskać jej fragment 8-bitowy?! Znamy już tę metodę – rzutowanie typów. Sprawdźmy więc ten kod:
int liczba=0b10011101;
liczba=~(byte)liczba;
System.out.println(Integer.toBinaryString(liczba));
Dokonaliśmy rzutowania zmiennej int na byte, więc zostały odcięte wszystkie bity bardziej znaczące (a ich miejsca w wyniku na powrót wypełnione 0). Można także rzutować typ już na etapie wyświetlania, chociaż może się to okazać ryzykowne, jeśli z taką liczbą binarną dokonujemy dalszych przekształceń np. przesunięć bitowych – w takim przypadku wynik operacji 8-bitowych może zostać przekłamany.
int liczba=0b10011101;
liczba=~liczba;
System.out.println(Integer.toBinaryString((byte)liczba)); //niezalecane jeśli operujemy dalej na rejestrze 8-bitowym...
XOR
Operacja XOR (ang: eXclusive OR), czyli Różnica Symetryczna jest często wykorzystywana w pewnych zagadnieniach programistycznych np. w szyfrowaniu. Posiada ona bowiem cechę symetrycznego, odwracalnego przekształcania wartości za pomocą tego samego klucza.
10011101
XOR
11010000
———————————
01001111
Operacja wykonywana jest zgodnie z logicznym równaniem:
(A AND !B) OR (!A AND B)
Tablica przejść wygląda następująco:
| Liczba | Klucz | Wynik |
| 0 | 0 | 0 |
| 0 | 1 | 1 |
| 1 | 0 | 1 |
| 1 | 1 | 0 |
Przykład realizacyjny może przybrać postać poniższego kodu:
System.out.println(Integer.toBinaryString(liczba));
int klucz =0b11010000; //użyty jako klucz szyfrujący...
liczba^=klucz;
System.err.println(Integer.toBinaryString(liczba));
liczba^=klucz;
System.out.println(Integer.toBinaryString(liczba));
UWAGA: Należy zauważyć, że użyty w Javie znak ^ nie jest oznaczeniem potęgowania, a operacji bitowej XOR!!!
Przesunięcia bitowe
Przesunięcia bitowe w rejestrach przesuwają bity w lewo (mnożenie o 2), lub w prawo (dzielenie przez 2). Bity skrajne „wypadają” z rejestru, a rejestr jest uzupełniany „0” 1Należy pamiętać, że liczby ze znakiem (-) są przesuwane bitowo nieco inaczej – jest to opisane w dalszej części materiału..
Dla przykładu:
liczba=0b00010000; //16
System.out.println("Wartość binarna to: "+Integer.toBinaryString((byte)liczba)+" wartość decymalna to: "+liczba);
liczba=liczba >> 2; // dwa przesunięcia w prawo, czyli dzielenie /2 /2
// liczba>>=2; // ten zapis jest także dozwolony i znaczy to samo co jego rozszerzony zapis powyżej!
System.out.println("Wartość binarna to: "+Integer.toBinaryString((byte)liczba)+" wartość decymalna to: "+liczba);
liczba<<=2; // 2-krotne przesunięcie w lewo ( *2 *2 )
System.out.println("Wartość binarna to: "+Integer.toBinaryString((byte)liczba)+" wartość decymalna to: "+liczba);
Przy okazji widać niedoskonałość metody Integer.toBinaryString(), gdzie nie są wyniki uzupełniane poprzedzającymi „0”. Można to oczywiście poprawić przygotowując własną metodę, co będzie wkrótce przepracowane.
Do przesunięć bitowych należy jeszcze dołożyć jedną kwestię, a mianowicie przesunięcia bitowe liczb ze znakiem i bez znaku. Operatory >> i << są operatorami przesunięć bitowych ze znakiem, jest jeszcze jednak operator przesunięcia w prawo bez znaku >>>. Prześledźmy poniższy przykład.
Wpierw przypiszmy zmiennej wartość maksymalną jaką może posiadać z wykorzystaniem Integer.MAX_VALUE, a następnie dodajmy do niej… 1. W wyniku czego nastąpi przepełnienie rejestru, a wartość zmiennej przyjmie najmniejszą z możliwych wartość (zgodną z Integer.MIN_VALUE, z której mogliśmy skorzystać, ale wtedy nie zbadalibyśmy zachowania rejestru przy przepełnieniu)
1: 00000000 00000000 00000000 00000001+2147483647: 01111111 11111111 11111111 11111111================================================-2147483648: 10000000 00000000 00000000 00000000 // Przepełnienie/2================================================-1073741824: 11000000 00000000 00000000 00000000 // dzielenie ze znakiem, tak samo jak >> 1.
a teraz to samo z operatorem >>>
1: 00000000 00000000 00000000 00000001+2147483647: 01111111 11111111 11111111 11111111================================================-2147483648: 10000000 00000000 00000000 00000000 // Przepełnienie>>> 1================================================+1073741824: 01000000 00000000 00000000 00000000 // dzielenie bez znaku!!!
Wybrane operatory złożone (Math)
- abs() – wartość absolutna liczby,
- ceil() – zaokrągla w górę do liczby całkowitej,
- floor() – zaokrągla w dół do liczby całkowitej,
- max() – porównuje dwie liczby i zwraca jako wynik tę większą,
- min() – porównuje dwie liczby i zwraca jako wynik tę mniejszą,
- pow() – potęguje pierwszy argument o potęgę drugiego argumentu,
- round() – zaokrągla wartość liczby zmiennoprzecinkowej do najbliższej liczby całkowitej (zgodnie z zasadami matematycznymi),
- sqrt() – zwraca wartość będącą pierwiastkiem kwadratowym argumentu.
Zadania do samodzielnego wykonania: Przećwicz operatory, tak abyś swobodnie używał właściwych, nie myląc się i nie szukając pomocy. To są najczęściej wykonywane operacje. To trzeba mieć w małym palcu… Poeksperymentuj z kodem. Powodzenia.