→ Slide 1

Programowanie obiektowe w języku Java

→ Slide 2

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:

→ Slide 3

Diagram: schemat klas (przykład)

Składowe klasy:

→ Slide 4

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

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
    }
}
→ Slide 6

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;
}
public Complex() {
    this(0.0, 0.0);  // wywołanie innego konstruktora
    // dodatkowa logika, jeśli potrzebna
}
→ Slide 7

Destruktor i garbage collection

→ Slide 8

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.

public Complex add(Complex other) {
    return new Complex(this.re + other.re, this.im + other.im);
}
→ Slide 9

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
    }
}
→ Slide 10

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:

→ Slide 11

Modyfikatory dostępu

→ Slide 12

Zasady stosowania

→ Slide 13

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;
    }
}
→ Slide 14

Read-only i Write-only

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

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

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

→ Slide 17

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); 
    }
}
→ Slide 18

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);
    }
}
→ Slide 19

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

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

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
}
→ Slide 22

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

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
}
→ Slide 24

final - klasy

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

public final class MathUtils {
    public static double PI = 3.141592653589793;
    public static double E = 2.718281828459045;
}
→ Slide 25

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

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

Ć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ć:

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

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

→ Slide 28

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

→ Slide 29

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ć:

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

Opcjonalnie:

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.