« poprzedni punkt 

6. Struktura programu. Działanie aplikacji

Jak wiemy, program w Javie jest zestawem definicji klas.

Poza ciałem klasy nie może być żadnego kodu programu - oprócz dyrektywy package, dyrektyw importu oraz komentarzy.

Strukturę programu obrazuje poniższy schemat.

package ...   // deklaracja pakietu (niekonieczna)
import ...    // deklaracje importu; zwykle, ale nie zawsze potrzebne
import ...


// To jest klasa A

public class A {
...
}


// To jest klasa B

class B {
...
}
...


Dyrektywa package służy do "umieszczenia" kompilowanych klas w nazwanym pakiecie (w tym semestrze nie będziemy sią nią jednak zajmować). Znaczenie importów - poznaliśmy przed chwilą.

Program może być zapisany w jednym lub wielu plikach źródłowych (.java)
(w szczególności: wszystkie klasy składające się na program można umieścić w jednym pliku albo każdą klasę można umieścić w odrębnym pliku).

Nie ma kodu poza klasą... A z drugiej strony wiemy też, że klasa jest swoistym wzorcem, szablonem określającym właściwości obiektów.

Jak zatem możliwe jest w ogóle działanie programu napisanego w Javie? Gdzie i jak zaczyna się wykonanie?
Powtórzmy sobie najpierw to wszystko co już wiemy

Oczywiście, najpierw jest kompilacja do B-kodu.
Wszystkie wybrane pliki żródłowe .java podlegają kompilacji za pomocą kompilatora Javy (javac.exe). Z każdej klasy w pliku(ach) źródłowym powstaje plik B-kodu o rozszerzeniu .class

Mamy teraz trzy możliwości: albo nasz program jest aplikacją, albo apletem, albo serwletem. Zostańmy na razie przy pierwszej (dwie pozostałe omówimy w przyszłym semestrze).
 
Jeżeli nasz program jest aplikacją, to jedna z  klas musi zawierać metodę:

    public static void main(String[ ] args)

Np. spójrzmy na klasę Work i klasę Inna zdefiniowane w pliku Work.java

public class Work {
 public static void main(String[] args) {
    ...
  }
...
}
class Inna {
...
}
 
// Koniec pliku Work.java

Po kompilacji (javac Work.java) powstają dwa pliki:
Work.class
Inna.class
 
Maszyna wirtualna Javy jest wywołana za pomocą polecenia java z argumentami:

  • nazwa_pliku_class_zawierającego_metodę_main (bez rozszerzenia .class)
  • argumenty_wołania

 java Work [ew. argumenty przekazywane do main jako String[ ] args]

Klasa Work zostaje załadowana przez JVM i sterowanie zostaje przekazane do metody main. W tej metodzie zaczyna się "życie": tworzenie obiektów, odwołania do innych klas aplikacji.

Zauważmy jednak szczególnie jedną ważną rzecz: metoda main jest metodą statyczną. Wobec tego - działanie (jakiegoś) programu nie wymaga wcale by istniały lub były tworzone obiekty (o ile posługujemy się w metodzie main tylko zmiennymi typów pierwotnych).
Nawet jeśli posługujemy się obiektami (takimi jak łańcuchy znakowe) - nie musimy tworzyć obiektu klasy, w której zawarta jest metoda main.

Takie podejście jest jednak możliwe tylko wtedy, gdy nasz program jest stosunkowo niewielki. Większe programy powinniśmy co najmniej dobrze strukturyzować (rozbić na funkcje - metody).

Zobaczmy bardzo prosty przykład. Mamy trzy liczby i powinniśmy policzyć i wyprowadzić ich sumę oraz średnią, po czym wszystkie trzy liczby dwa razy zwiększyć o 1, za każdym razem wyprowadzając nową sumę i średnią.

Bardzo złe rozwiązanie tego problemu może wyglądac tak.

public class Main1 {

  public static void main(String[] args) {
    double a = 12.0,
           b = 14.0,
           c =  4.0;
    double sum = a + b + c;
    double avg = (a + b + c)/3;
    System.out.println("Suma " + sum);
    System.out.println("Srednia " + avg);
    a++;
    b++;
    c++;
    sum = a + b + c;
    avg = (a + b + c)/3;
    System.out.println("Suma " + sum);
    System.out.println("Srednia " + avg);
    a++;
    b++;
    c++;
    sum = a + b + c;
    avg = (a + b + c)/3;
    System.out.println("Suma " + sum);
    System.out.println("Srednia " + avg);

  }

Oczywiście na myśl przychodzi od razu podział programu na metody.
Być może, długo nie zastanawiając się napiszemy tak:

public class Main2 {

 double sum(double a, double b, double c) {
   return a + b + c;
 }

 double average(double a, double b, double c) {
   return (a + b + c)/3;
 }

 void report(double a, double b, double c) {
    System.out.println("Suma " + sum(a, b, c));
    System.out.println("Srednia " + average(a, b, c));
 }


  public static void main(String[] args) {
    double a = 12.0,
           b = 14.0,
           c =  4.0;
    report(a, b, c);
    a++;
    b++;
    c++;
    report(a, b, c);
    a++;
    b++;
    c++;
    report(a, b, c);
  }
} 

Chociaż wielkość programu zmieniła się nieznacznie, to jednak klarowne wyodrębnienie pewnych powtarzalnych czynności w postaci metod czyni go bardziej czytelnym i elastycznym (latwiej modyfikowalnym).
Niestety, spotka nas rozczarowanie...
Kompilator zgłosi:


Main2.java:21: non-static method report(double,double,double) cannot be referenced from a static context
    report(a, b, c);
    ^
Main2.java:25: non-static method report(double,double,double) cannot be referenced from a static context
    report(a, b, c);
    ^
Main2.java:29: non-static method report(double,double,double) cannot be referenced from a static context
    report(a, b, c);
    ^
3 errors

Aha, przecież metoda main jest statyczna! Nie możemy odwoływac się z jej wnętrza do niestatycznych składowych klasy Main2 (a taką jest metoda report).

Gdy uczynimy ją statyczną:

    static void report(...)

problem przeniesie się w inne miejsce: ze statycznej metody report nie możemy wywołać niestatycznych metod sum i average.
Możemy uczynić je wszystkie statycznymi - i wtedy nasz program zadziała.
Albo - pozostawiając wszystkie metody niestatycznymi  - w metodzie main utworzyć obiekt klasy i na jego rzecz wywołać metodę report().
Może to wyglądać tak:

public class Main3 {

 double sum(double a, double b, double c) {
   return a + b + c;
 }

 double average(double a, double b, double c) {
   return (a + b + c)/3;
 }

 void report(double a, double b, double c) {
    System.out.println("Suma " + sum(a, b, c));
    System.out.println("Srednia " + average(a, b, c));
  }


  public static void main(String[] args) {
    double a = 12.0,
           b = 14.0,
           c =  4.0;

    Main3 m = new Main3(); // utworzenie obiektu
    m.report(a, b, c);
    a++;
    b++;
    c++;
    m.report(a, b, c);
    a++;
    b++;
    c++;
    m.report(a, b, c);

  }

}

Ale skoro już tworzymy obiekt klasy, to nadajmy jej jakiś istotny sens. Na przykład - niech jej obiekty będą trójkami liczb rzeczywistych.

public class Trojka {

 double a, b, c;

 Trojka(double x, double y, double z) {
   a = x;
   b = y;
   c = z;
 }

 double sum() {
   return a + b + c;
 }

 double average() {
   return (a + b + c)/3;
 }

 void increase() {
   a++;
   b++;
   c++;
 }

 void report() {
    System.out.println("Suma " + sum());
    System.out.println("Srednia " + average());
  }


  public static void main(String[] args) {
    Trojka t = new Trojka(12, 14, 4);
    t.report();
    t.increase();
    t.report();
    t.increase();
    t.report();
  }

}

Główna "procedura" main (zauważmy zresztą, że umieszczona w klasie Trojka; metoda main może przecież znajdowac się w dowolnej klasie) stała sie dzięki temu jeszcze bardziej klarowna, a cały program jeszcze łatwiejszy do modyfikacji i uzupełnień, bowiem dane (zmienne a, b, c - zdefiniowane jako pola) stały się teraz dostępne dla wszystkich metod klasy i jednocześnie zachowują swoje wartości pomiędzy wywołaniami metod, co wykorzystaliśmy definiując metodę increase(), zwiększającą o1  wszystkie trzy dane.

Rozpatrując różne warianty strukturyzacji program warto podkreślić, że bardzo często obiekt klasy programu nie jest nam wcale potrzebny. "Praca" zapisywana jest w konstruktorze i w metodzie main (rozpoczynającej działanie programu) wystarczy samo wywołanie konstruktora. Czyni się tak szczegolnie często, gdy konstruktor tworzy graficzny interfejs użytkownika.
W naszym przypadku sumowania i uśredniania trzech liczb takie rozwiązanie jest raczej sztuczne, ale dla porządku można je podać.

public class Main4 {

 double a = 12, b = 14, c = 4;

 Main4() {
   report();
   increase();
   report();
   increase();
   report();
 }

 double sum() {
   return a + b + c;
 }

 double average() {
   return (a + b + c)/3;
 }

 void increase() {
   a++;   b++;   c++;
 }

 void report() {
    System.out.println("Suma " + sum());
    System.out.println("Srednia " + average());
  }

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

Zauważmy:

  • ponieważ odwołania do metod report i increase umieściliśmy w konstruktorze, to nie musimy specyfikowac obiektu do którego się odnoszą (niejawnie jest to obiekt oznaczany przez this)
  • metoda main służy tylko do utworzenia obiektu (i wywołania konstruktora); niepotrzebna jest nam tu żadna do niego referencja - dlatego wynik opracowania wyrażenia new nie podstawiamy na żadną zmienną.


« poprzedni punkt