Programowanie obiektowe

Wykład 4

Klasy i obiekty

Samodzielne pisanie klas

  1. Nazewnictwo:
    • Nazwy klas zaczynaj z wielkiej litery. Jeśli nazwa klasy składa się z wielu słów, każde kolejne słowo powinno zaczynać się z wielkiej litery (tzw. CamelCase). Na przykład: Samochod, OsobaKontaktowa.
    • Nazwy klas powinny być rzeczownikami i odzwierciedlać esencję tego, co reprezentują. Na przykład KontoBankowe zamiast PrzetwarzanieTransakcji.
    • Unikaj skrótów w nazwach klas, chyba że są powszechnie akceptowane.
  1. Modyfikatory dostępu:
    • Zawsze określaj modyfikator dostępu dla klasy, zmiennych i metod. Najczęściej używanymi modyfikatorami są public, private i protected.
    • Zmienne instancji powinny być zazwyczaj prywatne (private) aby chronić je przed niepożądanym dostępem z zewnątrz.
    • Metody, które mają być dostępne z zewnątrz klasy, powinny być public.
  1. Kolejność składowych:
    • W konwencjonalnej kolejności w klasie najpierw umieszczane są zmienne, a następnie metody.
    • Zmienne i metody powinny być pogrupowane według funkcjonalności.
    • Konstruktory zazwyczaj umieszcza się przed innymi metodami.
    • Jeśli klasa ma zagnieżdżone klasy wewnętrzne, umieść je na końcu.
  1. Konstruktory:
    • Konstruktory klasy powinny być zdefiniowane bezpośrednio po zmiennych instancji.
    • Jeśli klasa posiada wiele konstruktorów, warto je umieścić w kolejności od najmniej do najbardziej szczegółowego.
  1. Metody gettery i settery:
    • Dla każdej zmiennej instancji, która ma być dostępna z zewnątrz klasy, warto dostarczyć metody getter (do pobierania wartości) i setter (do ustawiania wartości).
    • Nazwy tych metod powinny zaczynać się od get lub set, a następnie nazwa zmiennej z wielkiej litery, np. getImie() lub setImie().
  1. Dokumentacja:
    • Używaj komentarzy Javadoc (/** ... */) do dokumentowania klas, zmiennych i metod. Dzięki temu inne osoby korzystające z Twojego kodu będą miały łatwy dostęp do opisu funkcjonalności.
  1. Konwencje kodowania:
    • Trzymaj się jednolitych konwencji kodowania w całym projekcie, takich jak wcięcia, spacje i umieszczanie klamer.
    • Używaj deskryptywnych nazw dla zmiennych i metod, aby kod był samodokumentujący się.

Kolejność jeszcze raz

https://www.oracle.com/java/technologies/javase/codeconventions-fileorganization.html

https://google.github.io/styleguide/javaguide.html#s3.4.2-class-member-ordering

  1. Deklaracje członków klasy:
    • Deklaracje zmiennych:
      1. Stałe (public static final zmienne).
      2. Zmienne statyczne.
      3. Zmienne instancji.
  2. Konstruktory:
    • Konstruktory powinny być umieszczone bezpośrednio po deklaracjach zmiennych.
    • Jeśli klasa posiada wiele konstruktorów, warto je umieścić w kolejności od najmniej do najbardziej szczegółowego.
  1. Metody:
    • Metody fabrykujące (jeśli istnieją): są to statyczne metody zwracające instancję klasy.
    • Metody dostępowe (gettery i settery): dla każdej zmiennej instancji, która ma być dostępna z zewnątrz klasy.
    • Metody publiczne: metody, które mają być dostępne dla innych klas.
    • Metody chronione (protected): metody dostępne dla podklas i klas w tym samym pakiecie.
    • Metody prywatne: metody dostępne tylko wewnątrz klasy.
  1. Wewnętrzne klasy, interfejsy i wyliczenia:
    • Jeśli klasa posiada zagnieżdżone (wewnętrzne) klasy, interfejsy lub wyliczenia, zaleca się umieszczenie ich na końcu definicji klasy głównej.
  2. Klasa główna może również zawierać:
    • Bloki inicjalizacyjne (statyczne i niestatyczne).
    • Wewnętrzne interfejsy i wyliczenia.

Tworzenie obiektów

W Javie, jeśli klasa nie ma zdefiniowanego żadnego konstruktora, kompilator dostarcza domyślny konstruktor bezargumentowy (nazywany również konstruktorem domyślnym). Ten konstruktor nie ma żadnego ciała i służy wyłącznie do tworzenia obiektów.

Jeśli jednak zdefiniujesz choć jeden konstruktor dla klasy, kompilator już nie dostarcza domyślnego konstruktora. W takim przypadku, jeśli chcesz mieć konstruktor bezargumentowy, musisz go jawnie zdefiniować.

Przykład

class Telephone {

    private String brand;
    private String model;


    public void call(String number) {
        System.out.println("Calling number " + number + " from " + brand + " " + model);
    }

    public void sendMessage(String number, String content) {
        System.out.println("Sending a message to " + number + " with content: " + content);
    }

}

Operator new

Operator new w Javie odgrywa kluczową rolę w tworzeniu nowych obiektów i jest nieodłącznym elementem programowania obiektowego w tym języku.

  1. Alokacja pamięci:
    • Gdy używamy operatora new, system przydziela pamięć dla nowego obiektu w stercie (ang. heap). Sterta jest obszarem pamięci przeznaczonym do alokacji pamięci dla obiektów w czasie działania programu.
  1. Inicjalizacja:
    • Operator new nie tylko alokuje pamięć, ale również inicjuje obiekt poprzez wywołanie odpowiedniego konstruktora klasy.
  2. Zwracanie referencji:
    • Po utworzeniu i zainicjowaniu obiektu, operator new zwraca referencję do tego obiektu. Referencję tę można przypisać do zmiennej, która jest zmienną referencyjną odpowiedniego typu.
  1. Współpraca z konstruktorem:
    • Po wywołaniu operatora new, można od razu użyć konstruktora klasy, aby zainicjować obiekt. Na przykład: new Dog("Rex") używa konstruktora klasy Dog, który przyjmuje jeden argument typu String.
  2. Tworzenie tablic:
    • Operator new jest również używany do tworzenia tablic w Javie. Na przykład: new int[5] tworzy tablicę pięciu elementów typu int.

Operator “kropki”

Operator kropki (wyboru składowej) . jest jednym z najczęściej używanych operatorów i ma wiele zastosowań w kontekście dostępu do składowych klasy lub obiektu.

Zastosowania:

  1. Dostęp do składowych obiektu:
    • Operator kropki pozwala na dostęp do pól (zmiennych instancji) oraz metod obiektu. Jeśli pole lub metoda są oznaczone jako public, można do nich bezpośrednio odwołać się za pomocą operatora kropki.

      Dog myDog = new Dog();
      myDog.name = "Rex"; // dostęp do pola
      myDog.bark();       // wywołanie metody
  1. Dostęp do składowych klasy (statycznych):
    • Można użyć operatora kropki do dostępu do statycznych składowych klasy (czyli tych, które są oznaczone słowem kluczowym static). W tym przypadku odnosimy się bezpośrednio do klasy, a nie do instancji klasy.

      int maxSpeed = Car.MAX_SPEED; // dostęp do statycznej zmiennej
      Car.showInfo();               // wywołanie statycznej metody
  1. Dostęp do pakietów i klas:
    • Operator kropki jest używany do odwoływania się do klas wewnątrz pakietów.

      import java.util.ArrayList;
  2. Odwołania zakwalifikowane:
    • W sytuacjach, gdy chcemy się odwołać do konkretnej klasy lub składowej w ramach określonego pakietu, używamy operatora kropki.

      java.util.Scanner scanner = new java.util.Scanner(System.in);
  1. Odwołania do składowych klasy bazowej:
    • W kontekście dziedziczenia, jeśli podklasa chce się odwołać do składowej klasy bazowej, która została przesłonięta w podklasie, można użyć słowa kluczowego super w połączeniu z operatorem kropki.

      super.methodName();
  2. Odwołania do wewnętrznych klas:
    • Jeśli klasa ma zagnieżdżoną klasę wewnętrzną, można odwołać się do niej za pomocą operatora kropki.

      OuterClass.InnerClass innerObject = new OuterClass().new InnerClass();

Słowo kluczowe this

słowo kluczowe this odnosi się do bieżącej instancji klasy. Może być używane do odnoszenia się do pól klasy wewnątrz metody, co jest użyteczne, gdy parametr metody ma taką samą nazwę jak pole klasy.

Słowo kluczowe this można pominąć w Javie, gdy:

  1. Nazwy lokalnych zmiennych lub parametrów metod nie kolidują z nazwami pól klasy.
  2. Dostęp do pól klasy jest jednoznaczny bez użycia this.

Przykład:

class Bicycle {
    int gear;

    public void setGear(int gear) {
        this.gear = gear;  // 'this' jest potrzebne, aby rozróżnić parametr od pola klasy
    }

    public int getGear() {
        return gear;  // 'this' można pominąć, ponieważ nie ma konfliktu nazw
    }
}

Bloki inicjujące

Bloki inicjujące w Javie to specjalne bloki kodu, które są wykonywane podczas tworzenia obiektu. Służą one do inicjalizacji zmiennych instancji oraz do wykonywania pewnych operacji przed wywołaniem konstruktora klasy.

Java oferuje dwa rodzaje bloków inicjujących:

  1. Statyczne bloki inicjujące: Wykonywane są tylko raz, gdy klasa jest ładowana do pamięci. Służą do inicjalizacji statycznych zmiennych oraz do wykonywania operacji, które mają miejsce tylko raz podczas cyklu życia klasy.
  1. Bloki inicjujące instancji: Wykonywane są każdorazowo, gdy tworzony jest nowy obiekt klasy. Wywoływane są przed konstruktorem klasy i służą do inicjalizacji zmiennych instancji.

Wartości domyślne:

Jeśli zmiennej instancji nie przypiszemy wartości, Java automatycznie przypisuje jej wartość domyślną w zależności od typu zmiennej:

  • Dla typów liczbowych (np. int, float, double): 0
  • Dla boolean: false
  • Dla char: \u0000 (null)
  • Dla obiektów i tablic: null

Przykład “poprawny”

public class TestSample {

    public static void main(String[] args) {
        System.out.println("Main method started");
        // Tworzenie obiektu klasy Sample
        Sample sample = new Sample();
        System.out.println("Instance Variable: " + sample.instanceVar);
    }
}

class Sample {
    // Zmienna instancji
    public int instanceVar;

    // Blok inicjujący instancji
    {
        instanceVar = 10;
        System.out.println("Instance block executed");
    }

    // Konstruktor
    public Sample() {
        System.out.println("Constructor executed");
    }
}

Przykład “pomieszania”

public class TestSample {

    public static void main(String[] args) {
        System.out.println("Main method started");
        Sample sample = new Sample();
        System.out.println("Instance Variable: " + sample.instanceVar);
    }
}

class Sample {
    public int instanceVar = 5;

    {
        System.out.println("Instance block before - variable: " + instanceVar);
        instanceVar = 10;
        System.out.println("Instance block after - variable: " + instanceVar);
        System.out.println("Instance block executed");
    }
    
    public Sample() {
        System.out.println("Constructor before - variable: " + instanceVar);
        instanceVar = 15;
        System.out.println("Constructor after - variable: " + instanceVar);
        System.out.println("Constructor executed");
    }
}

null

Wartość null jest specjalną referencją, która wskazuje, że dana zmienna referencyjna nie wskazuje na żaden konkretny obiekt w pamięci.

  1. Wartość domyślna: Gdy deklarujesz zmienną obiektową, ale nie przypisujesz jej żadnej wartości, domyślnie przyjmuje ona wartość null.

  2. Sprawdzanie istnienia obiektu: Można użyć null do sprawdzenia, czy dana zmienna została zainicjalizowana (czyli czy wskazuje na jakiś obiekt). To jest przydatne w sytuacjach, gdy nie chcesz ryzykować wystąpienia wyjątku NullPointerException.

  3. Zwalnianie obiektów: W Javie nie ma bezpośredniej metody na zwolnienie obiektu z pamięci. Zamiast tego Java korzysta z mechanizmu Garbage Collection (GC) do automatycznego usuwania obiektów, które nie są już dostępne. Przypisanie wartości null do zmiennej obiektowej jest jednym ze sposobów na “odłączenie” obiektu, co pozwala GC na jego ewentualne usunięcie.

  4. Semantyka: null może być używane do reprezentowania “braku wartości” lub “niezdefiniowanego stanu”. Na przykład, jeśli masz klasę reprezentującą użytkownika, ale nie znasz jeszcze jego adresu e-mail, możesz przypisać pole e-mail wartość null.

Obsługa null

Wyjątek NullPointerException (NPE) pojawia się, gdy program próbuje odwołać się do obiektu lub wywołać metodę na referencji, która jest null.

Podstawowe zasady (na ten moment)

Inicjalizuj zmienne: - Gdy to możliwe, inicjalizuj zmienne w momencie ich deklaracji. - Unikaj pozostawiania zmiennych referencyjnych bez wartości (niewypełnionych).

Sprawdzaj obiekty przed użyciem: - Zawsze sprawdzaj, czy referencja nie jest null, zanim użyjesz jej do wywołania metody lub dostępu do pola.

if (someObject != null) {
   someObject.someMethod();
}

Objects.requireNonNull(T obj):

  • Sprawdza, czy podany obiekt obj nie jest null.
  • Jeśli obiekt jest null, metoda rzuci wyjątek NullPointerException.
  • Jeśli obiekt nie jest null, zostanie zwrócony bez zmian.
  • Możesz także dostarczyć dodatkowy komunikat, który zostanie uwzględniony w wyjątku, używając wariantu metody: Objects.requireNonNull(T obj, String message).
  • Jest to często używane do weryfikacji argumentów przekazanych do metod lub konstruktorów, aby upewnić się, że nie są one null.
public void setProperty(String prop) {
    this.property = Objects.requireNonNull(prop, "Property cannot be null");
}

Objects.requireNonNullElse(T obj, T defaultObj):

  • Sprawdza, czy podany obiekt obj nie jest null.
  • Jeśli obiekt obj jest null, zwraca defaultObj jako wartość domyślną.
  • Jeśli obiekt obj nie jest null, zwraca obj.
  • Jest to przydatne, gdy chcesz mieć wartość domyślną w przypadku, gdy przekazany obiekt jest null.
String userInput = getUserInput(); // może zwrócić null
String value = Objects.requireNonNullElse(userInput, "Default Value");

Modyfikatory dostępu

Modyfikatory dostępu w Javie określają zakres widoczności i dostępności składowych klasy (tj. pól, metod, konstruktorów, itp.). Istnieje cztery główne modyfikatory dostępu:

private:

  • Składowa oznaczona jako private jest dostępna wyłącznie w obrębie klasy, w której została zdefiniowana.
  • Nie jest dostępna z żadnej innej klasy, nawet jeśli są one w tym samym pakiecie.
  • Często używane do ukrywania implementacji i enkapsulacji danych.

default (brak modyfikatora):

  • Jeśli składowa nie ma określonego modyfikatora dostępu, ma domyślny zakres pakietu.
  • Jest dostępna dla wszystkich klas w tym samym pakiecie, ale nie dla klas spoza tego pakietu.
  • Często używane, gdy chcemy udostępniać składowe dla klas w tym samym pakiecie, ale nie eksponować ich na zewnątrz.

protected:

  • Składowa oznaczona jako protected jest dostępna w obrębie jej klasy, w obrębie wszystkich klas w tym samym pakiecie, a także w subklasach (nawet jeśli są w innym pakiecie).
  • Jest to bardziej restrykcyjne niż public, ale mniej niż private i default.
  • Często używane w klasach bazowych, gdzie chcemy, aby pewne składowe były dostępne dla podklas, ale nie dla innych klas spoza hierarchii dziedziczenia.

public:

  • Składowa oznaczona jako public jest dostępna z każdej klasy, niezależnie od pakietu.
  • Jest to najmniej restrykcyjny modyfikator dostępu.
  • Często używane do eksponowania interfejsów API i danych, które powinny być dostępne dla wszystkich innych klas.

Hermatyzacja

Hermetyzacja (nazywana także enkapsulacją) to jedno z podstawowych założeń programowania obiektowego. Polega na ukrywaniu wewnętrznych detali i stanu obiektu przed kodem zewnętrznym, udostępniając jednocześnie dobrze zdefiniowany interfejs do interakcji z tym obiektem. Hermetyzacja pomaga w ochronie integralności danych i kontrolowaniu dostępu do nich.

  1. Modyfikatory dostępu

  2. Gettery i settery

  3. Zasada najmniejszego uprzywilejowania: Dobre praktyki hermetyzacji zakładają ograniczenie widoczności i dostępu do zmiennych i metod tak bardzo, jak to tylko możliwe. Dlatego zaleca się, aby pola klasy były zazwyczaj private, a metody publiczne stanowiły świadomie zaprojektowany interfejs klasy.

Przykład: Stwórzmy klasę Person z dwoma polami name i age.

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

Przeciążanie metod

Ang. overload

class Calculator {
    
    public int add(int a, int b) {
        return a + b;
    }
    
    public int add(int a, int b, int c) {
        return a + b + c;
    }
    
    public double add(double a, double b) {
        return a + b;
    }
    
}

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

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

}