Powrót

Klasy i interfejsy w Javie

logo

Krótki przegląd

Java jest obecnie jednym z najpopularniejszych języków programowania. Jak każdy nowoczesny język, oferuje programistom szeroki wachlarz możliwości. W tym poście przedstawię wam kilka najpopularniejszych i niezwykle użytecznych mechanizmów jakie można wykorzystwać w Javie. Będą to kolejno:

  • klasy
  • klasy abstrakcyjne
  • dziedziczenie
  • interfejsy
  • implementacja interfejsów
  • enumy
  • recordy

Klasy

Java to język typowo obiektowy, dlatego programując w tym języku trzeba mieć na uwadze, że z klasami i obiektami spotykamy się właściwie na każdym kroku. Dlatego właśnie nim przyjrzymy się bliżej jako pierwszym. Klasa jest to dosłownie szablon, który definuje nam obiekty. Klasy mogą zawierać różne metody oraz zmienne - są zestawem planów, które określają jakie własności będą miały przyszłe obiekty. To właśnie na nich oparte jest programowanie obiektowe. Definicja klasy w Javie jest dosyć prosta i intuicyjna.

  [modyfikator dostępu] class [nazwa klasy]{
    [modyfikator dostępu] typ zmienna;
   [modyfikator dostępu] typ [nazwa metody] ([parametry]){
      ciało metody
   }
  }

Zaczynając od podstaw. Modyfikator dostępu określa nam czy klasa dostępna jest publicznie, czy np. tylko w obrębie jakiegoś pakietu. Następnie kluczowe słówko class oraz nazwa klasy, którą tworzymy. Ciało klasy obejmuje w powyższym przypadku jedną zmienną oraz metodę. Klasa może zawierać dodatkowo takie rzeczy jak np. konstruktory czy gettery oraz settery służące do dostępu do pól, gdy są one np. prywatne. Tworzenie obiektu danej klasy odbywa się już poza nią, np. w funkcji main. Jeśli przygotowaliśmy uprzednio klasę Car, która ma zmienną colour oraz metodę drive, możemy utworzyć jej obiekt w taki sposób:

  Car car = new Car("blue");
  car.drive();

W powyższym przykładzie tworzymy obiekt klasy Car o nazwie car i od razu przez konstruktor nadajemy mu kolor blue, przez konstruktor. Następnie wywołujemy metodę drive, która pozwala na skorzystanie z niej w obrębie jednego obiektu. Można również utworzyć więcej obiektów tej klasy i również korzystać z niej oraz ze wszystkich metod danej klasy.

  Car car2 = new Car("blue");
  car2.drive();

Proste i intuicyjne, prawda? W tym przypadku widzimy, że tworząc obiekt klasy Car, podajemy do konstruktora jego kolor. Klasa zawiera polę colour typu string, więc musimy utworzyć konstruktor, aby ten kolor od razu przypisać do tej zmiennej.

    public Car(String colour) {
        this.colour = colour;
    }

Jeśli polę jest np. prywatne, a chcemy skorzystać z niego, musimy utworzyć gettery i settery na to polę. Robimy to w następujący sposób:

    public String getColour() {
        return colour;
    }

    public void setColour(String colour) {
        this.colour = colour;
    }

Dzięki temu możemy ustawić naszemu samochodowi kolor później, za pomocą metody setColour([tu kolor]) lub pobrać wartość tego koloru za pomocą getColour(). Dosyć ważną kwestią jest też konwencja nazw w Javie. Tworząc nową klasę nazwę piszemy tzw. CamelCasem, czyli kolejne wyrazy piszemy łącznie, rozpoczynając każdy kolejny wielką literą np. public class MyCar. Podobnie zmienne, jednak by łatwiej odróżnić klasy od zmiennych ich nazwy rozpoczynamy mała literą np. myNewCar. Dobrze jest znać takie praktyki, aby kod był schludny i przejrzysty dla Ciebie oraz innych osób czytających Twój kod.

Klasy abstrakcyjne

Jest to typ, na pierwszy rzut oka bardzo podobny do interfejsów, o których będzie mowa w dalszej części tego wpisu. Główne cechy klas abstrakcyjnych w Javie to:

  • możliwość posiadania tzw. metod abstrakcyjnych, czyli metod nie zawierających implementacji,
  • mogą zawierać “zwykłe” metody, które można dziedziczyć(o dziedziczeniu sobie jeszcze powiemy),
  • klasy abstrakcyjnej, nie można stworzyć. Oraz wiele innych. Główną ich zaletą jest możliwość stworzenia niezwykle elastycznych i bezpiecznych struktur co bywa dosyć istotne. Budowa klasy abstrakcyjnej:

    public abstract class Book {
    public static final String TITLE = "Typy w Javie";
    
    public abstract int pagesCount();
    
    public static void imReading(){
        System.out.println("Czytam teraz książkę o Javie!!");
    }
    }

    Jak widać w powyższym przykładzie klasę abstrakcyjną tworzymy dodając słowo abstract. Jak pisałem wyżej - takie klasy mogą zawierać stałe, metody bez implementacji oraz takie z implementacją. Należy jeszcze pamiętać, że klasy abstrakcyjnej nie można stworzyć. W pierwszej kolejności musi ją coś rozszerzyć i zaimplementować.

Dziedziczenie

Dzięki dziedziczeniu jesteśmy w stanie utworzyć spójną i przejrzystą hierarchię klas, dzięki czemu później będzie nam łatwiej modyfikować kod. Dziedziczenie samo w sobie jest to przekazywanie cech danej klasy, klasie podrzędnej. Tak samo jak w życiu. Gdy się rodzimy to dziedziczymy po rodzicach niektóre cechy. Tak samo w programowaniu - klasy również mogą po sobie dziedziczyć cechy. W Javie, w przeciwieństwie do innych języków, nie występuje dziedziczenie wielokrotne, tzn. klasa podrzędna może dziedziczyć tylko po JEDNEJ klasie nadrzędnej. Spójrzmy teraz na prosty przykład dziedziczenia w języku Java:

class Figure {
    int side;
    int height;
}
class Triangle extends Figure {
    int area = (side*height)/2;
}

Mamy na początku daną klasę Figure, zawierającą 2 pola: bok i wysokość. Następnie tworzymy klasę reprezentującą trójkąt, która ma zmienną o wartości pola danego trójkąta. Widzimy, że klasa Triangle nie ma własnych pól, tylko korzysta z pól klasy Figure. Wyboraźmy sobie sytuację, kiedy mamy klasy wszystkich figur geometrycznych. Wiele z nich zawiera te same pola, więc nie ma sensu tworzenia ich dla każdej klasy osobno. Dzięki dziedziczeniu klas mamy możliwość skorzystania z pól klasy nadrzędnej(poza prywatnymi), dla każdej z klas podrzędnych. I to jest właśnie cała magia dziedziczenia. Dziedziczenie z konkretnej klasy sygnalizujemy słowem extends - czyli rozszerza. W wolnym tłumaczeniu - “klasa trójkąt rozszerza klasę figury”, w związku z tym ma dostęp do jej zawartości. Tworzenie konstruktora w klasie podrzędnej, wygląda tak jak zawsze, jednak gdy korzystamy z pól klasy nadrzędnej korzystamy w konstruktorze z metody super(), aby wywołać konstruktor klasy wyżej:

class Figure {
    int side;
    int height;
    Figure(){
      System.out.println("Utworzono figurę.");
    }
}
class Triangle extends Figure {
    int area = (side*height)/2;
    Triangle(){
        super();
        System.out.println("Utworzono trójkąt.");
    }
}
class Test{
  public static void main(String[] args) {
    Triangle triangle = new Triangle();
  }
}

Widzimy na przykładzie powyżej, że w klasie Triangle wywołaliśmy konstruktor klasy nadrzędnej za pomocą super(). Efekt kompilacji tego programu prezentuje się w następujący sposób:

  Utworzono figurę.
  Utworzono trójkąt.
  
  Process finished with exit code 0

Interfejsy i ich implementacja

O interfejsach wspominałem już wyżej, gdy mowa była o klasach abstrakcyjnych. W gruncie rzeczy są do nich dosyć podobne, jest jednak kilka różnic o których warto wspomnieć. Interfejs to taki typ, który jedynie mówi nam, jakie metody należy zaimplementować w ramach danej klasy. Te metody nie posiadają ani ciała, ani nawiasów klamrowych. Tworzymy go podobnie jak zwykłą klasę jednak zamiast słowa class, korzystamy ze słowa interface, jak w poniższym przykładzie:

public interface Code {
    public void learnFromYoutube();
    public int hoursToLearn();
    public boolean isMyProjectFinnished();
}

Jak widzimy, interfejs zawiera jedynie metody, które programista korzystający z danego interfejsu MUSI zaimplementować. Jest to w dużym skrócie po prostu gotowy szablon klasy, z której zamierzamy korzystać. Jest to niezwykle przydatne w praktyce, podczas planowania schematu naszego programu. Istotne jest to, że interfejsy mogą rozszerzać jedynie inne interfejsy. Poza zwykłymi metodami, interfejsy mogą zawierać dodatkowo metody domyślne, prywatne, statyczne oraz stałe. Implementacja danego interfejsu jest również niezwykle prosta. Jedyne co należy zrobić, to zaimplementować metody tego interfejsu jak na poniższym przykładzie, dodając słowo implements sygnalizująć, z jakiego interfejsu korzysta nasza klasa:

    Class Me implements Code {
        @Override
        public void learnFromYoutube() {

        }

        @Override
        public int hoursToLearn() {
            return 0;
        }

        @Override
        public boolean isMyProjectFinnished() {
            return false;
        }
    };

Tutaj widzimy domyślnie wygenerowane przez interfejs metody do implementacji. Adnotacja override sygnalizuje nam, że musimy te metody nadpisać. Poniżej można zapoznać się z przykładową implementacją tych metod:

    Class Me implements Code {
        @Override
        public void learnFromYoutube() {
            System.out.println("Muszę obejrzeć w końcu ten kurs Javy...");
        }

        @Override
        public int hoursToLearn() {
            System.out.println("Będę się uczył dopóki nie osiągne pułapu 30 godzin.");
            return 30;
        }

        @Override
        public boolean isMyProjectFinnished() {
            System.out.println("Nie no dopiero co zaczynam :(");
            return false;
        }
    };

Przydatność tego rozwiązania możecie ocenić już sami, po przeczytaniu tej części wpisu.

Enumy

Enumy to typ dodany w Javie 7. Jest to najprościej mówiąc zwykły typ wyliczeniowy, który pozwala programistom z góry zadeklarować konkretną liczbę możliwych wartości.

    public enum Language {
        JAVA, PYTHON, C, PHP, JAVASCRIPT;
    }

W tym konkretnym przypadku skorzystaliśmy z typu wyliczeniowego do deklaracji określonej liczby języków programowania. Konwencja nazwnictwa tego co wyliczamy, widoczna jest powyżej. Pisanie małą literą jest dozwolone, ale tak jak wyżej już zwracałem uwagę - powinno się pisać z wielkiej litery.

Zastosowanie enuma w praktyce, polega na utworzeniu obiektu klasy (w tym wypadku) Languages i skorzystania w ramach niego z naszego enuma, tak jak na poniższym przykładzie:

public class Main {
    public static void main(String[] args) {
        String test = "JAVA";
        if(test.equals(Language.JAVA)){
            System.out.println("O tym właśnie jest ten BLOG :)");
        }
        else{
            System.out.println("Nie tego się aktualnie uczymy :(");
        }
    }
}

Co dostaniemy na wyjściu tego programu? Odpowiedź jest oczywista! Obiekt test korzysta z wartości JAVA z danego enuma. Nic więc dziwnego, że wykona się pierwszy if i dostaniemy wynik:

O tym właśnie jest ten BLOG :)

Process finished with exit code 0

No bo właśnie o tym jest… :) W przeciwnym razie jak obiekt test przyjmie inną wartość, np PHP to if się nie wykona:

public class Main {
    public static void main(String[] args) {
      String test = "PHP";
      if(test.equals(Language.JAVA)){
        System.out.println("O tym właśnie jest ten BLOG :)");
      }
        else{
            System.out.println("Nie tego się aktualnie uczymy :(");
        }
    }
}

a na wyjściu dostaniemy:

Nie tego się aktualnie uczymy :(

Process finished with exit code 0

Bo PHP się właśnie nie uczmy. Całkiem proste prawda?

Recordy

Ostatnim przystankiem tego bloga będą recordy. Stosunkowo świeży typ w Javie, dodany dopiero w wersji 14. Tworząc klasę z przykładowo 4 polami, faktycznie wypisujemy tylko te 4 pola. Takie rzeczy jak konstruktor, gettery czy np. metoda toString(), może być generowana automatycznie w naszym środowisku. Tutaj z pomocą przychodzą właśnie recordy. Ich deklaracja jest również nieco inna niż klasy, co pokazuję poniżej:

public record Route(int kilometers,
                    String departure,
                    String arrival,
                    double fuelCost) {}

W tym przykładzie widzimy utworzony rekord Route z czterema polami: odległość w kilometrach, miejsce startu, miejsce zakończenia podróży oraz koszt paliwa. Parametry te znalazły się w nawiasach okrągłych, wypisane w oddzielnych wersach, lecz równie dobrze możemy to zapisać tak:

 public record Route(int kilometers, String departure, String arrival, double fuelCost) {}

Ten zapis również jest poprawny, ale tak jak zaznaczam po raz kolejny - przyjęło się to zapisywać troszkę inaczej podobnie jak np. w strumieniach. Rekordy charakteryzują się tym, że takie rzeczy jak konstruktor czy nadpisane metody jak np. toString() są już zawarte w takim rekordzie, dzięki czemu nasz kod jest bardziej przejrzysty dla oka. Zabrakło w tym przypadku getterów i setterów. Jest to spowodowane tym, że pola rekordów są niemodyfikowalne, czyli nie możemy zmienić później ich wartości np. stosując settery. Do pól odwołujemy się później również troszkę inaczej, po prostu zamieszczając nazwę konkretnego pola po kropce przy tworzeniu obiektu:

 Route route = new Route(300, "Poznań", "Warszawa", 150.4);
 double fuelCost = route.fuelCost();
 System.out.println("Ten wyjazd kosztuję mnie "+fuelCost+" zł.");

Po skompilowaniu rekordu, zamienia się on w klasę finalną - przez co nie można po niej dziedziczyć. Mimo, że rekordy umożliwiają nam nadpisywanie implementacji metod takich jak hashCode(), czy toString(),to całkowicie mija się to z celem, ponieważ zaletą rekordów jest to, że mają już te metody i to dlatego są tak lubiane przez programistów.

void isItFar(){
            if(route.kilometers()>200){
                System.out.println("Zdecydowanie tak");
            }
            else{
                System.out.println("W sumie to nie aż tak");
            }
        }

Korzystanie z rekordów nie różni się od korzystania z klas niemodyfikowalnych. Tak jak pisałem wyżej - rekord po kompilacji zmienia się na właśnie taką klasę.

To by było właściwie na tyle, jeśli chodzi o klasy w Javie. Niebawem na naszym blogu pojawi się więcej postów dotyczących nie tylko samej Javy, ale również najpopularniejszego frameworka do niej jakim jest Spring. Zachęcam do zapoznania się również z tymi późniejszymi wpisami.

Źródła

- Szymon Zalewski

Jeśli masz jakieś uwagi lub sugestie podeślij nam je na adres kontakt@akai.org.pl lub kontrybuuj do naszego repozytorium.