Programowanie obiektowe

Wykład 1

Sprawy organizacyjne

Sprawy organizacyjne

Wymagania wstępne

  • Znajomość podstawowych konstrukcji programistycznych (z pierwszego roku).

Ewentualne braki należy opanować w samodzielnym zakresie.

W razie problemów zapraszam na konsultacje.

Programowanie

Programowanie - co to jest?

Programowanie:

  • to instruowanie komputera, co ma robić.

  • to co robi “programista” gdy pisze “program”.

  • przekazanie fragmentu rzeczywistości do komputera.

Paradygmaty programowania

Paradygmat programowania

Paradygmat programowania — sposób patrzenia programisty na przepływ sterowania i wykonywanie programu komputerowego.

Po co nam progrmaowanie obiektowe?

Przeanalizujmy przykłady.

Python:

a = 5
b = a
print(a, b)
b += 1
print(a, b)
l = [1, 2, 3]
k = l
print(k, l)
l[1] = -7
print(k, l)

C

void idzPrawoDol(int x, int y)
{
    x=x+1;
    y=y-1;
}

int main()
{
    int x=20, y=15;
    idzPrawoDol(x,y);
    printf("Aktualna pozycja: [ %d, %d ] \n",x,y);
    return 0;
}

C

void idzPrawoDol(int *x, int*y)
{
    *x=*x+1;
    *y=*y-1;
}

int main()
{
    int x=20, y=15;
    idzPrawoDol(&x,&y);
    printf("Aktualna pozycja: [ %d, %d ] \n",x,y);
    return 0;
}
#include <stdio.h>
#include <string.h>

struct Laptop {
    char model[30];
    float cena;
};

struct Laptop initLaptop(char* model, float cena) {
    struct Laptop nowyLaptop;
    strncpy(nowyLaptop.model, model, sizeof(nowyLaptop.model) - 1);
    nowyLaptop.model[sizeof(nowyLaptop.model) - 1] = '\0';
    nowyLaptop.cena = cena;
    return nowyLaptop;
}

void pokazLaptop(struct Laptop laptop) {
    printf("Model: %s\n", laptop.model);
    printf("Cena: %.2f\n", laptop.cena);
}

void zmniejszCene(struct Laptop* laptop) {
    laptop->cena *= 0.95;
}

int main() {
    struct Laptop mojLaptop = initLaptop("Dell XPS 15", 5000.0);
    pokazLaptop(mojLaptop);
    zmniejszCene(&mojLaptop);
    printf("Po obnizce ceny:\n");
    pokazLaptop(mojLaptop);
    return 0;
}
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

typedef struct Ksiazka {
    char tytul[50];
    int liczba_stron;
} Ksiazka;

Ksiazka* initKsiazka(const char *tytul, int liczba_stron) {
    if(strlen(tytul) < 5 || liczba_stron <= 50)
        return NULL;

    Ksiazka* nowa_ksiazka = (Ksiazka*)malloc(sizeof(Ksiazka));
    strcpy(nowa_ksiazka->tytul, tytul);
    nowa_ksiazka->liczba_stron = liczba_stron;

    return nowa_ksiazka;
}

void pokazKsiazka(Ksiazka ksiazka) {
    printf("Tytul: %s, Liczba stron: %d\n", ksiazka.tytul, ksiazka.liczba_stron);
}

void dodajStrony(Ksiazka *ksiazka) {
    ksiazka->liczba_stron += 10;
}

int main() {
    Ksiazka* ksiazka1 = initKsiazka("Wojna i pokoj", 1200);

    if(ksiazka1 != NULL) {
        pokazKsiazka(*ksiazka1);
        dodajStrony(ksiazka1);
        pokazKsiazka(*ksiazka1);
    } else {
        printf("Nie udalo sie utworzyc ksiazki.\n");
    }

    free(ksiazka1);

    return 0;
}

Inne podejście do paradygmatu obiektowego

  • Jeśli chcemy zbudować 5 identycznych domów, nie potrzeba nam 5 projektów.
  • Jeśli chcemy wyprodukować 4 samochody, nie potrzebujemy oddzielnych projektów.
  • Programowanie obiektowe stara się pogrupować podobne „byty” na pewnym poziomie abstrakcji.

  1. Abstrakcja: Programowanie obiektowe pozwala na modelowanie rzeczywistych obiektów z życia codziennego w postaci klas i obiektów w programie. Dzięki temu, tworzenie i zarządzanie złożonymi systemami staje się bardziej intuicyjne.

  2. Enkapsulacja: Klasy (formuły na obiekty) mogą ukrywać swoje dane i metody przed innymi klasami, co zwiększa bezpieczeństwo i chroni przed nieautoryzowanym dostępem.

  3. Dziedziczenie: Dzięki dziedziczeniu możemy tworzyć nowe klasy na podstawie już istniejących, dziedzicząc ich atrybuty i metody. Umożliwia to ponowne wykorzystanie kodu i organizuje kod w hierarchiczny sposób.

  4. Polimorfizm: Różne klasy mogą definiować metody o tej samej nazwie, ale zachowujące się inaczej w zależności od klasy. Polimorfizm ułatwia rozszerzanie i modyfikowanie kodu.

  1. Modularność: Programowanie obiektowe promuje podział aplikacji na mniejsze, niezależne moduły (klasy), co ułatwia zarządzanie, testowanie i rozwijanie oprogramowania.

  2. Zwiększenie czytelności

  3. Elastyczność

Zadanie

  • Chcemy napisać program wyliczający wartość rynkową domu w zależności od ilości pokoi, posiadania garażu, ogrodu, ilości pięter, powierzchni, itp…

Pierwsza klasa - House

  • Na pewnym poziomie abstrakcji opisuje kawałek rzeczywistości.
  • Zawiera dane opisujące możliwe stany.
  • Zawiera metody opisujące możliwe zachowania.
  • Możemy ją zastosować do wielu budynków.
class House
{
    public int area;
    public boolean garage;
    public int rooms;
    public boolean garden;
    public int floors;

    public int getPrice()
    {
        return area * 3000;
    }
}

Pierwszy obiekt - johnHouse

  • Konkretny byt.
  • Ma konkretne własności.
House johnHouse = new House();
johnHouse.area= 200;
johnHouse.garage = true;
johnHouse.rooms = 7;
johnHouse.garden = false;
johnHouse.floors = 1;

System.out.printf("Price: %d%n",johnHouse.getPrice());

Po co taka konstrukcja?

  • Zmniejszenie luki reprezentacji.
  • Ułatwia podział pracy i współpracę między programistami.
  • Lepsze grupowanie, czytelność kodu.

Lego jako model programowania obiektowego.

Czy w Java jest możliwość skompilowania kodu bez żadnej klasy?

  • Odpowiedź brzmi nie. Do kompilacji potrzebna jest “widoczna” klasa ze statyczną metodą main.
  • Język Java jest silnie “zorientowany obiektowo”.

Definicje

Klasa – częściowa lub całkowita definicja dla obiektów. Definicja obejmuje dopuszczalny stan obiektów oraz ich zachowania. Obiekt, który został stworzony na podstawie danej klasy nazywany jest jej instancją. Klasy mogą być typami języka programowania - przykładowo, instancja klasy Owoc będzie mieć typ Owoc. Klasy posiadają zarówno interfejs, jak i strukturę. Interfejs opisuje, jak komunikować się z jej instancjami za pośrednictwem metod, zaś struktura definiuje sposób mapowania stanu obiektu na elementarne atrybuty.

Obiekt - jest to struktura zawierająca:

  • dane,
  • metody, czyli funkcje służące do wykonywania na tych danych określonych zadań.

(Źródło: Wikipedia.)

Java

Historia Javy

  1. 1991 rok: W firmie Sun Microsystems powstał projekt o nazwie “Green Team”, kierowany przez Jamesa Goslinga. Początkowy cel to stworzenie języka dla cyfrowych urządzeń konsumenckich, takich jak telewizory czy telefony.

  2. 1995 rok: Pierwsza oficjalna wersja Javy - Java 1.0, została zaprezentowana publicznie. Jej celem było zapewnienie “Napisz raz, uruchom wszędzie” (ang. “Write Once, Run Anywhere”).

  3. 1997 rok: Wydanie Java 1.1 przynosi wiele nowości, w tym pakiet AWT (Abstract Window Toolkit) umożliwiający tworzenie graficznych interfejsów użytkownika.

  1. 2000 rok: Debiut Java 2 (J2SE), wraz z wprowadzeniem wielu kluczowych technologii, takich jak Swing, Collections Framework i Java Naming and Directory Interface (JNDI).

  2. 2004 rok: Wydanie Javy 5 (lub Java 1.5). Wprowadzono wiele ważnych uaktualnień, takich jak generyki, metadata (adnotacje) oraz usprawnioną pętlę for (for-each).

  3. 2006 rok: Sun Microsystems udostępnia Javę jako wolne oprogramowanie na licencji GNU General Public License (GPL).

  4. 2010 rok: Firma Oracle przejmuje Sun Microsystems, stając się głównym opiekunem technologii Java.

Najnowsza wersja LTS 21 - 19 września 2023.

https://en.wikipedia.org/wiki/Java_(programming_language)

https://www.tiobe.com/tiobe-index/

Ustawienia wykładu

  • środowisko IntelliJ Idea Ultimate
  • OpenJDK 21

Dokumentacja https://docs.oracle.com/en/java/javase/21/docs/api/index.html

Ustawienia projektu w IntelliJ

Pierwszy kod - Hello World

public class Main {
    public static void main(String[] args) {
        System.out.println("Hello world!");
    }
}

Zasady nazewnictwa

  1. Klasy: Nazwy klas powinny zaczynać się wielką literą. Jeśli nazwa składa się z kilku słów, każde kolejne słowo powinno zaczynać się wielką literą. Nie używa się podkreślników. Przykład: UserData, StringBuilder.

  2. Interfejsy: Tak samo jak klasy, nazwy interfejsów powinny zaczynać się wielką literą i używać notacji wielbłądzia. Przykład: Listenable, Serializable.

  3. Metody: Nazwy metod zaczynają się małą literą, a każde kolejne słowo z wielkiej. Przykład: calculateTotal(), getUserData().

  1. Zmienne: Podobnie jak metody, nazwy zmiennych zaczynają się małą literą, a każde kolejne słowo z wielkiej. Przykład: totalCount, userData.

  2. Stałe: Stałe (czyli zmienne, które są final i static) pisane są wielkimi literami, a słowa oddzielane są podkreślnikami. Przykład: MAX_VALUE, PI.

  1. Pakiety: Nazwy pakietów powinny być pisane małymi literami. Jeśli pakiet składa się z kilku segmentów, powinny one być odseparowane kropkami. Zaleca się również, aby nazwy pakietów były unikalne, np. poprzez dodanie nazwy firmy lub domeny jako prefixu. Przykład: com.mycompany.mypackage.

  2. Typy generyczne: Jednoliterowe nazwy typów generycznych są powszechnie używane, chociaż nie jest to obowiązkowe. Typowy przykład to T w definicji klas i metod generycznych.

Zasady metody main

  1. Modyfikator dostępu: Metoda main musi być public, aby JVM (Java Virtual Machine) mógł ją wywołać z poza klasy, w której jest zdefiniowana.

  2. Modyfikator statyczny: Metoda musi być static, ponieważ JVM musi mieć możliwość jej wywołania bez tworzenia instancji klasy.

  3. Zwracany typ: Metoda main nie zwraca wartości, więc jej typem zwracanym jest void.

  1. Nazwa: Nazwą metody musi być dokładnie main.

  2. Parametry: Metoda main przyjmuje jeden argument, którym jest tablica ciągów znaków (String[]). Zwykle nazywa się go args, ale możesz użyć innej nazwy, jeśli chcesz.

Nazwa pliku a nazwa klasy

  1. Nazwa pliku: Nazwa pliku musi dokładnie odpowiadać nazwie klasy lub interfejsu, który jest w nim zdefiniowany, z zachowaniem wielkości liter.

  2. Rozszerzenie pliku: Pliki źródłowe Javy mają rozszerzenie .java.

  3. Jedna publiczna klasa lub interfejs na plik: Jeśli klasa lub interfejs jest zdefiniowany jako public, musi być to jedyna publiczna klasa lub interfejs w tym pliku, a nazwa pliku musi dokładnie odpowiadać nazwie tej klasy lub interfejsu. Na przykład, jeśli masz publiczną klasę o nazwie MyClass, musi ona być zdefiniowana w pliku o nazwie MyClass.java.

  1. Niepubliczne klasy: Możesz mieć wiele niepublicznych klas w jednym pliku źródłowym, ale pamiętaj, że nadal obowiązuje konwencja, że tylko jedna klasa lub interfejs może być publiczny w danym pliku.

  2. Pliki z główną metodą: Jeśli klasa zawiera metodę main (punkt wejścia do programu), zalecane jest, aby taka klasa była jedyną publiczną klasą w pliku, choć nie jest to wymogiem.

Komentarze w kodzie

  1. Komentarze jednoliniowe: Używane do krótkich uwag lub komentarzy dotyczących pojedynczej linii kodu.

    // To jest komentarz jednoliniowy
    int x = 5; // Komentarz po instrukcji
  1. Komentarze wieloliniowe: Używane do opisów wymagających kilku linii. Mogą one pojawiać się w dowolnym miejscu w kodzie.

    /* 
    To jest komentarz wieloliniowy.
    Może on rozciągać się na wiele linii.
    */
    int y = 10;

  1. Komentarze dokumentacyjne (JavaDoc): Używane do tworzenia dokumentacji API z kodu źródłowego. Narzędzie javadoc może przetwarzać te komentarze i generować sformatowaną dokumentację w postaci stron HTML.

    /**
     * Reprezentuje obiekt typu Example.
     * <p>
     * Używaj tej klasy, gdy potrzebujesz przykładu.
     * </p>
     *
     * @author Imię Nazwisko
     * @version 1.0
     */
    public class Example {
        /**
         * Zwraca przykładową wartość.
         *
         * @return wartość typu int
         */
        public int getSampleValue() {
            return 42;
        }
    }
  • Nie nadużywaj komentarzy: Dobrze napisany kod powinien być na tyle czytelny, że nie wymaga nadmiernego komentowania. Nazwy zmiennych, metod i klas powinny być wyraźne i opisowe.

  • Unikaj komentarzy oczywistych: Komentarze takie jak

i++; // zwiększ i o 1

są zbędne i tylko zaśmiecają kod.

  • Utrzymuj aktualność komentarzy: Upewnij się, że komentarze odzwierciedlają aktualny stan i funkcjonalność kodu. Stare, nieaktualne komentarze mogą wprowadzać w błąd czytającego.

  • Używaj komentarzy do wyjaśnienia “dlaczego”: Często bardziej wartościowe niż komentarze mówiące “co robi kod”, są te wyjaśniające “dlaczego kod działa w taki sposób”. Mogą one pomóc innym programistom zrozumieć założenia czy trudne decyzje projektowe.

Podstawowe typy danych

Java oferuje zestaw typów podstawowych (nazywanych też typami prostymi), które są używane do reprezentacji prostych wartości, takich jak liczby całkowite, liczby zmiennoprzecinkowe, znaki czy wartości logiczne.

  1. Liczby całkowite:
    • byte: 8-bitowa wartość liczby całkowitej; zakres od \(-128\) do \(127\).
    • short: 16-bitowa wartość liczby całkowitej; zakres od \(-32,768\) do \(32,767\).
    • int: 32-bitowa wartość liczby całkowitej; zakres od \(-2^{31}\) do \(2^{31}-1\).
    • long: 64-bitowa wartość liczby całkowitej; zakres od \(-2^{63}\) do \(2^{63}-1\).
  1. Liczby zmiennoprzecinkowe:
    • float: 32-bitowa wartość liczby zmiennoprzecinkowej (jednokrotnej precyzji). Zaleca się używanie sufiksu F lub f przy inicjalizacji wartości stałych.
    • double: 64-bitowa wartość liczby zmiennoprzecinkowej (podwójnej precyzji). Jest to domyślny typ dla wartości zmiennoprzecinkowych w Javie.
  1. Znak:
    • char: Reprezentuje pojedynczy znak w standardzie Unicode i zajmuje 16 bitów. Znaki są zamykane w pojedynczych cudzysłowach, np. 'A'.
  2. Wartość logiczna:
    • boolean: Reprezentuje wartość prawda/fałsz. Może przyjmować tylko jedną z dwóch wartości: true lub false.

Uwagi:

  • Typy podstawowe w Javie są zawsze o stałej wielkości, niezależnie od platformy.
  • Każdy z tych typów podstawowych (oprócz boolean) ma odpowiadającą mu klasę opakowującą w pakiecie java.lang (np. Integer dla int, Double dla double itp.). Klasy te są używane, gdy potrzebujemy reprezentować typ podstawowy jako obiekt, oraz oferują wiele pomocniczych metod do pracy z danym typem.
  • Inicjalizacja zmiennych typów prostych zawsze nadaje im domyślną wartość (np. 0 dla typów liczbowych, false dla boolean), ale wartości te są ustawiane domyślnie tylko dla zmiennych klasowych lub instancyjnych, nie dla lokalnych zmiennych wewnątrz metod.
public class Main {
    public static void main(String[] args) {
        int localVar; // zmienna lokalna niezainicjalizowana

        // Poniższa linia spowoduje błąd kompilacji, ponieważ próbujemy użyć zmiennej,
        // która nie została zainicjalizowana
        // System.out.println(localVar);

        localVar = 10; // inicjalizacja zmiennej lokalnej
        System.out.println(localVar); // teraz jest w porządku, ponieważ zmienna
        // została zainicjalizowana
    }
}

Operacje wyjścia na konsolę

W języku Java operacje wyjścia na konsolę odnoszą się głównie do wydruków tekstowych prezentowanych użytkownikowi na standardowym wyjściu (czyli zazwyczaj konsoli terminala).

  1. System.out.print() i System.out.println():

    • System.out.print("Tekst"): Drukuje podany tekst na konsoli bez przechodzenia do nowej linii.
    • System.out.println("Tekst"): Drukuje podany tekst na konsoli i przechodzi do nowej linii.

    Przykład:

    System.out.print("Witaj, ");
    System.out.println("świecie!");

    Wyjście:

    Witaj, świecie!
  1. System.out.printf(): Pozwala na formatowane wyjście. Umożliwia wstawianie zmiennych w określone miejsca w tekście oraz kontrolowanie sposobu ich prezentacji.

    Przykład:

    double cena = 12.5;
    int ilosc = 5;
    System.out.printf("Cena: %.2f, Ilość: %d, Łącznie: %.2f", cena, ilosc, cena * ilosc);

    Wyjście:

    Cena: 12.50, Ilość: 5, Łącznie: 62.50

Metoda System.out.printf() w Javie oraz funkcja printf() w języku C są do siebie podobne pod względem ogólnego przeznaczenia i użycia, ponieważ obie służą do formatowanego wydruku napisów i są inspirowane tym samym konceptem. Niemniej jednak, istnieje kilka różnic w używaniu tych funkcji/metod, zarówno pod względem składni, jak i funkcjonalności, które mogą wpłynąć na to, jak są używane w obu językach.

Formatowanie: dokumentacja.

Operacje wejścia z konsoli

Klasa Scanner jest często używana do wczytywania danych wejściowych z konsoli, ponieważ jest łatwa w użyciu i oferuje szeroki zakres funkcji do wczytywania różnych typów danych.

  1. Inicjalizacja: Aby użyć klasy Scanner, najpierw musisz ją zaimportować z pakietu java.util, a następnie utworzyć jej obiekt, używając System.in jako źródła wejściowego.

    import java.util.Scanner;
    
    public class Main {
        public static void main(String[] args) {
            Scanner scanner = new Scanner(System.in);
        }
    }
  1. Wczytywanie danych:

    • Wczytywanie całego wiersza tekstu:

      String line = scanner.nextLine();
    • Wczytywanie pojedynczego słowa:

      String word = scanner.next();
    • Wczytywanie danych liczbowych:

      int number = scanner.nextInt();       // dla int
      double value = scanner.nextDouble();  // dla double
      float valueFloat = scanner.nextFloat(); // dla float
    • Wczytywanie wartości logicznych:

      boolean isTrue = scanner.nextBoolean();
  1. Zamykanie skanera: Po zakończeniu wczytywania danych z konsoli warto zamknąć obiekt Scanner, aby zwolnić zasoby.

    scanner.close();

Więcej info: w dokumentacji.

Deklaracja bez inicjalizacji

Jeśli nie przypiszesz wartości początkowej do zmiennej podczas deklaracji, dla zmiennych lokalnych musisz to zrobić przed ich pierwszym użyciem. W przeciwnym razie kompilator zgłosi błąd. Zmienne instancji (pola klasy) otrzymują domyślne wartości.

int count;
double rate;

Deklaracja z inicjalizacją

int age = 30;
double salary = 5500.75;
char letter = 'A';
boolean isActive = true;

Słowo kluczowe var

Słowo kluczowe var zostało wprowadzone w Javie 10 w celu poprawy czytelności kodu poprzez lokalne inferencje typów dla deklaracji zmiennych. Umożliwia to deklarowanie zmiennych bez konieczności jawnego podawania ich typu, o ile można jednoznacznie wywnioskować typ zmiennej na podstawie przypisywanej jej wartości.

  1. Lokalne inferencje typów: var może być używane tylko do deklaracji lokalnych zmiennych (wewnątrz metod, bloków, pętli itp.), inicjalizowanych zmiennych dla instrukcji for oraz zmiennych lokalnych w try-with-resources.

  2. Nie można używać z wartościami null: Ponieważ var opiera się na inferencji typów, nie można zadeklarować zmiennej jako var i jednocześnie przypisać jej wartości null, ponieważ kompilator nie będzie mógł wywnioskować typu.

    // Błędne
    // var something = null;
  1. Nie można używać dla pól klasy, metod, ani zmiennych zewnętrznych: Słowo kluczowe var nie jest dozwolone dla deklaracji pól klasy, sygnatur metod ani jako typ zwracany.
  1. Czytelność kodu: Używanie var może poprawić czytelność, zwłaszcza gdy typ zmiennej jest oczywisty z kontekstu lub kiedy typ właściwy jest długi i skomplikowany (np. typy generyczne). Jednak w innych sytuacjach jawnie podane typy mogą być bardziej czytelne. Należy stosować var z rozwagą.

    var list = new ArrayList<String>();  // List<String>
    var stream = list.stream();           // Stream<String>
  2. Nie można używać dla zmiennych bez inicjalizacji: Deklaracja zmiennej z użyciem var musi odbywać się jednocześnie z jej inicjalizacją.

  1. Typ jest nadal statyczny: Mimo że jawnie nie deklaruje się typu zmiennej, Java jest językiem o statycznej typizacji, więc typ zmiennej jest ustalony w czasie kompilacji.

Przykład:

var number = 10;  // inferencja typu int
var text = "Hello";  // inferencja typu String

Stałe

Deklaracja: Używając modyfikatora final podczas deklarowania zmiennej, sprawiamy, że staje się ona stałą. Oznacza to, że jej wartość nie może zostać zmieniona po pierwszym przypisaniu.

final int MAX_COUNT = 100;

Inicjalizacja: Stała zmienna musi być zainicjowana w momencie jej deklaracji lub w konstruktorze (w przypadku zmiennych instancji).

Nazewnictwo: Konwencją jest nazywanie stałych zmiennych używając wielkich liter i podkreślników, aby łatwo je rozpoznać w kodzie, choć to nie jest wymogiem języka.