« poprzedni punkt  następny punkt »

4. Tablice "obiektowe"

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;

Problem Proszę sprawdzić działanie tych fragmentów we własnym programie i - dodatkowo - rozwiązać zadanie, którego treść pojawi się po kliknięciu w ikonkę "Problem do rozwiązania".


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 wykładu 8. 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ępujący 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.

r

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 Button)
  1. Deklaracja tablicy :  Button[] b;
  2. Utworzenie tablicy:  b = new Button[n];
  3. 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 Button(); 
 

Naturalnie, również w przypadku tablic referencji do obiektów możemy 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ć przykłady takich inicjacji:

    Para[] tabPara = { new Para(1,1), new Para(2,3), new Para(4,5) };
    Button[] b = { new Button("A"), new Button("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.  

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 typów: 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 myślowy): 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ż wszystkie 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.
W tym kontekście bardzo wygodną konstrukcją składniową Javy jest wyrażenie ad hoc tworzące i inicjujące tablicę referencji do obiektów.

        Wyrażenie:
                            new klasaA[] { refB, refC, ... }

tworzy tablicę typu klasaA[] i inicjuje ją referencjami podanymi w nawiasach klamrowych, przy czym każda z tych referencji może wskazywać obiekt klasy klasaA lub dowolnej klasy pochodnej od klasy klasaA.
Wynikiem opracowania tego wyrażenia jest referencja do zmiennej tablicowej typu klasaA[]

Najczęściej wyrażenie to ma zastosowanie na liście argumentów wywołania metody, której parametrem jest tablica. W ten sposób możemy uzyskać efekt wywołania metody ze zmienną liczbą i (do pewnego stopnia) zmiennymi typami argumentów.

Na przykład, metodę xyz , która jako parametr ma tablicę publikacji, możemy wywołać ze zmienną liczbą i typami publikacji tak:

xyz( new Publication[] { new Book(...), new Book(...) , new Journal(...) } );
xyz( new Publication[] { new Journal(...),  new Journal(...) } );
Book b1 = new Book(...),
         b2 = new Book(...);
xyz( new Publication[] { b1, b2 } );

Bardziej rozbudowany przykład prezentuje poniższy program.
Proszę go przeanalizowac pod kątem zastosowania omówionych wyżej reguł.

public class VarArg {

  public VarArg() {
    // wywołanie metody "z dwoma" argumentami
    showMsgs( new String[] { "Warszawa", "Kraków" } );
    // wywołanie metody "z trzema" argumemntami
    showMsgs( new String[] { "Ala", "Kot", "Pies" } );
    // "trzy argumenty": dwie książki, jedno czasopismo
    showIncome( new Publication[]
                { new Book("P. Pies", "Psy", "WydPP", 2002, "ISBN01", 25, 100),
                  new Book("K. Kot", "Koty", "WydPP", 2002, "ISBN02", 22, 90),
                  new Journal(1, "Kwiaty", "WydAN", 2002, "ISSN03", 10, 200),
                }
              );

    // "dwa argumenty": książka i czasopismo
    showIncome( new Publication[]
                { new Book("A. Koń", "Konie", "Tur", 2001, "ISBN01", 35, 50),
                  new Journal(1, "Ryby", "W&S", 2002, "ISSN03", 20, 100),
                }
              );
  }

  // Wypisuje w kolejnych wierszach napisy - elementy przekazanej tablicy
  public void showMsgs(String[] s) {
    for (int i=0; i<s.length; i++) System.out.println(s[i]);
  }

  // Pokazuje dochód jaki można uzyskać ze sprzedaży publikacji
  // przekazanych w tablicy
 
  public void showIncome(Publication[] p) {
    double d = 0;
    String opis = "";
    for (int i=0; i<p.length; i++) {
      opis += " " + p[i].getIdent();
      d += p[i].getPrice() * p[i].getQuantity();
    }
    System.out.println("Dochód z pozycji " + opis);
    System.out.println(d);
  }


  public static void main(String[] args) {
    new VarArg();
  }

}

Wydruk działania programu:


Warszawa
Kraków
Ala
Kot
Pies
Dochód z pozycji  ISBN01 ISBN02 ISSN03
6480.0
Dochód z pozycji  ISBN01 ISSN03
3750.0

Zadanie.
W księgarni są półki. Każda półka ma numer i może pomieścić zadaną liczbę publikacji (książek, czasopism, płyt CD). Każda książka, czasopismo i płyta zajmują tyle samo miejsca.
Stworzyć klasę Bookshelf reprezentującą półki, na ktore można wstawiać publikacje. Jak pamiętamy każdy obiekt-publikacja przechowuje jako swój "stan" liczbę składowanych pozycji danej publikacji. Na półkę można wstawić cały taki komplet albo nic (jeśli brakuje już miejsca).
W klasie Bookshelf zapewnić metodę uzyskiwania informacji o zawartości półki.
Zmodyfikować klasę Publication w taki sposób, by o każdej publikacji można było się dowiedzieć na której półce stoi.
Dla uproszczenia: proces wstawiania na półki jest jednorazowy, nie ma zdejmowania z półek.

W klasie testującej dostarczyć:
tablicę półek,
tablicę książek,
tablicę czasopism.
i po kolei wstawiać książki i czasopisma na półki. Obiekty półiki (elementy tablicy półek) tworzyć tylko w miarę potrzeby (np. jeśli tablica półek ma 5 elementów, a książki i czasopisma mieszczą się na trzech półkach, to nie ma potrzeby tworzyć dwóch półek - ostatnich elementów tablicy półek).
Pokazać, "przebiegając po elementach" tablicy książek i tablicy czasopism - na jakich półkach stoją. Przebiegając zaś "po elementach" tablicy półek - wylistować jakie pozycje wydawnicze znajdują się na każdej z użytych półek.


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


Możliwe rozwiązanie.

W klasie Publication dodamy pole, które będzie referencją do obiektu klasy Bookshelf i będzie oznaczać półkę na której stoją egzemplarze danej pozycji wydawniczej.
Inicjalnie (zgodnie z regulami inicjacji pól) - pole to będzie miało wartość null ( nie  na półce). Dodatkowa metoda setBookshelf - wywoływana przy wstawianiu egzemplarzy publikacji na półkę -  będzie ustalać wartość pola-półki (gdzie stoją). Metoda whereIs - zwróci tę wartość i w ten sposób będziemy mogli dowiadywać się na której półce stoją dane publikacje.

// Modyfikacje klasy Publication
public class Publication {

  //...
  private Bookshelf bs;  // refrencja do półki na której stoją te publikacje

  // ustal półkę gdzie stoją (przy wstawianiu)
  public void setBookshelf(Bookshelf b) {
    bs = b;
  }

  // zwraca półkę, gdzie stoją te publikacje
  public Bookshelf whereIs() {
    return bs;
  }

// ...
}

W klasie Bookshelf publikacje stojące na półce będziemy rejestrować w tablicy Publication[] pubs. Rozmiary tej tablicy określają ile można wstawić egzemplarzy różnych publikacji. Po wstawieniu egzemplarzy jakiejś publikacji - zmienna freeSpace (wartośc której inicjalnie równa jest rozmiarowi półki)  będzie zmniejszona o liczbę egzemplarzy. Przy wstawianiu egzemplarzy publikacji na półkę,  do tablicy pubs będziemy wpisywac referencję do wstawianej publikacji (da nam to informacje o tym jakie publikacje są na półce). takiego wpisu dokonujemy jednokrotnie dla publikacji (a nie dla każdego egzemplarza). W rezultacie większość miejsca w tablicy pubs będzie niewykorzystana (zakładamy przecież, że rozmiary tablicy określają ile można wstawić na półkę egzemplarzy).

public class Bookshelf {

  private int bsnr;             // numer półki
  private Publication[] pubs;   // tablica publikacji
  private int freeSpace;        // wolne miejcce

  private int currIndex;        // bieżący indeks tablicy publikacji,
                                // pod tym indeksem wpiszemy referencję
                                // do publikacji umieszczanej w tablicy


  Bookshelf(int nr, int size) {
    pubs = new Publication[size];
    freeSpace = size;
    bsnr = nr;
  }

  // Zwraca numer półki
  public int getNr() {
    return bsnr;
  }

  // Umieszcza egzemplarze przekazanej jako argument publikacji
  // na półce. Zwraca true, jeśli to się powiodło - w przeciwnym razie false

  public boolean put(Publication p) {

    // jeżeli już na półce - nie robimy nic
    if (p.whereIs() != null) {
      System.out.println("Publikacje już są na półce");
      return false;
    }

    int n = p.getQuantity();  // ile egzemplarzy danej pozycji wyd.

    if ( n > freeSpace ) {    // gdy brak wolnego miejsca na półce
      System.out.println("Brak miejsca na półce");
      return false;
    }

    freeSpace -= n;        // zmniejszenie dostępnego miejsca na półce

    // referencja do obiektu-publikacji jest wpisywana do tablicy publikacji
    // indeks przesuwamy o 1 a nie o n, gdyż nie ma sensu powielać
    // informacji o publikacji dla wszystkich jej egzemplarzy

    pubs[currIndex++] = p;

    // w obiekcie-publikacji zapisujemy referencję do półki na której
    // znalazły się egzemplarze tej publikacji

    p.setBookshelf(this);
    return true;
  }

  // zwraca tablicę publikacji na półce
  // tablica ta ma currIndex elementów
  // gdyż nie powielamy informacji dla > 1 egzemplarzy
  // tej samej publikacji, wobec czego pozostałe elementy
  // tablicy publikacji pubs są null i nie uwzględniamy ich

  public Publication[] getPubs() {
    Publication[] p = new Publication[currIndex];
    for (int i=0; i < currIndex; i++) p[i] = pubs[i];
    return p;
  }
}

W klasie testującej posługujemy się tablicami półek, książek i czasopism.
Kolejne publikacje staramy się wstawić na "bieżącą" półkę. Jeżeli brakuje miejsca - to tworzymy nowy obiekt półkę.

public class BookshelfTest {

  public static void main(String[] args) {

    final int BSNUM = 5;     // liczba półek, których można użyć
    final int BSIZE = 100;   // rozmiar półki

    Bookshelf[] bs = new Bookshelf[BSNUM];  // tablica półek;
                                            // samych półek jeszcze nie ma

    Book[] bk = { new Book("P. Pies", "Psy", "WydPP", 2002, "ISBN01", 25, 100),
                  new Book("K. Kot", "Koty", "WydPP", 2002, "ISBN02", 22, 90),
                  new Book("A. Koń", "Konie", "Tur", 2001, "ISBN01", 35, 50),
                };

    Journal[] jr = { new Journal(1, "Kwiaty", "WydAN", 2002, "ISSN03", 10, 20),
                    new Journal(1, "Ryby", "W&S", 2002, "ISSN03", 20, 100),
                  };

    bs[0] = new Bookshelf(1, BSIZE);  // pierwsza półka

    int k = 0;  // bieżący indeks tablicy półek -
                // daje nam tę półkę na którą mamy wstawiać publikacje


    for (int i=0; i<bk.length; i++) // wstawiamy po kolei wszystkie książki

       // jeżeli nie udało się na półkę k - to bierzemy nową połkę (k+1)
       while (!bs[k].put(bk[i]) && k < BSNUM ) {
         k++;
         bs[k] = new Bookshelf(k+1, BSIZE);
       }

    for (int i=0; i<jr.length; i++) // wstawiamy po kolei wszystkie czasopisma

       // jeżeli nie udało się na półkę k - to bierzemy nową połkę (k+1)
       while (!bs[k].put(jr[i]) && k < BSNUM ) {
         k++;
         bs[k] = new Bookshelf(k+1, BSIZE);
       }


    // gdzie są książki ?
    for (int i=0; i<bk.length; i++) {
       Bookshelf b = bk[i].whereIs();
       String s = b == null ? " nie stoi na półce"
                            : " jest na półce "+ b.getNr();
       System.out.println( bk[i].getTitle() + s);
    }

    // gdzie są czasopisma ?
    for (int i=0; i<jr.length; i++) {
       Bookshelf b = jr[i].whereIs();
       String s = b == null ? " nie stoi na półce"
                            : " jest na półce "+ b.getNr();

       System.out.println(jr[i].getTitle() + s);
    }

    // co jest na półkach (tylko tych co zostały użyte)
    for (int i=0; i < bs.length; i++) {
      if (bs[i] == null) break;
      Publication[] p = bs[i].getPubs();
      System.out.println("Półka nr " +  bs[i].getNr());
      for (int j=0; j < p.length; j++)  System.out.println(p[j].getTitle());
    }


  }

}

Program wyprowadzi następujące wyniki.

Brak miejsca na półce
Brak miejsca na półce
Brak miejsca na półce
Psy jest na półce 1
Koty jest na półce 2
Konie jest na półce 3
Kwiaty jest na półce 3
Ryby jest na półce 4
Półka nr 1
Psy
Półka nr 2
Koty
Półka nr 3
Konie
Kwiaty
Półka nr 4
Ryby


« poprzedni punkt  następny punkt »