Wykład 5
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;
}
}
}
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.
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:
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.
com.example.mypackage
, odpowiednia struktura katalogów to com/example/mypackage/
.Organizacja: Klasy o podobnej funkcjonalności lub przeznaczeniu powinny być grupowane w tym samym pakiecie. Pomaga to w organizacji kodu i jego zrozumieniu.
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.
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.
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.
Unikanie konfliktów nazw: Dzięki pakietom możesz mieć klasy o tej samej nazwie w różnych pakietach bez konfliktów.
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.
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.
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 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:
void
.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
}
}
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.
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ą.
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.
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.
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.
null
), jeśli wiesz, że obiekt nie będzie już używany, chociaż nie jest to zazwyczaj konieczne.try-with-resources
lub jawnie zamykać te zasoby za pomocą odpowiednich metod (np. close()
).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 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);
}
}
}
Gettery i settery to metody, które umożliwiają odczytywanie (gettery) i modyfikowanie (settery) wartości pól prywatnych (private) klas.
private
. Getter i setter są publicznymi metodami do odczytywania i modyfikowania tych prywatnych pól.null
, czy znajdują się w akceptowalnym zakresie wartości).ArrayList
, getter powinien zwracać kopię obiektu, a nie jego rzeczywiste odniesienie.synchronized
w getterach i setterach, aby zapewnić bezpieczeństwo wątkowe.final
. Pomaga to w zapewnieniu bezpieczeństwa wątkowego.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.
Pole statyczne deklarowane jest w klasie z użyciem słowa kluczowego static
.
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.Przykład
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.
Przynależność do klasy: Metoda statyczna należy do klasy, a nie do instancji tej klasy.
Wywołanie: Metodę statyczną można wywołać bezpośrednio na klasie, np. ClassName.staticMethodName()
.
Dostęp do pól statycznych: Metody statyczne mogą bezpośrednio dostępować inne statyczne składowe (pola i metody) danej klasy.
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.
Używanie: Są one często używane jako metody pomocnicze (np. metody fabrykujące, metody utilitarne, metody obliczeniowe).
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.
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ą.
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.
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;
}
}
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?
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 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.
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);
}
}
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.
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).
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ą.
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 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:
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.
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.
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.
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.
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.
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.");
}
}
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.
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:
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.
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.
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.
getClass()
: Zwraca obiekt Class
, który reprezentuje klasę bieżącego obiektu. Może być używany do uzyskania informacji o klasie w runtime.
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
.
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.
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:
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.
Sygnatura: Metoda toString()
musi być publiczna, nie przyjmować żadnych argumentów, i musi zwracać String
.
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.
Reprezentacja: Zwrócony ciąg znaków powinien być zwięzły, ale wystarczająco informatywny, aby przedstawić najważniejsze informacje o obiekcie.
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).
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.
@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.
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).
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ą.
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.
Dokumentacja: Adnotacja @Override
służy jako część wewnętrznej dokumentacji kodu, wskazując, że dana metoda jest powiązana z hierarchią dziedziczenia klasy.
@Override
spowoduje błąd kompilacji, sygnalizując problem.