· rszmit · 14 min

Wyzwanie Java #3: Programowanie obiektowe

W dzisiejszym poście dotyczącym programowania w języku Java powiemy sobie o programowaniu obiektowym (ang. Object Oriented Programming, OOP). Wszystko co nas otacza, jest obiektem - przedmioty, zwierzęta, budynki, ludzie itd. itp. Żyjemy w świecie obiektów. Dlatego by łatwiej opisywać otaczający nas świat, podjęto próby przeniesienia go do języków programowania.

Jako pierwsi podjęli się tego zadania Ole-Johana Dahla i Kristena Nygaarda z Norweskiego ośrodka obliczeniowego w Oslo, którzy pracowali nad symulacjami zachowania się statków. Wprowadzili oni obiekty, klasy, dziedziczenie i wiele innych pojęć do języka Simula 67. Koncepcja OOP została następnie dopracowana w języku Smalltalk, który uważany jest za pierwszy w pełni obiektowy język programowania. Idea OOP zainspirowała duńskiego informatyka Bjarne Stroustrup’a do rozszerzenia języka C o paradygmat programowania obiektowego, dzięki czemu narodził się jeden z najpopularniejszych języków programowania jakim jest C++. Od tamtego momentu, czyli połowy lat 80tych, programowanie obiektowe stało się najpopularniejszym paradygmatem programowania. Innymi popularnymi paradygmatami jest programowanie strukturalne występujące w starszych językach programowania lub językach skryptowych oraz obecnie coraz popularniejsze programowanie funkcyjne, któremu poświęcimy oddzielne wyzwanie!

Z racji tego, że dobre zrozumienie idei programowania obiektowego jest niezmiernie ważne, by płynnie posługiwać się tym językiem programowania, prosimy Was o rzetelne i uważne przeczytanie poniższego artykułu.

Java i programowanie obiektowe

Java od początku istnienia jest językiem programowania, który jest językiem obiektowym. Koncepcja obiektów jest tutaj na tyle silna, że do tej pory, nie wiedząc jeszcze co to OOP, my także w swoich programach tworzyliśmy obiekty! Jedynym wyjątkiem jest optymalizacja użyta przez twórców języka, mianowicie typy proste, czyli poznane wcześniej typy jak int, long, double lub boolean. Twórcy zdecydowali się na wprowadzenie tego wyjątku, żeby zapewnić wysoką wydajność - zwłaszcza w początkach istnienia języka Java, gdy komputery były znacznie gorsze niż obecne. Jak się za chwilę dowiemy, te typy mają jednak swoich obiektowych braci.

Obiekty i klasy

Obiekt to jest jakiś byt, coś co istnieje w naszym programie. Oznacza to, że chcąc skorzystać z jakiegoś obiektu, musimy go najpierw stworzyć. Do tego celu nasz program musi mieć opis takiego obiektu, który nazywamy klasami. Gdyby chcieć przenieść to na świat codzienny, klasy są czymś w rodzaju projektu domu lub kodu DNA, zaś obiekt jest już zbudowanym budynkiem lub stworzeniem z danym kodem DNA. Do tworzenia klas w Javie używamy słówka class. Poniżej przykład takiej klasy:

package pl.kodolamacz;

public class MyFirstJavaApplication {

}

Powyższy kod jest nam bardzo dobrze znany, stworzyliśmy go w naszym pierwszym wyzwaniu! Widać w nim, że MyFirstJavaApplication jest właśnie klasą, czyli DNA naszego obiektu.

Pakiety

W pierwszej linii naszego powyższego kodu widzimy instrukcję package. Jest to tak zwany pakiet lub ścieżka pakietowa naszej klasy. Pakiety służą do logicznego podziału i segregacji naszego kodu oraz niwelują możliwość pojawienia się kolizji, gdy dwie osoby stworzą dwie klasy o takiej samej nazwie, pod warunkiem, że będą miały inne pakiety.

Zwyczajowo ścieżka pakietowa zaczyna się odwróconą domeną internetową naszej firmy, organizacji lub naszą własną, stąd kodolamacz.pl dał nam pl.kodolamacz, choć tak naprawdę możemy dowolnie kreować nazwy naszych pakietów, z wyjątkiem pakietów zaczynających się od słowa java, takich nie można tworzyć. Nazwy pakietów można w sobie dowolnie zagłębiać, dlatego zamiast pl.kodolamacz moglibyśmy śmiało skorzystać z ścieżki pl.kodolamacz.moj.super.pakiet. Fizycznie, pakiety są reprezentowane przez katalogi, dlatego jeśli odnajdziecie na swoim lokalnym dyski katalog z projektem i odnajdziecie tam katalog z źródłami waszego programu, czyli src (ang. source), to zobaczycie katalog pl a w nim podkatalog kodolamacz.

Klasy są zapisywane w plikach *.java. Jeden plik może zawierać tylko jedną klasę publiczną (słowo public przed class) oraz nazwa klasy i pliku musi być identyczna. (O innych typach klas powiemy sobie w kolejnym wyzwaniu)

Jeśli chcemy w naszym kodzie użyć innej klasy, z innego pakietu, musimy ją zaimportować do naszej klasy. Służy do tego słówko import. Jedynym wyjątkiem są klasy z pakietu java.lang, które są zawsze domyślnie importowane oraz klasy z tego samego pakietu co nasza klasa, ich także nie trzeba importować.

Poniżej przykład naszego programu z importem:

import java.util.Scanner;

Jeśli chcemy zaimportować kilka klas z jakiegoś pakietu, możemy użyć symbolu ” * ” w celu załadowania całego pakietu:

import java.util.*;

Pytanie, co jeśli chcemy użyć w naszym kodzie dwóch klas o takiej samej nazwie, ale w innym pakiecie? Na przykład klasa Date znajduje się zarówno w pakiecie java.util jak i java.sql (oczywiście są to dwie zupełnie inne klasy). Możemy wtedy pisząc nasz kod wskazać pełną nazwę klasy wraz z pakietami, jak w kodzie poniżej. Wtedy polecenie import nie będzie już nam potrzebne.

package pl.kodolamacz;

public class MyFirstJavaApplication {

    public static void main(String[] args) {
        java.util.Date date1 = new java.util.Date();
        java.sql.Date date2 = new java.sql.Date(System.currentTimeMillis());
    }

}

Od Java w wersji 5, można importować nie tylko klasy, ale także metody i pola statyczne. Korzystamy wtedy z polecenia import static. Poniżej przykład kodu Witaj Świecie z pierwszego wyzwania z użyciem importu statycznego, gdzie zaimportowaliśmy pole statyczne System.out, dzięki czemu nie musieliśmy pisać System.out.println(“Witaj świecie!”);

package pl.kodolamacz;

import static java.lang.System.out;

public class MyFirstJavaApplication {

    public static void main(String[] args) {
        out.println("Witaj świecie!");
    }

}

Metody i pola

Klasy składają się z metod (ang. methods) i pól (ang. fields). Pola klasy możemy rozumieć jako pewien zbiór cech, które możemy przypisać obiektowi, np. klasa człowiek może mieć pole wiek. Metody możemy traktować jako pewne akcje które możemy wykonać na obiekcie, jak np. zjedz czy pracuj. Poniżej przykład takiej klasy:

package pl.kodolamacz.myclasses;

public class Person {

    /**
     * Trzy pola naszej klasy
     */
    public String forename;
    public String surname;
    public int age;

    /**
     * Metoda przedstaw się wyświetlająca w konsoli napis
     */
    public void introduceYourself() {
        System.out.println("Nazywam się " + forename + " " + surname + ". Mam " + age + " lat.");
    }

}

Pola deklarujemy podobnie jak zmienne z poprzedniego wyzwania. Jedyna różnica jest taka, że mają zdefiniowany dostęp (słowo public), o którym powiemy więcej w dalszej części.

Metoda to jakiś kawałek kodu, który możemy wykonać na obiekcie. Metody mają zawsze dostęp do pól zadeklarowanych w tej samej klasie. Nasza metoda introduceYourself wyświetla nam informację o imieniu i nazwisku osoby. Metodę też możemy porównać do funkcji znanej z matematyki, np. y = ax + b. Taka funkcja y przyjmuje parametr x i korzystając z dwóch liczb a i b, które w naszym przypadku mogą być polami klasy, zwraca wynik tego działania. Tak samo metody w Javie mogą zwracać jakiś wynik oraz przyjmować parametry. Typ wyniku (np. int) podajemy przed nazwą metody, zaś przyjmowane parametry w nawiasach. Poniżej przykład metody przyjmującej parametr i zwracającej wynik, w tym przypadku zwiększającej i zwracającej ten wiek. Do zwrócenia wyniku używamy słowa kluczowego return. Jeśli metoda nie zwraca żadnej wartości, zamiast typu używamy słowa kluczowego void.

package pl.kodolamacz.myclasses;

public class Person {

    public String forename;
    public String surname;
    public int age;

    public void introduceYourself() {
        System.out.println("Nazywam się " + forename + " " + surname + ". Mam " + age + " lat.");
    }

    /**
     * Metoda dorośnij, zwiększająca wiek osoby o wskazaną ilość lat
     */
    public int growOld(int years) {
        age += years;
        return age;
    }

}

Gdy już mamy naszą klasę osoba, możemy stworzyć na jej podstawie obiekt, przypisać jego zmiennym wartości i wywołać jego metody. Do tworzenia klas służy słowo kluczowe new. Poniżej przykład wykonania wszystkich powyższych operacji:

package pl.kodolamacz;

import pl.kodolamacz.myclasses.Person;

public class MyThirdJavaApplication {

    public static void main(String[] args) {
        Person person1 = new Person(); // tworzymy obiekt
        person1.forename = "Jan"; // przypisujemy polu imię wartość
        person1.surname = "Kowalski";
        person1.age = 35;
        person1.introduceYourself(); // wywołujemy metodę
        person1.growOld(5); // metoda z parametrem
        person1.introduceYourself(); // jeszcze raz się przedstawiamy z innym wiekiem
    }

}

Proszę zauważyć, że klasa Person znajduje się w pakiecie pl.kodolamacz.myclasses, przez co musieliśmy użyć słowa kluczowego import by skorzystać z tej klasy w klasie MyThirdJavaApplication.

Dostęp do pól i metod obiektu odbywa się za pomocą znaku kropki podając najpierw nazwę obiektu, a następnie pole lub metodę z tego obiektu.

Nazwy klas i obiektów piszemy używając CamelCase, z tą różnicą, że w klasach pierwsza litera jest duża, zaś w nazwach zmiennych mała.

Konstruktor

Do stworzenia obiektu osoba z powyższego kodu użyliśmy słowa new. Podczas tej operacji wywoływany jest tak zwany konstruktor, czyli specjalna metoda, która tworzy nam nasz obiekt. Jeśli nie napiszemy sami konstruktora, klasa dostaje tak zwany konstruktor domyślny, który wygląda tak jak na poniższym przykładzie:

package pl.kodolamacz.myclasses;

public class Person {

    public String forename;
    public String surname;
    public int age;

    /**
     * Konstruktor domyślny
     */
    public Person() {

    }

    public void introduceYourself() {
        System.out.println("Nazywam się " + forename + " " + surname + ". Mam " + age + " lat.");
    }

    public int growOld(int years) {
        age += years;
        return age;
    }

}

Konstruktory nieco różnią się od zwykłych metod takich jak introduceYourself czy growOld. Ich nazwa musi być dokładnie taka sama jak nazwa klasy. Konstruktory także nie mają słowa void lub zwracanego typu.

Niestety, konstruktor domyślny nie pozwala nam przypisać wartości do tworzonych pól, dlatego zawsze możemy taki konstruktor zamienić na własny:

package pl.kodolamacz.myclasses;

public class Person {

    public String forename;
    public String surname;
    public int age;

    /**
     * Konstruktor domyślny
     */
    public Person() {

    }

    /**
     * Konstruktor z trzema parametrami
     */
    public Person(String initForename, String initSurname, int initAge) {
        forename = initForename;
        surname = initSurname;
        age = initAge;
    }

    public void introduceYourself() {
        System.out.println("Nazywam się " + forename + " " + surname);
    }

    public int growOld(int years) {
        age += years;
        return age;
    }

}

Mając nasz konstruktor możemy już w zupełnie inny sposób napisać nasz program:

package pl.kodolamacz;

import pl.kodolamacz.myclasses.Person;

public class MyThirdJavaApplication {

    public static void main(String[] args) {
        Person person1 = new Person(); // tworzymy obiekt
        person1.forename = "Jan"; // przypisujemy polu imię wartość
        person1.surname = "Kowalski";
        person1.age = 35;
        person1.introduceYourself(); // wywołujemy metodę
        person1.growOld(5); // metoda z parametrem
        person1.introduceYourself(); // poprzednia metoda po zmianie parametrów

        Person person2 = new Person("Tomasz", "Nowak", 56); // konstruktor z parametrami
        person2.introduceYourself();
    }

}

W powyższym kodzie stworzyliśmy już dwa obiekty klasy Person. W tym przypadku nie musimy ustawiać parametrów przez odwołanie się do obiektu, wystarczy przekazać je do konstruktora.

Gdybyśmy chcieli użyć parametru metody lub konstruktora o takiej samej nazwie jak jedno z pól, możemy nadal odwołać się do pola, poprzez użycie słowa this. Poniżej przykład konstruktora oraz metody z kolizją nazw parametrów i pól:

package pl.kodolamacz.myclasses;

public class Person {

    public String forename;
    public String surname;
    public int age;

    /**
     * Konstruktor domyślny
     */
    public Person() {

    }

    /**
     * Konstruktor z trzema parametrami
     */
    public Person(String forename, String surname, int age) {
        this.forename = forename;
        this.surname = surname;
        this.age = age;
    }

    public void introduceYourself() {
        System.out.println("Nazywam się " + forename + " " + surname + ". Mam " + age + " lat.");
    }

    public int growOld(int age) {
        this.age += age;
        return this.age;
    }

}

Kontrola dostępu do klas

Klasy mają swój zasięg. Dzielimy je na klasy publiczne public class oraz na pakietowe (package-private), kiedy to mamy tylko słówko class bez public. Klasy publiczne można importować w dowolnej innej klasie, zaś klas pakietowych możemy używać tylko w klasach w tym samym pakiecie!

Dodatkowo wyróżniamy 4 modyfikatory dostępu do pól i metod klasy:

  • public - wszyscy mają dostęp
  • protected - widzą wszyscy w tym pakiecie i Ci co rozszerzają tą klasę
  • default - widać tylko w tym pakiecie
  • private - tylko ta klasa widzi swoje pola

Powyższą zasadę można przedstawić za pomocą tabeli:

KlasaPakietPodklasaWszyscy
public++++
protected+++
default++
private+

Należy zauważyć, że widoczność default uzyskujemy nie wpisując żadnego słowa przy polu, metodzie lub klasie! W Javie mamy słowo kluczowe default, ale służy do zupełnie czegoś innego, o czym przekonamy się w kolejnym wyzwaniu.

Metody i pola statyczne

We wcześniejszych przykładach tworzyliśmy pola i metody które należały do obiektu, to znaczy, by się do nich odwołać (pola) lub je wykonać metody musieliśmy najpierw stworzyć obiekt z naszej klasy. Są jednak sytuacje, gdzie dla skorzystania z jakichś pól lub metod, nie chcemy tworzyć nowych obiektów. Dobrym przykładem tutaj mogą być jakieś stałe wartości jak liczba PI lub funkcje matematyczne. Poniżej klasa Circle reprezentująca koło/okrąg wraz z promieniem (radius) oraz metodami zwracającymi pole powierzchni koła oraz obwód okręgu.

package pl.kodolamacz;

public class Circle {

    private int radius;

    public Circle(int radius) {
        this.radius = radius;
    }

    public double area() {
        return Math.PI * Math.pow(radius, 2);
    }

    public double circumference() {
        return 2 * Math.PI * radius;
    }

}

W powyższym kodzie użyta została stała Math.PI oraz metoda Math.pow() klasy Math z pakietu java.lang (niepotrzebny import!). By z nich skorzystać nie tworzyliśmy obiektu klasy Math (nie jest to nawet możliwe gdyż konstruktor ma dostęp oznaczony jako private), a jedynie skorzystaliśmy bezpośrednio z samej metody i pola klasy. Takie pola i metody z których możemy skorzystać bez obiektu nazywamy statycznymi. Tworzymy je za pomocą słowa static. Poniżej interesująca nas metoda i stała klasy java.lang.Math:

package java.lang;

public final class Math {

    /**
     * Don't let anyone instantiate this class.
     */
    private Math() {}
    
    public static final double PI = 3.14159265358979323846;

    public static double pow(double a, double b) {
        return StrictMath.pow(a, b);
    }

}   

W powyższym przykładzie skorzystaliśmy z klasy wbudowanej w język Java. Dodatkowo sami także możemy tworzyć oraz wykorzystywać pola i metody statyczne. Także metoda main, której używaliśmy w tym i poprzednich wyzwaniach, jest właśnie metodą statyczną!

Zarówno pole Math.PI oraz metodę Math.pow() możemy zaimportować statycznie. Nasz kod będzie wtedy wyglądać tak:

package pl.kodolamacz;

import static java.lang.Math.PI;
import static java.lang.Math.pow;

public class Circle {

    private int radius;

    public Circle(int radius) {
        this.radius = radius;
    }

    public double area() {
        return PI * pow(radius, 2);
    }

    public double circumference() {
        return 2 * PI * radius;
    }

}

W niektórych językach programowania wprowadza się oddzielnie pojęcie metody i funkcji. Metody są związane z jakimś obiektem, zaś funkcje są samoistnymi bytami. W języku Java nie ma możliwości stworzenia funkcji, czyli metody poza klasą, dlatego nawet metody statyczne z których możemy korzystać bezpośrednio wskazując klasę, a nie obiekt, są tak naprawdę metodami, a nie funkcjami.

Typy proste a obiekty

Jak dowiedzieliśmy się wcześniej, typy proste takie jak int, long, double czy boolean nie są obiektami. Zdecydowali to twórcy języka w celu osiągnięcia większej wydajności. Mimo to, każdy z typów prostych ma swój “obiektowy” odpowiednik. Dodatkowo w javie mamy mechanizm zwany Autoboxing oraz Unboxing automatycznie konwertujący typy proste w obiekty i odwrotnie. Poniżej przykłady konwersji typów prostych na i z obiektów:

package pl.kodolamacz;

public class AutoboxingAndUnboxing {

    public static void main(String[] args) {
        int a1 = 5;
        int a2 = new Integer(7); // konstruktor jest deprecated
        Integer a3 = new Integer(-8);
        Integer a4 = -19;
        Integer a5 = Integer.valueOf(34);

        String s1 = "Text 1";
        String s2 = new String("Text 1");

        Character ch1 = 'a';
        char ch2 = 'a';
    }

}

Poniżej tabela pokazująca mapowanie typów prostych na ich wersje obiektowe lub inaczej opakowujące (ang. wrapper):

Typ prostyWrapper
booleanBoolean
byteByte
charCharacter
floatFloat
intInteger
longLong
shortShort
doubleDouble

Wyzwanie

A teraz nasze wyzwanie. Nasz kalkulator, który stworzyliśmy w poprzednim wyzwaniu, miał jeden mały problem - operacje zmiennoprzecinkowe, czyli takie które przyjmowały lub tylko zwracały liczby niecałkowite (np. 10 podzielić na 3) zaokrąglały wynik. Niestety jest to problem, gdy zależy nam na bardzo dużej dokładności i nie możemy pozwolić sobie na zaokrąglenia (np. operacje finansowe). Dlatego w tym wyzwaniu chcielibyśmy, byście stworzyli ulepszoną wersję kalkulatora, która nie pracuje na liczbach typu double, a na waszej klasie Fraction (ułamek). Klasa ta powinna posiadać pole licznika, mianownika i części całkowitej oraz metody pozwalające na dodawanie, odejmowanie, mnożenie, dzielenie i skracanie ułamków, czyli wszystkie najważniejsze operacje jakich mogliśmy się nauczyć w szkole. Program powinien wszystkie wczytywane liczby w kalkulatorze od razu zamieniać na ułamki i na nich dokonywać wszystkich działań. Należy pamiętać o sprowadzaniu ułamków do wspólnego mianownika. Oczywiście chcemy stworzyć klasę Fraction samodzielnie, bez posiłkowania się klasami z pakietu java.math.


Rozwiązanie wyzwania #3 opublikujemy w piątek na stronie naszego wydarzenia na Facebooku. W międzyczasie będziemy tam też zamieszczać materiały dodatkowe, które uzupełnią Waszą wiedzę na temat Javy i pracy programisty.

Jak uda się Wam poprawnie wykonać zadanie - pochwalcie się tym koniecznie w przeznaczonym do tego poście na FB!

Kolejna część wyzwania za tydzień, 7 maja o godz. 10:00.

Powodzenia!


Gotowe rozwiązanie zadania 3 znajdziecie tutaj.


Wszystkie wspisy z serii #javowewyzwanie:

Wprowadzenie

Wyzwanie 1 - Hello world!

Wyzwanie 2 - Podstawowe instrukcje

Wyzwanie 3 - Programowanie obiektowe

Wyzwanie 4 - Algorytmy i struktury danych w języku Java

Wyzwanie 5 - Interfejsy i dziedziczenie

Wyzwanie 6 - Operacje wejścia - wyjścia

Wyzwanie 7 - Programowanie funkcyjne


Materiały dodatkowe do wyzwania:

Wprowadzenie do świata języka Java

Czego się uczyć by zostać programistą?

Java od środka, czyli jak to wszystko działa?

Wprowadzenie do Apache Maven, czyli jak tworzy się projekty w świecie Java

Wprowadzenie do testowania aplikacji w środowisku Java

6 książek, które powinien przeczytać każdy programista Java

Powrót do bloga