Elementarz Java #3 – Operatory i konstrukcje warunkowe
Wstęp
W kolejnym materiale z cyklu #ElementarzJava, poruszyłem temat związany z operatorami oraz konstrukcjami warunkowymi. Często w aplikacjach to właśnie niepoprawne użycie operatorów powoduje wiele błędów, które potem trudno wyłapać. Zagadnienie to jest więc warte do przeanalizowanie, a przywołane przeze mnie przykłady na pewno dostarczą Ci dużo nowej wiedzy.
Seria #ElementarzJava składa się z następujących artykułów:
- Podstawy języka Java (kompilacja, zmienne, struktura klasy, pakiety),
- Typy danych w języku Java (deklaracja i inicjalizacja zmiennych, różnica między typami, garbage collection, typy opakowujące),
- Operatory i konstrukcje warunkowe w Java (użycie operatorów, porównywanie obiektów, instrukcje:
if
,if/else
,switch
), - Tablice (charakterystyka tablic, tablice jedno i wielowymiarowe),
- Pętle (tworzenie pętli, instrukcje
break
icontinue
, etykiety), - Metody oraz hermetyzacja (metody statyczne, przeciążanie metod, konstruktory, modyfikatory widoczności, hermetyzacja elementów klasy, parametry),
- Dziedziczenie (implementacja, przysłanianie metod, polimorfizm, rzutowanie),
- Obsługa wyjątków (kategorie wyjątków, łapanie wyjątków, klasy wyjątków),
- API Java (
String
,StringBuilder
, data i czas, kolekcje, wyrażenia lambda).
Jesteś tu pierwszy raz? Polecam rozpocząć lekturę od materiału: Elementarz Java #0 – wprowadzenie.
Użycie operatorów w języku Java
Operatory dostępne w Javie w zasadzie niczym nie różnią się od tych które możemy spotkać w innych językach programowania. Jednak jak to zwykle bywa, zawsze można znaleźć jakieś „ciekawe” przypadki, gdzie końcowy rezultat działania programu nie jest tak oczywisty jak mogło by się wydawać.
Pre i post inkrementacja
Przykładowo dość często możemy spotkać się z post i pre inkrementacją. Jak ona działa?
public static void main(String[] args) { int x = 1; System.out.println("++x = " + ++x); x = 1; System.out.print("x++ = " + x++); }
Powyższy kod wyświetli na ekranie liczby 2
oraz 1
. Jak nietrudno się domyślić w pierwszym przypadku przed wypisaniem wartości zmiennej x
, została ona powiększona o 1
. Tutaj mamy zaprezentowany sposób działania pre-inkrementacji. W drugiej sytuacji jest dokładnie odwrotnie. Wartość zmiennej x
jaka została wypisana to 1
, a zaraz po tej operacji została ona zmieniona na 2
.
Logiczny operator OR
Ciekawym przykładem jest wykorzystanie logicznego operatora XOR. Zastanów się przez chwilę, jaki może być rezultat wykonania poniższego kodu?
boolean x = true, z = true; int y = 10; z = ( y != 5 ) ^ (x = false); System.out.println(z); System.out.print(x);
Na początek należy zacząć od tego, że operator ^
to exclusive-or (xor
). W jaki sposób działa xor
?
0 – false, 1 – true
0 ^ 0 = 0
0 ^ 1 = 1
1 ^ 0 = 1
1 ^ 1 = 0
Teraz przeanalizujmy dokładnie wyrażenie (y != 5) ^ (x = false)
. W pierwszej jego części (y != 5)
mamy true
. W drugiej (x = false)
do zmiennej x
, który ma wartość true
przypisujemy false
, czyli w efekcie mamy false
. True xor false
to true
. Wypiszemy więc true
. Następnie dla x
zostanie wypisany false
.
Operator +
Zastanówmy się jaki będzie rezultat uruchomienia kodu, gdzie operator dodawania będzie „łączył” zarówno obiekty typu String
oraz liczby.
System.out.println(10 + 2 + "SUN" + 4 + 5);
Wydawać by się mogło, że będzie to 12SUN9
. Nie jest to jednak prawdą. Po napotkaniu przez operator +
obiektu typu String
, wszystkie pozostałe obiekty traktowane są jako Stringi
– nawet jeśli są to liczby. Poprawna odpowiedź to 12SUN45
.
Zmiana kolejności wykonywania operacji przy pomocy nawiasów
Wiedza matematyczna przydaje się programiście nawet w najmniej oczekiwanym momencie. Warto więc przypomnieć sobie reguły dotyczące kolejności wykonywania działań. Tak z ciekawości czy wiesz jaką zawartość będzie miała zmienna x z poniższego przykładu?
int x = 2 * 3 % 5;
Poprawna odpowiedź to 1. 2 * 3 = 6
, a 6 mod 5
to 1
. Oczywiście stosując nawiasy tak jak w matematyce możemy zmieniać kolejność wykonywania działań.
int x = 2 * (3 % 5);
W tym przypadku poprawna odpowiedź to 6
. 3 mod 5
to 3
, a 2 * 3 = 6
.
Porównywanie obiektów przy pomocy metody equals() i operatora ==
Najciekawszy punkt programu przy omawianiu operatorów w języku Java to oczywiście operator ==
oraz metoda equals()
. Ich niepoprawne użycie często prowadzi do wielu błędów, które trudno jest wykryć. Warto więc dobrze zrozumieć mechanizm działania obu konstrukcji. Zerknijmy na taki kod.
String s = "1"; int x = 1; if(x == s) { System.out.print("true"); }
Na pierwszy rzut oka wydaje się wszystko w porządku. Jednak nic bardziej mylnego. Jeśli przepiszemy ten fragment do IDE i spróbujemy go skompilować to procedura ta zakończy się niepowodzeniem. Dlaczego? Operator == nie może porównywać dwóch różnych typów. To ograniczenie dotyczy również Stringa
oraz StringBuilder’a
ale już typy proste i ich odpowiedniki obiektowe będą działały poprawnie (porównywana jest wtedy faktyczna wartość).
Porównanie operatora == oraz equals?
Czemu w niektórych sytuacjach nie powinniśmy używać operatora ==
, a w innych metody equals
? Kluczem na znalezienie odpowiedzi na to pytanie jest oczywiście zrozumienie w jaki sposób te konstrukcje działają. Zanim do tego przejdę, popatrzmy na poniższy kod.
String s1 = "Hello"; String s2 = new String(s1); if("Hello".equals(s1)) System.out.println("one"); // wypisze one if(s1 == s2) System.out.println("two"); // sprawdzanie po referencjach nic nie wypisze if(s1.equals(s2)) System.out.println("three"); // wypisze three if("Hello" == s1) System.out.println("four"); // UWAGA: tutaj wypisze "four" bo ciąg znaków "Hello" ma taką samą referencję jak ciąg "Hello" który został przypisany do s1 if("Hello" == s2) System.out.println("five"); // nic nie wypisze, s2 ma inną referencję
Jak widać, operator ==
porównuje ze sobą dwa obiekty sprawdzając ich referencję. Metoda equals()
działa nieco inaczej. Instrukcja ta porównuje zawartość stringów.
Tak jak wspomniałem wcześniej String
i StringBuilder
nie możemy porównywać operatorem ==
. Jedyna możliwość to właśnie skorzystanie z metody equals()
.
String s1 = "Hello"; StringBuilder sb1 = new StringBuilder("Hello"); if(s1 == sb1) System.out.println("one"); // błąd kompilacji if(s1.equals(sb1)) System.out.println("two");
Ten kod nie zostanie poprawnie skompilowany. Nie można porównywać operatorem ==
dwóch różnych obiektów.
Przeanalizujmy również w jaki sposób będzie działało porównywanie dwóch obiektów typu StringBuilder
. Jaki będzie rezultat po uruchomieniu poniższego kodu?
StringBuilder sb1 = new StringBuilder("a"); StringBuilder sb2 = new StringBuilder("a"); if(sb1.equals(sb2) { System.out.println("equals"); } if(sb1 == sb2) { System.out.println("=="); } if(sb1.toString().equals(sb2.toString()) { System.out.print("toString"); }
Na ekranie komputera pojawi się napis „toString
”. Klasy takie jak StringBuilder
czy StringBuffer
nie nadpisują metody equals
i w takim przypadku zwraca zawsze false
. Jak więc porównać dwa obiekty StringBuilder
? Możemy to zrobić tak jak na przykładzie, poprzez zastosowanie metody toString
.
Jeśli StringBuilder
nie nadpisuje metody equals
to również zwykła klasa, którą stworzymy, zachowuje się tak samo.
class SecondClass { } public class MyClass { public static void main(String... args) { System.out.println(new SecondClass().equals(new SecondClass())); } }
W powyższym przykładzie mamy klasę SecondClass
, gdzie jak widać nie nadpisujemy equals
. Porównanie, które mamy w metodzie main
zwróci więc false
(będą porównywane referencje, które są różne).
Porównywanie tablic i ArrayList
Interesującym przypadkiem jest kwestia porównywania kolekcji. Na tapetę weźmy tablice oraz ArrayList.
int[] tab1 = new int[]{1, 2}; int[] tab2 = new int[]{1, 2}; List list1 = new ArrayList(); list1.add(1); List list2 = new ArrayList(); list2.add(1); if(tab1.equals(tab2)) System.out.println("array equals"); if(tab1 == tab2) System.out.println("array =="); if(list1.equals(list2)) System.out.println("arrayList equals"); if(list1 == list2) System.out.println("arrayList ==");
Jak zadziała taki kod? Tutaj znowu cała kwestia rozbija się o to czy ArrayList
oraz Array
przeładowują metodę equals
. Odpowiedź jest następująca, na ekranie komputera zobaczymy napis „arrayList equals
”. Jak więc widać na przykładzie, metoda equals
jest przeładowywana przez ArrayList
, ale już przez Array
nie. Jeśli chodzi o operator ==
to po analizie wcześniejszych przykładów powinno być już oczywiste, że w takiej sytuacji dostaniemy false
(będą porównywane referencje). Jak więc porównywać zawartość tablic? Możemy to tego celu wykorzystać metodę equals
z obiektu Arrays
.
int[] tab1 = new int[]{1, 2}; int[] tab2 = new int[]{1, 2}; if(Arrays.equals(tab1, tab2) { System.out.print("Arrays.equals"); }
W ten właśnie sposób możemy porównywać zawartość tablic jednowymiarowych. Jeśli chodzi o tablice wielowymiarowe to stosujemy metodę deepEquals
.
int[][][] tab1 = new int[][][]{ {{1}, {1}, {1}} }; int[][][] tab2 = new int[][][]{ {{1}, {1}, {1}} }; if(Arrays.deepEquals(tab1,tab2)) { System.out.println("Ok"); }
Użycie konstrukcji if oraz if/else
Jak działa instrukcja warunkowa if
to chyba wszyscy wiedzą. Przenalizujmy więc kilka ciekawych przypadków jej wykorzystania w języku Java.
if(1 == 1) { System.out.println("=="); } else { System.out.println("!=="); } else { System.out.println("!=="); }
Taki kod spowoduje błąd kompilacji. Nie możemy mieć dwóch bloków else
w jednej instrukcji if
.
boolean test = true; if(test = false) { System.out.print("true"); } else { System.out.print("false"); }
Dość ciekawy przykład, o którym już pisałem wcześniej. Wynikiem działania powyższego kodu będzie „false
”. W klauzuli if
nie mamy operatora porównania. Do zmiennej test
zostanie przypisana wartość false
, a operator „=
” zwróci false
. Na ekranie komputera zobaczymy napis „false
”.
Skrócona wersja if
Używając operatora if
możemy w prosty sposób przypisać odpowiednią wartość do zmiennej w zależności od ustalonego wcześniej warunku logicznego.
String result = 1 > 2 ? "yes" : "no";
Instrukcja ta może również zostać zagnieżdżona.
int x = 1; System.out.println(x > 0 ? x < 3 ? 4 : 5 : 6);
Wynik działania powyższego kodu to 4
. Dlaczego? Najpierw będzie sprawdzany warunek czy x
jest większe od 0
. Oczywiście jest. Kolejno czy x
jest mniejsze od 3
co również jest prawdą. W efekcie zwracamy liczbę 4
.
Użycie konstrukcji switch
Instrukcja wielkokrotnego wyboru switch
to taki nieco bardziej rozbudowany if
. Sposób jej działania jest również bardzo prosty. Na wstępie warto jednak zwrócić uwagę na jeden warunek o którym już wspominałem w artykule na temat typów danych. Zgodnie ze specyfikacją Javy instrukcja wielokrotnego wyboru switch
może przyjmować wyrażenia typu: char
, byte
, short
, int
, Character
, Byte
, Short
, Integer
, String
lub enum
. Na tej liście nie ma na przykład double
.
Tym co będzie nas interesowało to działanie operatora break
, które nie jest tak oczywiste jak mogło by się wydawać. Popatrzmy na przykładowy kod.
int counter = 1; switch(counter) { case 0: case 1: System.out.println("1"); case 2: System.out.println("2"); break; case 3: System.out.println("3"); }
Po jego uruchomieniu na ekranie zostaną wypisane liczby 1
oraz 2
, mimo, że wydawać by się mogło, iż liczba 2
nie powinna zostać wyświetlona. Warto jednak dodać, że po tym, kiedy już natrafimy na właściwe porównanie w instrukcji case
, będziemy wykonywali kod ze wszystkich pozostałych instrukcji (nawet jeśli wyrażenie logiczne w danym case
zwraca false
), aż do momentu natrafienia na słówko kluczowe break
.
int x = 1; switch (x) { case 1: System.out.println("1"); default: System.out.println("default"); // wartość domyślna, będzie wyświetlona, jeśli żadna instrukcja case nie zostanie dopasowana case 0: System.out.println("0"); case 2: System.out.println("2"); break; }
Po uruchomieniu tego przykładu otrzymamy: 1 default 0 2
.
Wstawienie słówka kluczowego contiune
w przypadku instrukcji switch
nie ma za bardzo sensu i powoduje błąd kompilacji.
switch (x) { case 1: System.out.println("1"); continue; case 2: System.out.println("default"); }
Ten kod nie zostanie skompilowany.
W instrukcji switch
nie możemy również duplikować wartości.
switch (x) { case 1: System.out.println("1"); case 1 + 1: System.out.println("1 + 1"); case 1 + 0: System.out.println("1 + 0"); }
Powyższy przykład nie zadziała. Mamy zduplikowane wartości w pierwszej i ostatniej instrukcji case
.
Instrukcje case
nie mogą zawierać wyrażeń, które nie są „stałe” czyli nie możemy wstawiać zmiennych.
int x = 1; int y = 2; switch (x) { case x + 0: System.out.println("1"); case y: System.out.println("1 + 1"); }
Taki program nie zostanie skompilowany. W instrukcji case
nie możemy umieszczać zmiennych. Dopuszczalne są stałe.
int x = 1; final int myAge = 1; switch(x) { case 0: case myAge: System.out.print("Hello"); break; // stała jest dozwolona }
Powyższy kod zostanie skompilowany poprawnie. Jednak zanim przejdę dalej chciałem jeszcze zwrócić uwagę jeden istotny szczegół.
int arg = 20; final int num; num = 20; switch(arg) { default: System.out.print("Default"); case num: System.out.print("Hello"); break; }
Wydawać by się mogło, że kod zaprezentowany wyżej zostanie skompilowany i uruchomiony poprawnie. W instrukcjach case
znajduje się stała więc na pierwszy rzut oka wszystko jest w porządku. To jednak mylne przekonanie. W tym przypadku dostaniemy błąd kompilacji. Dlaczego? Stała num
nie jest inicjalizowana w momencie deklaracji, robimy to linijkę niżej. To powoduje, że zmienna ta w momencie kompilacji nie będzie traktowana jako stała. Co, jak już wiemy, powoduje błąd kompilacji, gdyż w instrukcji case
nie możemy podawać zwykłych zmiennych.
Case powinien zawierać num zamiast num2?
Przy okazji dzięki za ogrom pracy jaki włożyłeś w przygotowanie tej strony.
Póki co zgłębiam zagadnienia z Javy i muszę przyznać, że serwujesz najlepsze i najświeższe materiały w tym temacie. Duży plus za obszerne tłumaczenia z uwzględnieniem kodów, które się nie skompilują z precyzyjnymi tłumaczeniami dlaczego tak się stanie. Jeszcze raz dzięki!
int arg = 20;
final int num;
num = 20;
switch(arg) {
default: System.out.print(„Default”);
case num2: System.out.print(„Hello”); break;
}
Hej Tomek,
Dzięki za informację, poprawiłem kod :)
Fajnie, że materiały Ci się przydały, życzę owocnej nauki!
Pozdrawiam,
Łukasz