Programowanie obiektowe

Wykład 2

Java

Podstawowe typy danych

Java oferuje zestaw typów podstawowych (nazywanych też typami prostymi), które są używane do reprezentacji prostych wartości, takich jak liczby całkowite, liczby zmiennoprzecinkowe, znaki czy wartości logiczne.

  1. Liczby całkowite:
    • byte: 8-bitowa wartość liczby całkowitej; zakres od \(-128\) do \(127\).
    • short: 16-bitowa wartość liczby całkowitej; zakres od \(-32,768\) do \(32,767\).
    • int: 32-bitowa wartość liczby całkowitej; zakres od \(-2^{31}\) do \(2^{31}-1\).
    • long: 64-bitowa wartość liczby całkowitej; zakres od \(-2^{63}\) do \(2^{63}-1\).
  1. Liczby zmiennoprzecinkowe:
    • float: 32-bitowa wartość liczby zmiennoprzecinkowej (jednokrotnej precyzji). Zaleca się używanie sufiksu F lub f przy inicjalizacji wartości stałych.
    • double: 64-bitowa wartość liczby zmiennoprzecinkowej (podwójnej precyzji). Jest to domyślny typ dla wartości zmiennoprzecinkowych w Javie.
  1. Znak:
    • char: Reprezentuje pojedynczy znak w standardzie Unicode i zajmuje 16 bitów. Znaki są zamykane w pojedynczych cudzysłowach, np. 'A'.
  2. Wartość logiczna:
    • boolean: Reprezentuje wartość prawda/fałsz. Może przyjmować tylko jedną z dwóch wartości: true lub false.

Uwagi:

  • Typy podstawowe w Javie są zawsze o stałej wielkości, niezależnie od platformy.
  • Każdy z tych typów podstawowych (oprócz boolean) ma odpowiadającą mu klasę opakowującą w pakiecie java.lang (np. Integer dla int, Double dla double itp.). Klasy te są używane, gdy potrzebujemy reprezentować typ podstawowy jako obiekt, oraz oferują wiele pomocniczych metod do pracy z danym typem.
  • Inicjalizacja zmiennych typów prostych zawsze nadaje im domyślną wartość (np. 0 dla typów liczbowych, false dla boolean), ale wartości te są ustawiane domyślnie tylko dla zmiennych klasowych lub instancyjnych, nie dla lokalnych zmiennych wewnątrz metod.
public class Main {
    public static void main(String[] args) {
        int localVar; // zmienna lokalna niezainicjalizowana

        // Poniższa linia spowoduje błąd kompilacji, ponieważ próbujemy użyć zmiennej,
        // która nie została zainicjalizowana
        // System.out.println(localVar);

        localVar = 10; // inicjalizacja zmiennej lokalnej
        System.out.println(localVar); // teraz jest w porządku, ponieważ zmienna
        // została zainicjalizowana
    }
}

Operacje wyjścia na konsolę

W języku Java operacje wyjścia na konsolę odnoszą się głównie do wydruków tekstowych prezentowanych użytkownikowi na standardowym wyjściu (czyli zazwyczaj konsoli terminala).

  1. System.out.print() i System.out.println():

    • System.out.print("Tekst"): Drukuje podany tekst na konsoli bez przechodzenia do nowej linii.
    • System.out.println("Tekst"): Drukuje podany tekst na konsoli i przechodzi do nowej linii.

    Przykład:

    System.out.print("Witaj, ");
    System.out.println("świecie!");

    Wyjście:

    Witaj, świecie!
  1. System.out.printf(): Pozwala na formatowane wyjście. Umożliwia wstawianie zmiennych w określone miejsca w tekście oraz kontrolowanie sposobu ich prezentacji.

    Przykład:

    double cena = 12.5;
    int ilosc = 5;
    System.out.printf("Cena: %.2f, Ilość: %d, Łącznie: %.2f", cena, ilosc, cena * ilosc);

    Wyjście:

    Cena: 12.50, Ilość: 5, Łącznie: 62.50

Metoda System.out.printf() w Javie oraz funkcja printf() w języku C są do siebie podobne pod względem ogólnego przeznaczenia i użycia, ponieważ obie służą do formatowanego wydruku napisów i są inspirowane tym samym konceptem. Niemniej jednak, istnieje kilka różnic w używaniu tych funkcji/metod, zarówno pod względem składni, jak i funkcjonalności, które mogą wpłynąć na to, jak są używane w obu językach.

Formatowanie: dokumentacja.

Znacznik %n oznacza koniec linii w Javie.

Operacje wejścia z konsoli

Klasa Scanner jest często używana do wczytywania danych wejściowych z konsoli, ponieważ jest łatwa w użyciu i oferuje szeroki zakres funkcji do wczytywania różnych typów danych.

  1. Inicjalizacja: Aby użyć klasy Scanner, najpierw musisz ją zaimportować z pakietu java.util, a następnie utworzyć jej obiekt, używając System.in jako źródła wejściowego.

    import java.util.Scanner;
    
    public class Main {
        public static void main(String[] args) {
            Scanner scanner = new Scanner(System.in);
        }
    }
  1. Wczytywanie danych:

    • Wczytywanie całego wiersza tekstu:

      String line = scanner.nextLine();
    • Wczytywanie pojedynczego słowa:

      String word = scanner.next();
    • Wczytywanie danych liczbowych:

      int number = scanner.nextInt();       // dla int
      double value = scanner.nextDouble();  // dla double
      float valueFloat = scanner.nextFloat(); // dla float
    • Wczytywanie wartości logicznych:

      boolean isTrue = scanner.nextBoolean();
  1. Zamykanie skanera: Po zakończeniu wczytywania danych z konsoli warto zamknąć obiekt Scanner, aby zwolnić zasoby.

    scanner.close();

Więcej info: w dokumentacji.

Deklaracja bez inicjalizacji

Jeśli nie przypiszesz wartości początkowej do zmiennej podczas deklaracji, dla zmiennych lokalnych musisz to zrobić przed ich pierwszym użyciem. W przeciwnym razie kompilator zgłosi błąd. Zmienne instancji (pola klasy) otrzymują domyślne wartości.

int count;
double rate;

Deklaracja z inicjalizacją

int age = 30;
double salary = 5500.75;
char letter = 'A';
boolean isActive = true;

Słowo kluczowe var

Słowo kluczowe var zostało wprowadzone w Javie 10 w celu poprawy czytelności kodu poprzez lokalne inferencje typów dla deklaracji zmiennych. Umożliwia to deklarowanie zmiennych bez konieczności jawnego podawania ich typu, o ile można jednoznacznie wywnioskować typ zmiennej na podstawie przypisywanej jej wartości.

  1. Lokalne inferencje typów: var może być używane tylko do deklaracji lokalnych zmiennych (wewnątrz metod, bloków, pętli itp.), inicjalizowanych zmiennych dla instrukcji for oraz zmiennych lokalnych w try-with-resources.

  2. Nie można używać z wartościami null: Ponieważ var opiera się na inferencji typów, nie można zadeklarować zmiennej jako var i jednocześnie przypisać jej wartości null, ponieważ kompilator nie będzie mógł wywnioskować typu.

    // Błędne
    // var something = null;
  1. Nie można używać dla pól klasy, metod, ani zmiennych zewnętrznych: Słowo kluczowe var nie jest dozwolone dla deklaracji pól klasy, sygnatur metod ani jako typ zwracany.
  1. Czytelność kodu: Używanie var może poprawić czytelność, zwłaszcza gdy typ zmiennej jest oczywisty z kontekstu lub kiedy typ właściwy jest długi i skomplikowany (np. typy generyczne). Jednak w innych sytuacjach jawnie podane typy mogą być bardziej czytelne. Należy stosować var z rozwagą.

    var list = new ArrayList<String>();  // List<String>
    var stream = list.stream();           // Stream<String>
  2. Nie można używać dla zmiennych bez inicjalizacji: Deklaracja zmiennej z użyciem var musi odbywać się jednocześnie z jej inicjalizacją.

  1. Typ jest nadal statyczny: Mimo że jawnie nie deklaruje się typu zmiennej, Java jest językiem o statycznej typizacji, więc typ zmiennej jest ustalony w czasie kompilacji.

Przykład:

var number = 10;  // inferencja typu int
var text = "Hello";  // inferencja typu String

Stałe

Deklaracja: Używając modyfikatora final podczas deklarowania zmiennej, sprawiamy, że staje się ona stałą. Oznacza to, że jej wartość nie może zostać zmieniona po pierwszym przypisaniu.

final int MAX_COUNT = 100;

Inicjalizacja: Stała zmienna musi być zainicjowana w momencie jej deklaracji lub w konstruktorze (w przypadku zmiennych instancji).

Nazewnictwo: Konwencją jest nazywanie stałych zmiennych używając wielkich liter i podkreślników, aby łatwo je rozpoznać w kodzie, choć to nie jest wymogiem języka.

Typ wyliczeniowy enum

Podstawy podobnie jak w C. Inne elementy będą omówione później.

public class Main {
    public static void main(String[] args) {
        for (Dzien d : Dzien.values()) {
            System.out.println(d);
        }
    }
}

enum Dzien {
    PONIEDZIALEK, WTOREK, SRODA, CZWARTEK, PIATEK, SOBOTA, NIEDZIELA
}

Operatory arytmetyczne

  1. Dodawanie (+)
  2. Odejmowanie (-)
  3. Mnożenie (*)
  4. Dzielenie (/)
  5. Reszta z dzielenia (%)
  6. Inkrementacja (++)
  7. Dekrementacja (--)
  8. Operator przypisania z operacją (+=, -=, *=, /=, %=)

Ważne uwagi:

  • Podczas dzielenia liczb całkowitych, jeśli dzielnik i dzielna są liczbami całkowitymi, wynik też jest liczbą całkowitą, a wszelkie ułamki są odrzucane.
  • Operacje arytmetyczne na liczbach zmiennoprzecinkowych (np. double czy float) mogą prowadzić do błędów zaokrągleń, co jest typowe dla arytmetyki zmiennoprzecinkowej.

“Matma”

Dokumentacja

Stałe:

  1. Math.PI: Stała reprezentująca wartość liczby π (przybliżenie do 3.141592653589793).
  2. Math.E: Stała reprezentująca bazę logarytmu naturalnego (przybliżenie do 2.718281828459045).

Funkcje:

  1. Math.abs(x): Zwraca wartość bezwzględną argumentu.

  2. Math.ceil(x): Zwraca najmniejszą liczbę całkowitą większą lub równą argumentowi.

  3. Math.floor(x): Zwraca największą liczbę całkowitą mniejszą lub równą argumentowi.

  4. Math.round(x): Zaokrągla liczbę zmiennoprzecinkową do najbliższej liczby całkowitej. Istnieją wersje dla float i double.

  5. Math.sqrt(x): Zwraca pierwiastek kwadratowy z argumentu.

  1. Math.cbrt(x): Zwraca pierwiastek sześcienny z argumentu.

  2. Math.pow(a, b): Podnosi liczbę a do potęgi b.

  3. Math.max(a, b): Zwraca większą z dwóch wartości.

  4. Math.min(a, b): Zwraca mniejszą z dwóch wartości.

  5. Math.sin(x), Math.cos(x), Math.tan(x): Zwracają wartości funkcji trygonometrycznych sinus, cosinus i tangens odpowiednio.

  1. Math.asin(x), Math.acos(x), Math.atan(x): Zwracają arcus sinus, arcus cosinus i arcus tangens odpowiednio.

  2. Math.exp(x): Zwraca wartość e podniesioną do potęgi x.

  3. Math.log(x): Zwraca logarytm naturalny liczby x.

  4. Math.log10(x): Zwraca logarytm o podstawie 10 liczby x.

  5. Math.random(): Zwraca losową liczbę zmiennoprzecinkową z zakresu [0, 1).

Operatory relacyjne:

  1. Równość (==)
  2. Nierówność (!=)
  3. Większy niż (>)
  4. Mniejszy niż (<)
  5. Większy lub równy (>=)
  6. Mniejszy lub równy (<=)

Operatory logiczne:

  1. Koniunkcja (AND) (&&)
  2. Alternatywa (OR) (||)
  3. Negacja (NOT) (!)
  • Operatory relacyjne zwracają wartość true lub false.
  • Operatory logiczne działają na wartościach logicznych (boolean).
  • Operatory && i || są operatorami skróconej ewaluacji. Oznacza to, że drugi argument nie jest oceniany, jeśli pierwszy argument determinuje wynik.

Zasięg blokowy

Zasięg blokowy jest zdefiniowany przez nawiasy klamrowe { }. Zmienne zadeklarowane wewnątrz tych nawiasów są dostępne tylko w tym konkretnym bloku kodu.

Zasięg wewnątrz metody:

public void mojaMetoda() {
    int x = 10; // x ma zasięg tylko wewnątrz tej metody

    if (x > 5) {
        int y = 5; // y ma zasięg tylko wewnątrz tego bloku if
    }

    // y nie jest dostępne tutaj
}

Zasięg w blokach sterujących:

if (someCondition) {
    int a = 10; // a jest dostępne tylko wewnątrz tego bloku if
} else {
    int b = 20; // b jest dostępne tylko wewnątrz tego bloku else
}

Zasięg w pętlach:

for (int i = 0; i < 10; i++) {
    int j = i * 2; // j oraz i są dostępne tylko wewnątrz tej pętli
}
// i oraz j nie są dostępne poza pętlą

Instrukcje warunkowe

Instrukcja if:

if (warunek) {
    // Kod do wykonania, jeśli warunek jest prawdziwy
}

Instrukcja if-else:

if (warunek) {
    // Kod do wykonania, jeśli warunek jest prawdziwy
} else {
    // Kod do wykonania, jeśli warunek jest fałszywy
}

Instrukcja if-else if-else:

if (warunek1) {
    // Kod do wykonania, jeśli warunek1 jest prawdziwy
} else if (warunek2) {
    // Kod do wykonania, jeśli warunek2 jest prawdziwy
} else {
    // Kod do wykonania, jeśli żaden z powyższych warunków nie jest prawdziwy
}

Instrukcja switch:

switch (wartosc) {
    case wartosc1:
        // Kod do wykonania, jeśli wartosc == wartosc1
        break;
    case wartosc2:
        // Kod do wykonania, jeśli wartosc == wartosc2
        break;
    // ... (możesz dodać więcej przypadków 'case')
    default:
        // Kod do wykonania, jeśli żadna z wartości 'case' nie pasuje
}

Pętle

Pętla for:

for (int i = 0; i < 10; i++) {
    // Kod do wykonania dla każdej iteracji
}

Pętla for-each (zwana też “enhanced for”):

int[] tablica = {1, 2, 3, 4, 5};
for (int liczba : tablica) {
    // Kod do wykonania dla każdego elementu tablicy
}

Pętla while:

int i = 0;
while (i < 10) {
    // Kod do wykonania dla każdej iteracji
    i++;
}

Pętla do-while:

int i = 0;
do {
    // Kod do wykonania dla każdej iteracji
    i++;
} while (i < 10);

Funkcje a metody w Javie

public class Main {
    public static void main(String[] args) {
        System.out.println(suma(3,4));
    }

    public static int suma(int a, int b)
    {
        return a+b;
    }
}

Metody o zmiennej liczbie argumentów

varargs (variable-length argument) to konstrukcja, która pozwala metodzie przyjmować dowolną liczbę argumentów tego samego typu, traktując je jako tablicę. Umożliwia to definiowanie metod, które mogą być wywoływane z różnymi ilościami argumentów.

Definiowanie metody z varargs:

Aby zdefiniować metodę z varargs, użyj trzech kropek (...) przed typem argumentu. Na przykład:

public static void printNumbers(int... numbers) {
    for (int num : numbers) {
        System.out.print(num + " ");
    }
    System.out.println();
}

Wywoływanie metody z varargs:

Możesz wywołać metodę z varargs, przekazując dowolną liczbę argumentów (lub nawet brak argumentów) tego typu:

printNumbers(1, 2, 3, 4, 5);   // Wydrukuje: 1 2 3 4 5 
printNumbers(10, 20);         // Wydrukuje: 10 20 
printNumbers();               // Nic nie wydrukuje

Uwagi dotyczące używania varargs:

  • Jeden varargs na metodę: W definicji metody możesz mieć tylko jeden argument varargs, i musi on być na końcu listy argumentów.

  • Traktuj varargs jako tablicę: Wewnątrz metody argument varargs jest traktowany jako tablica. W powyższym przykładzie numbers jest traktowane jako tablica int[].

  • Ostrożnie z przeciążaniem: Przeciążanie metod z varargs może prowadzić do niejednoznaczności i błędów w czasie kompilacji. Dlatego warto być ostrożnym i upewnić się, że przeciążone wersje metody są łatwo rozróżnialne.

  • Przesyłanie tablicy do metody z varargs: Możesz również przekazać gotową tablicę do metody z varargs:

    int[] array = {1, 2, 3};
    printNumbers(array);  // Wydrukuje: 1 2 3 

Przeciążanie metod w jednej klasie

Przeciążanie metod (ang. method overloading) to technika w Javie, która pozwala na definiowanie wielu metod o tej samej nazwie w tej samej klasie lub klasie pochodnej, ale z różnymi listami parametrów. Metody te mogą różnić się liczbą, typem lub kolejnością parametrów.

Przykład:

public void display(int a) {
    System.out.println("Wartość liczby całkowitej: " + a);
}

public void display(double b) {
    System.out.println("Wartość liczby zmiennoprzecinkowej: " + b);
}

public void display(int a, int b) {
    System.out.println("Dwie wartości liczbowe: " + a + " i " + b);
}

Reguły przeciążania:

  • Metody muszą różnić się liczbą, typem lub kolejnością parametrów.
  • Zwracany typ metody NIE jest brany pod uwagę podczas różnicowania przeciążonych metod, tzn. metody nie mogą być różnione tylko przez swój typ zwracany.
  • Modyfikatory dostępu (np. public, private itp.) również nie wpływają na przeciążenie metody.

Generowanie liczb pseudolosowych

Klasa Random w Javie służy do generowania strumieni pseudolosowych liczb. Jest ona częścią pakietu java.util.

Oto kilka najczęściej używanych metod klasy Random:

  1. nextInt(): Zwraca losową liczbę całkowitą (typu int).

    • nextInt(): zwraca dowolną wartość int.
    • nextInt(int bound): zwraca wartość od 0 (włącznie) do podanej wartości (wyłącznie).
  2. nextLong(): Zwraca losową liczbę całkowitą typu long.

  3. nextDouble(): Zwraca losową liczbę zmiennoprzecinkową typu double z zakresu od 0.0 (włącznie) do 1.0 (wyłącznie).

  4. nextFloat(): Zwraca losową liczbę zmiennoprzecinkową typu float z zakresu od 0.0 (włącznie) do 1.0 (wyłącznie).

  1. nextBoolean(): Zwraca losową wartość boolean, czyli true lub false.

  2. nextBytes(byte[] bytes): Wypełnia podaną tablicę bajtów losowymi bajtami.

  3. nextGaussian(): zwraca losową liczbę zmiennoprzecinkową typu double wygenerowaną według standardowego rozkładu normalnego (rozkładu Gaussa)o średniej równej 0 i odchyleniu standardowym równym 1.

  1. setSeed(long seed): Ustawia ziarno generatora liczb losowych. Jeśli użyjesz tego samego ziarna dla dwóch różnych instancji klasy Random, będą one generować te same sekwencje liczb. To jest przydatne, gdy chcesz uzyskać powtarzalne wyniki dla testów czy symulacji.
import java.util.Random;

public class Example {
    public static void main(String[] args) {
        Random random = new Random();
        System.out.println("Losowa liczba całkowita: " + random.nextInt());
    }
}

Tablice jednowymiarowe

Tablice jednowymiarowe to struktury danych, które przechowują wiele wartości tego samego typu w jednym obiekcie.

Deklaracja: Aby zadeklarować tablicę, używa się typu danych, jaki ma być przechowywany, a następnie nawiasów kwadratowych. Na przykład, aby zadeklarować tablicę liczb całkowitych, używa się:

int[] mojaTablica;

Inicjalizacja: Można zainicjować tablicę w kilka sposobów: - Przy pomocy operatora new oraz określenia rozmiaru tablicy:

mojaTablica = new int[5]; // tablica z 5 elementami typu int
  • Przy pomocy literału tablicowego:
int[] innaTablica = {1, 2, 3, 4, 5}; // tablica zawierająca 5 liczb całkowitych

Uwaga: Wersja z C też działa, ale nie jest dobrą praktyką:

int innaTablica[] = {1, 2, 3, 4, 5};

Dostęp do elementów: Aby uzyskać dostęp do elementu tablicy, używa się indeksu (numerowane od 0) w nawiasach kwadratowych:

int pierwszyElement = mojaTablica[0]; // uzyskanie dostępu do pierwszego elementu

Modyfikacja elementów: Można modyfikować zawartość tablicy, odnosząc się do konkretnego indeksu:

mojaTablica[0] = 42; // przypisanie wartości 42 do pierwszego elementu tablicy

Długość tablicy: Aby uzyskać długość tablicy, używa się atrybutu length:

int dlugosc = mojaTablica.length;

Ograniczenia:

  • Tablice w Javie mają stałą długość, co oznacza, że po ich utworzeniu nie można zmieniać ich rozmiaru.
  • Wszystkie elementy tablicy muszą być tego samego typu.

Przypisanie:

W Javie możesz przypisać jedną tablicę do innej zmiennej tablicowej, ale ważne jest zrozumienie, co się wtedy dzieje.

Gdy przypisujesz tablicę do innej zmiennej tablicowej, nie tworzysz nowej kopii tej tablicy. Zamiast tego tworzysz drugą referencję do tej samej tablicy w pamięci. Oznacza to, że obie zmienne wskazują na tę samą tablicę, a zmiany dokonane za pomocą jednej zmiennej będą widoczne przy użyciu drugiej zmiennej.

int[] tablica1 = {1, 2, 3};
int[] tablica2 = tablica1;

tablica2[0] = 42;

System.out.println(tablica1[0]); // wydrukuje "42", a nie "1"

W przykładzie, po przypisaniu tablica1 do tablica2, obie zmienne wskazują na tę samą tablicę. Dlatego zmiana wartości w tablica2 wpływa również na tablica1.

Jeśli chcesz mieć dwie różne kopie tej samej tablicy, musisz skopiować zawartość jednej tablicy do drugiej, na przykład za pomocą pętli lub używając metody System.arraycopy():

int[] tablica1 = {1, 2, 3};
int[] tablica2 = new int[tablica1.length];
System.arraycopy(tablica1, 0, tablica2, 0, tablica1.length);

Teraz tablica2 jest osobną kopią tablica1, a zmiany w jednej z nich nie wpłyną na drugą.

Przekazywanie tablic do metody:

public class TestTablicy {

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

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

Metody “systemowe” do obsługi tablic

Dokumentacja

W Javie do obsługi tablic dostępna jest klasa java.util.Arrays, która dostarcza szereg przydatnych metod. Oto niektóre z nich:

  1. sort: Sortuje elementy tablicy. Może być używane do sortowania całej tablicy lub tylko jej części.

  2. binarySearch: Wyszukuje określony element w posortowanej tablicy, używając wyszukiwania binarnego.

  1. equals: Sprawdza, czy dwie tablice są równe pod względem zawartości.

  2. fill: Ustala wszystkie elementy tablicy na określoną wartość.

  3. copyOf: Kopiuje określoną liczbę elementów tablicy źródłowej do nowej tablicy.

  4. copyOfRange: Kopiuje zakres elementów z tablicy źródłowej do nowej tablicy.

  5. deepEquals: Sprawdza, czy dwie tablice wielowymiarowe są równe pod względem zawartości.

  1. deepHashCode: Oblicza kod hash dla tablicy, biorąc pod uwagę wszystkie elementy w tablicach wielowymiarowych.

  2. deepToString: Konwertuje tablicę wielowymiarową na łańcuch reprezentujący jej zawartość (w sposób głęboki).

  3. hashCode: Oblicza hash dla tablicy.

  4. stream: Dostarcza sekwencyjny strumień zawierający elementy tablicy, co pozwala na używanie operacji strumieniowych (od Javy 8).

  5. setAll: Ustala wartości wszystkich elementów tablicy, używając dostarczonej funkcji generującej.

  1. parallelSetAll: Podobnie jak setAll, ale używa wielowątkowości do jednoczesnego ustalania wartości dla wielu elementów.

  2. parallelSort: Sortuje tablicę używając wielowątkowości.

  3. mismatch: Znajduje i zwraca indeks pierwszego niezgodnego elementu między dwiema tablicami (od Javy 9).

  4. toString: Konwertuje tablicę wielowymiarową na łańcuch reprezentujący jej zawartość (w sposób płytki).

import java.util.Arrays;

public class Main {
    public static void main(String[] args) {
        int[] tablica = {34, 12, 5, 78, 2, 10, 8};
        Arrays.sort(tablica);
        System.out.println(Arrays.toString(tablica));
    }
}
import java.util.Arrays;

public class SortowanieNierosnace {
    public static void main(String[] args) {
        int[] tablica = {34, 12, 5, 78, 2, 10, 8};
        Arrays.sort(tablica);
        for (int i = 0; i < tablica.length / 2; i++) {
            int temp = tablica[i];
            tablica[i] = tablica[tablica.length - 1 - i];
            tablica[tablica.length - 1 - i] = temp;
        }
        System.out.println(Arrays.toString(tablica));
    }
}
import java.util.Arrays;

public class PorownanieTablic {
    public static void main(String[] args) {
        int[] tablica1 = {1, 2, 3, 4, 5};
        int[] tablica2 = {1, 2, 3, 4, 5};
        int[] tablica3 = {5, 4, 3, 2, 1};
        boolean czyRowne1i2 = Arrays.equals(tablica1, tablica2); // true
        boolean czyRowne1i3 = Arrays.equals(tablica1, tablica3); // false
        System.out.println("Czy tablica1 jest równa tablica2? " + czyRowne1i2);
        System.out.println("Czy tablica1 jest równa tablica3? " + czyRowne1i3);
    }
}
import java.util.Arrays;

public class WypelnienieTablicy {
    public static void main(String[] args) {
        int[] tablica = new int[5];
        Arrays.fill(tablica, 10);
        System.out.println(Arrays.toString(tablica));  
    }
}
import java.util.Arrays;

public class KonwersjaTablicyNaString {
    public static void main(String[] args) {
        int[] tablica = {1, 2, 3, 4, 5};
        String reprezentacjaTablicy = Arrays.toString(tablica);
        System.out.println(reprezentacjaTablicy);  
    }
}
import java.util.Arrays;

public class KopiowanieTablicy {
    public static void main(String[] args) {
        int[] oryginalnaTablica = {1, 2, 3, 4, 5};
        int[] nowaTablica = Arrays.copyOf(oryginalnaTablica, 3);
        System.out.println(Arrays.toString(nowaTablica));  
    }
}

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'

Napisy, łańcuchy znaków

Inicjalizacja:

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

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 +:
String s1 = "Hello";
String s2 = "World";
String s3 = s1 + " " + s2;  // "Hello World"
String s4 = s1 + 4;  // "Hello4"

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.

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.