Definiowanie klas


1. Do czego służą klasy?

W programowaniu obiektowym posługujemy się obiektami.
Obiekty charakteryzują się:
Obiekty w programie odzwierciedlają rzeczywiste obiekty, które mogą być konkretne (fizyczne) lub abstrakcyjne.

Na przykład, gdyby nasz program symulował ruch uliczny to musielibyśmy zapewne odzwierciedlić w nim takie konkretne obiekty jak samochody.
Każdy z obiektów- samochodów ma jakieś cechy (atrybuty, stany) np.
oraz udostępnia jakieś usługi, wykonanie których możemy mu zlecić za pomocą odpowiednich poleceń np.
Skąd wiemy jakie atrybuty mają obiekty-samochody? Skąd wiemy jakie polecenia możemy do nich posyłać?

O tym decyduje definicja klasy samochodów, którą nasz program musi albo skądś pobrać albo sam dostarczyć.

Klasa - to opis takich cech grupy podobnych obiektów, które są dla nich niezmienne (np. zestaw atrybutów i usług, które mogą świadczyć)

Można by więc symbolicznie zapisać coś takiego:

Klasa Samochod

    atrybuty:

        ciężar
        wysokość
        akualna prędkość

    usługi - operacje:

        włącz_się_do_ruchu
        zatrzymaj_się
        zwiększ_prędkość
        skręć_w_lewo



Dopiero teraz będziemy wiedzieć co charakteryzuje każdy obiekt-samochód w naszym programie i co możemy z każdym takim obiektem robić w programie.

Ale skąd się biorą obiekty-samochody w naszym programie?
Otóż musimy je tworzyć.

Obiekty tworzymy za pomocą wyrażenia new

Wyrażenie new ma postać:

    new NazwaKlasy(parametry)

gdzie:  parametry (wyrażenia, rozdzielone przecinkami) zazwyczaj określają inicjalne wartości wszystkich lub wybranych atrybutów.

Skąd wiadomo jakie parametry i w jakiej kolejności podać? Otóż każda klasa może zdefiniować specjalne operacje inicjacji obiektu, których można użyć w trakcie jego tworzenia.
Załóżmy, że w klasie Samochod jest zdefiniowana taka operacja, która  nadaje atrybutom obiektów kolejne - przekazane przez parametry - wartości (ciężar, wysokość, aktualna prędkość).

Możemy więc stworzyć obiekty:

samA = new  Samochod(500, 1.5, 0);  
samB = new Samochod(1000, 2.2, 60);

Teraz mamy dwa obiekty-samochody, oznaczane przez zmienne samA i samB.
Wartości ich atrybutów zostały zainicjowane i mają następująca postać:

Samochód A (oznaczony w programie samA)
Samochód B (oznaczony w programie samB)
ciężar = 500
wysokość = 1.5
aktualna prędkość = 0
ciężar = 1000
wysokość = 2.2
aktualna prędkość = 60

Na obiektach samochodach możemy wykonywać operacje, posyłać do nich polecenia.

Do obiektów posyłamy polecenia za pomocą kropki.

Na przykład:

samA.włącz_się do_ruchu();

samB.zatrzymaj_się();

Warto zwrócić uwagę, że dzięki zastosowaniu klas mamy możliwość programowania w języku problemu (np. symulacji ruchu samochodów). Klasy pozwalają nam wprowadzać do języka nowe typy danych (takie jak Samochod) z właściwymi dla nich zestawami dopuszczalnych wartości (tu: możliwe wartości atrybutów, takich jak ciężar, wysokość, aktualna prędkość jazdy)  i dopuszczalnymi operacjami.
Nie należy myśleć, że np. definicja klasy samochodów jest "naturalnie" ustalona, jedyna, dana raz na zawsze.
Konkretne obiekty samochody możemy przecież w naszych programach opisywać bardzo różnie w zależności od tego jaki problem ma do rozwiązania nasz program.
Np. w przypadku symulacji ruchu ulicznego nie będzie pewnie nas interesować taka cecha samochodu jak kolor (zatem ten atrybut nie znajdzie się w definicji klasy jako wspólna cecha wszystkich obiektów samochodów).
Ale być może gdyby nasz program zajmował się zagadnieniem sprzedaży samochodów, to cecha "kolor" znalazłaby się jako istotny atrybut w definicji klasy. A zamiast operacji: włącz się_do ruchu itp. potrzebne byłyby całkiem inne operacje na obiektach (np. sprzedaj).

Korzyść w przypadku odzwierciedlania rzeczywistych obiektów jest oczywista: piszemy program w języku problemu, który mamy rozwiązać.
A co z obiektami abstrakcyjnymi?
Rozważmy np.  pary liczb całkowitych.
Niewątpliwie para liczb całkowitych jest obiektem abstrakcyjnym (bowiem nie istnieje fizycznie).
W naszym programie odzwierciedlamy właściwości tych abstrakcyjne obiektów za pomocą definicji klasy par liczb całkowitych. Taka definicja określa atrybuty pary oraz operacje, które na parach można wykonywać.

Klasa Para

    atrybuty:

        pierwsza_liczba_pary
        druga_liczba_pary

    usługi - operacje:
        operacja_inicjacji  // zainicjuj parę dwoma podanymi liczbami
        set                     // ustal wartość pary na podstawie wartości innej pary
        add                    // dodaj do pary inną parę
        show                  // pokaż parę



Znowu: ta definicja nie określa wartości cech pojedynczego obiektu. Możemy mieć wiele obiektów par-liczb całkowitych, każdy z których ma podane atrybuty (ale np. różne ich wartości) oraz na każdym z których możemy wykonywac podane operacje (set,add itd). Przy czym niekiedy możemy zdefiniować klasę Para w taki sposób, że dopuszczalne jest odejmowanie par; a innym razem ta operacja akurat będzie nam niepotrzebna - i wtedy definicja klasy nie będzie jej zawierać.

Zobaczmy teraz na co w ogóle może się przydać definicja klasy Para. Wyobrażmy sobie, że w programie mamy zapisać dodawanie par liczb calkowitych. Możemy to oczywiście zrobić, korzystając z prostych typów danych:

// Niech a1 i a2 oznaczają składniki pierwszej pary
//          b1 i b2 - składniki drugiej pary
//          c1 i c2 - składniki pary wynikowej (sumy par)
int a1 = 1;
int a2 = 2;
int b1 = 3;
int b2 = 4;
int c1;
int c2;
c1 = a1 + b1;
c2 = a2 + b2;
System.out.println(c1 + " " + c2);

Jednak mając definicję klasy Para możemy zapisać tę operację w dużo prostszy i bardziej zrozumiały sposób:


Para a = new Para(1,2); // utwórz i zainicjuj parę dwoma podanymi liczbami
Para b = new Para(3,4); // i drugą też
Para c = a.add(b);      // dodaj do siebie dwie pary; wynikowa para będzie oznaczana przez zmienną c
c.show();               // pokaż wynik

Podkreślmy: to, że akurat można użyć takiego zapisu - zależy od definicji klasy Para.

Zatem:

Definicja klasy określa:
 



W wielu językach obiektowych (w tym w Javie):
Definicja klasy stanowi zatem definicję:
Klasę winniśmy traktować jako swoisty wzorzec, szablon opisujący powstawanie obiektów (konstruktory), ich cechy (pola) oraz sposób komunikowania się z obiektami (metody).



W Javie do definiowania klas używa się słowa kluczowego class. Samą definicję umieszcza się w następujących po nim nawiasach klamrowych. Kod definicji (pomiędzy nawiasami klamrowymi) nazywa się ciałem klasy.


        [ public ] class NazwaKlasy  {

                // definicje pól
                // definicje konstruktorów
                // definicje metod
        }

gdzie:


Przykłady "szablonów" definicji klas:

public class Para {  // definicja klasy par liczb całkowitych

    // cialo klasy

}

public class Car  {
    // ciało klasy
}


class TestPara {
    // ciało klasy
}


Pola i metody klasy nazywają się składowymi klasy.

        Składowe klasy = pola  + metody




2. Definiowanie pól


Pola klasy określają (zazwyczaj) z jakich elementów będą składać się obiekty tej klasy.
Na przykład obiekty-pary liczb całkowitych składają się z dwóch liczb całkowitych.

W definicji klasy Para trzeba to jakoś zapisać. Naturalnym sposobem jest zadeklarowanie zmiennych odpowiednich typów.

public class Para {

        int a;
        int b;

// dalej będą następować definicje konstruktorów i metod klasy....

}

Taki zapis oznacza, że każdy z obiektów klasy Para będzie zawierał  dwie liczby całkowite. Będzie się składał z dwóch elementów - liczb całkowitych.  Identyfikatory zmiennych (a i b) są oczywiście dowolne, a potrzebne są po to, by do tych zmiennych móc odwoływać się w metodach klasy.

Jedną z waznych cech programowania obiektowego jest hermetyzacja. Polega ona (między innymi) na tym, że działając na obiektach jakiejś klasy powinniśmy wyłącznie posługiwac się dostępnymi dla nas jej metodami, a nie grzebać w "środku obiektów".
Dlatego pola klasy deklaruje się zwykle ze specyfikatorem dostępu private, co oznacza, że dostęp do nich możliwy jest tylko z wnętrza danej klasy (m.in z jej metod), a odwołania spoza klasy są niedopuszczalne.

Definiowanie pól klasy

[public] class NazwaKlasy {

    [ specyfikator_dostępu ] [static] nazwa_typu nazwa_zmiennej [ inicjator ];
    //....         

}
uwagi:


Na przykład:

public class Para {
    private int a;
    private int b;
    // ...
}

Polami klasy mogą być zmienne obiektowe (zmienne oznaczające obiekty). Zobaczmy jak mógłby wyglądać fragment definicji klasy Book, która opisuje książki:

public class Book {
    private String author;  // autor
    private String title;      // tytuł
    private double price;   // cena
    // ....
}

Należy wyraźnie dostrzegać różnicę pomiędzy definicją pól klasy, a elementami obiektów. Zestaw pól klasy określa jakie elementy mogą mieć obiekty tej klasy. Elementy są natomiast konkretnymi obszarami pamięci alokowanymi "w środku" konkretnych obiektów.

Np. definicja klasa Para mówi o tym, że każdy jej obiekt zawiera dwa elementy - liczby całkowite. Po utworzeniu obiektu i jego inicjacji obiekt będzie zawierał dwa elementy - liczby całkowite o konkretnych wartościach. Inny obiekt klasy Para  będzie też zawierał dwie liczby całkowite, ale (być może) o innych wartościach niż ten pierwszy.

r


Nie każde pole klasy określa elementy zawarte w obiektach. Pola deklarowane ze specyfikatorem static są polami statycznymi - nie są zawarte w obiektach, dla każdej zmiennej statycznej wydzielany jest tylko jeden obszar pamięci na całą klasę. Więcej na temat składowych statycznych - w dalszej części tekstu.

Przy tworzeniu obiektów pola klasy zawsze uzyskują inicjalne, domyślne wartości (powiemy: mają zagwarantowaną inicjację). Ogólnie są to wartości ZERO (np. dla liczb całkowitych - całkowite zero, dla typu boolean - wartość false).


3. Definiowanie metod

Zestaw operacji na obiektach określany jest przez definicję metod klasy.
Pojęcie metody zbliżone jest do znanego z innych języków programowania pojęcia funkcji lub procedury.

Metoda - tak samo jak funkcja -  to wyodrębniony zestaw czynności, zapisywany jednorazowo w postaci fragmentu kodu, który może być wywoływany wielokrotnie z innych miejsc programu.


Metody służą głównie (ale nie tylko i niekoniecznie) do wykonywania operacji na obiektach.

Zatem - w odróżnieniu od funkcji -  metody zwykle wywoływane są na rzecz konkretnych obiektów.


Wywołania "na rzecz" obiektu (jak już widzieliśmy) dokonuje się za pomocą "operatora" kropka. Np. jeśli p - oznacza obiekt klasy Para, a w klasie tej zdefiniowano metodę show, to wywołanie tej metody na rzecz tego obiektu zapisujemy jako:

    p.show();

"Wywołanie na rzecz obiektu" oznacza to samo, co "posłanie polecenia do obiektu" lub "komunikatu do obiektu" lub  "wykonanie operacji na obiekcie".
W tym przypadku (dla metody show):

wywołanie metody show na rzecz obiektu p 
=
posłanie komunikatu/polecenie show do obiektu p
=
wykonanie operacji uwidocznienia obiektu p


Schematyczna postać definicji metody jest następująca:

class NazwaKlasy {
    // ...

    [specyfikator_dostępu] [static]  typ_wyniku nazwa_metody( lista_parametrów ) {
         // ... instrukcje wykonywane po wywołaniu metody
    }

}

Uwagi:


Specyfikator dostępu określa czy metoda może być wywołana spoza klasy w której jest zdefiniowana. W szczególności:
Nazwę metody zaczynamy od malej litery, stosując dalej notację węgierską, np. count, setPrice, getAuthor
Te metody, które chcemy udostępnić jako ogólniedostępne operacje na obiektach oznaczamy słowem public; metody "robocze", które mają znaczenie tylko dla nas (twórców klasy) i nie powinny być dostępne dla innych użytkowników klasy - oznaczamy słowem private.

Lista parametrów zawiera rozdzielone przecinkami deklaracje parametrów, które metoda otrzymuje przy wywołaniu. Lista może być pusta (brak argumentów).

Metoda może zwracać wynik (wtedy w jej definicji musimy podać konkretny typ wyniku, a zakończenie działania metody powinno następować na skutek instrukcji return zwracającej dane podanego typu). Jeśli metoda nie zwraca żadnego wyniku to jej typ wyniku określamy słowem kluczowym void, a metoda może skończyć działanie na skutek dobiegnięcia do zamykającego nawiasu klamrowego lub wykonania instrukcji return bez argumentów.

Instrukcja return ma postać:

        return [ wyrażenie ];


Np. metoda zwracająca sumę dwóch liczb całkowitych może wyglądać tak:

int suma(int x, int y) {
    int z = x + y;
    return z;
}

lub tak

int suma(int x, int y) {
    return x + y;
}

Przykład wywołania metody suma:
int sum = suma(10,11);

Przy wywolaniu metoda suma uzyskuje dwa przekazane jej argumenty (10 i 11) jako parametry x i y. Jej działanie polega na dodaniu obu wartości parametrów i zwróceniu (do miejsca wywołania) wyniku. Obowiązkowo, w definicji metody trzeba było podać typ zwracanego wyniku. Po wywołaniu zmienna sum będzie miała wartość 21.

Przyklad innej metody:

void say(String s) {
    System.out.println(s);
}

Wywołanie metody say spowoduje wyprowadzenie na konsolę przekazanego jako argument napisu. Metoda nie zwraca żadnego wyniku, mimo to trzeba było określić typ wyniku słowem kluczowym void (dokladnie "nie dotyczy", znaczy - brak wyniku).


W Javie  argumenty przekazywane są metodom wyłącznie przez wartość.
Oznacza to, że w samej metodzie odwołujemy się nie do faktycznego argumentu, ale do jego kopii. Zatem zmiany przekazanego metodzie argumentu są lokalne, dotyczą wyłącznie kopii i nie dotykają oryginału.

Np. mając metodę:

void incr(int x) {
    ++x;   
 }

i wywołując ją w następującym kontekście:

int z = 1;
incr(z);
System.out.println(z);

uzyskamy następujący efekt:
w samej metodzie zmienna (parametr) x uzyska wartość 2, ale po zakończeniu działania metody i powrocie sterowania do punktu wywołania zmienna z będzie miała nadal wartość 1 i ta wartość zostanie wyprowadzona na konsolę.

Metody statyczne (definiowane ze specyfikatorem static) nie są wywoływane na rzecz obiektów, mogą więc być wywoływane nawet wtedy gdy nie istnieje żaden obiekt. Więcej na temat składowych statycznych w dalszej części tekstu.

4. Definiowanie konstruktorów

Specjalną operacją jest operacja tworzenia obiektu.

Jak wiemy, wykonywana jest ona za pomocą wyrażenia new.
Okazuje się, że to co w nim zapisujemy oznacza wywołanie konstruktora klasy.

Konstruktor służy (głównie) do inicjowania pól obiektów.
O konstruktorze można myśleć jako o specjalnej metodzie, która:


Podobnie jak przy definicji metod - w definicji konstruktora możemy podać specyfikator dostępu, który określa czy konstruktor może być wywołany spoza klasy.

Postać definicji konstruktora:
   
[ public] class nazwa_klasy {

    // Definicja konstruktora
    [ specyfikator_dostępu ] nazwa_klasy(lista_parametrów) {
          // czynności wykonywane przez konstruktor
    }         
}


W klasie Para możemy mieć np. takie konstruktory:

public class Para {
 private int a;
 private int b;

 public Para(int x, int y) { // Nadaje polom a i b wartości
   a = x;                    // przekazane konstruktorowi jako
   b = y;                    // argumenty
 }
 ...
}

albo:

public class Para {
 
  private int a, b;
  
  public Para(int x) { // Konstruktor ma jeden parametr:
    a = x;             // oba pola są nim inicjowane
    b = x;
  }
...
}

Możemy też w tej samej klasie mieć kilka konstruktorów, które różnią się listą parametrów (np. oba w/w konstruktory w klasie Para).

Mając tak zdefiniowane dwa konstruktory w klasie Para, możemy teraz łatwo tworzyć obiekty-pary o zadanych wartościach np.

Para p1 = new Para(10,11);  // para 10, 11
Para p2 = new Para(2);         // para 2, 2


Konstruktory zawsze są wywoływane za pomocą wyrażenia new

Szczególnym rodzajem konstruktora jest konstruktor bezparametrowy.
Jest on automatycznie dodawany do definicji klasy, gdy nie zdefiniowano żadnego konstruktora (przy czym jego ciało jest puste). Zatem jeśli nie dostarczymy w klasie żadnego konstruktora, to przy tworzeniu obiektu zostanie wyowlany automatycznie dodany konstruktor bezparametrowy( który nie robi nic ).

Uwaga: konstruktor bezparametrowy nie jest dodawany, gdy w klasie zdefiniowano jakikolwiek konstruktor.


5. Przykład definiowania klasy

Jako podsumowanie powyższych rozważań przeanalizujemy pełny przykład definicji klasy.
Zobaczymy przy okazji, że definiowanie klas jest bardzo łatwe, czasem nawet trochę nudnawe, choć może być też i zabawne.

Wyobraźmy sobie, że prowadzimy księgarnię. Księgarnia zajmuje się sprzedażą publikacji (książek, czasopism, płyt CD itp.). Zatem głównym obiektem naszego zainteresowania będą publikacje.
Zauważmy, że budując klasę publikacji, staramy się znaleźć wspólne atrybuty wszystkich publikacji. Zatem np. właściwość "autor" zostaje tu pominięta, bo nie wszystkie publikacje (np. czasopisma) mają autorów
O każdej publikacji powinniśmy wiedzieć:

Te wszystkie atrybuty - w naturalny sposób - będą stanowić pola klasy.
public class Publication {

  private String title;
  private String publisher;
  private int year;
  private String ident;
  private double price;
  private int quantity;
 ...
}
Każda publikacja może pojawić się jako obiekt w naszym programie, gdy użyjemy wyrażenia new. Obiekt ten powinien być jakoś zainicjowany - dlatego musimy dostarczyć odpowiedni konstruktor, który będzie inicował podanymi argumentami elementy obiektu.
public class Publication {

  private String title;
  private String publisher;
  private int year;
  private String ident;
  private double price;
  private int quantity;

  public Publication(String t, String pb, int y,
                     String i, double pr, int q)
  {
    title = t;       // pole title uzyskuje wartość parametru t
    publisher = pb;  // pole publisher uzyskuje wartość parametru pb
    year = y;
    ident = i;       // itd...
    price = pr;
    quantity = q;
  }
...
}
Teraz - w innej klasie ( np. w metodzie main umieszczonej w innej klasie) możemy stworzyć obiekt - książkę pt. "Psy", wydaną przez wydawnictwo "Dog & Sons", o cenie  21 zł. Na razie nie mamy jeszcze żadnego egzemplarza tej książki (wartość pola quantity będzie równa 0).
 Publication b = new Publication("Psy", "Dog & Sons", 2002, "ISBN6789", 21.0, 0);
Co możemy robić z publikacjami?
Możemy je kupować, możemy sprzedawać, możemy wreszcie uzyskać informacje o każdej publikacji: jej dane bibliograficzne (tytuł, wydawca, rok, identyfikator), jej aktualną cenę, liczbę egzemplarzy, znajdujących się w księgarni. Może się także okazać, że cena publikacji uległa zmianie, musimy zatem mieć jakiś sposób, by zmienić ten element obiektu- publikacji.

Te wszystkie "operacje" na publikacjach zdefiniujemy jako metody klasy.
public class Publication {

 ...
 // Metody klasy

 // Zwraca tytuł

 public String getTitle() {
   return title;
 }

 // Zwraca wydawcę

 public String getPublisher() {
   return publisher;
 }

 // Zwraca rok wydania

 public int getYear() {
   return year;
 } 

 // Zwraca numer identyfikacyjny

 public String getIdent() {
   return ident;
 }

 // Zwraca cenę

 public double getPrice() {
   return price;
 }

 // Zmienia cenę

 public void setPrice(double p) {
   price = p;
 }

 // Zwraca liczbę egzemplarzy

 public int getQuantity() {
   return quantity;
 }

 // Zakup n egzemplarzy

 public void buy(int n) {
   quantity += n;
 }

 // Sprzedaż n egzemplarzy

 public void sell(int n) {
   quantity -= n;
 }

}

Mając gotową klasę Publikacji możemy przetestować jej działanie.
Powiedzmy, że testowanie odbywać się będzie w klasie TestPub (tradycyjnie w metodzie main, zawartej w tej klasie).

public class PubTest {

  public static void main(String[] args) {

    // Tworzenie obiektu - publikacji

    Publication b = new Publication("Psy", "Dog & Sons", 2002,
                                    "ISBN6789", 21.0, 0);

    int n = 10; // kupimy n = 10 egzemplarzy
    b.buy(n);

    // łatwo policzyć koszt zakupu
    double koszt = n * b.getPrice();

    System.out.println("Na zakup " + n + " publikacji:");
    System.out.println(b.getTitle());
    System.out.println(b.getPublisher());
    System.out.println(b.getYear());  
    System.out.println(b.getIdent());
    System.out.println("---------------\nwydano: " + koszt);

    // teraz sprzedamy 4 egzemplarze i zobaczymy ile zostało
    b.sell(4);
    System.out.println("Po sprzedaży zostało " + b.getQuantity() + " pozycji");
  }
}
Zobacz demo programu.


Program wyprowadzi na konsolę następujące dane.

Na zakup 10 publikacji:
Psy
Dog & Sons
2002
ISBN6789
---------------
wydano: 210.0
---------------
Po sprzedaży zostało 6 pozycji



6. Składowe statyczne

Jak pamiętamy, pola i metody klasy mogą być statyczne i niestatyczne. Dotąd poznaliśmy tylko jeden rodzaj składowych - składowe niestatyczne.

Składowe niestatyczne zawsze wiążą się z istnieniem jakiegoś obiektu (pola – reprezentują dane obiektu, metody muszą być wywoływane na rzecz obiektu, określają polecenia wysyłane do obiektu)

Składowe statyczne (pola i metody):


Do statycznych składowych możemy odwoływać się za pomocą konstrukcji:

NazwaKlasy.NazwaSkładowej


Już od samego początku mieliśmy do czynienia z metodą statyczną, mianowicie metodą main.
Ta metoda musi być statyczna, ponieważ od niej zaczyna się wykonanie aplikacji Javy, a w tym momencie (na samym początku( - nie istnieje jeszcze żaden obiekt.
Inny przykład: w klasie System (dostarczanej jako klasa standardowa w dystrybucji Java) jest statyczna metoda exit(int) kończąca działanie aplikacji i zwracająca podany kod powrotu (wynik działania aplikacji).

Zatem, aby zakończyć działanie aplikacji z kodem powrotu 0 piszemy:

System.exit(0);


W klasie System jest też stała statyczna o nazwie out, która określa obiekt – konsolę (standardowe wyjście). Temu obiektowi możemy wydać polecenie println, co spowoduje wyprowadzenie podanego argumentu jako napisu na konsolę (println jest metodą z klasy PrintStream, a out oznacza obiekt-konsolę, który jest obiektem klasy PrintStream).
Zobaczmy. Schematyczna deklaracja stałej statycznej out w klasie System wygląda tak:

public class System {
       ...
       public final static PrintStream out;
       ...
}

W klasie PrintStream (też dostępnej  jako jedna ze standardowych klas Javy) zdefiniowano metody println, które wypisują do strumienia wyjściowego (PrintStream) podane argumenty. Np.

public class PrintStream  {
       ...
       public void println(String s) {
       ...
       }

}

Zatem, po kolei:

W sumie: System.out.println(...).


Ze specyfikatorem static można deklarowac tylko zmienne, będące polami klasy
Bardzo ważną kwestią jest uświadomienie sobie różnicy pomiędzy składowymi statycznymi i niestatycznymi.
Można powiedzieć, że skladowe statyczne stanowią właściwości całej klasy, a nie poszczególnych jej obiektów.
W przypadku pól rozróżnienie jest następujące:
Na przykład, w klasie opisującej dyski będziemy mieli pole statyczne vat (stawka vat) oraz pola niestatyczne model (opis modelu dysku), capacity (pojemność w GB), price (cenę netto). Każdy dysk będzie się różnił modelem, pojemnością i ceną netto (zatem te elementy będą stanowiły zawartość obiektów), natomiast stawka vat dla wszystkich dysków jest taka sama - nie ma więc sensu zapisywać jej w każdym obiekcie, będzie ona wspólną właściwością klasy, a jako zmienna statyczna - będzie zajmować tylko 8 bajtów na całą klasę, niezależnie od tego czy w programie stworzymy 1 czy 10000 obiektów - dysków.
public class Disk {

  private static double vat;

  private String model;
  private int capacity;
  private double price;

  public Disk(String m, int c, double p) {
    model = m;
    capacity = c;
    price = p;
  }

  public String getDescription() {
    return model + ", " + capacity + " GB";
  }

  public double getBruttoPrice() {
    return price * (1 + vat / 100);
  }

  public static void setVat(double v) {
    vat = v;
  }

}

class Test {

  public static void main(String[] args) {
    Disk.setVat(22.0);
    Disk d1 = new Disk("Seagate Barracuda", 500, 200.0);
    System.out.println(d1.getDescription() + " - cena "
                       + d1.getBruttoPrice() + " zł");
  }
}
Seagate Barracuda, 500 GB - cena 244.0 zł
Zwróćmy uwagę, że metodę setVat wywołaliśmy przed stworzeniem jakiegokolwiek obiektu i pole vat uzyskalo wartość, mimo, że żaden obiekt nie istniał.

Ze statycznych metod nie wolno odwoływać się do niestatycznych składowych klasy (obiekt może nie istnieć). Możliwe są natomiast odwołania do innych statycznych składowych, np. innych metod statycznych.


Zauważmy też, że z metod niestatycznych możemy w sposób naturalny odwoływac się do składowych (pól i metod) statycznych, co widac wyraźnie w metodzie getBruttoPrice().


7. Przeciążanie metod i konstruktorów

W klasie (i/lub jej klasach pochodnych) możemy zdefiniować metody o tej samej nazwie, ale różniące się liczbą i/lub typami parametrów.
Nazywa się to przeciążaniem metod.

Po co istnieje taka możliwość?
Wyobraźmy sobie, że na obiektach  klasy par liczb całkowitych (znanej nam z poprzednich rozdziałów) chcielibyśmy wykonywać operacje:

Gdyby nie było przeciążania metod musielibyśmy dla każdej operacji wymyślać inną nazwę metody. A przecież istota operacji jest taka sama (wystarczy więc nazwa add), a jej użycie powinno być jasne z kontekstu (określanego przez argumenty).

Dzięki przeciążaniu można w klasie Para np. zdefiniować metody:

  void add(Para p)  //  dodaje do pary, na rzecz której wywołano metodę, parę
                            //  podaną  jako argument
  void add(int i)      // do obu składników pary dodaje podaną liczbę
  void add(int i, int k) // pierwszą podaną liczbę dodaje do pierwszego składnika pary
                                // a drugą - do drugiego

i  użyć – gdzie indziej – w naturalny sposób:

  Para p;.
  Para jakasPara;
  ....
  p.add(3);             // wybierana jest ta metoda, która pasuje (najlepiej) do argumentów
  p.add(1,2);
  p.add(jakasPara);

Innym przykładem przeciążonej metody jest metoda println(...). Ma ona bardzo wiele wersji - z argumentami różnych typów (m.in. wszystkich prostych, String i Object). I bardzo dobrze, bo w przeciwnym przypadku musielibyśmy pisać np. printInt(3) i printString("Ala"), aby wyprowadzić odpowiednio liczbę całkowitą i napis.


Identyfikatory metod definiowanych w klasie muszą być od siebie różne.
Wyjątkiem od tej reguły są metody przeciążone tj. takie, które mają tę samą nazwę (identyfikator), ale różne typy i/lub liczbę argumentów

Z przeciążaniem metod związany jest pewien problem. Otóż dopasowanie wywołania do odpowiedniej wersji metody jest dokonywane przez kompilator na podstawie liczby i typów argumentów. Musimy przy tym uważać, bowiem kiedy liczba parametrów jest w różnych wersjach metody przeciążonej taka sama, a ich typy zbliżone - to może się okazać, że źle interpretujemy działanie programu. Co się np stanie, jeśli mamy dwie  metody o nazwie show, pierwsza z parametrem typu short, a druga z parametrem typu int, i wywołujemy metodę show z argumentem typu char ? Powiedzieliśmy przed chwilą, że zostanie wywołana metoda, której parametry najlepiej pasują do argumentów wywołania. Ponieważ typ short jest "bliższy" typowi char niż typ int - mogłoby się wydawać, że zostanie wywołana metoda show(short).
Tymczasem - jak pamiętamy  - wykonywana jest promocja argumentu typu char do typu int i będzie wywołana metoda show(int).

Jeżeli nie znamy dobrze mechanizmów automatycznych konwersji, to w metodach przeciążonych specyfikujmy różną liczbę parametrów lub radykalnie różne typy parametrów

Ogólnie, algorytm wyboru przez kompilator odpowiedniej metody przeciążonej jest dość skomplikowany i może mieć zaskakujące, nieintuicyjne konsekwencje (zob. specyfikację języka: JLS 15.12.2.5). Również z tego powodu należy wyraźnie różnicować liczbę/typy argumentów.

Zwróćmy też uwagę, że przeciążanie "rozciąga się" na różne rodzaje metod. Dwie metody - statyczną i niestatyczną - o tej samej nazwie, ale o różnych typach/liczbie argumentów są przeciążone.

Podobnie jak w przypadku metod, możemy przeciążać konstruktory, Znaczy to, że w jednej klasie możemy mieć kilka wersji konstruktorów z różnymi parametrami. W ten sposób udostępniamy różne sposoby inicjacji obiektów klasy.

W takim przypadku, po to by nie powtarzać wspólnego kodu w różnych konstruktorach, wygodna okazuje się możliwość wywoływania konstruktora z innego konstruktora. Do takiego wywołania stosujemy słowo kluczowe this, co ilustruje  poniższy fragment składni:

r