Tablice. Wprowadzenie do kolekcji.
1. Pojęcie tablicy. Realizacja tablic w Javie
1.1. Tablice w Javie. Deklarowanie i tworzenie
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.
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 prostych lub referencyjnych.
Zatem, w szczególności elementami tablic mogą być referencje do innych tablic. Mamy
wtedy do czynienia z odpowiednikiem tablic 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
JButton[] b; // jest deklarację tablicy referencji do obiektów klasy JButton
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
przy deklaracji z inicjacją za pomocą nawiasów klamrowych albo w wyniku
użycia wyrażenia new.
Deklaracja tablicy z inicjacją za pomocą nawiasów klamrowych ma postać:
typ[] zm_tab = {
wart_1, wart_2, .... wart_N }
gdzie:
typ - typ elementów tablicy,
zm_tab - nazwa zmiennej tablicowej,
wart_i - wartość i-go elementu tablicy
Np.
int[] arr = { 1, 2, 7, 21 };
deklaruje tablicę o nazwie arr, tworzy ją i inicjuje jej elementy; kolejno:
- Wydzielana jest pamięć dla zmiennej arr, która będzie przechowywać referencję do obiektu-tablicy.
- Wydzielana jest pamięć (dynamicznie, na stercie) potrzebna do przechowania 4 liczb całkowitych (typu int).
- Kolejne wartości 1,2,7,21 są zapisywane kolejno w tym obszarze pamięci.
- 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 (bez inicjacji elementów) 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.
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.
Elementy tablic tworzonych za pomocą wyrażenia new T[n] mają inicjalne wartości ZERO (zera arytmetyczne, false dla typu boolean, null dla typów referencyjnych).
Wyrażenia
new można też użyć do utworzenia i równoczesnej inicjacji elementów
tablicy wartościami zapisanymi w nawiasach klamrowych.
Tworzenie tablicy za pomocą wyrażenia new (z inicjacją elementów) ma postać
new T[] { wart_1, wart_2, .... wart_N };
gdzie:
- T - typ elementów tablicy
- wart_i - wartość i-go elementu tablicy
Np.
int[] a;
// ....
a = new int[] {1, 2, 3, 4 };
Jest
to szczególnie wygodne, gdy chcemy ad hoc utworzyć i zainicjować
tablicę elementami np. przy przekazywaniu tablicy jako argumentu
jakiejś metodzie.
Przykladowo, w kontekście:
// definicja metody, działającaj na tablicy przekazanej jako argument
void metoda(int[] tab) {
//....
}
// ...
// wywołanie metody z ad hoc utworzoną tablicą
metoda(new int[] { 7, 7, 7, 7 });
1.2. Odwołania do elementów tablic
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 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 do nieistniejącego elementu tablicy lub podania indeksu
mniejszego od 0. Próba takiego odwołania spowoduje
powstanie wyjątku ArrayIndexOutOfBoundsException, na skutek czego zostanie wyprowadzony odpowiedni komunikat i wykonanie programu
zostanie przerwane (ew. taki wyjątek możemy obsłużyć).
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)
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]);
}
}
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]);
}
}
Spróbujmy teraz odwrócić kolejność wypisywania elementów tablicy (czyli po kolei od pierwszego poczynając).
Jak powinna wyglądać pętla for?
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.
2. Użycie zmiennych tablicowych w przypisaniach. Zmienne tablicowe jako argumenty i wyniki metod.
Ponieważ zmienna oznaczająca tablicę zawiera referencje do tablicy, to -
pod pewnymi warunkami - możemy jej przypisać referencję do innej tablicy.
Zmiennej tablicowej typu typA[] można przypisać wartość zmiennej tablicowej
typu typB[] pod warunkiem, że dopuszczalne jest przypisanie wartości typu
B zmiennej typu A.
Każdej zmiennej tablicowej - jak każdej zmiennej zawierającej referencję - można przypisać wartość null
W bardzo naturalny sposób możemy przypisywać referencje w przypadku gdy mamy do
czynienia z tym samym typem tablic (a zatem - z tym samym typem elementów
oraz z tą samą liczbą wymiarów).
Tak samo jak w przypadku innych obiektów - nie należy mylić przypisania
zmiennych tablicowych (czyli referencji) z kopiowaniem zawartości tablic.
Na przykład poniższy program:
public class Test {
public static void main(String[] args) {
byte[] b1 = {1, 2, 3 };
byte[] b2 = {1, 2, 3, 5, 5 };
byte[] b = b2;
b2 = b1;
b2[0] = 77;
b[0] = 99;
System.out.print("\nTablica \"b1\":");
for (int i=0; i < b1.length; i++) System.out.print(" " + b1[i]);
System.out.print("\nTablica \"b2\":");
for (int i=0; i < b2.length; i++) System.out.print(" " + b2[i]);
System.out.print("\nTablica \"b\":");
for (int i=0; i < b.length; i++) System.out.print(" " + b[i]);
}
}
Wyprowadzi pokazane wyniki:
Tablica "b1": 77 2 3
Tablica "b2": 77 2 3
Tablica "b": 99 2 3 5 5
Zwróćmy uwagę:
- na początku b1 oznaczało tablicę {1,2,3} a b2 tablicę {1,2,3,5,5}
- po przypisaniu b2 = b1; b2 oznacza tablicę {1, 2, 3}
- zatem przypisanie b2[0] = 77 zmieni w tej tablicy pierwszy element
- ale na tę samą tablicę nadal wskazuje b1, dlatego wypisanie elementów tablicy "b1" da 77 2 3
- zmiennej b przypisano wcześniej referencję do tablicy oznaczanej przez
b2 {1,2,3,5,5} - dlatego za pomocą b możemy zmienić jej pierwszy element
i wypisać wszystkie: 99 2 3 5 5
W tym przykładzie przez chwilę mogło dziwić, że "na tablicę" b2 "podstawiamy
tablicę" (o innych rozmiarach!) - b1. Nie przeczy to jednak zasadzie, że
- po ustaleniu - rozmiary tablic nie mogą być zmieniane. No, tak - przecież
przypisanie dotyczy referencji do tablic, a nie samych tablic. W przypadku
referencji różne rozmiary tablic na które one wskazują nie są żadną przeszkodą
przy przypisaniu - rozmiary tablic pozostają bez zmian.
I druga ważna kwestia, wymagająca jeszcze raz szczególnego podkreślenia:
na jedną tablicę może wskazywać kilka zmiennych tablicowych. Za pomocą każdej
z nich (i operacji indeksowania) możemy zmieniać wartości elementów tej jednej
tablicy, a odwolania do tej tablicy poprzez inne zmienne będą - oczywiście
- uwzględniać te zmiany (np. pokazywać zmienione wartości).
Przekazanie argumentów metodzie i zwrot jej wyniku mogą być traktowane jako szczególny rodzaj operacji przypisania.
Zatem metody mogą działać na tablicach, do których referencje otrzymują w postaci
parametrów i mogą zwracać wyniki - referencje do tablic.
W przypadku tablic jednowymiarowych typem zmiennych "tablicowych" (inaczej referencji do tablic) jest T
[], gdzie T - nazwa typu elementów tablicy. Zatem w nagłówku metod - parametry
(które mają oznaczać tablice; są referencjami do tablic) deklarujemy właśnie
za pomocą takich nazw typu. Oczywiście, gdy wywołujemy metodę - to na liście
argumentów podajemy nazwy zmiennych tablicowych (już bez nawiasowych kwadratowych).
Jeśli metoda zwraca wynik - referencję do tablicy, to typem wyniku jest również odpowiedni typ tablicowy.
Np. metoda o nazwie dblVal, która zwraca referencję do nowoutworzonej tablicy
liczb całkowitych, wartości elementów której są podwojonymi wartościami elementów
tablicy liczb całkowitych, do której referencja przekazana została metodzie
jako argument, może być zdefiniowana i użyta tak:
public class Test {
Test() {
int[] a = {1, 2, 3, 4 };
int[] wynik = dblVal(a);
for (int i=0; i < wynik.length; i++)
System.out.print(" " + wynik[i]);
}
int[] dblVal(int[] tab) {
int[] w = new int[tab.length]; // utworzenie tablicy "pod wynik"
// jej rozmiary muszą być równe
// rozmiarom tablicy-argumentu
for (int i=0; i < w.length; i++) w[i] = tab[i]*2;
return w;
}
public static void main(String[] args) {
new Test();
}
}
Dobremu zrozumieniu przekazywania argumentów tablicowych i zwracania tablicowych wyników powinien sprzyjać poniższy rysunek:
Proszę zwrócić uwagę jak niezręczny był opis działania metody dblVal:
"zwraca referencję do nowoutworzonej tablicy
liczb całkowitych, wartości elementów której są podwojonymi wartościami elementów
tablicy liczb całkowitych, do której referencja przekazana została metodzie
jako argument"
Aby podkreślić sens metody dblVal powinniśmy raczej powiedzieć - nie tylko nieprecyzyjnie, ale nawet nieprawdziwie:
"Metoda dblVal zwraca tablicę, wartości elementów której są podwojonymi wartościami
elementów tablicy przekazanej jako argument".
Często dla uproszczenia będziemy mówić, że metoda otrzymuje jako argument
- tablicę i zwraca jako wynik - tablicę. Jest to skrót myślowy: pamiętajmy,
że zawsze chodzi o referencje do tablic
Pamiętamy, że w Javie argumenty przekazywane są metodom przez wartość.
Gdy argumentem jest zmienna tablicowa - przekazywana jest referencja do tablicy
- i tej oczywiście w metodzie nie jesteśmy w stanie efektywnie zmienić. Nic
jednak nie stoi na przeszkodzie, by zmienić elementy przekazanej tablicy.
Obrazuje to poniższy program.
public class Test3 {
public static void main(String[] args) {
new Test3();
}
Test3() {
int[] tab = { 2, 5, 7 };
chgTab1(tab);
showTab("Po wywołaniu metody chgTab1 tablica oznaczana przez tab", tab);
chgTab2(tab);
showTab("Po wywołaniu metody chgTab2 tablica oznaczana przez tab", tab);
}
void chgTab1(int[] tab) {
int[] nowa = { 3, 6, 8 };
tab = nowa;
showTab("W metodzie chgTab1 tablica oznaczana przez tab", tab);
}
void chgTab2(int[] tab) {
for (int i=0; i < tab.length; i++) tab[i]++;
}
void showTab(String s, int[] tab) {
System.out.println(s);
for (int i=0; i < tab.length; i++) System.out.print(" " + tab[i]);
System.out.print('\n');
}
}
który da następujący wynik:
W metodzie chgTab1 tablica oznaczana przez tab
3 6 8
Po wywołaniu metody chgTab1 tablica oznaczana przez tab
2 5 7
Po wywołaniu metody chgTab2 tablica oznaczana przez tab
3 6 8
3. Rozszerzona instrukcja for dla tablic
Rozszerzona instrukcja znana jest rownież jako instrukcja "for-each". Służy do
przebiegania po zestawach danych (kolekcjach, tablicach, innych). Dla
każdego elementu takiego zestawu wykonywane są instrukcje zawarte w
ciele rozszerzonego for.
Składnia
for ( Typ id : expr )
stmt
gdzie:
- expr - wyrażenie, którego typem jest (m.in.) typ tablicowy,
- Typ
- nazwa typu elementów zestawu danych (np.
int albo String)
- id - identyfikator zmiennej, na
którą będzie
podstawiany kolejny element
zestawu danych; do tej zmiennej mamy dostęp w stmt (czyli instrukcji
wykonywanej
w każdym kroku for).
Np.
double[] nums = { 1, 2, 3 };
for (double d : nums) System.out.println(d + 1);
wypisze w kolejnych wierszach 2.0, 3.0 i 4.0
String[] names = { "A", "B", "C" }
for (String s : names) System.out.println(s);
wypisze w kolejnych wierszach A, B, C.
Nieprzypadkowo w opisie składni "for-each" mówi się o "wyrażeniu, ktorego typem jest typ tablicowy".
Naturalnie, zmienna tablicowa jest wyrażeniem typu tablicowego, ale będą nim również:
- wyrażenie new ad-hoc tworzące tablicę.
- wywołanie metody, zwracającej referencję do tablicy.
Pokazuje to poniższy program (w którym też jeszcze raz przyjrzymy się róznym metodom tworzenia tablic).
import java.util.*;
public class DeclCreSamples {
// Metoda wypisująca elementy tablicy
// przekazanej jako argument
private static void show(int[] a) {
for (int elt : a) { // rozszerzone for
System.out.print(elt + " ");
}
System.out.println();
}
// Metoda tworzy tablice napisow w postaci
// 1.a 1.b 1.c ...
private static String[] generateStringTab(int n) {
String[] stab = new String[n];
for (int i = 0; i < stab.length; i++) {
stab[i] = i + 1 + "." + (char)('a' + i);
}
return stab;
}
public static void main(String[] args) {
// Deklaracja z inicjacją
int[] a1 = {1, 2, 3, 4 };
show(a1);
// Deklaracja tablicy n-elementowej
Scanner sc = new Scanner(System.in);
System.out.println("Podaj rozmiar tablicy");
int n = sc.nextInt();
int[] a2 = new int[n];
// nadanie wartości elementom tablicy
for (int i = 0; i < a2.length; i++) {
a2[i] = n;
}
show(a2);
// Tworzenie tablicy z inicjacją ad hoc
show (new int[] { 7, 9, 11 });
// W for-each użyjemy wyrażenia new
for (boolean b : new boolean[] { true, false, true } ) {
System.out.print(!b + " ");
}
System.out.println();
// W for-each użyjemy wywolania metody zwracającej referencję do tablicy
for (String s : generateStringTab(5)) System.out.print(s + " ");
}
}
Dzialanie programu (widok konsoli):
1 2 3 4
Podaj rozmiar tablicy
3
3 3 3
7 9 11
false true false
1.a 2.b 3.c 4.d 5.e
Przypomnijmy
w tym kontekście, że dla typów wyliczeniowych dostępna jest statyczna
metoda values(), która zwraca zestaw wartości (stałych) danego typu
jako tablicę.
Zatem poniższy program:
public class EnumsVals {
enum Month {
JAN, FEB, MAR, APR, MAY, JUN,
JUL, AUG, SEP, OCT, NOV, DEC
}
public static void main(String[] args) {
for (Month m : Month.values()) {
System.out.print(" " + m);
if (m == Month.JUN) System.out.println();
}
}
}
wyprowadzi na konsolę:
JAN FEB MAR APR MAY JUN
JUL AUG SEP OCT NOV DEC
Warto zauważyć, że instrukcja "for-each" :
for ( Typ id : expr )
stmt
w przypadku tablic jest równoważna następującemu zapisowi:
Typ[] $a = expr;
for (int $i = 0; $i < $a.length;
$i++) {
Typ id = $a[ $i ] ;
stmt
}
Uwaga: symbolem $ oznaczane są tymczasowe, wewnętrzne zmienne, niedostępne w ciele instrukcji for-each.
Oznacza
to, że rozszerzone for ma swoje ograniczenia. W ciele (zestawie
instrukcji rozszerzonego for) mamy dostęp do elementów tablicy, ale nie
możemy ich zmieniać.
Istotnie, taki zapis nie ma sensu:
int[] tab = {1, 2, 3 };
for(int a : tab) a = a*2;
bowiem a jest zmienną lokalną w ciele instrukcji for-each.
Oczywiście nie mamy też dostępu do indeksów kolejnych elementów (numerów iteracji).
Wprowadzenie
i rozwój domknięć w nowych wersjach Javy może zmienić tę sytuację i być
może uzyskamy strukturę sterująca znaną z innych języków jako
"forEachWithIndex".
4. Metody ze zmienną liczbą argumentów
Zastosowanie
wyrażenia new wraz z inicjatorem klamrowym do tworzenia tablic pozwala
symulować wywołanie metod ze zmienną liczbą argumentów.
Mając np. metodę sum, która sumuje liczby całkowite z przekazanej jako argument tablicy:
public static long sum(int[] args) {
long sum = 0;
for (int n : args) {
sum += n;
}
return sum;
}
możemy ją wywołać raz z trzema liczbami "do zsumowania":
long suma = sum( new int[] { 1, 2, 3} );
a innym razem z dwoma:
suma = sum( new int[] { 10, 21} );
Poczynając od wersji 1.5 w Javie łatwiej (i bezpośrednio) deklarujemy i wywołujemy
metody ze zmienną liczbą argumentów.
Deklaracja metody ze zmienną liczbą argumentów
Zmienną liczbę
argumentów oznacza się za pomocą wielokropka (...):
typ ...
parms
gdzie typ - typ argumentów,
parms - oznacza argumenty,
których może być
0, jeden lub wiele.
Taki zapis powinien być
ostatni na liście argumentów (parametrów) metody.
Np.
typWyniku nazwaMetody(typ1 parm1, typ2 parm2, typ ... parms)
typWyniku nazwaMetody( typ ... parms)
Dostęp w metodzie do
przekazanych argumentów:
- poprzez rozszerzoną instrukcję for:
for ( typ
v : parms) ...
- do poszczególnych argumentów:
parms[0], parms[1] ...
- ile argumentów przekazano:
parms.length
Jak łatwo można się domyślić kompilator ukrywa przed nami
zwykłą
tablicę parametrów (nazwaMetody(typ[] parms).
Jak
można
wywoływac metodę ze zmienną liczbą argumentów
Podając argumenty - różną liczbę przy
różnych wywołaniach:
metoda(a);
metoda(a, b, c);
A także podając tablicę:
String[] tab = ....
metoda(tab);
Poprzedni przykład zapiszemy teraz w nieco przyjemniejszej formie.
public class VarArg2 {
public static long sum(int ... args) {
long sum = 0;
for (int n : args) {
sum += n;
}
return sum;
}
public static void main(String[] args) {
long suma = sum( 1, 2, 3 );
System.out.println(suma);
suma = sum( 10, 21 );
System.out.println(suma);
}
}
5. Argumenty wiersza poleceń
Obowiązkowym parametrem metody public static void main(...) jest referencja do tablicy łańcuchów znakowych - String[].
Po uruchomieniu programu, tablica ta zawiera słowa podane w wierszu poleceń jako argumenty wywolania programu.
Co znaczy: "argumenty wywołania"?
Weźmy dowolną aplikację, np. notepad.
W sesji znakowej - inaczej terminalu (w Windows - "okienko DOS")
możemy go uruchomić przeglądarkę za pomocą polecenia notepad.
Dodatkowo możemy podać nazwę pliku, który ma być w edytorze otwarty np.:
notepad tekst.txt
Ten dodatkowy napis - to właśnie argument wywołania programu.
Uwaga. Korzystając ze środowisk uruchomieniowych nalezy sprawdzić jak
w konkretnym środowisku podaje się argumenty wiersza poleceń. Zwykle będzie
to opcja w którymś z menu o nazwie "command line arguments"
Teraz - jeśli
mamy program napisany w Javie:
public class Program {
//....
public static void main(String[] args) {
// ...
}
}
to - po kompilacji - w wywołania programu możemy podać w wierszu poleceń argumenty:
java Program arg1 arg2 ...
i podane w wierszu poleceń argumenty - będą dostępne w metodzie main jako
elementy tablicy args, przy czym pierwszy argument będzie pierwszym elementem
tablicy (elementem o indeksie 0), drugi - drugim itd.
W wielu systemach operacyjnych argumentami są kolejne słowa podane po
wywołaniu aplikacji. Jeśli chcemy przekazać napis składający się z kilku
słów - to należy ująć go w cudzysłów
Przyklad 1. Wypisać argumenty wywołania programu
public class Test {
public static void main(String[] args) {
System.out.println("Liczba argumentów wywołania: " + args.length);
for (String s : args)
System.out.println(s);
}
}
Przykład 2. Zabezpieczyć się przed niewłaściwym wywołaniem, gdy wiadomo, że mają być dwa argumenty:
public class Test {
public static void main(String[] args) {
if (args.length != 2) syntax();
System.out.println(args[0] + " " + args[1]);
}
static void syntax() {
System.out.println("Syntax: ... ");
System.exit(1); // metoda exit(c) kończy dzialanie aplikacji z kodem c
}
}
Innym sposobem reagowania na niedostatek argumentów może być obsługa wyjątku ArrayIndexOutOfBoundsException:
public class Args {
public static void main(String[] args) {
try {
System.out.println(args[0] + " " + args[1]);
} catch (ArrayIndexOutOfBoundsException exc) {
syntax();
}
}
static void syntax() {
System.out.println("Syntax: ... ");
System.exit(1);
}
}
6. Tablice "obiektowe"
6.1. Tworzenie tablic zwierających referencje do obiektów
Jak wiemy już, i jak widzieliśmy przed chwilą - tablice mogą zawierać referencje do dowolnych obiektów.
Bardzo naturalnie wygląda to w przypadku tablic elementów typu String.
Możemy np. zadeklarować i stworzyć tablicę:
String[] town = { "Warszawa", "Poznań", "Kraków", "Gdańsk" };
i operować na jej elementach, np. wypisać je:
for (int i=0; i<town.length; i++) System.out.println(town[i]);
albo przestawić miejscami pierwszy i ostatni element:
String last = town[town.length -1];
town[town.length -1] = town[0];
town[0] = last;
Składniowo wygląda to identycznie jak operowanie na tablicach liczb
całkowitych czy rzeczywistych. Wydaje się, że nie ma różnicy.
Jednak różnica jest, a niedostrzeganie jej prowadzi do częstych błędów, szczególnie w początkowej fazie nauki języka Java.
Przypomnijmy sobie klasę Para z rozdziału 4. Jej obiekty reprezentują pary liczb całkowitych.
Powiedzmy, że chcielibyśmy w programie operować na tablicy par liczb całkowitych .
Przede wszystkim konieczna jest deklaracja:
Para[] tabPar;
Dobrze już wiemy, że taka deklaracja nie tworzy tablicy.
Zatem następny krok - stworzenie tablicy
tabPar = new Para[10];
Czy mamy już obiekty-pary? Czy możemy zobaczyć jak wyglądają "na samym początku"?
Zobaczmy.
public class TabPar {
public static void main(String[] args) {
Para[] tabPar = new Para[10];
for (int i=0; i < tabPar.length; i++) tabPar[i].show("Para " + (i+1));
}
}
Ten program skompiluje się bezbłędnie, ale przy jego wykonaniu otrzymamy następujący komunikat.
Exception in thread "main" java.lang.NullPointerException
at TabPar.main(TabPar.java:6)
Jest on skutkiem odwołania do nieistniejącego obiektu - wywołania metody
show z klasy Para za pomocą referencji o wartości null.
Faktycznie, przecież obiekty trzeba tworzyć!. Stworzenie tablicy nie tworzy obiektów, które chcielibyśmy traktować jako jej elementy.
Tablica przechowuje referencje do obiektów, czyli jej elementy na
początku będą miały domyślne wartości null - a dopiero po stworzeniu obiektów
i przypisaniu referencji (ich adresów) elementom tablicy będziemy mogli używać
elementów tablicy w operacjach na obiektach.
Powinniśmy zatem napisać coś w rodzaju:
public class TabPar {
public static void main(String[] args) {
Para[] tabPar = new Para[10];
for (int i=0; i < tabPar.length; i++) tabPar[i] = new Para(i+1, i+2);
for (int i=0; i < tabPar.length; i++) tabPar[i].show("Para " + (i+1));
}
}
co w wyniku da następujacy wydruk:
Para 1 ( 1 , 2 )
Para 2 ( 2 , 3 )
Para 3 ( 3 , 4 )
Para 4 ( 4 , 5 )
Para 5 ( 5 , 6 )
Para 6 ( 6 , 7 )
Para 7 ( 7 , 8 )
Para 8 ( 8 , 9 )
Para 9 ( 9 , 10 )
Para 10 ( 10 , 11 )
To co się dzieje wyjaśnia następujący rysunek.
Można przypuszczać, że w przypadku takiej klasy jak Para nie zdarzy nam się
błąd "braku obiektów" (bowiem zwykle będziemy chcieli mieć jakieś konkretne
pary i będziemy pamiętać, że trzeba je stworzyć za pomocą wyrażenia new).
Jednak nie jest to tak oczywiste w przypadku wielu klas zawartych w standardowych pakietcha Javy.
Np. możemy mieć do czynienia z zestawami przycisków (klasy Button czy JButton).
Naturalne jest myślenie o nich jako o tablicach przycisków (zresztą taki
jest sens - i tak często będziemy opisywać programy). Zatem - możemy pomyśleć,
że po:
Button b = new Button[10];
już mamy 10 przycisków i możemy coś z nimi robić.
Ale przycisków jeszcze nie ma i nasz program wpada w kłopoty.
Tworzenie tablic z elementami oznaczającycmi obiekty - podsumowanie
(na przykładzie klasy JButton)
- Deklaracja tablicy : JButton[] b;
- Utworzenie tablicy: b = new JButton[n];
- Tworzenie obiektów i przypisywanie referencji, które na nie wskazują - elementom tablicy, np.: for (int i=0; i<b.length; i++) b[i] = new JButton();
Naturalnie, również w przypadku tablic referencji do obiektów mozemy
użyć inicjatorów klamrowych, które pozwalają - przy deklaracji - stworzyć
i zanicjować tablicę. Robiliśmy to już zresztą przy okazji inicjacji tablicy
miast literałami łańcuchowymi.
Można podać przyklady takich inicjacji:
Para[] tabPara = { new Para(1,1), new Para(2,3), new Para(4,5) };
JButton[] b = { new JButton("A"), new JButton("B") };
String[] s = { "Ala", "Kot", "Pies" };
Para p1 = new Para(2,4);
Para p2 = new Para(7,8);
Para[] tabP = { p1, p2 };
Tak naprawdę przykłady te nie różnią się między sobą. We wszystkich w/w inicjacjach
zmiennych tablicowych w nawiasach klamrowych podajemy referencje do obiektów
odpowiedniej klasy.
6.2. Tablice heterogeniczne
Połączmy ze sobą znane nam fakty i spróbujmy je wykorzystać.
- Każda klasa w Javie dziedziczy pośrednio lub bezpośrednio klasę Object.
- Zatem referencji do obiektu klasy Object możemy przypisać referencję do obiektu dowolnej innej klasy.
- Tablice mogą zawierać referencje do obiektów.
Wyobraźmy
sobie teraz, że mamy jakąś tablicę, w której kolejnych elementach
chcemy zapisywać dowolne informacje. Czy to możliwe? Ależ tak, właśnie
dzięki wymienionym wyżej trzem właściwościom, pod warunkiem jednak że
informacja ta będzie przedstawiana w sposób obiektowy (jako obiekty).
W tablicy elementów typu Object możemy przechowywać referencje do obiektów dowolnych klas.
Możemy
zatem stworzyć tablicę, za pomocą której jednocześnie będziemy
rejestrować np. i napisy (łańcuchy znakowe) i pary liczb całkowitych
(obiekty klasy Para).
Object[] tab = { "Tekst" , new Para(10,11) };
Ta
właściwość jest szczególnie użyteczna wtedy, gdy w tablicach
przechowujemy referencje do obiektów podklas jakiejś klasy bazowej.
W tym miejscu warto przypomnieć klasę Publication z wykładu 7 i dziedziczące
ją klasy Book, Journal i CDisk. Obiektowe konwersje rozszerzające pozwalają
przypisywać zmiennym oznaczającym obiekty klasy bazowej referencje do obiektów
klas pochodnych. A ponieważ elementy tablic "obiektowych" zawierają referencje
- to na przykład w przypadku naszych klas opisujących publikacje - elementom
jednej tablicy typu Publication[] można przypisać wartości różnych klas: Publication, Book, Journal i CDisk.
Elementy tablicy mogą zawierać referencje wskazujące na obiekty różnych
klas, pod warunkiem, że klasy te dziedziczą tę samą klasę - określającą ogólny,
niejako wspólny dla wszystkich, typ elementów tablicy
Dość zawikłaną treść najlepiej wyjaśni schematyczny przykład:
Publication[] p = new Publication[3];
p[0] = new Book(...);
p[1] = new Journal(...);
p[2] = new CDisk(...);
Możemy powiedzieć (stosując skrót myslowy): pierwszy element tablicy publikacji
jest książką, drugi czasopismem, a trzeci - płytą CD. Jest to możliwe dlatego,
że i książka i czasopismo i płyta CD są publikacjami (to znaczy oprócz tego,
że obiekty te są obiektami specyficznych klas Book, Journal i CDisk - mogą
być również traktowane jako obiekty klasy Publication, ponieważ wszystki
trzy klasy dziedziczą klasę Publication).
Oczywiście, inicjacje są przypisaniami, zatem możemy pisać:
Publication[] p = { new Book(..), new Book(...), new Journal(...) };
Również przekazywanie argumentów i zwracanie wyników metod jest swoistym przypisaniem.
Zatem
możemy łatwo napisać uniwersalną metodę, która liczy dochód ze
sprzedzazy różnych rodzajóą publikacji podanych w tablicy przekazanej
jako argument.
// Zrwaac dochód jaki można uzyskać ze sprzedaży publikacji
// przekazanych w tablicy (publikacje mogą być róznych rodzajów)
public double getIncome(Publication[] p) {
double d = 0;
String opis = "";
for (int i=0; i<p.length; i++) {
d += p[i].getPrice() * p[i].getQuantity();
}
retun(d);
}
7. Tablice wielowymiarowe
Bardzo krótko i syntetycznie omówimy teraz tablice wielowymiarowe.
Tablice wielowymiarowe w Javie realizowane są jako tablice elementów, będących referencjami do tablic.
Liczba wymiarów określana jest przez liczbę nawiasów [].
Przykładowe sposoby deklaracji i inicjacji:
a. inicjacja w nawiasach klamrowych
int[][] mac1 = { { 1, 2, 3, }, { 4, 5, 6, } };
b. dynamicznie
int[][] mac2 = new int[n][m];
c. tablica składa się z wektorów o różnych rozmiarach, zadawanych przez tablicę w
public class MultiArr {
public static void main(String[] arg) {
int w[] = { 2, 3, 4 };
int n = 3;
int[][] m3 = new int[n][]; // rozmiary wierszy
// będą zmienne dynamicznie
for(int i = 0; i < m3.length; i++) {
m3[i] = new int[w[i]];
for (int j = 0; j < m3[i].length; j++) m3[i][j] = i + j;
}
for (int i = 0; i < m3.length; i++) {
System.out.println("Rozmiar " + i + "-go wiersza " + m3[i].length);
String out = " ";
for(int j = 0; j < m3[i].length; j++) out += " " + m3[i][j];
System.out.println(out);
}
}
}
Programik wyprowadzi następujące napisy:
Rozmiar 0-go wiersza 2
0 1
Rozmiar 1-go wiersza 3
1 2 3
Rozmiar 2-go wiersza 4
2 3 4 5
8. Wprowadzenie do kolekcji
Kolekcja jest obiektem, który grupuje elementy danych (inne obiekty)
i pozwala traktować je jak jeden zestaw danych, umożliwiając jednocześnie
wykonywanie operacji na zestawie danych np. dodawania i usuwania oraz przeglądania
elementów zestawu
Naturalną realizacją koncepcji kolekcji jest poznana przed chwilą tablica.
Jest to jednak przypadek szczególny, niewystarczający wobec bogactwa różnych, użytecznych w programowaniu, struktur danych.
Dlatego w Javie, w pakiecie java.util, zdefiniowano narzędzia, służące
do tworzenia i posługiwania się różnymi rodzajami kolekcji.
Rozpatrzymy
teraz przykładowe zastosowania wybranych kolekcji z pakietu java.util.
Będzie to rodzaj pragmatycznego wprowadzenia, w którym - ze względu na
wstępny charakter - nie są stosowane właściwe
zasady używania kolekcji. Na zasady te zwrócimy szczególną uwagę
później (w wykładzie pt. "Kolekcje").
W przykładowych programach - po to by utrzymać minimalne rozmiary funkcjonującego
kodu - nie stosujemy właściwych obiektowych konstrukcji (ograniczamy się
zwykle do metody main()) i unikamy obsługi wyjątków. Jest to oczywiście sposób programowania, którego nie należy stosować w praktyce.
Wyobraźmy sobie, że wczytujemy z pliku listę firm.
W programie możemy przedstawić ją jako tablicę. Ale jaki rozmiar ma mieć
ta tablica? Z góry nie wiemy: w jednym pliku może być 100 firm - w innym
3 miliony.
A tablice w Javie mają ustalone (przy tworzeniu), niezmienne rozmiary, zatem
nie nadają się do przedstawienia listy firm (których raz może być kilka,
innym razem kilka milionów).
Oczywiście, możemy "ręcznie" dynamicznie realokować tablicę, zwiększając
(w miarę potrzeby) jej rozmiary i przepisując zwartość, ale jest lepszy i
prostszy sposob.
O
liście możemy myśleć jako o zestawie elementów, z których każdy znajduje się na określonej pozycji w zestawie.
Klasa ArrayList
z pakietu java.util stanowi łatwe rozwiązanie problemu: jest ona konkretną
realizacją listy w postaci tablicy o dynamicznie (w miarę potrzeby) zmieniających
się rozmiarach. Dodajemy element elt do końca listy za pomocą metody add(elt), uzyskujemy element znajdujący się na pozycji ind za pomocą metody get(ind), możemy też dowiedzieć się ile elementów aktualnie zawiera lista za pomocą metody size().
Zatem krótki ilustracyjny programik, który tworzy listę firm, dodaje do niej
dowolną liczbę elementów (nazw firm zapisanych w kolejnych wierszach pliku),
po czym wyprowadza zawartość listy na konsolę mógłby wyglądać tak:
import java.util.*;
import java.io.*;
class Intro1 {
public static void main(String args[]) throws IOException {
Scanner scan = new Scanner(new File("firms.txt"));
// Utworzenie obiektu klasy ArrayList
ArrayList list = new ArrayList();
while (scan.hasNextLine()) {
String firm = scan.nextLine();
// dodanie kolejnego elementu do listy
list.add(firm);
}
// wyprowadzenie zawartości listy
for (int i = 0; i < list.size(); i++)
System.out.println(list.get(i));
}
}
Jeśli w pliku mamy zapisane w kolejnych wierszach: IBM, Sun, Sun, Apple to wynik będzie następujący:
IBM
Sun
Sun
Apple
Można też użyć innego (bardziej ogólnego i lepszego - dlaczego zobaczymy dalej) sposobu przeglądania elementów listy.
Iterator jest obiektem służącym do przeglądania kolekcji.
Od każdej kolekcji za pomocą metody iterator() możemy uzyskać obiekt-iterator,
służący do jej przeglądania, po czym za pomocą metody next() uzyskiwać dostęp do kolejnych
elementów, a za pomocą metody hasNext() - sprawdzać, czy kolejny element można
pobrać (czy nie dobiegliśmy do końca kolekcji). W naszym przykładzie listy
firm użycie iteratora może wyglądać następująco:
Iterator iter = list.iterator();
while( iter.hasNext()) System.out.println(iter.next());
co da taki sam efekt w postaci wyprowadzenia listy firm.
Alternatywną - nieco krótszą - formą iterowania po kolekcji jest użycie instrukcji for:
for (Iterator iter = list.iterator(); iter.hasNext(); ) System.out.println(iter.next());
Najlepiej jednak jest używać rozszerzonego for (for-each). Jego forma dla dowolnych kolekcji jest następująca:
for (Typ id : kol) stmt
co oznacza, że w każdym kroku iteracji z kolekcji kol pobierany jest (za pomocą jej iteratora) następny element i podstawiany na zmienną id, która może być następnie użyta w instrukcji stmt.
A jaki jest typ oznaczony w powyższej ramce napisem Typ?
Otóż to zalezy od tego czy używamy kolekcji sparametryzowanych typami czy też kolekcji surowych.
Surowe
kolekcje moga zawierać referencje do dowolnych obiektów (ich elementy
są formalnie typu Object). Metoda next() iteratorów takich kolekcji ma
typ wyniku Object.
W przedstawionym przykładzie listy mamy właśnie
do czynienia z taką surową kolekcją. Zarówno iterator, jak i metoda
get() zwracają wyniki typu Object.
Powinniśmy więc napisać:
for (Object elt : list) System.out.println(elt);
Zauważmy,
że przekazanie metodzie println argumentu typu Object powoduje
wyprowadzenie napisu zwróconego przez metodę toString() z klasy
argumentu. Nie mieliśmy więc kłopotu z faktycznym typem (którym był
String).
Jednak gdyby w powyższym przykładzie chcieć wywołać na
rzecz zmiennej elt np. metodę length() z klasy String, to kompilator
zgłosiłby błąd (statyczna ścisła kontrola typów: istotnie w klasie
Object - a takiego typu jest elt - nie ma metody length()!).
Musielibyśmy więc dokonywać referencyjnej konwersji zawężającej
(mówiliśmy o nich krótko w wykładzie "Klasy i Obiekty" przy okazji
wprowadzenia do dziedziczenia):
// Wypisuje długości napisów z kolekcji list
for (Object elt : list) System.out.println( ((String) elt).length());
Użycie kolekcji sparametryzowanych polega na podaniu typu jej elementów w nawiasach kątowych np.
ArrayList<String> = new ArrayList<String>();
Zapewnia
to, że do takiej kolekcji nie będzie można dodać elementu innego typu
niż String, a także, że wszelkie metody zwracające elementy tej
kolekcji (m.in. get() oraz next() iteratora) będą miały typ wyniku
String.
W tym przypadku typ zmiennej w rozszerzonym for może być String i wobec tego możemy pisać tak:
ArrayList<String> list = new ArrayList<String>();
// ....
// Wypisuje długości napisów z kolekcji list
for (String elt : list) System.out.println( elt.length());
O
tzw. generics i parametryzacji klas więcej dowiemy się z dalszych
wykładów o programowaniu obiektowym, a teraz - intuicyjnie stosując
parametry typów - wróćmy do głównego nurtu dyskusji -
zapoznawania się z wybranymi kolekcjami Javy.
Zauważmy, że w pliku firm dwa razy powtórzono nazwę Sun. Co zrobić, jeśli chcemy mieć wynikowy zestaw firm bez powtórzeń nazw?
Oczywiście, można własnoręcznie oprogramować sprawdzanie elementów zestawu i usuwać z niego duplikaty.
Zbiór jest kolekcją, reprezentującą zestaw niepowtarzających się
elementów
Ale po co, jeśli istnieje prostszy sposób - zastosowanie kolekcji
typu zbiór.
Możemy np. użyć konkretnej klasy realizującej koncepcję zbioru - klasy HashSet.
class Intro2 {
public static void main(String args[]) throws IOException {
Scanner scan = new Scanner(new File("firms.txt"));
// Utworzenie obiektu klasy HashSet z parametrem typu <String>
HashSet<String> set = new HashSet<String>();
while (scan.hasNextLine()) {
String firm = scan.nextLine();
// dodanie kolejnego elementu do zbioru
set.add(firm);
}
// wyprowadzenie zawartości zbioru
for (String elt : set) System.out.println(elt);
}
}
wynik będzie następujący (już bez duplikatów):
IBM
Sun
Apple
Zwróćmy uwagę, że - ogólnie:
- w zbiorze elementy nie mają pozycji. Przy przeglądaniu mogliśmy zatem
zastosować wyłącznie iterator (dostęp "po indeksach" nie jest możliwy).
- porządek iterowania (przeglądania) zbioru nie jest określony (jak
widać kolejność wyprowadzonych wyników jest inna niż kolejność firm w pliku).
Co zrobić, jeśli od naszego programu wymagane jest wyprowadzenie uporządkowanego
zestawu firm np. w alfabetycznym porządku ich nazw?
Możemy zastosować kolekcję stanowiącą zbiór uporządkowany. W zbiorze
uporządkowanym kolejność przeglądania jego elementów za pomocą iteratora
jest określona (np. w rosnącym porządku alfabetycznym nazw firm, będących
elementami zbioru). Konkretną realizacją zbioru uporządkowanego jest w Javie
klasa TreeSet.
Zatem następujący program:
import java.util.*;
import java.io.*;
class Intro3 {
public static void main(String args[]) throws IOException {
Scanner scan = new Scanner(new File("firms.txt"));
TreeSet<String> set = new TreeSet<String>();
while (scan.hasNextLine()){
set.add(scan.nextLine());
}
for (String elt : set) System.out.println(elt);
}
}
wyprowadzi nazwy firm, pobrane z pliku, w porządku rosnącym i bez duplikatów:
Apple
IBM
Sun
TreeSet zapewnia uporządkowanie elementów kolekcji, ale usuwa
duplikaty. Co zrobić, jeśli chcemy duplikaty zachować i posortować
kolekcję? Zachowanie duplikatów zapewnia lista (np. ArrayList).
Możemy wobec niej zastosowac gotowy algorytm sortowania zapisany w
postaci statycznej metody klasy Collections (klasa ta zawiera metody
realziujące rózne algorytmy działania na kolekcjach).
Poniższy program:
import java.util.*;
import java.io.*;
class Intro4 {
public static void main(String args[]) throws IOException {
Scanner scan = new Scanner(new File("firms.txt"));
ArrayList<String> list = new ArrayList<String>();
while (scan.hasNextLine()){
list.add(scan.nextLine());
}
Collections.sort(list);
int i= 0;
for (String firm : list) {
System.out.println(++i + ": " + firm);
}
}
}
wyprowadzi firmy w rosnącym alfabetycznym porządku ich nazw:
1: Apple
2: IBM
3: Sun
4: Sun
Wyobraźmy sobie dalej, że w
pliku znajdują się nazwy i adresy firm. Nasz program po wczytaniu pliku
ma za zadanie dostarczenie prostego interfejsu wyszukiwania adresu dla podanej
nazwy firmy. Jak to zrobić?
Tablica asocjacyjna jest zestawem elementów, do których zapewniono
swobodny, bezpośredni (czyli bez konieczności przeglądania elementów zestawu)
dostęp za pomocą kluczy. Można sobie wyobrażać, że jest to uogólnienie zwykłej
tablicy. W zwykłej tablicy kluczami są indeksy całkowite. W tablicy asocjacyjnej kluczami
mogą być dowolne obiekty, Efektywne realizacje tablic asocjacyjnych opierają
się na odpowiedniej implementacji
słowników (odwzorowujących klucze w odpowiadające im wartości). W Javie słownikowe implementacje tablic asocjacyjnych nazywane są słowem
map, pochodzącym od terminu
mapping
, oznaczającego jednoznaczne odwzorowanie (w tym przypadku zbioru kluczy
w zbiór wartości). Również po polsku krótko będziemy nazywać tablice asocjacyjne
mapami. Chwila zastanowienia prowadzi do wniosku, że proste sposoby rozwiązania
tego problemu (np. prowadzenie dwóch list - nazw i adresów - i liniowe wyszukiwanie
nazwy na liście nazw po to by otrzymać pozycję adresu na liście adresów)
są bardzo nieefektywne. No i wymagają od nas pisania kodu. Tym więcej (trudniejszego)
programowania czekałoby nas, gdybyśmy "od podstaw" próbowali samodzielnie
oprogramować bardziej efektywne podejścia do odnajdywania adresów po nazwach
(czyli bardziej efektywnie implementować abstrakcyjną strukturę danych jaką
jest tablica asocjacyjna).
Na szczęście w Javie mamy odpowiednie klasy, które efektywnie
wykonują za nas całą pracę. Należy do nich klasa HashMap, która reprezentuje
zestaw par klucz-wartość (ściślej: jednoznaczne odwzorowanie klucze->wartości)
i umożliwia - za pomocą metody put(klucz, wartość) dodawanie pary (czyli wartości "pod" podanym kluczem) oraz - za pomocą metody get(klucz) uzyskiwanie wartości, związanej z (znajdującej się "pod") podanym kluczem.
Zatem jeśli postac pliku wejściowego jest następująca:
nazwa_firmy1
adres
nazwa_firmy2
adres
...
nazwa_firmyN
adres
to problem wyszukiwania adresów dla
firm podawanych w dialogach wejściowych można oprogramować w następujący
sposób:
import java.util.*;
import java.io.*;
import javax.swing.*;
class Intro5 {
public static void main(String args[]) throws IOException {
// mapa odwzorowań : nazwa -> adres
// argumenty typu są dwa, bo dla klucza (nazwy) i dla wartości (adresu)
HashMap<String, String> map = new HashMap<String, String>();
// Wczytywanie danych
Scanner scan = new Scanner(new File("firmsAddr.txt"));
String firmName;
String address;
while (scan.hasNextLine()) {
firmName = scan.nextLine();
address = scan.nextLine();
// nazwa firmy będzie kluczem
// pod którym w mapie będzie jej adres
map.put(firmName, address); // dodanie pary klucz-wartość do mapy
}
// Interakcyjna część programu:
// dla podanej w dialogu nazwy firmy pokazywany jest jej adres
while ((firmName = JOptionPane.showInputDialog("Nazwa firmy")) != null) {
address = map.get(firmName);
if (address == null) address = "Nie ma takiej firmy";
JOptionPane.showMessageDialog(null, "Firma: " + firmName + '\n'
+ "Adres: " + address);
}
}
}
Działanie programu ilustrują rysunki:
To krótkie wprowadzenie do kolekcji przekonuje nas, że oprócz tablic w
Javie mamy do dyspozycji
wiele gotowych klas i metod, pozwalających łatwo
rozwiązywać problemy związane z reprezentacją w programie bardziej
zaawansowanych
struktur danych i operowaniem na nich.
W szczególności, widzieliśmy, że:
- możemy bardzo łatwo posługiwać się tablicami o dynamicznie zmieniających się rozmiarach,
- możemy bardzo łatwo zapewnić, by w kolekcji nie powtarzały się elementy danych (realizacja koncepcji zbioru),
- możemy bardzo łatwo porządkować dane (zbiory uporządkowane i sortowanie list),
- możemy bardzo łatwo posługiwać się efektywnymi realizacjami tablic asocjacyjnych.
Już teraz możemy korzystać z tych narzędzi.
Jednak dobre, zgodne z regułami sztuki posługiwanie się kolekcjami wymaga nieco więcej wiedzy o programowaniu obiektowym.
9. Podsumowanie
Zapoznaliśmy z ważną koncepcją reprezentacji zestawów danych w programach.
Tablice są tu elementarnym środkiem działania i wiedza o ich stosowaniu jest niezbędna.
Jednak
nie wszystkie zestawy i struktury danych udaje się reprezentować w
postaci tablic. W skrótowym, pragmatycznym wprowadzeniu do kolekcji
poznaliśmy więc inne struktury i sposoby działania, znacznie
ułatwiające programowanie. Pisanie programów w Javie powoli zaczyna być
przyjemnością.