Object-Oriented Programming (OOP) to paradygmat programowania, który grupuje dane (pola) i zachowania (metody) w jednym bycie: obiekcie.
Główne zasady OOP:
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"; } // ... }
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 } }
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() 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 }
finalize() - od Javy 9 oznaczona jako przestarzała (deprecated), jej działanie jest nieprzewidywalne i może prowadzić do problemów z wydajnością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); }
return - zwraca wartość z metody i kończy jej działanie.void - typ zwracany, gdy metoda nie zwraca wartościW 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 - 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:
final) public double getRe() { return re; } public void setRe(double re) { this.re = re; }
protected tylko wtedy, gdy jest to konieczne do dziedziczeniapackage private, chyba że jest to celowe (np. do testów jednostkowych)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; } }
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 (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 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).
this), więc nie mogą odwoływać się do pól i metod niestatycznychMath.abs(x)))main() jest statyczna bo jest punktem wejścia do programu i musi być wywoływana bez tworzenia obiektu klasy, w której się znajdujePrzykł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); } }
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); } }
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(); }
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(); } }
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 }
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("..."); } }
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 }
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; }
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>(); }
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(); } }
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ć:
re i im typu double+, -, *, /)toString() zwracającą reprezentację liczby zespolonej w postaci: 3 + 4 imodule() obliczającą moduł liczby zespolonej (moduł $z = \sqrt{re^2 + im^2}$)argument() obliczającą argument liczby zespolonej (argument $z = \arctan{\frac{im}{re}}$)conjugate() zwracającą sprzężenie liczby zespolonej (sprzężenie $z = re - im i$)valueOf(double re, double im) tworzącą nowy obiekt klasy Complex na podstawie podanych wartości rzeczywistej i urojonejI reprezentujące jednostkę urojoną $0 + 1 i$ZERO reprezentujące liczbę zespoloną $0 + 0 i$ONE reprezentujące liczbę zespoloną $1 + 0 i$
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.
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.
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ć:
add, subtract, multiply, dividetoString()) w postaci licznik / mianownikONE reprezentujące wartość $\frac{1}{1}$ZERO reprezentujące wartość $\frac{0}{1}$doubleValue() zwracającą wartość ułamka jako liczbę zmiennoprzecinkowąvalueOf(int licznik, int mianownik))
Zaimplementuj klasę statyczną FractionUtils zawierającą metody pomocnicze do operacji na ułamkach, takie jak:
Opcjonalnie:
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.