Podejście obiektowe służy uporządkowaniu konstrukcji programu, hermetyzacji danych i metod, oraz naturalnemu, intuicyjnemu grupowaniu funkcjonalności programu w obrębie zamkniętych obiektów cechujących się wyróżniającymi je elementami.

Takie działanie sprzyja przejrzystości programu, gdyż w podejściu obiektowym dosyć szybko możemy wyróżnić i zinterpretować obiekty nas interesujące. Hermetyzacja pomaga także utrzymać spójność danych utrudniając swobodną manipulację nimi, przez co zwiększając ich wiarygodność i odporność na błędy. Podejście obiektowe również pomaga w rozwoju oprogramowania, gdzie przy wdrażaniu poprawek czy aktualizacji danego obiektu możemy skupić się na poszczególnych elementach, niekoniecznie musząc analizować całość oprogramowania. Finalnie też programowanie obiektowe sprzyja krótszemu kodowi, gdzie nie musimy powielać tego samego kodu dla różnych obiektów wykorzystujących podobne lub wręcz tożsame funkcjonalności, a korzystamy z automatycznego mechanizmu bądź po prostu powoływania do „życia” wielu obiektów (instancji klasy), bądź mechanizmu dziedziczenia. Te wszystkie cechy podejścia obiektowego będą się uwidaczniać w miarę zgłębiania tematu.

Pojęcie klasy i obiektu

Klasą w ujęciu programistycznym nazywamy definicję obiektu, swoisty spis treści z czego zbudowany jest dany obiekt. Na tę definicję składają się informację o zmiennych czy stałych używanych w ramach danej struktury programistycznej, metody wraz z deklaracjami nazw, parametrów, zwracanych wartości oraz treści w postaci kodu programistycznego realizującego funkcję danej metody1za wyjątkiem metod wirtualnych, bowiem te nie posiadają „ciała” czyli części bloku programistycznego poza jedynie deklaracją nazwy, parametrów i zwracanej wartości.

Obiektem zaś jest powołana do „życia” klasa, to znaczy stworzona w obszarze pamięci operacyjnej struktura danych powstała w wyniku realizacji „przepisu”, czyli definicji klasy.

Przykładem definicji klasy jest poniższy przykład:

public class Klasa {

    public static void main(String[] argumenty) {

    }
    
}

Powyższy przykład jest… łudząco podobny do… podstawowego programu od którego zaczynaliśmy każdy projekt. To nie jest błąd – w zasadzie wszystko w Java jest oparte na notacji obiektowej. Stąd mamy tutaj do czynienia z plikiem o nazwie Klasa.java zawierającym definicję klasy o dostępie publicznym2dostęp do danej klasy, metody, zmiennych i stałych w ramach obiektu może być publiczny, prywatny i zastrzeżony (chroniony) – będzie o tym mowa w późniejszej części materiału i nazwie Klasa, w której to zdefiniowano metodę main z parametrem wejścia będącym tablicą stringów (String[] argumenty) i nie zwracającą niczego przy wyjściu (void). Zasada definicji klas wymaga, aby klasa ze zdefiniowanym modyfikatorem dostępu (tu: publiczna) opisywana w danym pliku miała nazwę zgodną z nazwą pliku (tu: Klasa.java). Nie dotyczy to definicji innych klas jako subklas (bez określania praw dostępu) w ramach tego samego pliku. Dostęp do nich jest jednak ograniczony i nie są one widoczne spoza pliku w którym zostały zdefiniowane. Widać to na poniższym przykładzie:

public class Klasa {

    public static void main(String[] argumenty) {
    }  
}

class AlaMaKota {
    public char c='A';
    public final char z='Z';
    
    void Zmien_c(char nowe_c) {
        c=nowe_c;
    }
}

Modyfikatory dostępu

Modyfikatory dostępu definiują prawa dostępu do określonego zasobu, niezależnie czy jest nim cały obiekt, czy też jego element składowy. Modyfikatorami są selektory public, privateprotected.

  • public – ten selektor określa, że oznaczona nim struktura NIE MA ograniczeń dostępu, jest dostępna dla wszystkich publicznie.
  • private – ten selektor określa, że oznaczona nim struktura NIE JEST dostępna spoza zdefiniowanego bloku (klasy) i jest widoczna tylko w ramach tego bloku.
  • protected – ten selektor określa, że oznaczona nim struktura NIE JEST dostępna spoza zdefiniowanego bloku (klasy) i jest widoczna tylko w ramach tego bloku i wszystkich klas z niego dziedziczących.

Działanie tych modyfikatorów przedstawia poniższy przykład:

public class Klasa {

    public static void main(String[] args) {
    
        AlaMaKota aMK = new AlaMaKota();
        System.out.println(aMK.c);
        
        //System.out.println(aMK.p);   // nie ma bezpośredniego dostępu do zmiennej p - jest ona prywatna w ramach klasy.
        
        aMK.ustaw_p('Z');
        System.out.println(aMK.podaj_p());

    }    
}

class AlaMaKota {
    public char c='A';
    public final char z='Z';
    private char p='M';
            
    void Zmien_c(char nowe_c) {
        c=nowe_c;
    }
    
    char podaj_p() {
        return p;
    }
    void ustaw_p(char ch) {
        p=ch;
    }
    
}

Użycie modyfikatorów dostępu (szczególnie używanie selektora private) prowadzi to do tzw. enkapsulacji, czyli zamknięcia dostępu do obiektu spoza niego. Jest to pozytywne dla utrzymania kontroli nad poprawnością danych przechowywanych i przetwarzanych w obiektach, gdzie odpowiednie metody publiczne pozwalają na ich modyfikowanie (po uprzedniej walidacji poprawności danej wartości) i odczytywanie tych wartości. Prowadzi to bezpośrednio do kolejnego tematu w tym materiale.

Setter’y i getter’y

Na powyższym przykładzie można zauważyć, że ustawienie modyfikatorów zmienia dostęp do struktur w ramach klasy. Dotyczy to metod i zmiennych. Jednocześnie w ramach tego przykładu można zaobserwować metody o specyficznej charakterystyce – są nimi tzw. settery 3metody, których funkcją jest ustawianie wartości wskazanej zmiennej i gettery 4metody, których funkcją jest odczytanie wartości wskazanej zmiennej. Ten sam przykład ma jeszcze jedną charakterystyczną cechę – inicjalizację zmiennych w… zasadzie… nieprawidłowy sposób.

W celu przedyskutowania ostatniej cechy musimy wprowadzić kolejną kategorię specyficznych struktur w ramach klasy, a są nimi… konstruktory.

Konstruktory i destruktory

Konstruktorem jest specyficzna metoda służąca do zbudowania obiektu na podstawie definicji zawartej w klasie i jego poprawna inicjalizacja. Destruktorem jest funkcjonalność zajmująca się zwolnieniem obszarów pamięci zajętych przez obiekt w sytuacji, gdy nie jest on (obiekt) więcej używany w ramach programu. W przypadku języka Java NIE MA jawnego destruktora, a funkcjonalność ta jest zapewniana mechanizmem Garbage Collector’a.

Przykładem użycia konstruktora i wzorem co do jego tworzenia jest poniższy kod:

public class Klasa {

    public static void main(String[] args) {
    
        AlaMaKota aMK = new AlaMaKota();
        System.out.println(aMK.c);
              
        aMK.ustaw_p('Z');
        System.out.println(aMK.podaj_p());
    }    
}

class AlaMaKota {
    public char c='A';
    public final char z='Z';
    private char p='M';
    
    AlaMaKota()  {
        System.out.println(c);
    }
           
    void Zmien_c(char nowe_c) {
        c=nowe_c;
    }
    
    char podaj_p() {
        return p;
    }
    void ustaw_p(char ch) {
        p=ch;
    }
    
}

Metoda, która w nazwie jest zgodna z nazwą klasy jest rozpoznawana za konstruktor klasy i jest wywoływana automatycznie w momencie tworzenia obiektu (instancji klasy). Ciekawostką jest… kolejna konstrukcja o kodzie umieszczonym poniżej, gdzie widać dwa konstruktory – jeden bezparametrowy, drugi z jednym parametrem. Taka funkcjonalność to tzw. przeciążanie metod, znane już z wcześniejszych użyć np. System.out.println(), gdzie parametrem mogą być różne typy danych.

public class Klasa {

    public static void main(String[] args) {
    
        AlaMaKota aMK = new AlaMaKota('Q');
        System.out.println(aMK.c);
            
        aMK.ustaw_p('Z');
        System.out.println(aMK.podaj_p());
    }    
}

class AlaMaKota {
    public char c='A';
    public final char z='Z';
    private char p='M';
    
    
    AlaMaKota()  {
        System.out.println(c);
    }
    
    AlaMaKota(char cc)  {
        c=cc;
    }
            
    void Zmien_c(char nowe_c) {
        c=nowe_c;
    }
    
    char podaj_p() {
        return p;
    }
    void ustaw_p(char ch) {
        p=ch;
    }
    
}

Z powyższego przykładu wynika, że można przygotować różne metody przyjmujące różne typy operatorów o tych samych nazwach wykorzystując mechanizm przeciążania metod. Zasadą podstawową jest to, że dana metoda MUSI być rozróżnialna typem i ilością parametrów, tak aby kompilator był w stanie rozróżnić, którą metodę uruchomić.

Zadanie do samodzielnego wykonania: Napisz program, w którym zdefiniujesz klasę Pojazd. Klasa powinna opisywać różne rodzaje pojazdów. Dane obiektów winne być prywatne i modyfikowane poprzez metody. Z uwagi na różne potrzeby użytkowników tego programu przygotuj konstruktory umożliwiające inicjalizację różnej ilości danych wejściowych tj. umożliwiające ustawienie np. koloru pojazdu i numeru VIN, lub ilości miejsc w pojeździe. W ten sam sposób przygotuj setter’y, modyfikujące parametry pojazdu.