Twitter: @grzegg
Kategoria: java, Tagi: - - - - .

Java [24] – Klasy i obiekty cz. 2

W dzisiejszej lekcji: konstruktory, przeciążanie, modyfikatory, akcesory, hermetyzacja i inne atrakcje.
Brzmi groźnie, prawda? Ale nie taki diabeł straszny… ;-)

Konstruktory

Kiedy w poprzedniej lekcji tworzyliśmy obiekty typu Telefon robiliśmy to tak:


Telefon tel1 = new Telefon();

Wyrażenie po prawej stronie wygląda trochę jak wywołanie metody, o czym świadczy para nawiasów. Rzeczywiście, tworząc obiekt wywołujemy metodę, zwaną konstruktorem. Konstruktory (nie konstruktorzy!) są szczególnymi metodami, które są zawsze wywoływane gdy powstaje nowy obiekt. Mają taką samą nazwę jak klasa, w której się znajdują, w ich nagłówku nie znajduje się deklaracja zwracanej wartości (nawet void), ponieważ jej nie zwracają i są wywoływane przy tworzeniu obiektu. Tworzy się je właśnie w celu „przygotowania” obiektu, zatem umieszcza się tam na przykład instrukcje ustawiające odpowiednie/domyślne wartości pól (zwanych też danymi składowymi).

Zaraz, ale przecież w klasie Telefon nie napisaliśmy konstruktora? Nie ma tam metody o nazwie Telefon. Jak więc jest to możliwe,że został on wywołany?

Jeśli nie stworzymy żadnego konstruktora, kompilator Javy sam stworzy domyślny konstruktor, który po prostu nie zawiera żadnych instrukcji i nie pobiera żadnych argumentów. Odpowiada on takiemu kodowi:


public Telefon() {

}

Dodaj taki kod do klasy Telefon(), po uruchomieniu programu nie zauważysz żadnych zmian w jego działaniu. Zmodyfikuj go nieco, dodając do wnętrza konstruktora polecenie: System.out.println("Tworzę się!");. Teraz powstawaniu obiektu będzie towarzyszył odpowiedni komunikat.

Oczywiście na ogół nie po to tworzy się konstruktory aby komunikować światu, że powstaje obiekt. Jak wspomniałem jednym ze standardowych zadań konstruktora jest ustawienie początkowych wartości pól. W przypadku konstruktora, który nie przyjmuje żadnych argumentów mogą to być wartości domyślne, na przykład:


public Telefon() {
    System.out.println("Tworzę się!");
    marka = "Nieznana";
    kolor = "czarny";
    numer = 0;
    liczbaPolaczen = 0;    
}

W takim przypadku nie jest konieczna inicjalizacja i nadanie wartości 0 zmiennej liczbaPolaczen na początku klasy, gdzie deklarowane są zmienne. Zatem pozostaw tam samą deklarację: int liczbaPolaczen;

Teraz, w klasie Telefony zaraz po linii, w której tworzymy obiekt tel1 umieść kod wyświetlający zawartość pól:


Telefon tel1 = new Telefon();
System.out.println("\n> Jestem produktem firmy " + tel1.marka
         + " mam kolor " + tel1.kolor
         + " i numer " + tel1.numer);

Po uruchomieniu programu zostanie wyświetlony komunikat:


Tworzę się!

>Jestem produktem firmy Nieznana mam kolor czarny i numer 0
...

W naszym kodzie wpisywaliśmy własne wartości pól obiektów po kolei je modyfikując:


tel1.marka = "LG";
tel1.kolor = "niebieski";
tel1.numer = 123456789;

Ten sposób działa, ale znacznie poręczniej byłoby podać wartości pól, w momencie tworzenia obiektu co oczywiście ma sens tylko wtedy, gdy wartości pól są znane. Można to zrobić wykorzystując konstruktor przyjmujący jako argumenty wartości pól. Nie zmieniaj poprzedniego konstruktora ale dodaj nowy:


public Telefon(String marka, String kolor, int numer) {
    this.marka = marka;
    this.kolor = kolor;
    this.numer = numer;
    liczbaPolaczen = 0;    
}

Warto tu zwrócić uwagę na kilka rzeczy. Nowy konstruktor nazywa się tak samo jak poprzedni, mamy więc dwa konstruktory o tej samej nazwie. Różnią się jednak od siebie przyjmowanymi argumentami. Zatem jeżeli utworzymy nowy obiekt w ten sposób:


Telefon tel3 = new Telefon("Samsung", "srebrny", 135791357);

zostanie użyty konstruktor, który przyjmuje pasujące argumenty. Ta technika, zwana przeciążaniem może być używana także do innych metod. Wrócę do tego później. Możesz więc napisać jeden konstruktor, który nie przyjmuje żadnych argumentów, drugi, który przyjmuje argumenty da ustawienia wszystkich pól, które tego wymagają, trzeci, który przyjmie jako argument markę itd. Należy tylko trzymać się zasady, że nie mogą one mieć takiego samego zestawu argumentów, czyli takiej samej liczby, takiego samego typu w takiej samej kolejności. Jest to zrozumiałe, jeśli pamiętamy, że używany konstruktor jest wybierany na podstawie podawanych argumentów. Jeśli dwa konstruktory przyjmują najpierw String a potem int to nie wiadomo, którego z nich użyć, komputer się nie domyśli, że na przykład w pierwszym przypadku to marka i numer a w drugim kolor i numer seryjny.

Drugą rzeczą na jaką warto zwrócić uwagę to nazwy argumentów, które pokrywają się z nazwami pól. Jak rozróżnić zmienną marka, która jest argumentem metody od pola marka w klasie? Jeśli napiszemy to w ten sposób jak powyżej, wszystko staje się jasne, this wskazuje na ten obiekt, czyli wyrażenie this.marka wskazuje na pole marka w tym obiekcie w którym działa kod. Ponieważ po drugiej stronie znaku równości znajduje się marka bez this, wiadomo, że tym razem chodzi o nazwę argumentu/zmiennej w metodzie.

Można zapytać, czy nie prościej byłoby nazywać inaczej argumentów i pól. Można tak robić, ale wbrew pozorom taka konwencja jak pokazana powyżej sprzyja przejrzystości kodu. Od razu wiadomo, że argumenty przekazywane do konstruktora czy innej metody są wartościami pól.

W konstruktorach zwykle zawarte są komendy związane z ustawianiem domyślnych i przekazanych jako argumenty pól, mogą tam znaleźć się także inne operacje konieczne do prawidłowego przygotowania obiektu. Należy jednak unikać wywoływania z konstruktora metod zdefiniowanych w tej samej klasie o ile nie są one opatrzone modyfikatorem finished lub static. Dlaczego tak jest, stopniowo się wyjaśni.

Jak wspomniałem, jeśli nie napiszemy żadnego konstruktora, kompilator tworzy domyślny konstruktor nie przyjmujący żadnych argumentów. Ale jeśli napiszemy jakikolwiek konstruktor, nawet przyjmujący argumenty, ten domyślny bezargumentowy konstruktor nie jest już tworzony. Więc jeśli na przykład w kodzie klasy Telefon znajdzie się tylko powyższy konstruktor przyjmujący jako argumenty wartości pól, nie uda się już stworzyć obiektu w ten sposób: Telefon tel1 = new Telefon();.

Poniżej znajduje się kod uaktualnionej klasy Telefony. Zauważ, że kod odpowiedzialny za wydrukowanie wartości pól został przeniesiony do oddzielnej metody.

Jeszcze o przeciążaniu metod

Jak napisałem powyżej, przeciążanie stosuje się nie tylko do konstruktorów, ale także „zwykłych” metod.
Możemy więc napisać kilka metod, które nazywają się tak samo, ponieważ wykonują zadanie tego samego typu, ale przyjmują różny zestaw argumentów. Na przykład możemy napisać dwie metody, które sumują liczby w tablicy. Jedną dla tablicy liczb typu int a drugą dla liczb double. W zależności od dostarczonego argumentu będzie uruchamiana jedna lub druga metoda.

Statyczne pola i metody

Kiedy omawiałem metody, wspomniałem o modyfikatorze static. Słowo kluczowe static może być użyte w stosunku do metod ale także danych składowych. W takim przypadku metody i pola są związane z klasą, jeśli nie ma tego modyfikatora, są one związane z konkretnym obiektem. Jakie to ma znaczenie? Wspomniałem już, że metody oznaczone jako statyczne mogą być używane bez tworzenia obiektu, dotyczy to także pól. W przypadku danych stosuje się je na przykład dla definiowania pól, które powinny mieć taką samą wartość niezależnie od tego w jakim obiekcie się znajdują i czy w ogóle znajdują się w obiekcie (np. stałe matematyczne). Z tego powodu często słowu kluczowemu static towarzyszy final, które jak wcześniej wyjaśniłem używamy przy tworzeniu stałych.

Hermetyzacja, mutatory i akcesory

Dotychczas zmienialiśmy wartości pól i pobieraliśmy je bezpośrednio. Taka możliwość nie zawsze jest pożądana, z różnych powodów. Zamykanie bezpośredniego dostępu nazywamy hermetyzacją.
Niektóre dane nie powinny być dostępne bezpośrednio np. z powodów bezpieczeństwa, albo dlatego, że mają one wyłącznie wewnętrzne znaczenie. Często przed ustawieniem wartości zmiennej, warto sprawdzić czy jest ona prawidłowa/sensowna. Zdarza się też, że ustawieniu wartości danych składowych powinny towarzyszyć dodatkowe operacje, np. obliczanie wartości innych pól.
W takich przypadkach należy zamknąć bezpośredni dostęp do pól klasy za pomocą słowa kluczowego private (zamiast public o ile tam się znajduje) a następnie utworzyć odpowiednie metody, które pozwolą na wprowadzanie wartości i ich pobieranie z obiektu.

Metody pozwalające na ustawianie i zmianę wartości zmiennych nazywamy mutatorami. Z kolei akcesory to metody służące pobieraniu wartości pól. W języku angielskim metody te nazywamy odpowiednio setters i getters. Angielskie terminy wynikają z konwencji nazywania tego typu metod. Polega ona na tym, że nazwa mutatora powinna zaczynać się od set, po czym następuje nazwa zmiennej, której dotyczy. Na przykład metoda za pomocą której możemy zmienić wartość zmiennej liczbaChromosomow powinna nazywać się setLiczbaChromosomow. Z kolei nazwy akcesorów powinny być tworzone wg. schematu getNazwaZmiennej. Wyjątkiem są pola przechowujące wartości boolean. W tym przypadku akcesor zaczyna się od is, np: isGood. Warto stosować się do tej konwencji, z różnych powodów. Jednym z nich jest łatwość automatycznego tworzenia obu typów metod przy pomocy np. NetBeans. Pokażę to na przykładzie.

Powiedzmy, że będziemy chcieli rozszerzyć program, który był tematem zadania z poprzedniej lekcji. Będziemy chcieli przechowywać informację na temat chromosomów wchodzących w skład genomu organizmu. Dla każdego z chromosomów będziemy chcieli przechowywać dane: numer chromosomu, długości obu ramion oraz czy jest chromosomem płciowym. Klasa Chromosom będzie zatem posiadać cztery pola, na razie żadnych metod i na początku będzie wyglądać tak:

Teraz dodamy mutatory i akcesory. Z menu NetBeans wybierz: Refractor -> Encapsulate Fields…
Pojawi się okienko:


Jak widać zawiera ono tabelkę, w której mamy wyszczególnione wszystkie pola w pierwszej kolumnie a w kolejnych dwóch zaznaczamy, do których pól stworzyć akcesor (Create Getter) i/lub mutator (Create Setter). W naszym przypadku można wybrać od razu wszystkie metody (kliknij przycisk „Select All”) ale czasem potrzebujemy tylko niektóre z nich.

Poniżej można wybrać różne opcje dotyczące umieszczenia generowanych metod, generowania komentarzy, widoczności pól i metod. Użyj ustawień jak na obrazku poniżej:


Następnie wciśnij „Refractor”. Teraz klasa Chromosom wygląda tak:

Oczywiście możesz napisać wszystkie metody ręcznie ;-)

Przyjrzyj się nazwom nowych metod, a także nazwom i rodzajom argumentów oraz sposobowi przypisywania wartości zmiennym składowym klasy. Powtarza się tu schemat: this.pole = pole;

Zauważ, że poza wygenerowaniem akcesorów i mutatorów, przed deklaracjami danych składowych pojawił się modyfikator private zamykający bezpośredni dostęp do pól. Teraz chcąc wprowadzić do obiektu wartość pola a następnie go pobrać należy to zrobić tak:


// Tworzenie obiektu
Chromosom chromosom = new Chromosom();

// Ustawienie wartości pola numer
chromosom.setNumer(1);

// Pobranie wartości pola numer
int numer = chromosom.getNumer();

Hermetyzacja nie ogranicza się do danych składowych, można ją także zastosować do metod, na przykład ze względów bezpieczeństwa albo po prostu jeśli nie ma potrzeby aby dostęp do pewnych metod z zewnątrz był możliwy. Oczywiście robi się dodając modyfikator private w nagłówku metody.

Zadanie

Rozwiń program „Gatunki” , w ten sposób, żeby:

  • obiekt typu Gatunek mógł przechowywać dane chromosomów. Powinna to być tablica obiektów typu Chromosom o długości odpowiadającej liczbie chromosomów
  • zmodyfikuj klasę Chromosom:
    • w ten sposób aby dane dotyczące numeru chromosomu i długości ramion były wprowadzane tylko wtedy, jeśli podane liczby są większe od 0 w przypadku numeru chromosomu i większe lub równe 0 w przypadku długości ramion
    • dodaj konstruktory:
      • przyjmujący jako argumenty numer i długości ramion, pole plciowy ustawiający domyślnie jako false
      • przyjmujący jako argumenty wszystkie dane
  • kolejne gatunki były przechowywane w tablicy obiektów typu Gatunek

Dodatkowo:

Zmodyfikuj program tak aby:

  • dane wprowadzał użytkownik (gatunki, ich parametry, łącznie z chromosomami)
  • użytkownik miał możliwość wprowadzania nowych gatunków

4 komentarze Java [24] – Klasy i obiekty cz. 2

Leave a Reply