Programowanie obiektowe

Wykład 5

Klasa/obiekt - cd.

Dalsze przykłądy

Przykład: Tablice i listy tablicowe obiektów.

class Book {
    private String title;
    private double price;

    public Book() {
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        if (title != null && !title.isEmpty()) {
            this.title = title;
        } else {
            this.title = "";
        }
    }

    public double getPrice() {
        return price;
    }

    public void setPrice(double price) {
        if (price >= 0) {
            this.price = price;
        } else {
            this.price = 0;
        }
    }
}

Przykład: metoda zwracająca obiekt.

class Book {
    private String title;
    private double price;

    public Book() {
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        if (title != null && !title.isEmpty()) {
            this.title = title;
        } else {
            this.title = "";
        }
    }

    public double getPrice() {
        return price;
    }

    public void setPrice(double price) {
        if (price >= 0) {
            this.price = price;
        } else {
            this.price = 0;
        }
    }
}

Przykład: opisywanie obiektów.

class Book {
    private String title;
    private double price;

    public Book() {
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        if (title != null && !title.isEmpty()) {
            this.title = title;
        } else {
            this.title = "";
        }
    }

    public double getPrice() {
        return price;
    }

    public void setPrice(double price) {
        if (price >= 0) {
            this.price = price;
        } else {
            this.price = 0;
        }
    }
}

Zasady tworzenia pakietów

Pakiety w Javie służą do grupowania klas, interfejsów i innych elementów w organizowane jednostki. Pakiety pełnią wiele funkcji, w tym zapewnianie przestrzeni nazw dla klas, ograniczanie dostępu do klas oraz pomaganie w zarządzaniu kodem.

  1. Deklaracja pakietu: W pliku źródłowym Java, deklaracja pakietu powinna znajdować się na samym początku (przed wszystkimi importami i deklaracjami klas). Wygląda to następująco:

    package com.example.mypackage;
  2. Konwencja nazewnictwa: Pakiety powinny być nazywane małymi literami. Zwykle stosuje się odwrotną notację domenową (np. com.example.projectname). Umożliwia to uniknięcie konfliktów nazw w międzynarodowym środowisku.

  1. Struktura katalogów: Struktura katalogów projektu powinna odzwierciedlać strukturę pakietów. Na przykład dla pakietu com.example.mypackage, odpowiednia struktura katalogów to com/example/mypackage/.

Dobre praktyki:

  1. Organizacja: Klasy o podobnej funkcjonalności lub przeznaczeniu powinny być grupowane w tym samym pakiecie. Pomaga to w organizacji kodu i jego zrozumieniu.

  2. Zachowanie niewielkich pakietów: Zamiast tworzyć jeden duży pakiet z wieloma klasami, lepiej jest stworzyć kilka mniejszych pakietów. Dzięki temu łatwiej zarządzać i rozumieć kod.

  3. Używanie podpakietów: Możesz organizować kody w hierarchii pakietów, tworząc podpakiety. Na przykład com.example.projectname.models, com.example.projectname.controllers itp.

  1. Ograniczenie dostępu: Za pomocą modyfikatorów dostępu (public, protected, private, domyślny) oraz pakietów można kontrolować dostępność klas, metod i zmiennych. Dzięki temu możesz ukryć szczegóły implementacji i eksponować tylko potrzebne interfejsy.

  2. Unikanie konfliktów nazw: Dzięki pakietom możesz mieć klasy o tej samej nazwie w różnych pakietach bez konfliktów.

  3. Zachowanie spójności: Jeśli pracujesz w zespole lub nad dużym projektem, dobrze jest przyjąć spójne konwencje nazewnictwa i struktury pakietów, aby kod był łatwo zrozumiały dla wszystkich uczestników.

Prywatna klasa w Javie?

W Javie nie możemy zadeklarować klas na poziomie najwyższym (top-level) jako private. Modyfikator private dla klasy na poziomie najwyższym byłby niezrozumiały, ponieważ nie miałby żadnego zastosowania: nie można by było uzyskać do niej dostępu z żadnego innego miejsca w kodzie.

Jednakże, możemy zadeklarować klasę jako private, jeśli jest to klasa wewnętrzna (inner class) lub klasa zagnieżdżona (nested class).

Jak czas będzie - to na koniec semestru.

Przypisywanie obiektów

Kiedy przypisujesz jeden obiekt do drugiego, faktycznie przypisujesz jedynie referencję do obiektu, a nie sam obiekt. Oznacza to, że obie zmienne odnoszą się do tego samego miejsca w pamięci (do tej samej instancji obiektu). Zmienienie jednego obiektu wpłynie więc na drugi, ponieważ obie zmienne wskazują na ten sam obiekt.

public class TestPerson {
    public static void main(String[] args) {
        Person person1 = new Person("John");
        Person person2 = person1;
        System.out.println(person1.getName());
        System.out.println(person2.getName());
        person2.setName("Doe");
        System.out.println(person1.getName());
        System.out.println(person2.getName());
    }
}


class Person {
    private String name;

    public Person(String name) {
        this.name = name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

}

Konstruktor

Konstruktor to specjalny rodzaj metody służącej do inicjalizacji obiektu. Jest on wywoływany podczas tworzenia instancji obiektu, najczęściej przy użyciu słowa kluczowego new. Konstruktory mają pewne unikalne cechy w porównaniu do standardowych metod:

  1. Nazwa konstruktora musi być taka sama jak nazwa klasy.
  2. Konstruktor nie ma typu zwracanego, nie nawet void.
  3. Klasy mogą mieć wiele konstruktorów, o różnych sygnaturach (różne argumenty). Jest to nazywane przeciążeniem konstruktora.
public class TestVehicle {

    public static void main(String[] args) {
        // Użycie konstruktorów:
        Vehicle vehicle1 = new Vehicle(); // używa konstruktora domyślnego
        Vehicle vehicle2 = new Vehicle("Toyota"); // używa konstruktora z jednym argumentem
        Vehicle vehicle3 = new Vehicle("Honda", 2); // używa konstruktora z dwoma argumentami
    }
}

class Vehicle {

    private String brand;
    private int wheels;

    // Konstruktor domyślny (bezargumentowy)
    public Vehicle() {
        this.brand = "Unknown";
        this.wheels = 4;
    }

    // Konstruktor z jednym argumentem
    public Vehicle(String brand) {
        this.brand = brand;
        this.wheels = 4;
    }

    // Konstruktor z dwoma argumentami
    public Vehicle(String brand, int wheels) {
        this.brand = brand;
        this.wheels = wheels;
    }

}

Wywołanie jednego konstruktora w innym to technika, która pozwala na ponowne użycie kodu konstruktora w obrębie tej samej klasy. Często jest stosowana, by unikać powtarzania tej samej logiki inicjalizacji w różnych konstruktorach. Aby wywołać jeden konstruktor z innego w Javie, używane jest słowo kluczowe this z odpowiednimi argumentami.

Wywołanie innego konstruktora za pomocą this musi być pierwszą instrukcją w konstruktorze.

public class TestLaptop {

    public static void main(String[] args) {
        // Użycie konstruktorów:
        Laptop laptop1 = new Laptop("Dell"); // używa konstruktora z jednym argumentem
        Laptop laptop2 = new Laptop("Apple", 16); // używa konstruktora z dwoma argumentami
    }
}


class Laptop {

    private String brand;
    private int memory;
    private boolean hasSSD;

    // Konstruktor główny
    public Laptop(String brand, int memory, boolean hasSSD) {
        this.brand = brand;
        this.memory = memory;
        this.hasSSD = hasSSD;
    }

    // Konstruktor z jednym argumentem - wywołuje konstruktor główny
    public Laptop(String brand) {
        this(brand, 8, true); // Wywołuje konstruktor główny z domyślnymi wartościami dla pamięci i SSD
    }

    // Konstruktor z dwoma argumentami - wywołuje konstruktor główny
    public Laptop(String brand, int memory) {
        this(brand, memory, true); // Wywołuje konstruktor główny z domyślną wartością dla SSD
    }

}

Niszczenie obiektów

W wielu językach programowania, w których obiekty są tworzone dynamicznie, kluczowym zagadnieniem jest zarządzanie pamięcią i niszczenie obiektów, które nie są już potrzebne. Java korzysta z mechanizmu zwanego “garbage collection” (GC) do automatycznego odzyskiwania pamięci zajmowanej przez obiekty, które nie są już osiągalne.

  1. Automatyczne zwalnianie pamięci: W Javie nie musisz jawnie usuwać obiektów. Mechanizm garbage collection automatycznie wykrywa obiekty, które nie są już osiągalne, i zwalnia pamięć, którą zajmują.

  2. Osiągalność: Obiekt jest uznawany za nieosiągalny, gdy nie istnieją żadne odniesienia do niego. Innymi słowy, jeśli nie ma sposobu dostępu do obiektu z żadnej części programu, staje się on kandydatem do garbage collection.

  1. Metoda finalize(): Każda klasa w Javie dziedziczy metodę finalize() z klasy Object. Możesz nadpisać tę metodę, aby dostarczyć kod, który zostanie wykonany tuż przed usunięciem obiektu przez garbage collector. Jednakże poleganie na finalize() nie jest zalecane z kilku powodów, w tym z powodu nieterminowości wywołań i dodatkowego obciążenia dla GC.

  2. Sugestia garbage collection: Możesz zasugerować JVM, by uruchomił garbage collection za pomocą System.gc(), ale nie ma gwarancji, że zostanie on natychmiast wykonany. Z reguły nie jest zalecane ręczne wywoływanie tej metody, ponieważ JVM jest dobrze zoptymalizowany do decydowania, kiedy uruchomić GC.

  1. Dobre praktyki: Aby pomóc mechanizmowi garbage collection w efektywnym zarządzaniu pamięcią:
    • Minimalizuj zakres zmiennych, aby były używane tylko tam, gdzie są potrzebne.
    • Anuluj odniesienia (ustaw wartość na null), jeśli wiesz, że obiekt nie będzie już używany, chociaż nie jest to zazwyczaj konieczne.
    • Unikaj wycieków pamięci, na przykład trzymając stałe odniesienia do obiektów w kolekcjach, które nigdy nie są czyszczone.
  1. Deterministyczne zwalnianie zasobów: W przypadku obiektów, które zarządzają zewnętrznymi zasobami (np. pliki, połączenia z bazą danych), lepiej jest korzystać z bloków try-with-resources lub jawnie zamykać te zasoby za pomocą odpowiednich metod (np. close()).

Przykłady z walidacją

public class TestRetangle {

    public static void main(String[] args) {
        Rectangle r = new Rectangle(150, -5);
        System.out.println(r.getWidth());
        System.out.println(r.getHeight());

        r.setWidth(50);
        r.setHeight(200);
        System.out.println(r.getWidth());
        System.out.println(r.getHeight());
    }
}


class Rectangle {
    private double width;
    private double height;
    
    public Rectangle(double width, double height) {
        if (width < 1) {
            this.width = 1;
        } else if (width > 100) {
            this.width = 100;
        } else {
            this.width = width;
        }
        if (height < 1) {
            this.height = 1;
        } else if (height > 100) {
            this.height = 100;
        } else {
            this.height = height;
        }
    }

    public double getWidth() {
        return width;
    }

    public void setWidth(double width) {
        if (width < 1) {
            this.width = 1;
        } else if (width > 100) {
            this.width = 100;
        } else {
            this.width = width;
        }
    }

    public double getHeight() {
        return height;
    }

    public void setHeight(double height) {
        if (height < 1) {
            this.height = 1;
        } else if (height > 100) {
            this.height = 100;
        } else {
            this.height = height;
        }
    }

}

Wersja z operatorem warunkowym

public class TestRectangle {

    public static void main(String[] args) {
        Rectangle r = new Rectangle(150, -5);
        System.out.println(r.getWidth());
        System.out.println(r.getHeight());

        r.setWidth(50);
        r.setHeight(200);
        System.out.println(r.getWidth());
        System.out.println(r.getHeight());
    }
}

class Rectangle {
    private double width;
    private double height;

    public Rectangle(double width, double height) {
        this.width = (width < 1) ? 1 : (width > 100) ? 100 : width;
        this.height = (height < 1) ? 1 : (height > 100) ? 100 : height;
    }

    public double getWidth() {
        return width;
    }

    public void setWidth(double width) {
        this.width = (width < 1) ? 1 : (width > 100) ? 100 : width;
    }

    public double getHeight() {
        return height;
    }

    public void setHeight(double height) {
        this.height = (height < 1) ? 1 : (height > 100) ? 100 : height;
    }
}
public class TestBox {

    public static void main(String[] args) {
        Box b = new Box(150, -5, 60, new int[]{101, 50, -10, 75});
        System.out.println(b.getWidth());    // Wypisze: 100
        System.out.println(b.getHeight());   // Wypisze: 1
        System.out.println(b.getDepth());    // Wypisze: 60
        for (int tag : b.getTags()) {
            System.out.print(tag + " ");     // Wypisze: 100 50 1 75
        }
    }
}

class Box {
    private double width;
    private double height;
    private double depth;
    private int[] tags = new int[4];

    public Box(double width, double height, double depth, int[] tags) {
        this.width = (width < 1) ? 1 : (width > 100) ? 100 : width;
        this.height = (height < 1) ? 1 : (height > 100) ? 100 : height;
        this.depth = (depth < 1) ? 1 : (depth > 100) ? 100 : depth;

        for (int i = 0; i < 4; i++) {
            if (tags[i] < 1) {
                this.tags[i] = 1;
            } else if (tags[i] > 100) {
                this.tags[i] = 100;
            } else {
                this.tags[i] = tags[i];
            }
        }
    }

    // Gettery i settery
    public double getWidth() {
        return width;
    }

    public void setWidth(double width) {
        this.width = (width < 1) ? 1 : (width > 100) ? 100 : width;
    }

    public double getHeight() {
        return height;
    }

    public void setHeight(double height) {
        this.height = (height < 1) ? 1 : (height > 100) ? 100 : height;
    }

    public double getDepth() {
        return depth;
    }

    public void setDepth(double depth) {
        this.depth = (depth < 1) ? 1 : (depth > 100) ? 100 : depth;
    }

    public int[] getTags() {
        return tags.clone();
    }

    public void setTags(int[] tags) {
        for (int i = 0; i < 4; i++) {
            if (tags[i] < 1) {
                this.tags[i] = 1;
            } else if (tags[i] > 100) {
                this.tags[i] = 100;
            } else {
                this.tags[i] = tags[i];
            }
        }
    }

}
import java.util.ArrayList;

public class TestLibraryBook {

    public static void main(String[] args) {
        ArrayList<String> authorsList = new ArrayList<>();
        authorsList.add("J.K. Rowling");
        LibraryBook book = new LibraryBook("Harry Potter", authorsList);

        System.out.println(book.getTitle());   // Wypisze: Harry Potter
        for (String author : book.getAuthors()) {
            System.out.print(author + " ");     // Wypisze: J.K. Rowling
        }
    }
}

class LibraryBook {
    private String title;
    private ArrayList<String> authors = new ArrayList<>();

    public LibraryBook(String title, ArrayList<String> authors) {
        this.title = (title != null && !title.isEmpty()) ? title : "Unknown Title";

        if (authors != null && !authors.isEmpty()) {
            this.authors.addAll(authors);
        } else {
            this.authors.add("Unknown Author");
        }
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        if (title != null && !title.isEmpty()) {
            this.title = title;
        }
    }

    public ArrayList<String> getAuthors() {
        return new ArrayList<>(authors); // Zwrócenie kopii listy, by chronić jej zawartość
    }

    public void setAuthors(ArrayList<String> authors) {
        if (authors != null && !authors.isEmpty()) {
            this.authors.clear();
            this.authors.addAll(authors);
        }
    }

}

Parę zasad

Gettery i settery to metody, które umożliwiają odczytywanie (gettery) i modyfikowanie (settery) wartości pól prywatnych (private) klas.

Hermetyzacja

  • Używaj pól prywatnych: klasy powinny ukrywać swoje pola (czyli stan) przed innymi klasami, używając modyfikatora dostępu private. Getter i setter są publicznymi metodami do odczytywania i modyfikowania tych prywatnych pól.
  • Ogranicz dostęp: udostępniaj getter i setter tylko wtedy, gdy inne klasy rzeczywiście muszą mieć dostęp do tych pól. Nie twórz setterów dla pól, które nie powinny być modyfikowalne po utworzeniu obiektu.

Walidacja

  • Waliduj dane wejściowe w setterach: zawsze sprawdzaj, czy dane wejściowe spełniają oczekiwane kryteria (np. czy nie są null, czy znajdują się w akceptowalnym zakresie wartości).
  • Nie dopuszczaj do nieprawidłowych stanów obiektu: jeśli ustawienie pola na określoną wartość może doprowadzić do nieprawidłowego stanu obiektu, setter powinien to uniemożliwić.

Niezmienność

  • Unikaj setterów dla niezmienialnych obiektów: jeśli twoja klasa powinna być niezmienialna (co jest dobrą praktyką dla wielu przypadków), nie dostarczaj setterów.
  • Zwracaj kopie w getterach: jeśli pole jest mutowalnym obiektem, takim jak tablica lub ArrayList, getter powinien zwracać kopię obiektu, a nie jego rzeczywiste odniesienie.

Bezpieczeństwo wątkowe

  • Synchronizuj dostęp jeśli to konieczne: jeśli obiekty twojej klasy będą używane przez wiele wątków, rozważ użycie słowa kluczowego synchronized w getterach i setterach, aby zapewnić bezpieczeństwo wątkowe.
  • Używaj finalnych pól: jeśli pole nie ma być zmieniane po utworzeniu obiektu, oznacz je jako final. Pomaga to w zapewnieniu bezpieczeństwa wątkowego.

Ukrywanie danych

  • Nie zwracaj wewnętrznych struktur danych: jeśli twoja klasa używa wewnętrznych struktur danych, takich jak listy czy zbiory, nie zwracaj ich bezpośrednio. Zamiast tego, zwróć niezmienialny widok lub kopię.

Ograniczenie do koniecznego minimum

  • Nie dodawaj logiki biznesowej do getterów i setterów: chyba że jest to ściśle związane z walidacją. Gettery i settery powinny być prostymi metodami do odczytu i zapisu wartości.

Statyczność

Pola statyczne

Pola statyczne, często nazywane również zmiennymi klasowymi, są zmiennymi, które są wspólne dla wszystkich instancji klasy. Oznacza to, że jedna kopia pola statycznego istnieje niezależnie od liczby obiektów stworzonych z tej klasy. Każda instancja klasy ma dostęp do tego samego pola statycznego i każda modyfikacja tego pola przez jedną instancję jest widoczna dla wszystkich pozostałych instancji.

Deklaracja Pola Statycznego

Pole statyczne deklarowane jest w klasie z użyciem słowa kluczowego static.

class MyClass {
    private static int staticField;
}

  • Współdzielenie: Ponieważ pole statyczne jest współdzielone między wszystkimi instancjami klasy, jest ono używane do reprezentowania informacji, które mają zastosowanie do całej klasy, a nie do pojedynczych obiektów.
  • Inicjalizacja: Pola statyczne są inicjalizowane podczas ładowania klasy, a nie podczas tworzenia nowego obiektu. Można je również inicjalizować w statycznych blokach inicjalizacyjnych.
  • Dostęp: Dostęp do pola statycznego jest możliwy zarówno za pośrednictwem nazwy klasy, jak i instancji klasy, ale zalecany jest dostęp za pośrednictwem nazwy klasy.
  • Cykl życia: Pole statyczne istnieje od momentu, gdy klasa jest ładowana przez ClassLoader, do momentu zakończenia działania programu lub do momentu, gdy klasa jest wyładowana, co może nastąpić, jeśli ClassLoader, który załadował klasę, zostanie usunięty z pamięci.

Częste zastosowania

  • Liczniki: Pola statyczne są często używane do śledzenia liczby instancji danej klasy, które zostały utworzone.
  • Stałe: Zmienne statyczne, które są również oznaczone jako final, są traktowane jako stałe (np., static final int MAX_SIZE = 100;) i są często pisane wielkimi literami. Te stałe są wspólne dla wszystkich instancji.
  • Singleton: Wzorzec projektowy Singleton używa zmiennej statycznej do przechowywania jednej instancji klasy.

Przykład

class Book{
    private static int id;
    
}

Metody statyczne

Metody statyczne, znane także jako metody klasowe, to takie, które są zdefiniowane z użyciem słowa kluczowego static. Są one związane z klasą, w której zostały zdefiniowane, a nie z konkretnymi instancjami tej klasy. Oznacza to, że metoda statyczna może być wywołana bez tworzenia obiektu danej klasy.

Własności:

  1. Przynależność do klasy: Metoda statyczna należy do klasy, a nie do instancji tej klasy.

  2. Wywołanie: Metodę statyczną można wywołać bezpośrednio na klasie, np. ClassName.staticMethodName().

  3. Dostęp do pól statycznych: Metody statyczne mogą bezpośrednio dostępować inne statyczne składowe (pola i metody) danej klasy.

  1. Brak dostępu do pól niestatycznych: Metoda statyczna nie może dostępować niestatycznych pól (instancyjnych) ani wywoływać niestatycznych metod bezpośrednio, ponieważ te składowe wymagają referencji do konkretnego obiektu.

  2. Używanie: Są one często używane jako metody pomocnicze (np. metody fabrykujące, metody utilitarne, metody obliczeniowe).

  3. Przesłanianie: Metody statyczne nie mogą być przesłaniane w taki sposób jak metody instancyjne. Jeśli podklasa definiuje statyczną metodę o tej samej sygnaturze co statyczna metoda w klasie bazowej, to metoda w podklasie ukrywa metodę w klasie bazowej.

public class MathUtils {

    // Statyczna metoda pomocnicza
    public static int add(int a, int b) {
        return a + b;
    }
}

Zastosowania metod statycznych:

  1. Metody fabrykujące: Metody statyczne są często używane do tworzenia instancji obiektów, kiedy chcemy dostarczyć użytkownikowi alternatywę dla konstruktorów z dodatkową logiką.

  2. Narzędzia: W przypadku narzędzi matematycznych, stringowych lub związanych z plikami, metody statyczne są używane do dostarczania funkcjonalności, która nie jest powiązana z konkretnymi instancjami klas.

  3. Stałe konfiguracyjne: Czasami statyczne metody są używane do dostępu do zmiennych konfiguracyjnych, które są wspólne dla wszystkich instancji klasy.

public class TestCounter {

    public static void main(String[] args) {
        Counter c1 = new Counter();
        Counter c2 = new Counter();
        Counter c3 = new Counter();
        System.out.println(Counter.getCount());
    }
}

class Counter {
    private static int count = 0;

    public Counter() {
        count++;
    }

    public static int getCount() {
        return count;
    }
}

Metody fabrykujące

public class TestVehicle{
    public static void main(String[] args) {
        Vehicle car = new Vehicle("Bus");
        Vehicle truck = Vehicle.createTruck();

        System.out.println(car.getType());
        System.out.println(truck.getType());
    }
}

class Vehicle {

    private String type;

    public Vehicle(String type) {
        this.type = type;
    }

    public static Vehicle createCar() {
        return new Vehicle("Car");
    }

    public static Vehicle createTruck() {
        return new Vehicle("Truck");
    }

    public String getType() {
        return type;
    }
}

Prywatny konstruktor?

Singleton

public class TestSingleton {
    public static void main(String[] args) {
        // Pobranie instancji Singleton
        Singleton singleton = Singleton.getInstance();
        Singleton singleton2 = Singleton.getInstance();
    }
}

class Singleton {

    // Jedyna instancja tej klasy
    private static Singleton instance;

    // Prywatny konstruktor
    private Singleton() {
        // inicjalizacja
    }

    // Publiczna metoda dostępowa do instancji
    public static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

Statyczne bloki inicjujące

Statyczne bloki inicjujące są specjalnymi blokami, które służą do inicjalizacji zmiennych statycznych lub wykonywania operacji, które muszą zostać przeprowadzone tylko raz, gdy klasa jest po raz pierwszy załadowana.

public class MyClass {
    static {
        // kod wykonywany podczas ładowania klasy
    }
}

Kod w statycznym bloku inicjującym jest wykonany tylko raz, niezależnie od liczby obiektów klasy, które są tworzone. Wykonywany jest w momencie, gdy klasa jest pierwszy raz używana w programie, co oznacza, że może to być spowodowane stworzeniem pierwszej instancji klasy, odwołaniem do statycznej zmiennej klasy lub wywołaniem statycznej metody klasy.

Statyczne bloki inicjujące są użyteczne, gdy inicjalizacja zmiennych statycznych wymaga więcej niż jednej linii kodu lub kiedy inicjalizacja jest złożona i wymaga wykonania logiki, która nie mogłaby być wykonana w linii deklaracji zmiennej.

public class TestConfiguration {
    public static void main(String[] args) {
        Configuration.displayConfig();
    }
}


class Configuration {
    public static final int CONFIG_FEATURE;

    static {
        // Można tutaj wykonywać złożone operacje inicjalizacyjne
        CONFIG_FEATURE = initializeConfiguration();
    }

    private static int initializeConfiguration() {
        // Tutaj mogłoby być pobieranie danych z pliku konfiguracyjnego lub innego źródła
        // Na potrzeby przykładu, po prostu zwracamy wartość 42
        return 42;
    }

    public static void displayConfig() {
        System.out.println("Wartość konfiguracji: " + CONFIG_FEATURE);
    }
}

Import statyczny

Statyczne importy umożliwiają bezpośrednie odwołanie się do statycznych członków (pól i metod) klasy bez konieczności kwalifikowania ich nazwy za pomocą nazwy klasy. To znacznie upraszcza kod w sytuacjach, gdy potrzebujesz wielokrotnie korzystać z tych samych statycznych metod lub pól w wielu miejscach swojego kodu.

Na przykład, jeśli często używasz metody Math.sqrt() w swoim kodzie, zamiast pisać Math.sqrt(x) za każdym razem, gdy chcesz obliczyć pierwiastek kwadratowy, możesz zaimportować statycznie tę metodę i używać sqrt(x) bezpośrednio.

import static java.lang.Math.sqrt;

public class Calculator {
    public double calculateHypotenuse(double a, double b) {
        return sqrt(a * a + b * b);
    }
}
import static java.lang.Math.PI;

public class Circle {
    public double calculateArea(double radius) {
        return PI * radius * radius;
    }
}
import static java.lang.Math.*;

public class Calculator {
    public double calculateCircleCircumference(double radius) {
        return 2 * PI * radius;
    }

    public double calculateCircleArea(double radius) {
        return PI * pow(radius, 2);
    }
}
  1. Czytelność: Nadużywanie statycznych importów może uczynić kod mniej czytelnym, ponieważ może nie być od razu jasne, z której klasy pochodzi dany element (metoda lub pole).

  2. Konflikty: Jeśli importujesz statycznie członki z różnych klas, które mają takie same nazwy, możesz napotkać na konflikty, które będziesz musiał rozwiązać, kwalifikując nazwy klasą.

  3. Praktyki: Zazwyczaj zaleca się, aby używać statycznych importów umiarkowanie i tylko wtedy, gdy jest to uzasadnione, na przykład w przypadku stałych dobrze znanych w całym projekcie lub gdy jasne jest, skąd pochodzi importowana metoda lub pole.

Dziedziczenie

Dziedziczenie

Dziedziczenie to jedna z podstawowych koncepcji programowania obiektowego, w tym także w Javie. Jest to mechanizm, za pomocą którego jedna klasa (nazywana klasą pochodną, podklasą lub klasą dziecka) może dziedziczyć pola i metody innej klasy (nazywanej klasą bazową, nadklasą lub klasą rodzica). Dziedziczenie pozwala na ponowne wykorzystanie kodu i ustanowienie hierarchii między klasami.

Podstawowe aspekty dziedziczenia w Javie:

  1. Jedno dziedziczenie: W Javie klasa może dziedziczyć bezpośrednio tylko po jednej klasie. Wiele języków programowania, w tym Java, stosuje jedno dziedziczenie, aby uniknąć problemów związanych z dziedziczeniem wielokrotnym, takich jak tzw. problem diamentu.

  2. Wielopoziomowe dziedziczenie: Mimo że Java nie wspiera wielokrotnego dziedziczenia bezpośrednio (klasa pochodna może mieć tylko jedną klasę bazową), pozwala na tworzenie hierarchii klas, w której klasa może dziedziczyć po klasie, która sama dziedziczy po innej klasie, itd. To umożliwia tworzenie bardziej złożonych hierarchii.

  1. super: Słowo kluczowe super w Javie odnosi się do bezpośredniego rodzica klasy. Może być użyte do wywołania konstruktora klasy nadrzędnej lub dostępu do jej metod i pól, które zostały ukryte przez klasy potomne.

  2. Nadpisywanie metod: Jeśli klasa pochodna definiuje metodę, która istnieje już w klasie bazowej, mówi się, że metoda w klasie pochodnej “nadpisuje” metodę z klasy bazowej. To pozwala na dostosowanie lub rozszerzenie zachowania klasy bazowej.

  1. final: Słowo kluczowe final może być użyte w kontekście klasy lub metody. Jeśli klasa jest oznaczona jako final, nie może być rozszerzona (innymi słowy, nie można po niej dziedziczyć). Jeśli metoda jest oznaczona jako final, nie może być nadpisywana/przesłaniana przez klasy potomne.

  2. Klasy abstrakcyjne i interfejsy: W Javie klasy abstrakcyjne i interfejsy pozwalają na określenie metod, które muszą być zaimplementowane przez klasy pochodne, oferując bardziej elastyczny sposób tworzenia kontraktów między klasami.

//klasa testująca
public class TestVehicle {
    public static void main(String[] args) {
        Car myCar = new Car();
        myCar.display();
    }
}
// Klasa bazowa
class Vehicle {
    public void display() {
        System.out.println("To jest pojazd.");
    }
}

// Klasa pochodna
class Car extends Vehicle {
    @Override
    public void display() {
        super.display();  // Wywołanie metody z klasy bazowej
        System.out.println("To jest samochód.");
    }
}

Nadpisywanie a przesłanianie (ang. override)

W języku Java pojęcie nadpisywania i przesłaniania często stosowane jest zamiennie, choć nie jest to w pełni poprawne dla osób zajmującą się teorią programowania. W innych językach np. w C# są istotne różnice. Nadpisanie/przesłanianie często bywa mylone z przeciążeniem.

Dokładniej później.

Klasa Object

Klasa Object jest najbardziej fundamentalną klasą w hierarchii klas. Każda klasa w Javie domyślnie dziedziczy z klasy Object, jeśli nie jest zadeklarowana jako podklasa innej klasy. To oznacza, że Object jest superklasą dla wszystkich innych klas.

Dziedziczenie z klasy Object zapewnia kilka podstawowych metod, które są wspólne dla wszystkich obiektów w Javie.

Wybrane metody:

  1. toString(): Zwraca reprezentację ciągu znaków danego obiektu. Jeśli nie jest nadpisana, domyślna implementacja zwraca nazwę klasy, a następnie znak ‘@’ i nieformatowany adres hash obiektu.

  2. equals(Object obj): Określa, czy dwa obiekty są “równe”. Standardowa implementacja porównuje referencje, aby zobaczyć, czy wskazują one na ten sam obiekt. Często jest nadpisywana, aby umożliwić porównanie stanu obiektów.

  1. hashCode(): Zwraca wartość hashcode obiektu, która jest używana przez struktury danych oparte na hashowaniu, takie jak HashMap. Jeśli metoda equals() jest nadpisana, hashCode() również powinna być nadpisana w taki sposób, aby dwa “równe” obiekty miały ten sam kod hash.

  2. getClass(): Zwraca obiekt Class, który reprezentuje klasę bieżącego obiektu. Może być używany do uzyskania informacji o klasie w runtime.

  3. clone(): Domyślnie zwraca płytką kopię obiektu. Jednak aby można było skorzystać z tej metody, klasa musi implementować interfejs Cloneable, a metoda musi być nadpisana, ponieważ jest protected.

  1. finalize(): Jest wywoływana przez Garbage Collector przed usunięciem obiektu, gdy już nie jest dostępny. Jest to jednak metoda rzadko używana, ponieważ nie ma gwarancji, kiedy (a nawet czy w ogóle) będzie wywołana.

  2. notify(), notifyAll(), i wait(): Są to metody, które pozwalają na współpracę wątków na jednym obiekcie.

Klasa Object jest wykorzystywana, gdy chcemy mieć referencje do obiektów nieznanego typu lub chcemy zaimplementować metody, które są w stanie działać na dowolnym obiekcie, na przykład:

public void printObjectDetails(Object obj) {
    System.out.println(obj.toString());
}

Metoda toString()

Metoda toString() jest używana do dostarczenia reprezentacji tekstowej obiektu. Kiedy tworzysz klasę, możesz nadpisać tę metodę, aby zwrócić ciąg znaków, który opisuje obiekt w przydatny sposób. Jest to szczególnie przydatne podczas debugowania, gdy chcemy szybko zobaczyć ważne informacje o stanie obiektu.

Zasady

  1. Sygnatura: Metoda toString() musi być publiczna, nie przyjmować żadnych argumentów, i musi zwracać String.

  2. Nadpisywanie: Aby nadpisać metodę toString(), należy użyć adnotacji @Override. Dzięki temu kompilator Java będzie w stanie sprawdzić, czy faktycznie nadpisujesz metodę z klasy bazowej.

  3. Reprezentacja: Zwrócony ciąg znaków powinien być zwięzły, ale wystarczająco informatywny, aby przedstawić najważniejsze informacje o obiekcie.

  1. Spójność: Jeśli nadpisujesz również equals(), zwracany ciąg powinien zawierać wszystkie informacje, które są używane do porównania obiektów (dobra praktyka).

  2. Bezpieczeństwo: Nie należy w toString() uwzględniać wrażliwych informacji, które mogłyby prowadzić do problemów z bezpieczeństwem, takich jak hasła, klucze prywatne itp.

class Person {
    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

Adnotacja @Override

Adnotacja @Override jest mechanizmem służącym do wskazania, że dana metoda ma na celu nadpisanie metody z klasy bazowej. Używanie tej adnotacji nie jest wymagane, aby nadpisać metodę, ale jest to uznana praktyka programistyczna.

  1. Poprawność kodu: Kiedy używasz adnotacji @Override, kompilator sprawdza, czy metoda rzeczywiście nadpisuje metodę z klasy bazowej. Jeśli nie, otrzymasz błąd kompilacji. To zapobiega błędom wynikającym z niepoprawnych sygnatur metod (np. złe nazwy, typy parametrów).

  2. Czytelność kodu: Adnotacja @Override czyni intencje programisty jaśniejszymi dla osób czytających kod. Kiedy widzisz tę adnotację, wiesz, że metoda ma być wersją metody z klasy bazowej, zamiast być nową metodą.

  1. Refaktoryzacja: Gdy klasa bazowa jest modyfikowana, np. metoda jest usuwana lub zmieniana, używanie @Override pomaga zidentyfikować miejsca, które mogą wymagać aktualizacji. Kompilator może wygenerować błędy dla metod, które nie są już prawidłowymi nadpisaniami.

  2. Dokumentacja: Adnotacja @Override służy jako część wewnętrznej dokumentacji kodu, wskazując, że dana metoda jest powiązana z hierarchią dziedziczenia klasy.

  1. Zapobieganie błędom: Pomaga zapobiegać błędom spowodowanym przez przeciążanie (overloading) zamiast nadpisywania. Jeśli programista chciał nadpisać metodę, ale zamiast tego stworzył nową metodę o innej sygnaturze, adnotacja @Override spowoduje błąd kompilacji, sygnalizując problem.