Parametry IN, OUT i IN-OUT w javie
Dziś z rana naciąłem się na ciekawy błąd w kodzie jabolowym. Można powiedzieć, że generalnie problemem było niejavowe użycie parametru metody.
Jeżeli zdarzyło ci się programować w PL/SQL to zapewne spotkałeś się z parametrami IN, OUT i IN-OUT. Dla osób, które nie zostały dotknięte tym nieszczęściem krótkie wyjaśnienie. Jeżeli przekazujemy do funkcji jakiś parametr to może on działać na trzy sposoby:
IN – czyli parametr wejściowy. Funkcja może go tylko odczytywać, ale nie może modyfikować.
OUT – parametr wyjściowy. Działa to trochę na zasadzie wskaźnika. W przekazanej zmiennej będzie umieszczona jakaś wartość, ale nie ma możliwości odczytania wartości tej zmiennej.
INOUT – czyli połączenie dwóch poprzednich. Parametr może być zarówno odczytywany jak i nadpisywany.
Tak to wygląda w PL/SQL. Jak jednak wygląda to w Javie? Okazuje się, że tu sprawa się delikatnie komplikuje. Po pierwsze wszystkie parametry są typu IN-OUT o ile są obiektami i nie posiadają wewnętrznych mechanizmów zabezpieczających przed modyfikacją.
Przyjrzyjmy się poniższemu fragmentowi kodu:
Listing 1. final to pułapka
public Person mergePersons(final Person from, Person to){
from.setAge(to.getAge());
return to;
}
Kod jest oczywiście poprawny od strony formalnej i błędny od strony biznesowej. Klasa Person nie posiada zabezpieczeń przed modyfikacją zatem nagle obiekt ze zmiennej from zmienił swój stan. W dodatku wiele osób początkujących złapie się na modyfikator final, który nie oznacza, że nie można zmienić obiektu. Oznacza on, że zmienna z parametru nie może zostać nadpisana. Zabezpiecza to przed przypadkowym nadpisaniem sobie parametru w kodzie. Gwarantuje niezmienność wartości zmiennej zakresie równym zakresowi widoczności tej zmiennej.
Wspomniałem, że dotyczy to obiektów. W przypadku prymitywów sprawa jest o tyle prosta, że są one przekazywane nie przez referencję, a przez wartość. Zatem nie można ich zmodyfikować. Zawsze są traktowane jako IN, a modyfikator final zabezpiecza nas przed nadpisaniem sobie zmiennej.
Oczywiście uzyskanie parametrów typu IN, które przy okazji są normalnymi obiektami nie jest trudne. Pamiętać należy, że modyfikator final zapewnia niezmienność w pełnym zakresie widzialności danej zmiennej. Wystarczy zatem deklarować pola jako finalne. Mechanizmy języka załatwią resztę za nas.
Najbardziej zdradzieckie są jednak parametry typu OUT. Z kilku powodów. Po pierwsze nie występują w naturze. Poniższy kod jest doskonałym przykładem:
Listing 2. to tylko zmienna lokalna
public void initPerson(Person from){
from = new Person();
}
//...
Person jaś;
initPerson(jaś);
jaś po takiej operacji nadal będzie null. Stan ten jest przykry zarówno dla Jasia jak i programisty. Wierzcie bądź nie, ale KAŻDY początkujący popełnia ten błąd. Tego problemu nie rozwiążemy.
Oczywiście jest to cecha javy jako języka. W C można napisać:
Listing 3. tak to tylko w C
#include <stdio.h>
void przy(int *z){
*z = 10;
}
int main(){
int zmienna;
printf("Wartosc zmiennej: BRAK WAROTSCI; jej adres: %p.\n", (void*)&zmienna);
przy(&zmienna);
printf("Wartosc zmiennej: %d; jej adres: %p.\n", zmienna, (void*)&zmienna);
}</stdio.h>
I efekt będzie zgodny z oczekiwaniami. Jednak parametry OUT można emulować w Javie na kilka sposobów. Najprościej jest osiągnąć to za pomocą tzw. Fluent Interface, czyli na modłę StringBuffer. Zwracamy obiekt przyjmowany jako parametr.
Listing 4. Fluent interface
public Person initPerson(Person from){
from = new Person();
return from;
}
//...
Person jaś;
jaś = initPerson(jaś);
Oczywiście można użyć klonowania czy metod fabrykujących, które będą przepisywać poszczególne wartości.
Na koniec pułapki, czyli na czym polegał problem. Otóż stosując takie podejście należy pamiętać, że nie będziemy mieli do czynienia z NullPointerException. W zamian otrzymamy „coś dziwnego”. W moim przypadku było to przekroczenie zakresy w tablicy. Pierwszą pułapką jest zatem nadużywanie Null Object. Wzorzec ten choć przydatny to prowadzi do bardzo trudnych w diagnozowaniu błędów ponieważ puste implementacje mają tendencję do łączenia się w swoiste haremy i w efekcie dostajemy Null State. Drugą jest złe zrozumienie kodu. Jeżeli ktoś weźmie się za używanie danej metody bez analizy, które parametry są jakiego typu to może się boleśnie naciąć. Szczególnie jeżeli przez przypadek zwracamy nową instancję w miejsce tej ze zmiennej. Trzecia pułapka to zagmatwanie kodu. Tu w przeciwieństwie do drugiej nadużywamy różnych rodzajów zmiennych i w efekcie bez otwartej dokumentacji nie można pisać. Zatem po pierwsze umiar, po drugie konwencja i po trzecie rozsądek. Do tego dokładne testy i nie powinno się nic złego stać.
ps. piszę o tym ponieważ w starym kodzie wręcz roi się od tego typu niespodzianek. Warto zatem wiedzieć, że takie problemy można spotkać.