→ Slide 1

Programowanie obiektowe w języku Java

→ Slide 2

Object-Oriented Programming (OOP) to paradygmat programowania, który grupuje dane (pola) i zachowania (metody) w jednym bycie: obiekcie.

Główne zasady OOP:

  • Enkapsulacja - ukrywanie danych i kontrolowany dostęp do nich
  • Abstrakcja - modelowanie rzeczywistości przez klasy i obiekty
  • Dziedziczenie - ponowne użycie kodu
  • Polimorfizm - wiele zachowań pod jednym interfejsem
→ Slide 3

Składowe klasy:

  • pola
  • metody
  • konstruktory
→ Slide 4

Klasa - definicja obiektu (pól składowych i metod).
Nazwa pliku powinna być taka sama jak nazwa klasy.

package com.UMK;              // nazwa pakietu
 
public class Complex {        // nazwa klasy
 
    private double re;        // pola składowe
    private double im;
 
    public Complex() {        // konstruktor domyślny
        this.re = 0.0;
        this.im = 0.0;
    }
 
    public Complex(double re, double im) { // konstruktor z parametrami
        this.re = re;
        this.im = im;
    }
 
    public String toString() {    // metoda
        return re + " + " + im + "i";
    }
    // ...
}
→ Slide 5

Obiekt - instancja klasy; niezależny byt klasy, mający przypisane swoje miejsce w pamięci

// Main.java
package com.UMK;
 
public class Main {
    public static void main(String[] args) throws Exception {
 
        Complex z = new Complex();
        Complex w = new Complex(1.0, 2.0);
 
        z.add(w); 
 
        Complex x = null;          // deklaracja zmiennej referencyjnej
        x = new Complex(3.0, 4.0); // przypisanie obiektu do zmiennej x
        x = z;           // x i z wskazują na ten sam obiekt w pamięci
    }
}
→ Slide 6

Konstruktor kopiujący - konstruktor, który tworzy nowy obiekt na podstawie istniejącego obiektu tej samej klasy.

public Complex(Complex other) {
    this.re = other.re;
    this.im = other.im;
}
  • this - odwołanie do aktualnego obiektu
  • za pomocą this() można wywołać inny konstruktor z aktualnego konstruktora (constructor chaining). Wywołanie this() musi być pierwszą instrukcją w konstruktorze.
public Complex() {
    this(0.0, 0.0);  // wywołanie innego konstruktora
    // dodatkowa logika, jeśli potrzebna
}
→ Slide 7
  • W Javie nie ma destruktorów. Zarządzanie pamięcią realizowane przez mechanizm garbage collection.
  • Garbage collector automatycznie zwalnia pamięć zajmowaną przez obiekty, które nie są już używane (nie mają referencji)
  • Można nadpisać metodę finalize() - od Javy 9 oznaczona jako przestarzała (deprecated), jej działanie jest nieprzewidywalne i może prowadzić do problemów z wydajnością
  • Zamiast destruktorów, w Javie stosuje się wzorce projektowe, takie jak try-with-resources, które automatycznie zamykają zasoby, takie jak pliki czy połączenia sieciowe, po ich użyciu.
→ Slide 8

Metoda - funkcja zdefiniowana wewnątrz klasy, która operuje na danych tej klasy (pola) i może być wywoływana na obiektach tej klasy.

  • definicja: nazwa, lista parametrów, typ zwracany
  • metody mogą być przeciążane (overloading) - wiele metod o tej samej nazwie, ale różnych parametrach.
public Complex add(Complex other) {
    return new Complex(this.re + other.re, this.im + other.im);
}
  • return - zwraca wartość z metody i kończy jej działanie.
  • void - typ zwracany, gdy metoda nie zwraca wartości
→ Slide 9

W Javie można tworzyć metody rekurencyjne

public class Factorial {
    public static int factorial(int n) {
        if (n == 0) return 1; 
        return n * factorial(n - 1);  // wywołanie rekurencyjne
    }
}
→ Slide 10

Enkapsulacja - ukrycie implementacji klasy i udostępnienie tylko niezbędnego interfejsu (gettery, settery, metody). Chroni dane przed niepożądanymi modyfikacjami i poprawia modularność.

Korzyści z enkapsulacji:

  • Bezpieczeństwo danych - kontrola nad tym, jak dane są modyfikowane
  • Łatwiejsza konserwacja i zmiany implementacji
  • Kontrola poprawności przez walidację w setterach
  • Możliwość tworzenia pól tylko do odczytu lub tylko do zapisu
  • Ukrywanie złożoności implementacji przed użytkownikami klasy
  • Ułatwienie testowania i debugowania przez izolację danych i zachowań
→ Slide 11
  • private - pola i metody dostępne tylko wewnątrz klasy
  • brak modyfikatora package private - dostępny tylko w pakiecie, w którym jest zdefiniowany
  • protected - dostęp w pakiecie i w klasach pochodnych
  • public - dostęp globalny
→ Slide 12
  • stosowanie jak najbardziej restrykcyjnych modyfikatorów dostępu
  • unikanie publicznych pól, chyba, że są stałe (final)
  • tworzenie setterów i/lub getterów dla prywatnych pól w celu ich „odkrywania”
    public double getRe() { return re; }
    public void setRe(double re) { this.re = re; }
  • stosowanie modyfikatora protected tylko wtedy, gdy jest to konieczne do dziedziczenia
  • unikanie modyfikatora package private, chyba że jest to celowe (np. do testów jednostkowych)
  • stosuj walidację danych w setterach
→ Slide 13
public class Student {
    private String name;
    private int age;
 
    public Student(String name, int age) {
        this.name = name;
        setAge(age);     // walidacja w setterze
    }
 
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
 
    public int getAge() { return age; }
    public void setAge(int age) {
        if (age < 0) throw new IllegalArgumentException("Wiek nie może być ujemny");
        this.age = age;
    }
}
→ Slide 14
  • Read-only: brak settera — pole tylko do odczytu
  • Write-only: brak gettera — np. przechowywanie hasła tylko przez setter (hashowanie wewnątrz).
public class User {
    private final String id;      // tylko do odczytu
    private String passwordHash;  // tylko zapis przez setter
 
    public User(String id) { this.id = id; }
    public String getId() { return id; }
 
    public void setPassword(String password) {
        this.passwordHash = hash(password);
    }
 
    private String hash(String s) { 
        return Integer.toHexString(s.hashCode()); 
    }
}
→ Slide 15

Pola statyczne (static) są współdzielone między wszystkimi obiektami tej klasy

public class Complex {
    public static final double PI = 3.141592653589793;
}

Dostępne są bezpośrednio z poziomu klasy (np. Complex.PI) lub z poziomu obiektu (np. z.PI), ale to nie jest zalecane, bo sugeruje, że pole jest związane z obiektem, a nie z klasą.

public class Main {
    public static void main(String[] args) {
        System.out.println(Complex.PI); // poprawne
        Complex z = new Complex();
        System.out.println(z.PI); // niezalecane, ale zadziała
    }
}
→ Slide 16

Metody statyczne można wywoływać bez konieczności instancjonowania klasy.

public class Complex {
    public static Complex add(Complex a, Complex b) {
        return new Complex(a.getRe() + b.getRe(), a.getIm() + b.getIm());
    }
}
public class Main {
    public static void main(String[] args) {
        Complex z1 = new Complex(1.0, 2.0);
        Complex z2 = new Complex(3.0, 4.0);
        Complex z3 = Complex.add(z1, z2);
    }
}

Są odpowiednikiem funkcji globalnych w innych językach programowania (np. C, Python).

  • metody statyczne nie mają dostępu do instancji klasy (nie mogą używać słowa kluczowego this), więc nie mogą odwoływać się do pól i metod niestatycznych
  • metody statyczne są często używane do implementacji funkcji pomocniczych, które nie wymagają stanu obiektu (np. Math.abs(x)))
  • metoda main() jest statyczna bo jest punktem wejścia do programu i musi być wywoływana bez tworzenia obiektu klasy, w której się znajduje
→ Slide 17

Przykład metody statycznej jako fabryki:

public class Complex {
    private double re, im;
    public Complex(double re, double im){ 
        this.re = re; 
        this.im = im; 
    }
 
    public static Complex of(double re, double im) { 
        return new Complex(re, im); 
    }
}
→ Slide 18

Klasa statyczna zawiera tylko statyczne pola i metody, a konstruktor tej klasy jest oznaczony jako prywatny, aby uniemożliwić tworzenie obiektów tej klasy

public class ComplexMath {
    private ComplexMath() { }
 
    public static double module(Complex z) {
        return Math.sqrt(z.getRe() * z.getRe() + z.getIm() * z.getIm());
    }
}
public class Main {
    public static void main(String[] args) {
        Complex x = new Complex();
        // ComplexMath cm = new ComplexMath(); // To nie zadziała
        ComplexMath.module(x);
    }
}
→ Slide 19

W Javie istnieje jeden przypadek, gdzie do definicji klasy używa się static - zagnieżdżone klasy statyczne (inner class). Do ich instancjonowania nie trzeba instancjonować klasy zewnętrznej. Taka klasa wewnętrzna ma dostęp jedynie do statycznych pól i metod klasy zewnętrznej.

public class Foo {
    public static class Bar { }
    public class Bar2 { }
}
public static void main(String[] args) {
    Foo foo = new Foo();
    Foo.Bar bar = new Foo.Bar();
    // Foo.Bar2 bar2 = new Foo.Bar2(); // To nie zadziała, bo Bar2 jest niestatyczna
    Foo.Bar2 bar2 = foo.new Bar2();
}
→ Slide 20

Wewnątrz klasy można stworzyć blok statyczny, którego zawartość zostanie wywołana przed utworzeniem obiektu (przed wywołaniem konstruktora klasy). Stosuje się to do inicjalizacji pól statycznych, które nie mogą być zainicjalizowane w miejscu deklaracji (np. gdy wymagają obsługi wyjątków) lub do wykonania kodu, który musi być uruchomiony tylko raz, niezależnie od liczby obiektów.

public class Foo {
    static {
        System.out.println("HELLO WORLD PRZED KONSTRUKTOREM");
    }
 
    public Foo() {
        System.out.println("HELLO WORLD Z KONSTRUKTORA");
    }
}
public class Main {
    public static void main(String[] args) {
        Foo foo = new Foo();
    }
}
→ Slide 21

Zmienna oznaczona final może mieć przypisaną wartość jedynie raz (np w konstruktorze). Obiekt przypisany do zmiennej final może być modyfikowany, ale nie można przypisać do tej zmiennej innego obiektu.

public class Zwierze {
    public final int LICZBA_LAP = 4;
    public String imie = "";
}
public static void main(String[] args) {
    final Zwierze pies = new Zwierze();
    // pies.LICZBA_LAP = 8;   // To nie zadziała
    // pies = new Zwierze();  // To nie zadziała
    pies.imie = "Szarik";     // A to zadziała
}
→ Slide 22

W Javie wszystkie metody są wirtualne; aby zapobiec nadpisaniu metody w klasach potomnych, należy oznaczyć ją jako final.

public class Zwierze {  
    public final void dajGlos() {
        System.out.println("...");
    }
}
→ Slide 23

Argument oznaczony jako final nie może być modyfikowany wewnątrz metody (nie można mu przypisać innej wartości), ale można modyfikować stan obiektu, jeśli argument jest typem referencyjnym.

public void foo(final int x) {
    // x = 5; // To nie zadziała
}
→ Slide 24

Klasa oznaczona jako final nie może być klasą bazową dla innej klasy - nie można po niej dziedziczyć.

  • w klasie final nie można zdefiniować metod abstrakcyjnych
  • klasa final może być używana do tworzenia niezmiennych klas (immutable classes), które są bezpieczne do współdzielenia między wątkami i nie wymagają synchronizacji
public final class MathUtils {
    public static double PI = 3.141592653589793;
    public static double E = 2.718281828459045;
}
→ Slide 25

Typy ogólne (generics) to szablony pozwalające tworzyć klasy sparametryzowane typami. Pozwalają uniknąć powielania implementacji i niepotrzebnych rzutowań. Typy ogólne konkretyzować można jedynie typami obiektowymi. Parametrem nie może być typ prosty.

public class Complex<T> {
    private T re;
    private T im;
 
    public Complex<T> add(Complex<T> other) {  /* */ }
    public static <T> Complex valueOf(T real, T imag) { /* */ }
}
public static void main(String[] args) {
    Complex<Integer> complexInt = new Complex<Integer>();
    Complex<Float> complexFloat = new Complex<Float>();
}
→ Slide 26

Refleksja pozwala na dynamiczne ładowanie klas i tworzenie obiektów bez konieczności znajomości ich typu w czasie kompilacji.

Może rzucić wyjątek, jeśli klasa o podanej nazwie nie istnieje lub nie można jej zainicjalizować.

package com.UMK;
 
public class Main {
    public static void main(String[] args) throws Exception {
        Class<?> klasaComplex = Class.forName("com.UMK.Complex");
        Complex a = (Complex) klasaComplex.getDeclaredConstructor().newInstance();
    }
}
→ Slide 27

Zaimplementuj klasę Complex reprezentującą liczby zespolone w postaci z = re + im i, gdzie re to część rzeczywista, a im to część urojona liczby zespolonej.
Klasa powinna zawierać:

  • pola składowe re i im typu double
  • konstruktor domyślny (ustawiający $z = 0 + 0i$)
  • konstruktor z parametrami (ustawiający $a + b \, i$)
  • metody wykonujące podstawowe działania matematyczne (+, -, *, /)
  • metodę toString() zwracającą reprezentację liczby zespolonej w postaci: 3 + 4 i
  • metodę module() obliczającą moduł liczby zespolonej (moduł $z = \sqrt{re^2 + im^2}$)
  • metodę argument() obliczającą argument liczby zespolonej (argument $z = \arctan{\frac{im}{re}}$)
  • metodę conjugate() zwracającą sprzężenie liczby zespolonej (sprzężenie $z = re - im i$)
  • metodę statyczną valueOf(double re, double im) tworzącą nowy obiekt klasy Complex na podstawie podanych wartości rzeczywistej i urojonej
  • pole statyczne finalne I reprezentujące jednostkę urojoną $0 + 1 i$
  • pole statyczne finalne ZERO reprezentujące liczbę zespoloną $0 + 0 i$
  • pole statyczne finalne ONE reprezentujące liczbę zespoloną $1 + 0 i$

Utwórz klasę statyczną ComplexUtils zawierającą metody pomocnicze do operacji na liczbach zespolonych, takie jak:

  • obliczanie sumy, różnicy, iloczynu i ilorazu dwóch liczb zespolonych
  • obliczanie modułu, argumentu i sprzężenia liczby zespolonej
  • obliczanie potęgi liczby zespolonej (np. $z^n$) dla naturalnego $n$

Napisz program testujący działanie klasy Complex i ComplexUtils.

→ Slide 28

Zaimplementuj klasę Complex<T> reprezentującą liczby zespolone, gdzie T jest typem ogólnym. Zakładamy, że T będzie typem numerycznym (np. Integer, Double, BigDecimal). Zaimplementuj operacje podobne jak w poprzednim ćwiczeniu, ale z uwzględnieniem typów ogólnych.

→ Slide 29

Zaimplementuj klasę Fraction (ułamek) reprezentującą liczbę w postaci ułamka zwykłego (np. $\frac{13}{42}$), określonego przez licznik i mianownik typu całkowitego. Klasa powinna zawierać:

  • konstruktory
    • domyślny (ustawiający ułamek na $\frac{0}{1}$)
    • z parametrami (licznik i mianownik)
    • z parametrem typu zmiennoprzecinkowego (np. 0.75 → $\frac{3}{4}$). Wystarczy wartość przybliżona, dokładność może być określona w drugim argumencie konstruktora.
    • z parametrem typu całkowitego (np. 5 → $\frac{5}{1}$)
    • konstruktor kopiujący
  • metody wykonujące podstawowe operacje arytmetyczne: add, subtract, multiply, divide
  • settery i gettery dla licznika i mianownika
  • operację zamiany ułamka do napisu (toString()) w postaci licznik / mianownik
  • operację skracania ułamka do najprostszej postaci (do skracania użyj algorytmu Euklidesa wyznaczający największy wspólny dzielnik (NWD) licznika i mianownika)
  • statyczne pole finalne ONE reprezentujące wartość $\frac{1}{1}$
  • statyczne pole finalne ZERO reprezentujące wartość $\frac{0}{1}$
  • metodę doubleValue() zwracającą wartość ułamka jako liczbę zmiennoprzecinkową
  • metodę statyczną tworzącą nowy ułamek na podstawie dwóch liczb całkowitych (valueOf(int licznik, int mianownik))

Zaimplementuj klasę statyczną FractionUtils zawierającą metody pomocnicze do operacji na ułamkach, takie jak:

  • obliczanie największego wspólnego dzielnika (NWD) dwóch liczb całkowitych (wykorzystując algorytm Euklidesa)
  • obliczanie najmniejszej wspólnej wielokrotności (NWW) dwóch liczb całkowitych (wykorzystując NWD) $$NWW(a, b) = \frac{|a \cdot b|}{NWD(a, b)}$$
  • obliczenia sumy, różnicy, iloczynu i ilorazu dwóch ułamków

Opcjonalnie:

  • niech klasa reprezentująca ułamek Fraction oraz FractionUtils będzie klasą generyczną, która może przyjmować różne typy liczb, jeśli tylko są one typu całkowitego (np. Fraction<Integer>, Fraction<Long>, Fraction<BigInteger>)

Napisz program testujący działanie klasy Fraction i FractionUtils. Niech program wyznaczy przybliżoną sumę szeregu $\sum_{n=1}^{\infty} \frac{1}{n^2}$ dla pierwszych $N$ wyrazów (gdzie $N$ podaje użytkownik). Porównaj wynik z wartością $\frac{\pi^2}{6}$ (znaną z rozwiązania problemu bzylejskiego) i oblicz błąd bezwzględny między tymi wartościami.