Klasy i obiekty – podstawy OOP

TL;DR: Klasa to szablon/przepis na tworzenie obiektów. Obiekt to konkretna instancja klasy z własnymi danymi. Konstruktor inicjalizuje obiekt podczas tworzenia. Metody to funkcje które obiekt może wykonywać. To fundament programowania obiektowego.

Programowanie obiektowe (OOP) to sposób myślenia o kodzie jak o rzeczywistych obiektach. Zamiast pisać długie listy instrukcji, tworzysz obiekty które mają swoje cechy i umiejętności. To jak budowanie z klocków LEGO – każdy klocek ma swoją rolę i można je łączyć na różne sposoby.

## Dlaczego to ważne

W każdej poważnej aplikacji masz setki różnych „rzeczy” – użytkowników, produkty, zamówienia, faktury. Bez klas i obiektów Twój kod zamieni się w spaghetti. OOP pozwala organizować kod tak, żeby był łatwiejszy do zrozumienia, modyfikacji i wielokrotnego użycia.

Co się nauczysz:

  • Czym różni się klasa od obiektu i dlaczego to ważne
  • Jak tworzyć klasy z polami, metodami i konstruktorami
  • Jak tworzyć obiekty i używać ich w praktyce
  • Podstawowe zasady enkapsulacji – gettery i settery
  • Najważniejsze błędy początkujących w OOP
Wymagania wstępne: Podstawy składni Java – zmienne, metody, instrukcje warunkowe i pętle.

## Czym jest klasa?

Klasa to jak formularz w urzędzie. Formularz ma puste pola do wypełnienia (nazwa, adres, telefon). Każda osoba wypełnia ten sam formularz, ale wpisuje swoje dane. Formularz to klasa, wypełniony formularz to obiekt.

Klasa to szablon opisujący jakie dane może przechowywać obiekt i co może robić. To jak plan domu – opisuje ile ma być pokoi, gdzie kuchnia, gdzie łazienka. Ale nie możesz mieszkać w planie – musisz wybudować dom według tego planu.

### Pierwsza klasa w praktyce

// Klasa Person - szablon na osobę
public class Person {
    // Pola (cechy osoby)
    private String name;
    private int age;
    private String email;
    
    // Konstruktor - instrukcja jak tworzyć obiekt
    public Person(String name, int age, String email) {
        this.name = name;
        this.age = age;
        this.email = email;
    }
    
    // Metody (co osoba może robić)
    public void introduce() {
        System.out.println("Cześć, jestem " + name + 
                          " i mam " + age + " lat.");
    }
    
    public boolean isAdult() {
        return age >= 18;
    }
    
    // Gettery - pozwalają odczytać dane
    public String getName() {
        return name;
    }
    
    public int getAge() {
        return age;
    }
    
    public String getEmail() {
        return email;
    }
    
    // Settery - pozwalają zmienić dane
    public void setAge(int age) {
        if (age >= 0 && age <= 150) {
            this.age = age;
        } else {
            System.out.println("Nieprawidłowy wiek!");
        }
    }
    
    public void setEmail(String email) {
        if (email.contains("@")) {
            this.email = email;
        } else {
            System.out.println("Nieprawidłowy email!");
        }
    }
}
Pola (fields) - zmienne które przechowują dane obiektu. Metody - funkcje które obiekt może wykonywać. Konstruktor - specjalna metoda do tworzenia obiektów.

## Tworzenie i używanie obiektów

Mając klasę Person, możesz tworzyć konkretne obiekty - konkretne osoby z własnymi danymi.

public class PersonExample {
    public static void main(String[] args) {
        // Tworzenie obiektów - konkretnych osób
        Person person1 = new Person("Anna Kowalska", 25, "anna@email.com");
        Person person2 = new Person("Jan Nowak", 17, "jan@email.com");
        Person person3 = new Person("Maria Wiśniewska", 45, "maria@email.com");
        
        // Używanie metod obiektów
        person1.introduce(); // Cześć, jestem Anna Kowalska i mam 25 lat.
        person2.introduce(); // Cześć, jestem Jan Nowak i mam 17 lat.
        
        // Sprawdzanie czy osoba jest pełnoletnia
        if (person1.isAdult()) {
            System.out.println(person1.getName() + " jest pełnoletnia");
        }
        
        if (!person2.isAdult()) {
            System.out.println(person2.getName() + " jest niepełnoletni");
        }
        
        // Odczytywanie danych przez gettery
        System.out.println("Email Marii: " + person3.getEmail());
        
        // Zmienianie danych przez settery
        person2.setAge(18);
        person2.introduce(); // Teraz Jan ma 18 lat
        
        // Próba ustawienia nieprawidłowych danych
        person1.setAge(-5);     // Wypisze: "Nieprawidłowy wiek!"
        person1.setEmail("zly-email"); // Wypisze: "Nieprawidłowy email!"
    }
}
Pro tip: Słowo kluczowe new tworzy nowy obiekt w pamięci i wywołuje konstruktor. Każde użycie new Person(...) tworzy zupełnie nowy, niezależny obiekt.

## Konstruktory - jak obiekty się rodzą

Konstruktor to specjalna metoda która jest wywoływana automatycznie gdy tworzysz obiekt. Jego zadanie to przygotowanie obiektu do użycia - ustawienie początkowych wartości.

public class BankAccount {
    private String accountNumber;
    private String ownerName;
    private double balance;
    
    // Konstruktor 1 - nowe konto z depozytem
    public BankAccount(String accountNumber, String ownerName, double initialDeposit) {
        this.accountNumber = accountNumber;
        this.ownerName = ownerName;
        this.balance = initialDeposit;
        System.out.println("Utworzono konto " + accountNumber + 
                          " z saldem " + balance + " zł");
    }
    
    // Konstruktor 2 - nowe konto bez depozytu
    public BankAccount(String accountNumber, String ownerName) {
        this.accountNumber = accountNumber;
        this.ownerName = ownerName;
        this.balance = 0.0;
        System.out.println("Utworzono konto " + accountNumber + " z saldem 0 zł");
    }
    
    // Konstruktor 3 - domyślny (bezparametrowy)
    public BankAccount() {
        this.accountNumber = "TEMP-" + System.currentTimeMillis();
        this.ownerName = "Nieznany";
        this.balance = 0.0;
        System.out.println("Utworzono tymczasowe konto");
    }
    
    public void deposit(double amount) {
        if (amount > 0) {
            balance += amount;
            System.out.println("Wpłacono " + amount + " zł. Saldo: " + balance + " zł");
        }
    }
    
    public boolean withdraw(double amount) {
        if (amount > 0 && amount <= balance) {
            balance -= amount;
            System.out.println("Wypłacono " + amount + " zł. Saldo: " + balance + " zł");
            return true;
        } else {
            System.out.println("Brak środków lub nieprawidłowa kwota");
            return false;
        }
    }
    
    public double getBalance() {
        return balance;
    }
    
    public String getAccountInfo() {
        return "Konto " + accountNumber + " (" + ownerName + "): " + balance + " zł";
    }
}
public class BankExample {
    public static void main(String[] args) {
        // Używanie różnych konstruktorów
        BankAccount account1 = new BankAccount("12345", "Anna Kowalska", 1000.0);
        BankAccount account2 = new BankAccount("67890", "Jan Nowak");
        BankAccount account3 = new BankAccount();
        
        // Operacje na kontach
        account1.withdraw(200.0);  // Wypłacono 200 zł. Saldo: 800 zł
        account2.deposit(500.0);   // Wpłacono 500 zł. Saldo: 500 zł
        account3.deposit(100.0);   // Wpłacono 100 zł. Saldo: 100 zł
        
        // Wyświetlanie informacji o kontach
        System.out.println(account1.getAccountInfo());
        System.out.println(account2.getAccountInfo());
        System.out.println(account3.getAccountInfo());
    }
}
Typowy błąd początkujących: Zapominanie o konstruktorze i próba tworzenia obiektu bez podania wymaganych parametrów. Java automatycznie tworzy konstruktor bezparametrowy tylko jeśli nie napiszesz żadnego konstruktora.

## Enkapsulacja - ukrywanie szczegółów

Enkapsulacja to jedna z najważniejszych zasad OOP. Polega na ukrywaniu wewnętrznych szczegółów obiektu i udostępnianiu tylko tego co naprawdę potrzebne.

Enkapsulacja to jak pilot do telewizora. Nie musisz wiedzieć jak działa elektronika w środku - wystarczy że wiesz które przyciski nacisnąć. Pilot to interfejs, elektronika to ukryte szczegóły implementacji.

### Dlaczego pola powinny być private?

// ŹLE - publiczne pola
public class BadPerson {
    public String name;      // Każdy może zmienić
    public int age;          // Nawet na wartość ujemną!
    public String email;     // Nawet na nieprawidłowy email!
}

// DOBRZE - prywatne pola + gettery/settery
public class GoodPerson {
    private String name;     // Chronione przed bezpośrednim dostępem
    private int age;         // Kontrolowane przez settery
    private String email;    // Walidowane przed ustawieniem
    
    public GoodPerson(String name, int age, String email) {
        setName(name);       // Używamy setterów nawet w konstruktorze
        setAge(age);         // żeby walidacja była wszędzie
        setEmail(email);
    }
    
    // Gettery - bezpieczny dostęp do odczytu
    public String getName() {
        return name;
    }
    
    public int getAge() {
        return age;
    }
    
    public String getEmail() {
        return email;
    }
    
    // Settery z walidacją
    public void setName(String name) {
        if (name != null && !name.trim().isEmpty()) {
            this.name = name.trim();
        } else {
            throw new IllegalArgumentException("Imię nie może być puste");
        }
    }
    
    public void setAge(int age) {
        if (age >= 0 && age <= 150) {
            this.age = age;
        } else {
            throw new IllegalArgumentException("Wiek musi być między 0 a 150");
        }
    }
    
    public void setEmail(String email) {
        if (email != null && email.contains("@") && email.contains(".")) {
            this.email = email.toLowerCase();
        } else {
            throw new IllegalArgumentException("Nieprawidłowy format email");
        }
    }
}
Uwaga: Publiczne pola łamią enkapsulację. Inni programiści mogą zmieniać dane bezpośrednio, omijając Twoją walidację. To prowadzi do błędów które są trudne do znalezienia.

## Praktyczny przykład - klasa Car

Zobaczmy jak zastosować wszystkie poznane koncepty w praktycznej klasie reprezentującej samochód:

public class Car {
    // Prywatne pola - enkapsulacja
    private String brand;
    private String model;
    private int year;
    private double fuelLevel;        // 0.0 - 100.0 (procenty)
    private boolean engineRunning;
    private double odometer;         // kilometry
    
    // Konstruktor
    public Car(String brand, String model, int year) {
        this.brand = brand;
        this.model = model;
        this.year = year;
        this.fuelLevel = 50.0;       // Nowy samochód ma pół baku
        this.engineRunning = false;  // Silnik wyłączony
        this.odometer = 0.0;         // Zerowy przebieg
    }
    
    // Metody biznesowe - to co samochód może robić
    public boolean startEngine() {
        if (!engineRunning && fuelLevel > 0) {
            engineRunning = true;
            System.out.println(brand + " " + model + " - silnik uruchomiony");
            return true;
        } else if (fuelLevel <= 0) {
            System.out.println("Nie można uruchomić - brak paliwa!");
            return false;
        } else {
            System.out.println("Silnik już działa");
            return false;
        }
    }
    
    public void stopEngine() {
        if (engineRunning) {
            engineRunning = false;
            System.out.println("Silnik wyłączony");
        } else {
            System.out.println("Silnik już jest wyłączony");
        }
    }
    
    public boolean drive(double kilometers) {
        if (!engineRunning) {
            System.out.println("Najpierw uruchom silnik!");
            return false;
        }
        
        if (fuelLevel <= 0) {
            System.out.println("Brak paliwa!");
            stopEngine();
            return false;
        }
        
        // Spalanie: 7 litrów na 100km
        double fuelNeeded = kilometers * 0.07;
        
        if (fuelNeeded > fuelLevel) {
            System.out.println("Za mało paliwa na " + kilometers + " km");
            return false;
        }
        
        odometer += kilometers;
        fuelLevel -= fuelNeeded;
        
        System.out.println("Przejechano " + kilometers + " km. " +
                          "Przebieg: " + odometer + " km, " +
                          "Paliwo: " + String.format("%.1f", fuelLevel) + "%");
        
        // Automatyczne wyłączenie gdy skończy się paliwo
        if (fuelLevel <= 0) {
            System.out.println("Skończyło się paliwo!");
            stopEngine();
        }
        
        return true;
    }
    
    public void refuel(double liters) {
        double fuelToAdd = liters * 1.4; // 1 litr = 1.4% zbiornika (tank ~70L)
        
        if (fuelLevel + fuelToAdd > 100.0) {
            fuelToAdd = 100.0 - fuelLevel;
            liters = fuelToAdd / 1.4;
        }
        
        fuelLevel += fuelToAdd;
        System.out.println("Zatankowano " + String.format("%.1f", liters) + 
                          " litrów. Poziom paliwa: " + 
                          String.format("%.1f", fuelLevel) + "%");
    }
    
    // Gettery
    public String getBrand() { return brand; }
    public String getModel() { return model; }
    public int getYear() { return year; }
    public double getFuelLevel() { return fuelLevel; }
    public boolean isEngineRunning() { return engineRunning; }
    public double getOdometer() { return odometer; }
    
    // Metoda do wyświetlania pełnych informacji
    public String getCarInfo() {
        return brand + " " + model + " (" + year + ")" +
               " - Przebieg: " + odometer + " km" +
               ", Paliwo: " + String.format("%.1f", fuelLevel) + "%" +
               ", Silnik: " + (engineRunning ? "włączony" : "wyłączony");
    }
}

### Używanie klasy Car

public class CarExample {
    public static void main(String[] args) {
        // Tworzenie obiektu samochodu
        Car mojAudi = new Car("Audi", "A4", 2020);
        
        System.out.println("=== INFORMACJE O SAMOCHODZIE ===" );
        System.out.println(mojAudi.getCarInfo());
        
        System.out.println("\n=== PRÓBA JAZDY BEZ URUCHOMIENIA ===" );
        mojAudi.drive(50); // Nie zadziała - silnik wyłączony
        
        System.out.println("\n=== URUCHOMIENIE I JAZDA ===" );
        mojAudi.startEngine();
        mojAudi.drive(100);  // Pojedzie, zużyje paliwo
        mojAudi.drive(200);  // Pojedzie dalej
        
        System.out.println("\n=== STAN PO JEŹDZIE ===" );
        System.out.println(mojAudi.getCarInfo());
        
        System.out.println("\n=== TANKOWANIE ===" );
        mojAudi.refuel(30);  // Zatankuj 30 litrów
        
        System.out.println("\n=== DŁUGA JAZDA ===" );
        mojAudi.drive(500);  // Długa trasa
        mojAudi.drive(100);  // Może skończyć się paliwo
        
        System.out.println("\n=== STAN KOŃCOWY ===" );
        System.out.println(mojAudi.getCarInfo());
    }
}
Zauważ jak klasa Car ukrywa skomplikowaną logikę (obliczanie spalania, sprawdzanie stanu silnika) i udostępnia proste metody typu startEngine(), drive(). To jest siła enkapsulacji!

## Najczęstsze błędy początkujących

Błąd 1: Tworzenie getterów i setterów dla wszystkich pól automatycznie. Nie każde pole potrzebuje settera! Odometer w samochodzie nie powinien mieć settera - zmienia się tylko przez jazdę.
Błąd 2: Settery bez walidacji. Jeśli pozwalasz ustawić wiek na -50 lat, Twoja aplikacja będzie zawierać błędne dane.
Błąd 3: Porównywanie obiektów przez == zamiast equals(). person1 == person2 sprawdza czy to ten sam obiekt w pamięci, nie czy mają te same dane.
Błąd 4: Zapominanie o słowie kluczowym this gdy parametr ma taką samą nazwę jak pole klasy.
// BŁĄD - nie działa!
public void setName(String name) {
    name = name;  // Przypisujesz parametr do samego siebie!
}

// POPRAWNIE
public void setName(String name) {
    this.name = name;  // this.name to pole klasy, name to parametr
}
Po co tworzyć gettery i settery skoro mogę zrobić pola publiczne?

Gettery i settery dają kontrolę. Możesz walidować dane, logować zmiany, formatować wartości. Publiczne pola to jak pozostawienie klucza w drzwiach - każdy może zmienić dane jak chce.

Czy każda klasa musi mieć konstruktor?

Nie musisz pisać konstruktora - Java utworzy domyślny konstruktor bezparametrowy. Ale jeśli napiszesz jakikolwiek konstruktor, domyślny znika. Wtedy musisz go dodać ręcznie jeśli go potrzebujesz.

Ile obiektów mogę utworzyć z jednej klasy?

Teoretycznie nieograniczenie (ogranicza tylko pamięć). Każdy obiekt ma swoje własne pola z niezależnymi wartościami. Możesz mieć tysiące obiektów Person, każdy z innym imieniem i wiekiem.

Co się stanie jeśli zapomnę o słowie kluczowym 'new'?

Błąd kompilacji! Person person = Person(); nie zadziała. Słowo new jest obowiązkowe do tworzenia obiektów (wyjątek: String literals i autoboxing prymitywów).

Czy metody w klasie muszą być publiczne?

Nie! Możesz mieć metody private (pomocnicze, używane tylko wewnątrz klasy), protected (dla dziedziczenia) lub package-private (domyślne). Publiczne rób tylko te które mają być dostępne z zewnątrz.

Jak sprawdzić czy dwa obiekty są takie same?

Używaj equals(), nie ==. Ale musisz override'ować metodę equals() w swojej klasie, żeby porównywała zawartość obiektów, nie referencje w pamięci. Domyślna equals() działa jak ==.

Co to znaczy 'this' i kiedy go używać?

this odnosi się do bieżącego obiektu. Używaj gdy nazwa parametru jest taka sama jak nazwa pola, lub gdy chcesz wyraźnie zaznaczyć że odnoszisz się do pola/metody obiektu.

## Przydatne zasoby

- [Oracle Java Tutorial - Classes and Objects](https://docs.oracle.com/javase/tutorial/java/javaOO/) - oficjalny tutorial Oracle
- [Java Code Conventions](https://www.oracle.com/java/technologies/javase/codeconventions-contents.html) - jak pisać czytelny kod
- [Effective Java](https://www.goodreads.com/book/show/105099.Effective_Java_Programming_Language_Guide) - książka Joshua Blocha o najlepszych praktykach
- [Clean Code](https://www.goodreads.com/book/show/3735293-clean-code) - Robert Martin o pisaniu czystego kodu

🚀 Zadanie dla Ciebie

Stwórz klasę "Student" z następującymi wymaganiami:

  • Pola: imię, nazwisko, numer indeksu, lista ocen
  • Konstruktor przyjmujący imię, nazwisko i numer indeksu
  • Metoda addGrade(double grade) z walidacją (2.0-5.0)
  • Metoda getAverageGrade() licząca średnią
  • Metoda hasPassedExam() sprawdzająca czy średnia >= 3.0
  • Gettery dla wszystkich pól, settery tylko gdzie sensowne

Przetestuj klasę tworząc kilku studentów i sprawdzając różne scenariusze.

Czy już widzisz jak potężne jest myślenie obiektowe? Podziel się w komentarzach swoją pierwszą klasą - co reprezentowała i jakie problemy rozwiązała!

Zostaw komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *

Przewijanie do góry