Ślad: • 03_obiekty

Programowanie obiektowe w języku Java

Writing /home/grochu/public_html/wiki/data/cache/f/fbc5e7c568eb614b064536b933d9b2ee.i failed
Unable to save cache file. Hint: disk full; file permissions; safe_mode setting.
Writing /home/grochu/public_html/wiki/data/meta/zajecia/java_2026_1/03_obiekty.meta failed
Unable to save metadata file. Hint: disk full; file permissions; safe_mode setting.
Writing /home/grochu/public_html/wiki/data/cache/7/75e079481936e0d8ade254ee02e13f30.i failed
Writing /home/grochu/public_html/wiki/data/meta/zajecia/java_2026_1/sidebar.meta failed
Writing /home/grochu/public_html/wiki/data/cache/f/fbc5e7c568eb614b064536b933d9b2ee.i failed
Unable to save cache file. Hint: disk full; file permissions; safe_mode setting.

REVEAL theme=simple&disableLayout=0&transition=none&controls=1&show_progress_bar=1&build_all_lists=0&show_image_borders=0&horizontal_slide_level=2&enlarge_vertical_slide_headers=0&show_slide_details=1&open_in_new_window=1&size=1024×768

<html> <style type=„text/css”> p { text-align: left;} .reveal pre code {

height: 100%;
max-height: 100%;
box-sizing: border-box; }

</style> </html>

Programowanie obiektowe w języku Java

Programowanie obiektowe

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

Diagram: schemat klas (przykład)

Składowe klasy:

  • pola
  • metody
  • konstruktory

Definicja klasy

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";
    }
    // ...
}

Instancjonowanie klasy

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
    }
}

Konstruktory

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
}

Destruktor i garbage collection

  • 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.

Metody

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

Rekurencja

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
    }
}

Enkapsulacja

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ń

Modyfikatory dostępu

  • 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

Zasady stosowania

  • 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

Przykład enkapsulacji

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;
    }
}

Read-only i Write-only

  • 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()); 
    }
}

Pola statyczne

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
    }
}

Metody statyczne

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


Przykład

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); 
    }
}

Klasy statyczne

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);
    }
}

Klasy statyczne - cd

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();
}

Bloki statyczne

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();
    }
}

final - zmienne niezmienne

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
}

final - metody

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("...");
    }
}

final - argument metody

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
}

final - klasy

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;
}

Typy ogólne (generyczne)

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>();
}

Tworzenie obiektów przez reflekcję

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();
    }
}

Ćwiczenie: Klasa Complex

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.

Ćwiczenie: Klasa Complex z typami ogólnymi

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.

Zadanie 2. Klasa Fraction

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.