26 maja 2008

SCJP - Parametry metod

W ostatnim artykule z serii SCJP, tj. "SCJP – Zmienne typów prostych i referencyjnych" napisałem parę słów wstępu do tego, co będzie dzisiaj. Temat jest względnie prosty, więc nie rekomenduję usilnie uprzedniej lektury owego wstępu, ale jeśli coś z artykułu dzisiejszego okaże się być niejasne to pewnie tamten tekst pomoże. Dziś będzie o przekazywaniu parametrów do wywołań metod a zwłaszcza parametrów typów prostych kontra referencji.

Wywołując metodę przekazujemy pewną listę – potencjalnie pustą – parametrów typu prostego i/lub referencji do obiektów. Pierwszą i w zasadzie jedyną rzeczą, jaką w tym kontekście trzeba wiedzieć jest, że w języku Java wszystkie parametry są przekazywane przez wartość – reszta to już czysta konsekwencja tego prostego faktu. Przeanalizujmy to na przykładach.

public class Test {
public static void main(String[] args) {
int x = 2;

triple(x);

System.out.println("x: " + x);
}

static void triple(int x) {
x *= 3;
}
}

Efektem uruchomienia powyższego programu będzie wyświetlenie napisu "x: 2". Przeanalizujmy krok po kroku jak to się dzieje. W pierwszej linii metody ‘main(…)’ deklarujemy zmienną ‘x’ i przypisujemy jej wartość literału ‘2’. Następnie wywołujemy metodę ‘triple(…)’ przekazując jako argument zmienną ‘x’. Co to dokładnie znaczy przekazać zmienną ‘x’ jako argument? Zerknijmy na funkcję ‘triple(…)’. Widzimy, że deklaruje ona argument typu ‘int’ o nazwie ‘x’ – naturalnie zbieżność nazwy ze zmienną zadeklarowaną w metodzie ‘main(…)’ nie ma żadnego znaczenia. Zmienna zadeklarowana jako argument metody traktowana jest dokładnie tak samo jak zmienne zadeklarowane w jej ciele – jest to zmienna lokalna metody. Mamy zatem zmienną typu ‘int’ o nazwie ‘x’ w metodzie ‘main(…)’ oraz w metodzie ‘triple(…)’ i mimo, że nazywają się one tak samo to są to zupełnie inne zmienne, tj. odpowiadają innym obszarom pamięci. Jak już sobie powiedzieliśmy, zmienne w języku Java zawsze przekazywane są przez wartość, zatem wywołanie metody ‘triple(…)’ powoduje skopiowanie wartości zmiennej ‘x’ z metody ‘main(…)’ do zmiennej ‘x’ w metodzie ‘triple(…)’. I właśnie to skopiowanie jest przekazaniem parametru wywołania! Jaki zatem efekt ma wykonanie przypisania "x *= 3" w metodzie ‘triple(…)’? Naturalnie, potrojenie wartości zmiennej ‘x’ zadeklarowanej w metodzie ‘triple(…)’, ale nie zmiennej ‘x’ z metody ‘main(…)’. Nic więc dziwnego, że program wypisze "x: 2" – wypisujemy przecież wartość zmiennej lokalnej dla metody ‘main(…)’ a ta nie zmieniła się od czasu inicjacji liczbą 2. W powyższym przykładzie celowo nazwałem obie zmienne tak samo, w końcu moją intencją było wprowadzenie w błąd, zupełnie tak samo jak to ma miejsce na egzaminie SCJP, na co trzeba się wyczulić. Zerknijmy na kolejny przykład.

public class Test {
public static void main(String[] args) {
SomeClass obj = new SomeClass();

triple(obj);

System.out.println("x: " + obj.getX());
}

static void triple(SomeClass obj) {
obj.setX(obj.getX() * 3);
}
}

class SomeClass {
int x = 2;

public int getX() {
return x;
}

public void setX(int x) {
this.x = x;
}
}

Tym razem przekazujemy referencję do obiektu, a dokładniej, kopię tej referencji – jest to bardzo duża różnica, co pokażę w kolejnym przykładzie. Jak pisałem w artykule wspomnianym we wstępie – referencje to nic magicznego, zwykła zmienna przechowująca pewną wartość, tyle że ta wartość to "wskaźnik" na obiekt. Przeanalizujmy, co się dzieje w przedstawionym powyżej programie. W pierwszej linii metody ‘main(…)’ deklarujemy zmienną referencyjną oraz tworzymy nowy obiekt. Zmienna ta inicjowana jest wartością reprezentującą wskaźnik na ten obiekt. Wywołując metodę ‘triple(…)’ nie przekazujemy bynajmniej obiektu, tylko referencję. Przekazanie referencji oznacza skopiowanie wartości zmiennej ‘obj’ zadeklarowanej w metodzie ‘main(…)’ do zmiennej o tej samej nazwie zadeklarowanej w metodzie ‘triple(…)’. Obie zmienne nazywają się tak samo i mają tą samą wartość, jednak są to zupełnie inne zmienne, których wartości możemy zmieniać niezależnie, analogicznie jak wartości zmiennych typów prostych. Ale co oznacza instrukcja "obj.setX(…)" umieszczona w metodzie ‘triple(…)’? Nie jest to bynajmniej przypisanie nowej wartości zmiennej ‘obj’. Jest to wywołanie metody obiektu "wskazywanego" przez tą zmienną. Ale chwileczkę, przecież zmienna ‘obj’ lokalna dla metody ‘triple(…)’ ma tą samą wartość, co zmienna ‘obj’ lokalna dla metody ‘main(…)’, zatem "wskazuje" dokładnie ten sam obiekt. Nic więc dziwnego, że uruchomienie programu spowoduje wyświetlenie napisu "x: 6". Zmodyfikowaliśmy przecież dokładnie ten obiekt, który utworzyliśmy w metodzie ‘main(…)’ i którego stan następnie wyświetlamy. I jeszcze jeden przykład. Klasa SomeClass pozostała bez zmian, więc nie pokazuję jej ponownie.

public class Test {
public static void main(String[] args) {
SomeClass obj = new SomeClass();

triple(obj);

System.out.println("x: " + obj.getX());
}

static void triple(SomeClass obj) {
SomeClass localObj = new SomeClass();

localObj.setX(obj.getX() * 3);

obj = localObj;
}
}

Nadal przekazujemy referencję do obiektu, metoda ‘main(…)’ wcale się nie zmieniła, a mimo to program wypisuje "x: 2". Dlaczego? Właśnie dlatego, że referencje zupełnie tak jak zmienne typów prostych przekazywane są przez wartość, tj. przekazywane są kopie referencji, zatem przypisanie "obj = localObj" modyfikuje zmienną lokalną dla metody ‘triple(…)’ i nie ma żadnego wpływu na zmienną ‘obj’ z metody ‘main(…)’.

2 komentarze:

Ris pisze...

To zdanie się mi nie podoba:
"Obie zmienne nazywają się tak samo i mają tą samą wartość, jednak są to zupełnie inne zmienne, których wartości możemy zmieniać niezależnie, analogicznie jak wartości zmiennych typów prostych." Tyczy się drugiego przykładu. Przecież jak zrobię tak:

static void triple(SomeClass obj) {
obj.setX(obj.getX() * 3);

System.out.println("x: " + obj.getX());
}

Zmieniając obj w metodzie triple, to zmieniam również obj z metody main, bo wskazuje na to samo miejsce w pamięci, czyż nie? A Ty napisałeś, że są to zupełnie inne zmienne.

Mariusz Lipiński pisze...

W metodzie triple(...) nie zmieniamy zmiennej obj, tylko obiekt "wskazywany" przez tą zmienną, a to jest ogromna różnica. Zmienić wartość zmiennej obj możemy np. poprzez wykonanie instrukcji:

obj = null;

albo:

obj = new SomeClass();

Żadna z tych operacji, tj. zmiana wartości zmiennej obj z metody triple(...) nie ma wpływu ani na zmienną obj z metody main(...) ani na obiekt.

Pozdr. Mariusz