Informacje o nowych artykułach oraz akcjach edukacyjnych prosto na Twojej skrzynce e-mail!

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.

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 falseTrue 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: charbyteshortintCharacterByteShortIntegerString 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.

Spodobało się?

Jeśli tak, to zarejestruj się do newslettera aby otrzymywać informacje nowych artykułach oraz akcjach edukacyjnych. Gwarantuję 100% satysfakcji i żadnego spamowania!

, , , , , , , , , , , ,

Dodaj komentarz

Komentarze (2)

  • Tomek pisze:

    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;
    }

Odpowiedz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *

Pin It on Pinterest