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
## Czym jest klasa?
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!"); } } }
## 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!" } }
## 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()); } }
## 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.
### 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"); } } }
## 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()); } }
## Najczęstsze błędy początkujących
// 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 }
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.
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.
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.
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).
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.
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 ==.
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!