Kurs Java #9 Tablice

Java

Loading

Tablice w Javie to podstawowy, ale niezwykle istotny element pracy z danymi. Umożliwiają przechowywanie wielu wartości tego samego typu w jednej, uporządkowanej strukturze – dzięki czemu operacje na zbiorach danych stają się prostsze, szybsze i bardziej czytelne. W tym artykule omówimy sposób deklarowania i inicjalizowania tablic, podstawowe operacje na nich, sposoby ich wyświetlania, modyfikacji, porównywania i kopiowania, a także typowe pułapki na które łatwo natrafić.

Wprowadzenie

W programowaniu często zachodzi potrzeba przechowywania wielu wartości tego samego rodzaju – liczb, tekstów, czy obiektów – w sposób uporządkowany i łatwy do przetwarzania. Zamiast tworzyć osobne zmienne dla każdego elementu, z pomocą przychodzą struktury danych. Jedną z najprostszych i najczęściej używanych w Javie jest tablica.

Tablica (ang. array) to specjalny obiekt, który pozwala przechowywać wiele wartości jednego typu, takich jak liczby, napisy czy obiekty. Każdy element ma swój numer – indeks, a sama tablica ma stałą długość, której nie da się zmienić po utworzeniu. W Javie indeksowanie tablic zaczyna się od zera. Oznacza to, że pierwszy element tablicy ma indeks 0, drugi – 1, trzeci – 2 i tak dalej. To częste źródło pomyłek dla początkujących, którzy intuicyjnie oczekują, że pierwszy element to 1.

Tworzenie tablic

Tworzenie tablic polega na deklaracji tablicy, czyli określeniu typu przechowywanych w niej danych i nadaniu jej nazwy. Inicjalizacja to faktyczne utworzenie tablicy w pamięci i opcjonalne przypisanie jej elementom wartości. W Java można zadeklarować i zainicjalizować tablicę na kilka sposobów.

Inicjalizacja statyczna

Używamy, gdy znamy dane i rozmiar tablicy już w momencie jej deklaracji.

Przykład:

int[] liczby = {1, 2, 3, 4}; // rozmiar: 4, znany od razu

Tworzymy tablicę o nazwie liczby, która przechowuje 4 elementy typu int. Zarówno wartości, jak i długość tablicy są znane z góry i przypisywane bezpośrednio podczas tworzenia.

Inicjalizacja dynamiczna

W tym przypadku rozmiar tablicy jest ustalany w trakcie działania programu. Nie znamy długości tablicy na etapie pisania kodu lub ustalamy ją w sposób elastyczny. Mimo to po otworzeniu ma ona stały rozmiar, którego nie można później zmienić.

Przykład:

int[] liczby = new int[5]; //tworzymy tablicę
liczby[0] = 10; //pierwszy element (indeks 0)
liczby[4] = 20; //piąty i ostatni element tablicy (indeks 4)

Tworzymy tablicę o nazwie liczby, która może przechowywać 5 elementów typu int. Po jej utworzeniu wszystkie elementy mają domyślną wartość 0. Następnie przypisujemy wartość do pierwszego elementu (liczby[0]) i 20 do ostatniego (liczby[4]). Indeksy są numerowane od 0 do 4, co oznacza. że tablica ma dokładnie 5 miejsc.

Stan tablicy po tych operacjach:
liczby = [10, 0, 0, 0, 20];

Analogicznie możemy utworzyć tablicę typu String:

String[] imiona = new String[3]; 
imiona[0] = "Anna";   
imiona[2] = "Tomek";

Tworzymy tablicę imiona, która może przechowywać 3 elementy typu String. Przypisujemy wartość do pierwszego i trzeciego elementu, natomiast drugi (indeks 1) pozostaje pusty, czyli ma wartość null.

Stan tablicy po operacjach:
imiona = ["Anna", null, "Tomek"];

Podział tablic ze względu na liczbę wymiarów

W Java tablice możemy podzielić według liczby wymiarów na trzy podstawowe typy: jednowymiarowe, dwuwymiarowe i wielowymiarowe.

Tablice jednowymiarowe

To najprostszy typ tablicy o strukturze liniowej przechowujący elementy jednego typu, do którego dostęp uzyskamy przez pojedynczy indeks. Przechowują proste listy wartości.

Przykłady:

//inicjalizacja statyczna
int[] liczby = {10, 20, 30, 40};
//inicjalizacja dynamiczna
int[] liczby = new int[5];
liczby[0] = 10;
// ...

Tablice dwuwymiarowe

Najczęściej spotykane tablica. Można sobie ją wyobrazić jako tabelę (wiersze i kolumny), do której dostęp do elementów otrzymamy za pomocą dwóch indeksów. Są to macierze, dane tabelaryczne.

Przykłady:

//inicjalizacja statyczna
int[][] macierz = { 
          {1, 2, 3}, 
          {4, 5, 6}, 
          {7, 8, 9} 
};
int[][] macierz = new int[3][3];
macierz[1][2] = 7;

Tablice wielowymiarowe

W tych tablicach każdy element może być kolejną tablicą np. trójwymiarowa dla danych o strukturze sześciennej. Stosowane, gdy dane mają więcej niż dwa wymiary np. w skomplikowanych modelach danych, symulacjach, itp.

Przykłady:

//inicjalizacja statyczna
int[][][] sześcian = {
    {
      {1, 2}, 
      {3, 4}
    },
    {
      {5, 6},
      {7, 8}
    }
};
//inicjalizacja dynamiczna
int[][][] sześcian = new int[2][2][2];
sześcian[1][0][1] = 99;

Podział tablic ze względu na typ przechowywanych danych

W Java każdy element tablicy musi mieć ten sam, określony typ danych. Możemy wyróżnić dwa główne typy tablic ze względu na przechowywanie danych: tablice typów prostych (prymitywnych) oraz tablice obiektów.

Tablice typów prostych (prymitywnych)

Tablice przechowujące wartości typów podstawowych, czyli liczb całkowitych, zmiennoprzecinkowych, znaków lub wartości logicznych. Przykładowe typy prymitywne to: int, char, boolean, long, short, float, byte.

Przykłady:

int[] liczby = {5, 10, 15, 20, 25}; 

Tablice obiektów

Tablice w których, każdy element jest referencją do obiektu określonej klasy. Może to być klasa wbudowana (np. String) lub własna np. User, itp.

Przykłady:

String[] imiona = {"Adam", "Tomek", "Ola"};
User[] uzytkownicy = new User[10];

Wyświetlanie tablicy

W Java nie można wypisać tablicy w System.out.println, bo otrzyma się ciąg znaków, który jest nieczytelną reprezentację obiektu (adresem w pamięci).

Przykłady:

Aby prawidłowo wypisać tablicę, należy użyć klasy pomocniczej Arrays z pakietu java.util:

Przykłady:

Dla typów prostych używamy: Arrays.toString().

Dla typów wielowymiarowych używamy: Arrays.deepToString() – wypisuje tablicę tablic w zagnieżdżonej formie.

Iteracja po tablicy

Najczęściej używaną konstrukcją do odczytywania danych z tablicy jest pętla for. Pozwala nam kontrolować dokładnie, które elementy czytamy, w jakiej kolejności i umożliwia operowanie na indeksach.

Przykład:

  • zmienna sterująca i odpowiada za numer indeksu
  • imiona.length – długość tablicy, czyli liczba jej elementów

Jeśli nie potrzeba indeksu, a zależy nam tylko na odczytaniu wartości, można użyć pętli for-each.

Przykład:

Zaletą użyci pętli for-each jest mniej kodu oraz brak ryzyka błędu związany z indeksowaniem np. wyjście poza zakres (ArrayIndexOutOfBoundsException).

Przykład:

Tablica ma tylko 3 elementy: 0, 1, 2.

Modyfikacja elementów

Modyfikacja elementu tablicy polega na nadpisaniu jego wartości poprzez przypisanie nowej danej pod konkretnym indeksem.

nazwaTablicy[indeks] = nowa_wartość;

Można dowolnie zmieniać wartości wewnątrz istniejącej tablicy, ale nie można zmienić rozmiaru tablicy po jej utworzeniu.

Długość tablicy

Długość tablicy w Java oznacza liczbę elementów, które ta tablica może przechowywać. Każda tablica ma stały rozmiar określony w momencie jej tworzenia i wartość tę zapisuje w polu length

Aby sprawdzić długość (rozmiar) tablicy należy użyć składni (którą już użyliśmy w przykładzie powyżej):

nazwaTablicy.length

W tablicacy wielowymiarowej .length dotyczy aktualnego wymiaru.

Przykład:

Porównywanie tablic

W Javie porównywanie i kopiowanie tablic wymaga użycia dedykowanych metod z klasy Arrays z pakietu java.util, ponieważ operatory == oraz metoda .equals() dla tablic sprawdzają tylko, czy porównywane zmienne wskazują na ten sam obiekt w pamięci, a nie czy zawartość tablicy jest taka sama.

Przykład porównania tablic jednowymiarowych:

  public static void main(String[] args) {
        int[] tab1 = {1, 2, 3};
        int[] tab2 = {1, 2, 3};
        boolean czyRowne = Arrays.equals(tab1, tab2); 
    }
}

Arrays.equals(tab1, tab2) – zwróci true, jeśli tablice mają tę samą długość i identyczne wartości w tych samych miejscach.

Przykład porównania tablic wielowymiarowych:

int[][] tab1 = {{1, 2}, {3, 4}};
int[][] tab2 = {{1, 2}, {3, 4}};
boolean czyRowne = Arrays.deepEquals(tab1, tab2);

Arrays.deepEquals(tab1, tab2) sprawdza zawartość również w zagnieżdżonych tablicach.

Kopiowanie tablic

Aby skopiować tablicę w Javie (utworzyć nową z taką samą zawartością), nie wystarczy przypisanie referencji (tab2 = tab1;), bo wtedy oba identyfikatory wskazują na tę samą tablicę w pamięci.

Kopiowanie tablicy jednowymiarowej

Przykład:

int[] oryginal = {1, 2, 3, 4};
int[] kopia = Arrays.copyOf(oryginal, oryginal.length);

Array.copyOf() tworzy nową tablicę o takiej samej długości i zawartości jak oryginał.

Kopiowanie tablic dwy i wielowymiarowych

Przykład kopiowania fragmentu tablicy wielowymiarowej:

int[] oryginal = {10, 20, 30, 40, 50};
int[] fragment = Arrays.copyOfRange(oryginal, 1, 4); // kopiujemy elementy o indeksach 1, 2 i 3

System.out.println(Arrays.toString(fragment));
// Wynik: [20, 30, 40]

Arrays.copyOfRange() tworzy nową tablicę, która zawiera wybrane elementy ze starej tablicy. Kopiuje elementy od indeksu 1 do indeksu 4 (ale nie wliczając 4), czyli kopiuje elementy: 20, 30, 40. Powstaje nowa tablica fragment = {20, 30, 40}.

Przykład kopiowania tablicy wielowymiarowej:

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

int[][] kopia = new int[oryginal.length][];
for (int i = 0; i < oryginal.length; i++) {
    kopia[i] = Arrays.copyOf(oryginal[i], oryginal[i].length);
}

Dla tablic wielo i dwuwymiarowych sprawa jest trochę trudniejsza. Gdy użyjemy gotowych komend do kopiowania, np. Arrays.copyOf(), powstaje tylko tzw. „kopia powierzchowna”. To znaczy, że: tworzona jest nowa lista wierszy (nowa „główna” tablica), ale każdy wiersz (czyli podtablica) nie jest kopiowany osobno, tylko oba obiekty – stara tablica i ta nowa kopia – pokazują na ten sam wiersz w pamięci. Oznacza to, że jeśli zmieni się jakiś element w wierszu w tej „kopii”, to ten sam element zmieni się też w oryginale, bo oba „patrzą” na ten sam rząd danych. Aby zrobić w niezależną kopię (dla każdego wiersza osobno) należy przejść przez każdy wiersz za pomocą pętli i skopiować go pojedynczo dla każdej podtablicy, czyli wykonać tzw. „głęboką kopię”.

Podsumowanie

Tablice w Javie to jedna z podstawowych, ale bardzo potężnych struktur danych. Umożliwiają przechowywanie wielu wartości tego samego typu w uporządkowanej formie, co znacząco ułatwia pracę. W dzisiejszym artykule dowiedziałeś się:

  • jak zadeklarować i zainicjalizować tablicę (statycznie i dynamicznie),
  • jakie są rodzaje tablic w zależności od liczby wymiarów i typu przechowywanych danych,
  • jak wykonywać podstawowe operacje, takie jak odczyt, modyfikacja, iteracja, kopiowanie i porównywanie danych,
  • czym różni się płytka i głęboka kopia tablicy, oraz jak poprawnie stworzyć niezależną kopię dla tablic wielowymiarowych..

Warto dodać, że najlepszą praktyką podczas pracy z tablicami w Javie jest korzystanie z bogatego zestawu funkcji dostępnych w klasie java.util.Arrays. Dzięki tej klasie możesz szybko i efektywnie sortować tablice, kopiować je, porównywać, wypełniać wybraną wartością czy też przeszukiwać tablice. .

O autorze

Adam Mingielewicz

Tester oprogramowania z pasją do jakości i technologii. Łączy doświadczenie w testach manualnych i automatycznych, koncentrując się przede wszystkim na aplikacjach webowych oraz usługach SOAP i REST. Testowanie to dla niego nie tylko szukanie błędów, ale przede wszystkim kwestionowanie przyjętych założeń i usprawnianie tego, co nie działa – również w samym procesie testowym. Lubi zmieniać, angażować się i aktywnie budować proces testowy, tak by miał sens, a nie tylko formę. Prywatnie fan rocka, biwakowania, podróży.