Programowanie obiektowe

Wykład 3

Java - podstawy cd.

Tablice jednowymiarowe - sprostowanie i uzupełnienie

w Javie argumenty są zawsze przekazywane przez wartość.

public class Main {

    public static void main(String[] args) {
        int[] tablica = {1, 2, 3};
        modyfikujTablice(tablica);
        System.out.println(tablica[0]); // będzie 42
    }

    public static void modyfikujTablice(int[] arr) {
        arr[0] = 42;
    }
}

Sama deklaracja daje “niezainicjowaną tablicę”

public class Main {
    public static void main(String[] args) {
        int[] tab;
        System.out.println(tab.length);
    }
}

Tablica pusta w Javie to tablica, która ma rozmiar zero. Innymi słowy, nie może przechowywać żadnych elementów, ale to wciąż jest prawidłowy obiekt tablicy.

public class Main {
    public static void main(String[] args) {
        int[] tab = new int[0];
        System.out.println(tab.length);
    }
}

Jeśli tablica jest równa null, oznacza to, że nie odnosi się do żadnego obiektu w pamięci. Jest to specjalna wartość, która reprezentuje, że zmienna tablicy nie ma aktualnie przypisanego obiektu.

public class Main {
    public static void main(String[] args) {
        int[] tab = null;
        System.out.println(tab.length);
    }
}

Lista tablicowa ArrayList

ArrayList to jedna z najczęściej używanych implementacji interfejsu List. Jest to dynamicznie rozszerzalna tablica, która może zmieniać swój rozmiar w miarę dodawania i usuwania elementów. Wewnętrznie ArrayList używa tablicy do przechowywania elementów.

Własności:

  1. Wewnętrzna struktura danych: Jak wspomniano wcześniej, ArrayList wewnętrznie używa tablicy do przechowywania elementów. Gdy lista staje się pełna i potrzeba dodać kolejny element, Java tworzy większą tablicę i kopiuje elementy starej tablicy do nowej.

  2. Rozmiar vs. Pojemność:

    • Rozmiar to liczba elementów w liście.
    • Pojemność to liczba elementów, które lista może aktualnie przechowywać bez potrzeby alokacji większej tablicy.
  1. Zalety:
    • Dynamiczne rozszerzanie: ArrayList może zmieniać swój rozmiar w miarę potrzeb, co czyni ją bardziej elastyczną niż zwykłe tablice.
    • Dostęp indeksowany: Można łatwo uzyskać dostęp do elementu na podstawie jego indeksu.
    • Możliwość przechowywania wartości null.
  1. Wady:
    • Koszt wstawiania i usuwania: Wstawianie i usuwanie elementów (zwłaszcza w środku listy) może być kosztowne, ponieważ wymaga przesuwania innych elementów.
    • Zużywa więcej pamięci w porównaniu z LinkedList z powodu dodatkowej pojemności rezerwowej.
  1. Bezpieczeństwo wątkowe: Standardowa implementacja ArrayList nie jest synchronizowana, co oznacza, że nie jest bezpieczna do użytku w środowiskach wielowątkowych bez odpowiedniej synchronizacji. Jeśli potrzebujesz wersji synchronizowanej, możesz użyć Collections.synchronizedList().

  2. Pamięć: Jeśli znasz ostateczny rozmiar listy, warto zainicjalizować ArrayList z odpowiednią pojemnością początkową, aby uniknąć wielokrotnego rozszerzania wewnętrznej tablicy.

ArrayList<String> list = new ArrayList<>();

// Dodawanie elementów
list.add("A");
list.add("B");
list.add("C");

// Odczytywanie elementów
String element = list.get(1);  // B

// Ustalanie rozmiaru listy
int size = list.size();  // 3

// Usuwanie elementu
list.remove(1);  // Usuwa element "B"

Okej, przyjrzyjmy się niektórym z najczęściej używanych metod i właściwości klasy ArrayList w Javie:

Najczęściej używane metody:

  1. add(E element):

    • Dodaje element na końcu listy.
    ArrayList<String> list = new ArrayList<>();
    list.add("Element");
  2. add(int index, E element):

    • Wstawia określony element w określonej pozycji na liście.
    list.add(1, "Nowy element");
  1. get(int index):

    • Zwraca element z określonej pozycji.
    String el = list.get(1);
  2. remove(int index):

    • Usuwa element z określonej pozycji.
    list.remove(1);
  3. remove(Object o):

    • Usuwa pierwsze wystąpienie określonego elementu z listy (jeśli istnieje).
    list.remove("Element");
  1. size():

    • Zwraca liczbę elementów na liście.
    int rozmiar = list.size();
  2. isEmpty():

    • Sprawdza, czy lista jest pusta.
    boolean jestPusta = list.isEmpty();
  3. clear():

    • Usuwa wszystkie elementy z listy.
    list.clear();
  1. contains(Object o):

    • Sprawdza, czy lista zawiera określony element.
    boolean zawiera = list.contains("Element");
  2. indexOf(Object o):

    • Zwraca indeks pierwszego wystąpienia określonego elementu na liście lub -1, jeśli elementu nie ma na liście.
    int indeks = list.indexOf("Element");
  1. set(int index, E element):

    • Zastępuje element na określonej pozycji.
    list.set(1, "Zastąpiony element");
  2. toArray():

    • Zwraca tablicę zawierającą wszystkie elementy w liście w odpowiedniej kolejności.
    Object[] tablica = list.toArray();

Klasy takie jak ArrayList są generycznymi klasami kontenerowymi, które przechowują obiekty, a nie typy proste. Dlatego nie możemy bezpośrednio użyć int lub double jako typu elementu dla ArrayList.

Aby rozwiązać ten problem, Java dostarcza klasy opakowujące (wrapper classes) dla wszystkich typów prostych. Dla int mamy Integer, dla double mamy Double itd.

Dzięki autoboxingowi (automatyczne konwersje między typami prostymi a ich klasami opakowującymi) korzystanie z tych klas opakowujących jest stosunkowo proste i wygodne.

  1. Dla int:

    ArrayList<Integer> listaInt = new ArrayList<>();
    listaInt.add(1);  // Autoboxing konwertuje 'int' na 'Integer'
    int liczba = listaInt.get(0);  // Autounboxing konwertuje 'Integer' na 'int'
  1. Dla double:

    ArrayList<Double> listaDouble = new ArrayList<>();
    listaDouble.add(1.5);  // Autoboxing konwertuje 'double' na 'Double'
    double liczbaDouble = listaDouble.get(0);  // Autounboxing konwertuje 'Double' na 'double'

Domyślne wyświetlanie zawartości ArrayList jest dość proste dzięki nadpisanemu przez klasę ArrayList metodzie toString(). Gdy wywołasz metodę System.out.println na obiekcie ArrayList, Java automatycznie wywołuje metodę toString() klasy ArrayList.

import java.util.ArrayList;

public class Main {
    public static void main(String[] args) {
        ArrayList<String> list = new ArrayList<>();
        list.add("Java");
        list.add("Python");
        list.add("C++");
        System.out.println(list);
    }
}

Niezainicjowana lista tablicowa:

  • Dotyczy to zmiennej deklarującej ArrayList, ale nie przypisującej jej do żadnego obiektu.
  • Próba dostępu do niezainicjowanej listy spowoduje błąd w czasie kompilacji.
ArrayList<String> list; 
// list.add("test");  // Błąd! Zmienna "list" może nie zostać zainicjowana

Pusta lista tablicowa:

  • ArrayList został zainicjowany, ale nie zawiera żadnych elementów.
  • Jest to prawidłowy obiekt ArrayList i możesz wykonywać na nim operacje, takie jak dodawanie i usuwanie elementów.
ArrayList<String> list = new ArrayList<>();
System.out.println(list.size());  // Wydrukuje "0", ponieważ lista jest pusta

null:

  • Zmienna ArrayList jest jawnie ustawiona na wartość null lub domyślnie jest ustawiona na null i nie została zainicjowana później.
  • Jest to specjalny stan, który oznacza, że zmienna nie odnosi się do żadnego obiektu.
  • Próba dostępu do listy, która jest null, spowoduje błąd w czasie wykonywania zwanym NullPointerException.
ArrayList<String> list = null;
// list.add("test");  // Błąd! Wyjątek NullPointerException

Inne mechanizmy tworzenia arraylisty

import java.util.ArrayList;
import java.util.Arrays;

public class Main {
    public static void main(String[] args) {
        ArrayList<String> lt1 = new ArrayList<>();
        ArrayList<String> lt2 = new ArrayList<>(10); // Pojemność początkowa 10
        ArrayList<Integer> lt3 = new ArrayList<>(){{
            add(5);
            add(-4);
            add(3);
        }};        
    }
} 

Napisy, łańcuchy znaków

Inicjalizacja:

  • Można inicjalizować napisy na różne sposoby:
String s1 = "Hello";
String s2 = new String("Hello");

Używanie metody System.identityHashCode nie jest zalecane!

public class Main {

    public static void main(String[] args) {
        String s = "Hello";
        String s2 = "Hello";
        String s3 = new String("Hello");
        System.out.println(System.identityHashCode(s));
        System.out.println(System.identityHashCode(s2));
        System.out.println(System.identityHashCode(s3));
    }
}
public class Main {

    public static void main(String[] args) {
        String s1 = "Hello";
        String s2 = "Hello";
        String s3 = new String("Hello");
        System.out.println(s1 == s2); // true
        System.out.println(s1 == s3); // false
        System.out.println(s2 == s3); // false
    }
}

Niezmienność (Immutability):

  • Obiekty klasy String są niezmienne. Oznacza to, że raz utworzony łańcuch znaków nie może być zmieniony. Wszelkie operacje modyfikujące zawartość łańcucha (np. dodawanie, usuwanie znaków) skutkują stworzeniem nowego obiektu String.

Konkatenacja:

  • Napisy można łączyć za pomocą operatora +:
public class Main {

    public static void main(String[] args) {
        String s1 = "Hello";
        String s2 = "World";
        String s3 = s1 + " " + s2;  // "Hello World"
        String s4 = s1 + 4;  // "Hello4"
        String s5 = s1 + (4.0/5); // "Hello0.8"
    }
}

Porównywanie:

  • Aby porównać zawartość dwóch napisów, powinno się używać metody equals() zamiast operatora ==:
String s1 = "Hello";
String s2 = new String("Hello");
boolean isEqual = s1.equals(s2);  // true

Pamięć:

  • Ze względu na optymalizację pamięci, Java posiada tzw. “pulę napisów” (String Pool). Dwa napisy o tej samej zawartości często będą wskazywać na ten sam obszar pamięci, jeśli zostały zainicjowane bez użycia słowa kluczowego new.

Zmienne łańcuchowe:

  • Jeśli potrzebujesz modyfikowalnego łańcucha, Java oferuje klasy takie jak StringBuilder i StringBuffer. Są one szczególnie przydatne w sytuacjach, gdzie zachodzi wiele modyfikacji łańcucha, ponieważ operują one w miejscu (in-place) i są zwykle szybsze niż tworzenie wielu obiektów String.

Różnice między napisem pustym a null

Wartość:

  • Łańcuch pusty (""): To faktyczny obiekt klasy String, który ma wartość, ale ta wartość jest pusta. Inaczej mówiąc, jest to łańcuch, który nie zawiera żadnych znaków.
  • null: To specjalna wartość, która oznacza, że zmienna nie wskazuje na żaden obiekt. Dla zmiennej typu String, jeśli jest ona ustawiona na null, nie wskazuje ona na żaden łańcuch (pusty czy inny).

Długość:

  • Łańcuch pusty (""): Jego długość wynosi 0. Można to sprawdzić używając metody length(): "".length() zwróci 0.
  • null: Zmienna o wartości null nie posiada metod ani atrybutów. Próba wywołania metody, np. null.length(), spowoduje wyjątek NullPointerException.

Operacje:

  • Łańcuch pusty (""): Możesz wykonywać na nim różne operacje, takie jak konkatenacja czy wywoływanie innych metod klasy String.
  • null: Nie możesz wykonywać na nim żadnych operacji. Każda próba dostępu do metody lub atrybutu na zmiennej o wartości null spowoduje NullPointerException.

Porównywanie:

  • Możesz porównać zarówno łańcuch pusty, jak i null z innymi łańcuchami. Ale musisz być ostrożny z null, ponieważ:

    String s1 = "";
    String s2 = null;
    
    System.out.println(s1.equals(""));  // true
    System.out.println(s2.equals(null));  // Wyjątek: NullPointerException

Zastosowania:

  • Łańcuch pusty (""): Często używany do inicjowania łańcuchów bez konkretnej wartości lub do wskazania, że łańcuch powinien być “czysty” lub “bez wartości”, ale nadal istnieje.
  • null: Wskazuje, że zmienna nie odnosi się do żadnego obiektu. Jest używane w wielu przypadkach, np. gdy wartość nie jest jeszcze znana lub nie została ustawiona.

Metody z API

Dokumentacja

length():

  • Zwraca liczbę znaków w łańcuchu.

    String s = "Hello";
    int len = s.length();  // 5

charAt(int index):

  • Zwraca znak na określonej pozycji w łańcuchu.

    char ch = s.charAt(1);  // 'e'

substring(int beginIndex, int endIndex):

  • Zwraca nowy łańcuch zawierający znaki z oryginalnego łańcucha od beginIndex (włącznie) do endIndex (wyłącznie).

    String sub = s.substring(1, 4);  // "ell"

indexOf(String str) i lastIndexOf(String str):

  • Zwracają indeks pierwszego/ostatniego wystąpienia podciągu w łańcuchu. Jeśli podciąg nie jest znaleziony, zwraca -1.

    int first = s.indexOf("l");  // 2
    int last = s.lastIndexOf("l");  // 3

equals(Object obj):

  • Porównuje zawartość tego łańcucha z zawartością innego obiektu. Zwraca true, jeśli są równe.

    boolean isEqual = s.equals("Hello");  // true

equalsIgnoreCase(String anotherString):

  • Porównuje łańcuchy bez uwzględniania wielkości liter.

    boolean isEqual = s.equalsIgnoreCase("HELLO");  // true

startsWith(String prefix) i endsWith(String suffix):

  • Sprawdzają, czy łańcuch zaczyna się lub kończy danym ciągiem znaków.

    boolean starts = s.startsWith("He");  // true
    boolean ends = s.endsWith("lo");  // true

replace(char oldChar, char newChar) lub replace(CharSequence target, CharSequence replacement):

  • Zwraca nowy łańcuch, w którym wszystkie wystąpienia oldChar lub target są zastąpione przez newChar lub replacement.

    String replaced = s.replace("l", "w");  // "Hewwo"

trim():

  • Zwraca kopię łańcucha z usuniętymi białymi znakami na początku i końcu.

    String trimmed = "  Hello  ".trim();  // "Hello"

toLowerCase() i toUpperCase():

  • Zmieniają wielkość liter w łańcuchu.

    String lower = s.toLowerCase();  // "hello"
    String upper = s.toUpperCase();  // "HELLO"

split(String regex):

  • Dzieli łańcuch według podanego wyrażenia regularnego.

    String[] parts = "Hello-World".split("-");  // ["Hello", "World"]

isEmpty():

  • Sprawdza, czy łańcuch jest pusty (długość wynosi 0).

    boolean empty = "".isEmpty();  // true

valueOf() (statyczna metoda):

  • Konwertuje różne typy danych (np. int, char) na łańcuchy.

    String str = String.valueOf(12345);  // "12345"

Różnice w podejściu do napisów

String:

  • Niemutowalność: Obiekty String są niemutowalne, co oznacza, że po ich utworzeniu nie można ich zmienić. Każda operacja, która wydaje się modyfikować łańcuch (np. konkatenacja), faktycznie tworzy nowy obiekt String.
  • Wydajność: Ze względu na niemutowalność operacje modyfikujące mogą być mniej wydajne (szczególnie w długich pętlach), ponieważ za każdym razem tworzony jest nowy obiekt.
  • Bezpieczeństwo wątków: Jest bezpieczny w wielowątkowości ze względu na niemutowalność.

StringBuilder:

  • Mutowalność: Obiekty StringBuilder są mutowalne. Można dodawać, usuwać i modyfikować zawartość obiektu bez tworzenia nowych obiektów.
  • Wydajność: Jest zazwyczaj bardziej wydajny niż String w operacjach modyfikujących, szczególnie w intensywnych operacjach, takich jak budowanie łańcuchów w pętlach.
  • Bezpieczeństwo wątków: Nie jest synchronizowany, co oznacza, że może nie być bezpieczny w środowiskach wielowątkowych. Jeśli bezpieczeństwo wątków nie jest wymagane, StringBuilder jest zwykle lepszym wyborem niż StringBuffer.

Często używane metody:

append():

  • Dodaje wartość do końca obecnej zawartości StringBuilder.

  • Jest przeciążona, by obsłużyć różne typy danych: String, char, int, long, float, double itp.

  • Przykład:

    StringBuilder sb = new StringBuilder("Hello");
    sb.append(" World");

insert():

  • Wstawia wartość w określonej pozycji w StringBuilder.

  • Przykład:

    StringBuilder sb = new StringBuilder("Hello World");
    sb.insert(6, "Java ");

delete() i deleteCharAt():

  • delete(int start, int end) usuwa podciąg znaków od indeksu start do indeksu end - 1.

  • deleteCharAt(int index) usuwa znak w określonym indeksie.

  • Przykład:

    StringBuilder sb = new StringBuilder("Hello Java World");
    sb.delete(6, 11); // Usuwa słowo "Java "

replace():

  • Zamienia podciąg znaków w określonym zakresie na inny ciąg znaków.

  • Przykład:

    StringBuilder sb = new StringBuilder("Hello Java");
    sb.replace(6, 10, "World");

toString():

  • Konwertuje StringBuilder na String.

  • Przykład:

    StringBuilder sb = new StringBuilder("Hello World");
    String str = sb.toString();

length():

  • Zwraca liczbę znaków w StringBuilder.

  • Przykład:

    StringBuilder sb = new StringBuilder("Hello");
    int len = sb.length();  // len = 5

setLength():

  • Ustawia długość StringBuilder. Jeśli nowa długość jest krótsza niż obecna, ciąg zostanie obcięty. Jeśli jest dłuższy, dodane zostaną znaki o wartości \u0000.

  • Przykład:

    StringBuilder sb = new StringBuilder("Hello");
    sb.setLength(3);  // sb = "Hel"

charAt(), setCharAt():

  • charAt(int index) zwraca znak w określonym indeksie.

  • setCharAt(int index, char ch) ustawia znak w określonym indeksie.

  • Przykład:

    StringBuilder sb = new StringBuilder("Hello");
    char ch = sb.charAt(1);  // ch = 'e'
    sb.setCharAt(1, 'a');  // sb = "Hallo"

reverse():

  • Odwraca kolejność znaków w StringBuilder.

  • Przykład:

    StringBuilder sb = new StringBuilder("Hello");
    sb.reverse();  // sb = "olleH"

StringBuffer:

  • Mutowalność: Podobnie jak StringBuilder, obiekty StringBuffer są mutowalne.
  • Wydajność: Generalnie podobna wydajność do StringBuilder, ale operacje są synchronizowane.
  • Bezpieczeństwo wątków: Jest synchronizowany, co oznacza, że jest bezpieczny w środowiskach wielowątkowych. Jeśli potrzebujesz mutowalnego łańcucha w środowisku wielowątkowym, StringBuffer może być odpowiednim wyborem.

Metody dla StringBuffer są w większości analogiczne jak dla StringBuilder.

Funkcje znakowe

https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/Character.html

Klasy i obiekty

Klasa

Klasa w programowaniu obiektowym w języku Java to podstawowy koncept i “struktura”formuła”, która pozwala na tworzenie obiektów.

  1. Blueprint: Klasa jest jak “projekt” lub szablon, który opisuje, jakie zmienne (stany) i metody (zachowania) powinien posiadać obiekt tej klasy.

  2. Enkapsulacja (hermatyzacja): Klasa kapsułkuje dane dla obiektu i funkcje, które operują na tych danych. To pozwala na ukrycie szczegółów implementacji i eksponowanie tylko niezbędnych interfejsów.

  1. Konstruktor: Klasa może posiadać specjalne metody nazywane konstruktorami, które są używane do inicjalizacji obiektu podczas jego tworzenia.

  2. Dziedziczenia: Klasa może dziedziczyć zmienne i metody z innej klasy. Dziedziczenie pozwala na tworzenie nowej klasy na bazie już istniejącej, co ułatwia ponowne wykorzystanie kodu.

  1. Modyfikatory: Klasy mogą być opatrzone różnymi modyfikatorami, które decydują o dostępie do klasy oraz jej składników.

  2. Zmienne instancyjne: Wewnątrz klasy można zdefiniować zmienne, które będą przechowywać stan obiektu.

  3. Metody: Metody to funkcje zdefiniowane w klasie, które operują na zmiennych instancji i definiują zachowanie obiektu.

  4. Klasy wewnętrzne: Java pozwala na definiowanie klas wewnątrz innych klas, co nazywane jest klasami wewnętrznymi.

Obiekt

Obiekt to podstawowy byt w programowaniu obiektowym. Jest to konkretna instancja klasy, która jest jak “projekt” lub “szablon”. Obiekty stanowią podstawę działania wielu aplikacji i systemów. Aby zrozumieć obiekty, warto je podzielić na trzy główne aspekty: stan, zachowanie i tożsamość.

  1. Stan (State):
    • Stan obiektu odnosi się do informacji przechowywanych w obiekcie w danym momencie czasu.
    • W programowaniu obiektowym, stan obiektu jest reprezentowany przez zmienne instancji (lub atrybuty).
    • Na przykład, jeśli mamy obiekt klasy Samochod, stan tego obiektu mógłby być reprezentowany przez takie zmienne jak marka, model, rokProdukcji, kolor itp.
    • Stan obiektu może ulegać zmianie w czasie działania programu.
  1. Zachowanie (Behavior):
    • Zachowanie obiektu odnosi się do różnych operacji, które obiekt może wykonywać.
    • W kontekście programowania obiektowego, zachowania są reprezentowane przez metody klasy.
    • Metody operują na stanie obiektu i mogą modyfikować jego stan.
    • Dla klasy Samochod, przykładowe zachowania mogłyby obejmować przyspiesz(), zatrzymaj(), skręćWLewo() itp.
  1. Tożsamość (Identity):
    • Tożsamość obiektu odnosi się do unikalnej charakterystyki, która odróżnia jeden obiekt od innego, nawet jeśli oba obiekty mają taki sam stan.
    • W większości systemów programowania obiektowego, każdy obiekt ma unikalny identyfikator, który jest przypisywany mu podczas tworzenia.
    • W języku Java, na przykład, każdy obiekt ma domyślny adres pamięci, który służy jako jego tożsamość. Choć dwie instancje klasy Samochod mogą mieć taki sam stan (takie same marki, modele, kolory itp.), każdy z nich będzie miał unikalny adres pamięci, dzięki czemu można je odróżnić.

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.

Przykładowa klasa

public class Car {

    // 1. Deklaracje członków klasy

    // a. Stałe
    public static final int MAX_SPEED = 200;

    // b. Zmienne statyczne
    private static int totalCarsProduced;

    // c. Zmienne instancji
    private String make;
    private String model;
    private int year;
    private int speed;

    // 2. Konstruktory
    public Car(String make, String model, int year) {
        this.make = make;
        this.model = model;
        this.year = year;
        this.speed = 0;
        totalCarsProduced++;
    }

    // 3. Metody

    // a. Metody fabrykujące (jeśli by były potrzebne, dla demonstracji)
    public static Car createVintageCar(String make, String model) {
        return new Car(make, model, 1960);
    }

    // b. Metody dostępowe (gettery i settery)
    public String getMake() {
        return make;
    }

    public void setMake(String make) {
        this.make = make;
    }

    public String getModel() {
        return model;
    }

    public void setModel(String model) {
        this.model = model;
    }

    public int getYear() {
        return year;
    }

    public void setYear(int year) {
        this.year = year;
    }

    public int getSpeed() {
        return speed;
    }

    // c. Metody publiczne
    public void accelerate() {
        if (speed < MAX_SPEED) {
            speed += 10;
        }
    }

    public void decelerate() {
        if (speed > 0) {
            speed -= 10;
        }
    }

    // d. Metody chronione (dla demonstracji, w tym przypadku mogą nie mieć sensu)
    protected void resetCar() {
        speed = 0;
    }

    // e. Metody prywatne
    private void updateTotalCars() {
        totalCarsProduced++;
    }

    // 4. Wewnętrzne klasy (dla demonstracji)
    private class Engine {
        private int horsepower;
        private String type;

        Engine(int horsepower, String type) {
            this.horsepower = horsepower;
            this.type = type;
        }

        public int getHorsepower() {
            return horsepower;
        }

        public String getType() {
            return type;
        }
    }
}

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ć.

public class Dog {

    // Zmienne instancji
    private String name;
    private String breed;

    // Metody
    public String getName() {
        return name;
    }

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

    public String getBreed() {
        return breed;
    }

    public void setBreed(String breed) {
        this.breed = breed;
    }
}

// Główna klasa z metodą main do demonstracji
public class TestDog {

    public static void main(String[] args) {
        // Tworzenie obiektu klasy Dog przy użyciu domyślnego konstruktora
        Dog myDog = new Dog();

        // Ustawianie wartości za pomocą setterów, ponieważ nie mamy zdefiniowanego konstruktora z argumentami
        myDog.setName("Rex");
        myDog.setBreed("Golden Retriever");

        // Wydrukowanie wartości
        System.out.println("Name: " + myDog.getName());
        System.out.println("Breed: " + myDog.getBreed());
    }
}

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

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


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

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

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.