« poprzedni punkt  następny punkt »

5. Zasięg identyfikatorów. Zmienne lokalne. Czas życia danych. Specyfikatory dostępu

Gdy definiujemy jakąś klasę  niebagatelną kwestią okazuje się pytanie o możliwości działania na określonych zmiennych.  Zwykle sprawia to początkującym w Javie programistom wiele trudności.

Zacznijmy od pewnego banalnego (ale wcale nie tak oczywistego dla  kogoś kto zaczyna przygodę z Javą) stwierdzenia.

Wszystkie instrukcje (oprócz deklaracji) można umieszczać wyłącznie w metodach klasy

(jest od tego wyjątek: blok statyczny, ale w tym momencie go pominiemy)

W samych metodach możemy mieć jednak pewne niejasności: kiedy  możemy się odwoływać do określonych zmiennych i innych metod?

Jak pamiętamy - nazwy zmiennych, stałych, metod, klas są identyfikatorami.

Zasięgiem identyfikatora jest fragment programu, w którym może on być używany (w którym identyfikator jest rozpoznawany przez kompilator).

W każdej metodzie klasy możemy zawsze odwołać się do identyfikatorów składowych klasy (pól i metod), niezależnie od tego w którym miejscu klasy występuje deklaracja tych pól i metod.
W inicjatorach zawartych w deklaracjach pól klasy możemy odwoływac się do wcześniej inicjowanych (por. punkt o inicjacji) identyfikatorów pól klasy

Przypomnijmy: ciało metody czyli jej kod ujmowany jest w nawiasy klamrowe. Ujęty w nawiasy klamrowe kod nazywa się również blokiem

W każdej metodzie możemy deklarować nowe zmienne (lub stałe). Zasięg ich identyfikatorów jest lokalny - rozciąga się od miejsca deklaracji do końca metody (końca bloku stanowiącego ciało metody) w której zostały zadeklarowane. Mówimy o nich zmienne (stałe) lokalne.

Dotyczy to również parametrów (których deklaracje występują w nagłówku metody). Tak naprawdę, parametry są zmiennymi lokalnymi o zasięgu od miejsca deklaracji do końca bloku obejmującego ciało metody.
Również wewnątrz bloków lokalnych (zestawu instrukcji ujętych w nawiasy klamrowe wewnątrz metody) możemy wprowadzać deklaracje zmiennych. Ich zasięg obejmuje obszar od miejca deklaracji do końca bloku w którym zostały zadeklarowane.

Zatem, jeśli mamy następującą klasę:

class A {

    int a;

    void metoda1() {
        int b;
        ... 
       }

    void metoda2() {
        int c;
        ...
         }
 }

to w metodzie metoda1 możemy odwoływać się do zmiennej a, zmiennej b, oraz metody metoda2(), a w metodzie metoda2() możemy odwoływac się do zmiennej a, zmiennej c i metody metoda1. Blędem natomiast będzie próba odwołania się z metody1 do zmiennej c i z metody2 do zmiennej b.

W konstruktorach i metodach możemy przesłaniać identyfikatory pól klasy.
Np.

class A {

     int a;

     void metoda() {
         int a = 0;     // przesłonięcie identyfikatora pola
         a = a + 10;    // dotyczy zmiennej lokalnej;
         this.a++;      // dotyczy pola
     }

}

Tutaj w metodzie metoda() wprowadziliśmy zmiennę lokalną o tej samej nazwie co pole klasy (przesłonięcie identyfikatora). Samo odwołanie a będzie dotyczyć tej zmiennej lokalnej. Jak pamiętamy, przy takim przesłonięciu możemy odwolać się do pola używając zmiennej this.

W Javie nie wolno przesłaniać zmiennych lokalnych w blokach wewnętrznych.

Np. konstrukcja:
class A {
 ....
  void metoda() {
     int a;
     {
       int a;
         ...
     }
  }
}

jest niedopuszczalna.

Ważna kwestia dotyczy inicjacji zmiennych lokalnych. Otóż w przeciwieństwie do pól klasy, zmienne lokalne nie mają zagwarantowanej inicjacji i jeśli nie nadamy im wartości (czy to w jawnej inicjacji, czy za pomocą przypisania) ich wartość jest nieokreślona.
 

Zmienne lokalne muszą mieć na pewno nadane wartości. W przeciwnym razie wystąpi błąd w kompilacji, związany z naruszeniem tzw. "definite assignment rule"

Np. poniższy program:

import javax.swing.*;

public class DefAssgnm {

  public static void main(String[] args) {
    int len;
    String s = JOptionPane.showInputDialog("Napis?");
    if (s != null) len = s.length();
    System.out.println("Długosc napisu : " + len);
  }


nie skompiluje się poprawnie, a kompilator powiadomi nas, że:

DefAssgnm.java:9: variable len might not have been initialized
    System.out.println("Długosc napisu : " + len);
                                             ^
1 error

ponieważ zmienna len jest lokalna w metodzie main i w programie uzyska wartość tylko warunkowo (gdy s != null). Zatem może się zdarzyć, że nie będzia miała przypisanej żadnej wartości.

Rozwiązaniem tego problemu, jest zainicjowanie zmiennej len w deklaracji:

    int len = -1;

lub też w inny sposób zagwarantowanie, że w każdym przypadku będzie ona miała wartość. Np.

import javax.swing.*;

public class DefAssgnm {

  public static void main(String[] args) {
    int len;
    String s = JOptionPane.showInputDialog("Napis?");
    if (s != null) len = s.length();
    else len = -1;
    System.out.println("Długosc napisu : " + len);
  }

}

Z zasięgiem identyfikatorów wiąże się w pewnym sensie czas życia danych, ale nie są to pojęcia tożsame.

Czas życia danych to okres od momentu wydzielenia pamięci dla ich przechowywania do momentu zwolnienia tej pamięci.

Zmienne lokalne są powoływane do życia w momencie deklaracji (automatyczne wydzielenie pamięci na stosie) i likwidowane przy wyjściu sterowania z bloku, w którym zostału zadeklarowane (automatyczne zwolnienie pamięci). Dotyczy to również tych zmiennych, które są referencjami do obiektów.

Wartości zmiennych lokalnych są tracone po wyjściu sterowania z bloku - np. zakończeniu działania metody

Ta oczywista prawda niekiedy jest niedostrzegana i niektórzy starają się np za pomocą zmiennych lokalnych zliczać liczbę wywołań jakiejś metody (usiłują wymyślić sposob na to, a przecież to niemożliwe).

Natomiast pola klasy zachowują się inaczej. Stanowią one przecież elementy obiektów. Obiekty zaś są tworzone w momencie wykonania operacji new. Pamięć dla nich wydzielana jest dynamicznie - na stercie i zostanie zwolniona automatycznie tylko wtedy gdy żadna referencja nie odnosi się już do danego obiektu.
Zwalnianiem pamięci zajmuje się odśmiecacz (garbage collector), który według okreslonej strategii zarządza pamięcią. My tym zarządzaniem nie musimy się martwić.

Zatem pola klasy mogą stanowić coś w rodzaju "zmiennych globalnych", które zachowują swoje wartości pomiędzy wywołaniami metod.
Na przykład, poniższy program:

public class Count {

  private int counter;

  public void increase() {
    counter++;
  }

  public void show() {
    System.out.println(counter);
  }
}

class Test {

  public static void main(String[] args) {
    Count c = new Count();
    c.increase();
    c.increase();
    c.increase();
    c.show();
  }

}

Problemwyprowadzi na konsolę liczbę 3.


Różnicę pomiędzy życiem zmiennych lokalnych i obiektów dobrze ilustruje poniższy przykład:

class Pies {

  String s;

  void nowyPies() {
    String pies =  new String("pies główny");
    s = pies;
  }

  void jakieMamyPsy() {
    System.out.println("Jest " + s);
    String pies2 = pobierzInnegoPsa();
    System.out.println("Jest też " + pies2);
  }

  String pobierzInnegoPsa() {
    String p = new String("inny pies");
    return p;
  }

}

class Test {

  public static void main(String[] args) {
    Pies p =  new Pies();
    p.nowyPies();
    p.jakieMamyPsy();
  }
}
Jest pies główny
Jest też inny pies

Program wyprowadzi następującą informację, gdyż

  • obiekt "pies główny" został stworzony w metodzie nowyPies(). Lokalna zmienna pies, zawierająca referencję do tego obiektu, po zakończeniu działania tej metody przestaje istnieć. Ale ponieważ w metodzie przypisaliśmy jej wartość  zmiennej s  (polu klasy), to ta nadal wskazuje na obiekt "pies główny" i obiekt nadal istnieje. Odwolamy się do niego za pomoca zmiennej s w metodzie jakieMamyPsy().
  • w metodzie  pobierzInnegoPsa() tworzymy obiekt "inny pies" i przypisujemy referencję do niego zmiennej p. Instrukcja return p powoduje przypisanie tej wartości zmiennej p na zmienną pies2 w metodzie jakieMamyPsy (w miejscu wywołania metody pobierzInnegoPsa). Zatem mimo, że po zakończeniu działania metody pobierzInnegoPsa() lokalna zmienna p przestaje istnieć, to obiekt "inny pies" nadal istnieje, bo wskazuje na niego zmienna pies2 w metodzie jakieMamyPsy.
  • po zakończeniu działania metody jakieMamyPsy obiekt "inny pies" zostanie usunięty z pamięci, bowiem nie wskazuje na niego już żadna referencja.

A jak wygląda sytuacja z dostępem do pól i metod danej klasy w innej klasie?
Destęp ten regulują tzw. specyfikatory dostępu, których używamy w deklaracjach zmiennych, stałych i metod ( i które już cześciowo poznaliśmy).

Każda składowa klasy może być:

  • prywatna - dostępna tylko w danej klasie (specyfikator private)
  • zaprzyjaźniona - dostępna ze wszystkich klas danego pakietu; mówi się tu też o dostępie pakietowym lub domyślnym - domyślnym dlatego, iż ten rodzj dostępności występuje wtedy, gdy w deklaracji składowej nie użyjemy żadnego specyfikatora.
  • chroniona lub zabezpieczona - dostępna z danej  klasy, wszystkich klas ją dziedziczących oraz klas będących w tym samym pakiecie co dana klasa (specyfikator protected)
  • publiczna - dostępna zewsząd (specyfikator public)

Pewne powody stosowania specyfikatorów dostępu już omawialiśmy (zabronienie bezpośredniego dostępu do "wnętrza" obiektów, udostępnienie użytkownikom klasy okreslonego zestawu metod publicznych). Więcej o specyfikatorach dostępu dowiemy się w następnym semestrze, teraz zauwazmy tylko, że stanowią one prawdziwe restrykcje.

Np. następujący program:

import javax.swing.*;

public class Para {

  private int a;
  private int b;
  public String nazwa;

  public Para(int x, int y) {
    a = x;
    b = y;
  }

  private String makeString() {
    return nazwa + " " + a + " i " + b;
  }

  public void show() {
    JOptionPane.showMessageDialog(null, makeString()); // 6
  }
}


class Test {

  public static void main(String[] args) {
    Para p = new Para(17,20);
    p.nazwa = "Para liczb";               // 1

    p.a = 1;                              // 3
    System.out.println(p.makeString());   // 4

    p.show();                             // 5

  }

}

Spowoduje błędy w kompilacji:

Para.java:30: a has private access in Para
    p.a = 1;                              // 3
     ^
Para.java:31: makeString() has private access in Para
    System.out.println(p.makeString());   // 4
                        ^
2 errors

przy czym:

  • wiersz oznaczony //1 jest prawidłowy, bo pole nazwa jest publiczne (ale, jak wspomniano uprzednio, nie należy deklarować pól jako publicznych)
  • wiersze oznaczone // 3 i // 4 powodują błędy, bo nie mamy prawa dostępu do ptrywatnych składowych klasy z innej klasy
  • wiersz oznaczony // 5 jest poprawny, bo odwołujemy się do publicznej metody 
  • wiersz // 6 jest poprawny, bo z klasy możemy się odwoływac do jej prywatnych składowych

Mamy też w Javie pojęcie klas publicznych i pakietowych. Klasa pakietowa jest dostępna tylko z klas pakietu. Klasa publiczna jest dostępna zewsząd (z innych pakietów).

Klasę publiczną deklarujemy ze specyfikatorem public:.

 public class Para {
 ...
 }

W pliku źródłowym może być zdefiniowana tylko jedna klasa publiczna (ale nie musi być). Jeśli w pliku źródłowym jest zdefiniowana klasa publiczna, to plik musi mieć taką samą nazwę jak ta klasa - z dokładnością do wielkości liter

Definicji klas pakietowych (bez specyfikatora public) może być w jednym pliku wiele.


« poprzedni punkt  następny punkt »