następny punkt »

1. Pojęcie tablicy. Realizacja tablic w Javie

Dane w programie mogą  być organizowane w różny sposób. W szczególności  jako zestawy (powiązanych i/lub w okreslony sposób uporządkowanych) wartości. W tym kontekście mówimy o strukturach danych.
Jednym z ważnych rodzajów struktur danych - są tablice.

Tablice są zestawami elementów (wartości) tego samego typu, ułożonych na określonych pozycjach. Do każdego z tych elementów mamy bezpośredni ( swobodny - nie wymagający przeglądania innych elementów zestawu) dostęp poprzez nazwę tablicy i pozycję elementu w zestawie, określaną przez indeks lub indeksy tablicy.

Na przykład, tablica czterech liczb całkowitych może wyglądać tak.

Rys

Pierwszy element - liczba 21 ma indeks 0, drugi - liczba 13 indeks 1 itd.
Do elementów tablicy odwołujemy się za pomocą nazwy tablicy oraz  indeksu umieszczonego w nawiasach kwadratowych.
Jeżeli ta tablica ma nazwę tab, to do pierwszego elementu odwołujemy się poprzez nazwę tablicy i indeks 0: tab[0], do drugiego - tab[1] itd. Jak widać, odwołanie np. do 3-go elementu - nie wymaga przeglądania innych elementów.


W Javie tablice są obiektami, a nazwa tablicy jest nazwą zmiennej, będącej referencją do obiektu-tablicy. Obiekt-tablica zawiera elementy tego samego typu. Może to być dowolny z typów pierwotnych lub referencyjnych. Zatem, w szczególności elementami tablic mogą być referencje do innych tablic. Mamy wtedy do czynienia z odpowiednikiem tablic wielowymiarowych (zob. uwagi o tablicach wielowymiarowych).

Tak samo jak wszystkie inne zmienne - tablice musimy deklarować przed użyciem ich nazw w programie.

Deklaracja tablicy składa się z:
  • nazwy typu elementów tablicy,
  • pewnej liczby par nawiasów kwadratowych (liczba par okresla liczbę wymiarów tablicy),
  • nazwy zmiennej, która identyfikuje tablicę.
Np.
        int[]  arr;   // jest deklaracją tablicy liczb całkowitych (typu int),
        String[] s;  // jest deklarację tablicy referencji do obiektów klasy String
        Button[] b; // jest deklarację tablicy referencji do obiektów klasy Button
        double[][] d;  // jest deklaracją dwuwymiarowej tablicy liczb rzeczywistych

Ściślej można powiedzieć, że deklarowane są tu zmienne tablicowe.
Typ takiej zmiennej jest typem referencyjnym, a jego nazwa składa się z nazwy typu elementów tablicy i nawiasów kwadratowych. W powyższych przykładach:
    zmienna arr jest typu int[]
    zmienna s jest typu String[]
    zmienna d jest typu double[][]

Uwaga: rozmiar tablicy nie stanowi składnika deklaracji tablicy.

Np. taka deklaracja:
    int[5] arr;  
jest niedopuszczalna.

Skoro tablice są obiektami - to jakich klas? Otóż w trakcie kompilacji programu niejawnie tworzone są definicje klas dla tablic. Klasy te mają specjalne nazwy - tylko dla potrzeb JVM (np. klasa opisująca jednowymiarową tablicę liczb całkowitych ma nazwę [I) i jedno pole - stałą typu int o wartości równej liczbie elementów tablicy

Jeżeli oswoimy się z myślą, że tablice są obiektami, to - przez analogię do innych obiektów - będzie nam łatwo zrozumieć różnicę pomiędzy deklaracją i utworzeniem tablicy.


Deklaracja tablicy tworzy referencję.
 
int[] arr;   // arr jest referencją
               // arr jest zmienną typu int[], który jest typem referencyjnym

Taka deklaracja nie alokuje pamięci dla samej tablicy!

Pamięć jest alokowana dynamicznie albo w wyniku inicjacji za pomocą nawiasów klamrowych albo w wyniku użycia wyrażenia new.


Inicjacja tablicy za pomocą nawiasów klamrowych może wystąpić wyłącznie w wierszu deklaracji tablicy i ma postać:

        { element_1, element_2, .... element_N }

gdzie:
            element_i  - i-ty element tablicy (wartość)

Np.

        int[] arr = { 1, 2, 7, 21 };

deklaruje tablicę o nazwie arr, tworzy ją i inicjuje jej elementy; kolejno:

  1. Wydzielana jest pamięć dla zmiennej arr, która będzie przechowywać referencję do obiektu-tablicy.
  2. Wydzielana jest pamięć (dynamicznie, na stercie) potrzebna do przechowania 4 liczb całkowitych (typu int).
  3. Kolejne wartości 1,2,7,21 są zapisywane kolejno w tym obszarze pamięci.
  4. Adres tego obszaru (referencja) jest przypisywany zmiennej arr.

Drugi sposób utworzenia tablicy polega na zastosowaniu wyrażenia new.


Tworzenie tablicy za pomocą wyrażenia new ma postać

            new T[n];

gdzie:
  • T  - typ elementów tablicy
  • n  -  rozmiar tablicy (liczba elementów tablicy)

Uwaga: nawiasy są kwadratowe, a nie okrągłe, jak w przypadku użycia new z konstruktorem jakiejś klasy

Na przykład:

 int[] arr;               // deklaracja tablicy
 arr = new int[4];   // utworzenie tablicy 4 elementów typu int

Można to też zapisać od razu w wierszu deklaracji:

 int[] arr = new int[4];

Mechanizm działania jest tu identyczny jak w przypadku innych obiektów.
Przypomina go poniższy rysunek.


Rys

Zauważmy, że rozmiar tablicy może być ustalony dynamicznie, w fazie wykonania programu. Np.

int n;
//... n uzyskuje wartość
// np. na skutek obliczeń opartych na wprowadzonych przez użytkownika danych
//...
int[] tab = new int[n];

Ale - uwaga - po ustaleniu rozmiar nie może być zmieniony.

Jak już wspomniano do elementów tablic odwołujemy się za pomocą indeksów.

Indeksy tablicy mogą być wyłącznie wartościami typu int.

Mogą być dowolnymi wyrażeniami, których wyliczenie daje wartość typu int.

Tablice zawsze indeksowane są poczynając od 0.
Czyli np. pierwszy element n-elementowej tablicy ma indeks 0,
a ostatni - indeks n-1.

Ze względu na to, że wartości typu byte, char i short są w wyrażeniach "promowane" (przekształcane) do typu int), to również wartości tych typów możemy używać przy indeksowaniu tablic. Niedoposzczalne natomiast jest użycie wartości typu long.

Odwołanie do i-go elementu tablicy o nazwie tab ma postać:

                tab[i]

Ta konstrukcja składniowa traktowana jest jako zmienna, stanowi nazwę zmiennej - zatem możemy tej zmiennej przypisywać wartości innych wyrażeń oraz możemy używać jej wartości w innych wyrażeniach

Na przykład:
int[] a = new int[3];
a[1] = 1;  // nadanie DRUGIEMU elementowi tablicy a wartości 1
int c = a [1] + 1;  // c będzie miało wartość 2
int i = 1, j = 1;
a[i +j] = 7;  // nadanie elementowi o indeksie i+j (=2) wartości 7

Odwołania do elementów tablic są przez JVM sprawdzane w trakcie wykonania programu pod względem poprawności indeksów. Java nie dopuści do odwołania się do nieistniejącego elementu tablicy lub podania indksu mniejszego od 0. Próba takiego odwołania spowoduje powstanie wyjątku (czyli sygnału o błędzie) o nazwie ArrayIndexOutOfBoundsException , na skutek czego zostanie wyprowadzony odpowiedni komunikat i wykonanie programu zostanie przerwane (ew. taki wyjątek możemy obsłużyć - czyli w wybrany przez siebie sposób zareagować na jego powstanie, o czym w następnych wykladach).
Zobaczmy przykład.

public class Test {

  public static void main(String[] args) {
    int[] a = {1, 2, 3, 4 };
    System.out.println(a[4]);
    System.out.println(a[3]);
    System.out.println(a[2]);
    System.out.println(a[1]);
  }

} 

Zauważmy - mamy tu tablicę składającą się z 4 liczb całkowitych. Chcemy po kolei wyprowadzić jej elementy od ostatniego poczynając. Częstym błędem jest zapominanie o tym, że tablice indeksowane są od zera: w tym programie zapomniano o tym i próbowano odwołać się do ostatniego elementu tablicy a za pomocą a [4] (ktoś pomyślał: skoro są cztery elementy - to ostatni jest a[4]). Tymczasem jest to odwołanie poza zakres tablicy, do nieistniejącego 5-go elementu! Ten błąd zostanie wykryty, na konsoli pojawi się komunikat i program zostanie przerwany.

Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException
        at Test.main(Test.java:5)

Proszę skompilować ten program, przekonać się jak działa w tej wersji, po czym:

  • odwrócić kolejność wypisywania elementów tablicy (a[1], a[2], a[3], a[4]) i zobaczyć co teraz się stanie przy wykonaniu tego programu
  • poprawić program, tak by bezbłędnie wypisywał kolejne elementy tablicy

W powyższym przykładzie było nieco żmudne wypisywanie kolejnych elementów tablicy. W naturalny sposób powinniśmy to robić w pętli.

Może tak?

public class Test {

  public static void main(String[] args) {
    int[] a = {1, 2, 3, 4 };
    for (int i=3; i>=0; i--) System.out.println(a[i]);
  }

}

Mhm, a co się stanie gdy zmienimy rozmiar tablicy dodając kilka nowych elementów w inicjacji? Będziemy musieli od nowa  policzyć elementy i zmienić inicjację licznika pętli. Trochę niewygodne, a do tego naraża nas na błędy. A przecież rozmiar tablicy znany jest JVM, niech zatem "liczeniem" elementów zajmuje się komputer.

Zawsze możemy uzyskać informacje o rozmiarze (liczbie elementów) tablicy za pomocą odwołania:
 
                nazwa_tablicy.length

Uwaga: częstym błędem jest traktowanie tego wyrażenia jako wywołania metody. W tym przypadku length nie jest nazwą metody (lecz pola niejawnie stworzonej klasy, opisującej tablicę), dlatego NIE STAWIAMY po nim nawiasów okrągłych

Zatem poprzedni program można by zapisać tak:

public class Test {

  public static void main(String[] args) {
    int[] a = {1, 2, 3, 4 };
    for (int i=a.length-1; i>=0; i--) System.out.println(a[i]);
  }

}
Problem

Spróbujmy teraz odwrócić kolejność wypisywania elementów tablicy (czyli po kolei od pierwszego poczynając).
Jak powinna wyglądać pętla for?


Przed lekturą dalszego tekstu proszę rozwiązać to zadanie samodzielnie


Rozwiązanie:

public class Test {

  public static void main(String[] args) {
    int[] a = {1, 2, 3, 4 };
    for (int i=0; i<a.length; i++) System.out.println(a[i]);
  }

}

Przebięgając w pętli przez wszystkie (poczynając od pierwszego) elementy tablicy tab musimy zmieniać indeksy od 0 do tab.length-1, czyli zastosować następującą postać pętli for:

for (int i = 0; i < tab.length; i++) ... tab[i] ... ;

Użycie length wobec tablicy jest szczególnie wygodne w metodach, które otrzymują jako argumenty referencje do tablic: możemy w ten sposób pisać uniwersalne metody działające na tablicach o różnych rozmiarach.


 następny punkt »