Rozdział 9 - Klasy - Pakiety i importowanie klas

Pakiety klas

Do tej pory umieszczaliśmy nasze programy w tym samym katalogu na dysku. Prędzej czy później natrafimy jednak na sytuację, w której będziemy potrzebowali dwóch różnych klas, które będą miały taką samą nazwę – zdarzyło się to już w rozdziale o klasach – dwie przykładowe klasy używane do wyjaśnienia różnych zagadnień miały nazwę Punkt.

Biorąc pod uwagę, że tysiące osób programujących w języku Java może tworzyć własne klasy i udostępniać je do użycia przez inne osoby, potrzebny jest mechanizm, który pozwalałby na odróżnianie klas o takich samych nazwach. Jest to na tyle powszechny wymóg, że różne języki programowania dostarczają rozwiązania dające tę funkcjonalność.

W języku Java możemy określić w jakim pakiecie (package) znajduje się klasa. Pakiet ten stanie się integralną częścią nazwy naszej klasy, dzięki czemu potencjalny problem z powtórzeniem tej samej nazwy będzie mało prawdopodobny.

Nazwy klas można by porównać do imion i nazwisko osób – często zdarza się, że dwie osoby (bądź więcej) mają takie samo imię i nazwisko. Pakiety z kolei można by porównać do numerów PESEL – pomimo, że dwie osoby mogą nazywać się tak samo, to numer PESEL pozwala na unikalną identyfikację konkretnej osoby.

Spójrzmy na poniższy przykład klasy Powitanie, która zdefiniowana jest w pakiecie przykladowypakiet.pl:

Nazwa pliku: przykladowypakiet/pl/Powitanie.java
package przykladowypakiet.pl;

public class Powitanie {
  public static void main(String[] args) {
    wypiszKomunikat();
  }

  public static void wypiszKomunikat() {
    System.out.println("Witaj Swiecie!");
  }
}

Ta prosta klasa zdefiniowana jest w pakiecie przykladowypakiet.pl – w tym celu użyte zostało nowe słowo kluczowe package, po którym następuje nazwa pakietu.

Załóżmy teraz, że chcielibyśmy mieć drugą klasę o nazwie Powitanie, która wypisywałaby komunikat po angielsku – umieścimy ją w pakiecie przykladowypakiet.eng, dzięki czemu nie wystąpi kolizja nazw:

Nazwa pliku: przykladowypakiet/eng/Powitanie.java
package przykladowypakiet.eng;

public class Powitanie {
  public static void main(String[] args) {
    wypiszKomunikat();
  }

  public static void wypiszKomunikat() {
    System.out.println("Hello World!");
  }
}

Obie powyższe klasy nazywają się Powitanie, jednak pełna nazwa każdej z klas to:

  • przykladowypakiet.pl.Powitanie
  • przykladowypakiet.eng.Powitanie

Zanim zobaczymy te klasy w akcji, musimy jeszcze zwrócić uwagę na trzy istotne cechy klas pakietowych:

  • Przynależność klasy do pakietu (definiowana za pomocą słowa kluczowego package) musi być na początku pliku – jedyne, co może poprzedzać użycie package, to komentarze – w przeciwnym razie, nasza klasa w ogóle się nie skompiluje.
  • Najlepiej, by nazwy pakietów zawierały tylko litery i liczby. Dodatkowo, człony nazwy pakietu rozdzielane są kropkami oraz nie stosujemy camelCase – używamy zawsze małych liter.
  • Nazwa pakietu, w którym umieszczamy klasy, powinna odpowiadać strukturze katalogów na dysku, w których ta klasa się znajduje. Jest to bardzo ważne, ponieważ w przeciwnym razie Maszyna Wirtualna Java nie będzie w stanie znaleźć klasy o danej nazwie. Dla przykładu, pierwsza z powyższych klas Powitanie powinna być umieszczona w katalogu pl, który z kolei powinien być zawarty w katalogu o nazwie przykladowypakiet.

Ostatni podpunkt jest bardzo istotny – jeżeli definiujemy, że klasa ma być w pewnym pakiecie, to powinniśmy utworzyć na dysku komputera odpowiednią strukturę katalogów, odpowiadających temu pakietowi. Powyższe klasy Powitanie znajdują się w katalogach pl oraz eng, które to katalogi zawarte są w katalogu przykladowypakiet:

programowanie
|
`--przykladowypakiet
   |---eng
   |       Powitanie.java
   |
   `---pl
           Powitanie.java

Powyżej zaprezentowana jest struktura katalogów, w których umieszczone są klasy Powitanieprogramowanie, przykladowypakiet, eng, oraz pl, to foldery na dysku. Pierwszy folder to programowanie, ponieważ jest to nasz folder na przykłady z tego kursu, który utworzyliśmy w rozdziale pierwszym. Folder programowanie nie wchodzi w skład pakietu, w którym klasy są zdefiniowane – jest to po prostu folder nadrzędny dla naszego pakietu klas.

Mając już odpowiednią strukturę katalogów i dwie klasy pakietowe, spróbujmy teraz uruchomić jedną z nich – przejdziemy do katalogu przykladowypakiet/pl, a następnie skompilujemy klasę i spróbujemy ją uruchomić:

C:\programowanie> cd przykladowypakiet C:\programowanie\przykladowypakiet> cd pl C:\programowanie\przykladowypakiet\pl> javac Powitanie.java C:\programowanie\przykladowypakiet\pl> java Powitanie Error: Could not find or load main class Powitanie Caused by: java.lang.NoClassDefFoundError: przykladowypakiet/pl/Powitanie (wrong name: Powitanie)

Korzystają z komendy cd (change directory), przeszliśmy do katalogu, w którym zdefiniowana jest jedna z klas Powitanie, po czym bez problemu ją skompilowaliśmy. Niestety, próba uruchomienia zakończyła się błędem – Maszyna Wirtualna Java nie może znaleźć szukanej klasy.

Problem wynika z faktu, że pełna nazwa naszej klasy to teraz przykladowypakiet.pl.Powitanie, a nie Powitanie. Jeżeli spróbowalibyśmy jednak podać taką nazwę, to ponownie zobaczymy błąd:

C:\programowanie\przykladowypakiet\pl> java przykladowypakiet/pl/Powitanie Error: Could not find or load main class przykladowypakiet.pl.Powitanie Caused by: java.lang.ClassNotFoundException: przykladowypakiet.pl.Powitanie

Maszyna Wirtualna Java nadal nie widzi klasy, którą skompilowaliśmy. Dlaczego tak się dzieje?

Kilka paragrafów wcześniej dowiedzieliśmy się, że struktura katalogów, w której klasa jest umieszczona, musi odpowiadać pakietowi, do którego klasa należy – wymóg ten jest spowodowany tym, że gdy Maszyna Wirtualna Java otrzymuje nazwę klasy pakietowej, to szuka klasy do uruchomienia w katalogach definiowanych przez pakiet tej klasy. Dlatego powyższe uruchomienie klasy się nie powiodło – Maszyna Wirtualna Java szukała klasy Powitanie w katalogu pl zawartym w katalog przykladowypakiet – poniżej przedstawione jest, czego oczekiwała Maszyna Wirtualna Java:

programowanie
|
`--przykladowypakiet
   |---eng
   |       Powitanie.java
   |
   `---pl
       |   Powitanie.java
       |
       `--przykladowypakiet
          |
          `---pl
              |
              `---Powitanie.class

Maszyna Wirtualna Java potraktowała katalog pl, w którym aktualnie się znajdujemy, jako "główny" katalog. Otrzymując klasę o nazwie przykladowypakiet.pl.Powitanie, zaczęła szukać podkatalogu przykladowypakiet, a w nim kolejnego katalogu, o nazwie pl, w którym miała się znajdować skompilowana klasa Powitanie. Nie o to nam chodziło.

Jeżeli chcemy uruchomić klasę pakietową (tzn. zdefiniowaną w pewnym pakiecie przy użyciu słowa kluczowego package), to musimy znajdować się w katalogu, z którego Maszyna Wirtualna Java będzie mogła odnieść się do klasy, którą ma uruchomić, zgodnie z opisanym powyżej sposobem lokalizowania klas.

Jeżeli więc chcemy uruchomić klasę przykladowypakiet.pl.Powitanie, to musimy w linii poleceń znajdować się w katalogu, w którym znajduje się pierwszy katalog z pakietu klasy – w tym przypadku jest to katalog przykladowypakiet – ten katalog zawarty jest w katalog programowanie – i to jest właśnie miejsce, z którego musimy uruchomić naszą klasę:

C:\programowanie\przykladowypakiet\pl> cd .. C:\programowanie\przykladowypakiet> cd .. C:\programowanie> java przykladowypakiet.pl.Powitanie Witaj Swiecie!

Korzystając dwukrotnie z komendy cd (change directory) z parametrem .. (dwie kropki), przeszliśmy do katalogu nadrzędnego – programowanie. W tym katalogu wywołujemy Maszynę Wirtualną Java, podając jako argument pełną nazwę klasy przykladowypakiet.pl.Powitanie. Tym razem widzimy, że kod naszej klasy został wykonany przez Maszynę Wirtualną Java – na ekranie pojawił się komunikat Witaj Swiecie!.

Jeżeli chcemy skompilować klasę pakietową, to nie musimy przechodzić do katalogu, w którym jest zdefiniowana – możemy zrobić to z poziomu katalog nadrzędnego dla danego pakietu (w naszym przypadku jest to katalog programowanie). Aby to osiągnąć, podajemy ścieżkę do klasy, którą chcemy skompilować – spójrzmy na przykład kompilacji drugiej klasy Powitanie:

C:\programowanie> javac przykladowypakiet/eng/Powitanie.java

Kompilatorowi javac przekazaliśmy jako argument klasę do skompilowania, gdzie ścieżka do tej klasy to pakiet, w którym klasa się znajduje – zauważmy, że tym razem nie korzystamy z kropek do rozdzielenia członów pakietu i klasy, lecz znaków slash /. Klasa została skompilowana bez błędów, a wynikowy plik Powitanie.class został umieszczony w lokalizacji przykladowypakiet/eng.

Możemy teraz uruchomić tę klasę:

C:\programowanie> java przykladowypakiet.eng.Powitanie Hello World!

Druga klasa Powitanie wypisuje komunikat po angielsku.

Pełna nazwa klasy to pakiet i nazwa klasy, rozdzielone kropkami, na przykład przykladowypakiet.pl.Powitanie – takiej nazwy klasy oczekuje Maszyna Wirtualna Java jako argumentu.

Ścieżka do pliku z klasą do skompilowania, jakiej oczekuje kompilator języka Java (program javac), to nazwa klasy poprzedzona nazwami katalogów, w których klasa ta się znajduje (które mogą być częścią pakietu tej klasy) – w tym przypadku, nazwy katalogów oddzielamy znakiem slash / od nazwy klasy, np.:

javac przykladowypakiet/eng/Powitanie.java

Klasy nie muszą być zawarte w pakietach – do tej pory wszystkie klasy, które pisaliśmy, nie zawierały słowa kluczowego package. Takie klasy są wtedy po prostu w ogólnym, "domyślnym" pakiecie.

Konwencja nazewnicza pakietów klas

Istnieje konwencja nazewnicza pakietów klas, wedle której pakiety odzwierciedlają odwróconą domenę Internetową firm bądź osób, które są autorami klas.

Dla przykładu, pakiety klas napisane dla tego kursu mógłbym umieścić w pakiecie, którego nazwa zaczynałaby się od com.kursjava lub com.przemyslawkruglej. Kolejnym członem pakietu mogłyby być numery rozdziałów, z których pochodzą przykłady do tego kursu. Dla przykładu, programy z rozdziału o klasach mógłbym umieścić w pakiecie o nazwie com.kursjava.rozdzial9. W takim przypadku, kody źródłowe tych klas miałyby na początku następującą instrukcję package:

package com.kursjava.rozdzial9;

Po odwróconej domenie, firmy często umieszczają nazwę programu, do którego klasy należą, po której mogą wystąpić kolejne człony pakietu, w zależności od tego, jakie jest przeznaczenie klas. Jeżeli napisałbym grę kółko i krzyżyk, to "główna" część pakietu wszystkich klas mogłaby się nazywać com.przemyslawkruglej.kolko_i_krzyzyk.

Pakiety klas zdefiniowanych przez twórców języka Java znajdują się w pakietach zaczynających się od java oraz javax.

Importowanie klas

Aby skorzystać z klas, które zdefiniowane są w innych pakietach, musimy je zaimportować do naszych programów. Na przestrzeni ostatnich rozdziałów robiliśmy to już wielokrotnie – na przykład, gdy chcieliśmy pobrać od użytkownika dane – korzystaliśmy wtedy z następującej instrukcji importu:

import java.util.Scanner;

Aby pobrać dane od użytkownika, korzystaliśmy z klasy Scanner – jest to jedna z klas zdefiniowanych w jednym z pakietów Biblioteki Standardowej Java, czyli zestawu pakietów klas, które oddane są do użycia dla nas, programistów języka Java, przez twórców języka Java.

Jeżeli w programie, które chciałby korzystać z klasy Scanner, zabrakłoby instrukcji importu, to kompilacja programu zakończyłaby się błędem:

Nazwa pliku: WczytywanieDanychBezImport.java
public class WczytywanieDanychBezImport {
  public static int getInt() {
    // blad! braku import klasy Scanner
    return new Scanner(System.in).nextInt();
  }
}

Błąd kompilacji powyższego programu jest następujący:

WczytywanieDanychBezImport.java:4: error: cannot find symbol return new Scanner(System.in).nextInt(); ^ symbol: class Scanner location: class WczytywanieDanychBezImport 1 error

Aby zaimportować klasę, korzystamy ze słowa kluczowego import, po którym następuje nazwa klasy, którą chcemy zaimportować. Możemy też zaimportować wszystkie klasy w pakiecie – zamiast nazwy klasy na końcu pakietu, korzystamy w takim przypadku z * (gwiazdka), która oznacza "wszystkie klasy".

Instrukcje import muszą występować po (ewentualnej) instrukcji package, a przed definicją klasy – w przeciwnym razie kod klasy się nie skompiluje.

Wróćmy do klas Powitanie z poprzedniego rozdziału. Załóżmy, że w katalogu nadrzędnym programowanie istnieć będzie klasa PrzykladImportu, w której chcielibyśmy skorzystać z klasy Powitanie z pakietu przykladowypakiet.pl – hierarchia plików będzie następująca:

programowanie
|
| PrzykladImportu.java
|
`--przykladowypakiet
   |---eng
   |       Powitanie.java
   |
   `---pl
           Powitanie.java

Klasa PrzykladImportu nie znajduje się w żadnym pakiecie – nie korzystamy w niej z instrukcji package, jednak użyjemy w niej instrukcji import do zaimportowania klasy przykladowypakiet.pl.Powitanie:

Nazwa pliku: PrzykladImportu.java
import przykladowypakiet.pl.Powitanie;

public class PrzykladImportu {
  public static void main(String[] args) {
    // korzystamy z zaimportowanej klasy Powitanie
    // wywolujemy statyczna metoda tej klasy
    Powitanie.wypiszKomunikat();
  }
}

Na początku programu znajduje się instrukcja importu klasy Powitanie z pakietu przykladowypakiet.pl. Dzięki tej instrukcji uzyskujemy dostęp do klasy Powitanie, którą używamy do wypisania na ekran powitania, korzystając z jej metody statycznej wypiszKomunikat.

Zamiast podawać nazwę klasy, moglibyśmy skorzystać z * (gwiazdki), aby zaimportować wszystkie klasy z danego pakietu (ale nie z podpakietów!):

Nazwa pliku: PrzykladImportuZGwiazdka.java
import przykladowypakiet.pl.*;

public class PrzykladImportuZGwiazdka {
  public static void main(String[] args) {
    // korzystamy z zaimportowanej klasy Powitanie
    // wywolujemy statyczna metoda tej klasy
    Powitanie.wypiszKomunikat();
  }
}

Tym razem, zamiast podać nazwę klasy, którą chcemy zaimportować, podajemy gwiazdkę – zaimportowane zostaną wszystkie klasy z pakietu przykladowypakiet.pl, w tym – klasa Powitanie, która, w tym przypadku, jest jedyną klasą w tym pakiecie.

Należy tutaj zwrócić uwagę, że importowane są jedynie klasy z danego pakietu – jeżeli pakiet zawiera kolejne podkatalogi z klasami, to nie zostaną one zaimportowane.

Spójrzmy na poniższy, błędny przykład – importujemy wszystkie klasy z nadrzędnego pakietu przykladowypakiet – ten pakiet nie zawiera żadnych klas, a użycie gwiazdki nie spowoduje, że zaimportowane zostaną obie klasy Powitanie z pakietów podrzędnych pl oraz eng:

// uzycie * nie powoduje, ze zaimportowane zostana klasy
// z pakietow podrzednych - wystapi blad kompilacji,
// poniewaz kompilator nie wie, czym jest 'Powitanie'
import przykladowypakiet.*;

public class BlednyImport {
  public static void main(String[] args) {
    Powitanie.wypiszKomunikat();
  }
}

Próba kompilacji tej klasy zakończy się błędem.

Wiemy już, jak importować klasy z pakietów. Załóżmy teraz, że potrzebujemy w naszym programie skorzystać z obu klas Powitanie – tej z pakietu przykladowypakiet.pl i tej z pakietu przykladowypakiet.eng. Spróbujmy zaimportować je obie:

Nazwa pliku: ImportDwochKlasOTejSamejNazwie.java
import przykladowypakiet.pl.Powitanie;
import przykladowypakiet.eng.Powitanie;

public class ImportDwochKlasOTejSamejNazwie {
  public static void main(String[] args) {
    Powitanie.wypiszKomunikat();
  }
}

Niestety, próba kompilacji tego programu kończy się dwoma błędami:

ImportDwochKlasOTejSamejNazwie.java:2: error: a type with the same simple name is already defined by the single-type-import of Powitanie import przykladowypakiet.eng.Powitanie; ^ ImportDwochKlasOTejSamejNazwie.java:6: error: reference to Powitanie is ambiguous Powitanie.wypiszKomunikat(); ^ both class przykladowypakiet.pl.Powitanie in przykladowypakiet.pl and class pr zykladowypakiet.eng.Powitanie in przykladowypakiet.eng match 2 errors

Kompilator protestuje, ponieważ próbujemy zaimportować dwie klasy o tej samej nazwie i kompilator nie wie, czym jest Powitanie – czy chodzi nam o klasę z pakietu przykladowypakiet.pl, czy przykladowypakiet.eng? Jest jednak sposób, aby skorzystać z obu klas – zaimportujemy jedną z nich, a do drugiej odniesiemy się korzystając z jej pełnej nazwy, to znaczy dodamy przed nazwą klasy nazwę pakietu, w którym jest zawarta:

Nazwa pliku: KorzystanieZDwochKlasOTejSamejNazwie.java
import przykladowypakiet.pl.Powitanie;

public class KorzystanieZDwochKlasOTejSamejNazwie {
  public static void main(String[] args) {
    // klasa z pakietu przykladowypakiet.pl.Powitanie
    Powitanie.wypiszKomunikat();

    // inna klasa o nazwie Powitanie zdefiniowana w innym pakiecie
    // korzystamy z pelnej nazwy tej klasy, aby sie do niej odniesc
    przykladowypakiet.eng.Powitanie.wypiszKomunikat();
  }
}

Tym razem, program kompiluje się błędów, a na ekranie zobaczymy:

Witaj Swiecie! Hello World!

Jedną z klas zaimportowaliśmy do naszego programu, dzięki czemu możemy korzystać z jej skróconej nazwy – jest to klasa Powitanie z pakietu przykladowypakiet.pl. Do drugiej klasy odnosimy się podając jej pełną nazwę, tzn. nazwę poprzedzoną pakietem, w którym się znajduje. Dzięki temu, kompilator nie ma problemu z rozróżnieniem, o które klasy nam chodzi. Dodatkowo, pomimo, iż druga klasa Powitanie nie jest zaimportowana za pomocą instrukcji import, to kompilator i tak wie, gdzie tej klasy szukać – należy ona do pakietu przykladowypakiet.eng, więc będzie jej szukał w katalogu przykladowypakiet/eng.

Nasze programy mogą zawierać dowolną liczbę instrukcji import – możemy zaimportować tyle klas, ile nam potrzeba.

W programach pisanych w języku Java bardzo często korzysta się z klas z innych pakietów. Są różne konwencje dotyczące tego, czy powinniśmy korzystać z * (gwiazdki), czy używać konkretnych nazw klas, które chcemy zaimportować. Ja spotkałem się głównie z konwencją, w której zawsze podajemy nazwę klasy do zaimportowania, zamiast korzystać z gwiazdek. Dla przykładu, jeżeli chcemy skorzystać z klasy Scanner, to zamiast napisać:

import java.util.*;

powinniśmy użyć instrukcji:

import java.util.Scanner;

Importy statyczne – static import

Czasami w naszych programach potrzebujemy skorzystać z pewnej metody statycznej bądź stałej z innej klasy wiele razy – w takim przypadku, musimy za każdym razem przed nazwą metody (bądź stałej) umieścić nazwę klasy, z której pochodzi – spójrzmy na przykład z rozdziału o metodach i polach statycznych, w którym korzystaliśmy z klasy Math:

Nazwa pliku: KorzystanieZMath.java
public class KorzystanieZMath {
  public static void main(String[] args) {
    System.out.println("Liczba PI wynosi: " + Math.PI);
    System.out.println("Liczba E wynosi: " + Math.E);

    System.out.println(
        "Sinus 90 stopni wynosi: " + Math.sin(Math.toRadians(90))
    );
    System.out.println(
        "Zaokraglona liczba PI: " + Math.round(Math.PI)
    );
    System.out.println(
        "Pierwiastek liczby 100 to " + Math.sqrt(100)
    );
  }
}

Przed stałymi statycznymi PI oraz E, a także przed metodami sin, round, toRadians, oraz sqrt, musimy umieścić nazwę klasy, z której pochodzą, czyli Math.

Niekiedy korzystamy z pewnej metody bądź stałej z innej klasy tak często, że pisanie nazwy klasy za każdym razem staje się narzutem. Możemy w takim przypadku skorzystać ze statycznego importu.

Statyczny import to zaimportowanie metody bądź pola do naszej klasy w taki sposób, jakby ta metoda bądź stała była częścią naszej klasy, a nie pochodziła z innej klasy – dzięki temu, nie musimy pisać nazwy klasy za każdym razem, gdy z tej metody bądź stałej będziemy korzystać – spójrzmy na przykład:

Nazwa pliku: StatycznyImportMath.java
import static java.lang.Math.PI;
import static java.lang.Math.sqrt;

public class StatycznyImportMath {
  public static void main(String[] args) {
    System.out.println("Liczba PI wynosi: " + PI);
    System.out.println("Liczba E wynosi: " + Math.E);

    System.out.println(
        "Sinus 90 stopni wynosi: " + Math.sin(Math.toRadians(90))
    );
    System.out.println(
        "Zaokraglona liczba PI: " + Math.round(PI)
    );
    System.out.println(
        "Pierwiastek liczby 100 to " + sqrt(100)
    );
  }
}

W tym przykładzie korzystamy ze statycznego importu do zaimportowania do klasy StatycznyImportMath statyczną stałą PI z klasy Math, oraz statyczną metodę sqrt z tej samej klasy. Dzięki temu, w zaznaczonych liniach nie musimy umieszczać nazwy klasy Math przed stałą PI oraz metodą sqrt.

Statyczny import różni się tym od zwykłego importowania klas, że po słowie kluczowym import dodajemy słowo kluczowe static:

import static java.lang.Math.PI;
import static java.lang.Math.sqrt;

Moglibyśmy użyć * (gwiazdki), aby zaimportować wszystkie pola i metody publiczne z klasy Math – wtedy nazwę klasy Math moglibyśmy pominąć przed stałą E oraz metodami sin, toRadians, oraz round:

Nazwa pliku: StatycznyImportMathZGwiazdka.java
import static java.lang.Math.*;

public class StatycznyImportMathZGwiazdka {
  public static void main(String[] args) {
    System.out.println("Liczba PI wynosi: " + PI);
    System.out.println("Liczba E wynosi: " + E);

    System.out.println(
        "Sinus 90 stopni wynosi: " + sin(toRadians(90))
    );
    System.out.println(
        "Zaokraglona liczba PI: " + round(PI)
    );
    System.out.println(
        "Pierwiastek liczby 100 to " + sqrt(100)
    );
  }
}

Dzięki użyciu gwiazdki, mogliśmy usunąć nazwę klasy Math sprzed jej pól i metod, z których korzystamy w powyższym przykładzie.

Statyczny import przydaje się od czasu do czasu, gdy wielokrotnie korzystamy z pewnych metod bądź pól innych klas, w szczególności z Biblioteki Standardowej Java. Nie powinniśmy jednak nadużywać tej funkcjonalności, ponieważ może to powodować, że nasz kod będzie trudniejszy w zrozumieniu, lub wystąpi kolizja nazwy metody bądź pola z naszej klasy i klasy, z której statycznie importujemy metodę bądź pole.

Nie powinniśmy także korzystać z opcji z * (gwiazdka), ponieważ powoduje to "hurtowy" import wszystkich publicznych pól i metod – najlepiej, tak jak w przypadku zwykłych importów, zawsze podawać nazwę obiektu, który chcemy zaimportować, zamiast korzystać z gwiazdki.

Lokalizacja klas – classpath

Czasem pisząc nasze programy będziemy chcieli korzystać z różnych klas, które niekoniecznie będą znajdować się w tym samym katalogu nadrzędnym, jak nasz program.

Załóżmy, że na innym dysku naszego komputera, np. na dysku D, znajduje się klasa WykorzystanieClasspath, w której chcielibyśmy skorzystać z klas Powitanie, zdefiniowanych wcześniej w tym rozdziale. Lokalizacja tych klas przedstawiona jest poniżej:

C:
|
`--programowanie
   |
   | PrzykladImportu.java
   |
   `--przykladowypakiet
      |---eng
      |       Powitanie.java
      |
      `---pl
              Powitanie.java

D:
|
`--programy
   |
   |
   `--WykorzystanieClasspath.java

Jeżeli w klasie WykorzystanieClasspath spróbowalibyśmy użyć którejś z klas Powitanie, to kompilator zaprotestuje – nie będzie on w stanie odnaleźć na dysku D klasy, której chcemy użyć:

Nazwa pliku: WykorzystanieClasspath.java
import przykladowypakiet.pl.Powitanie;

public class WykorzystanieClasspath {
  public static void main(String[] args) {
    Powitanie.wypiszKomunikat();
  }
}

Kompilator poinformuje nas o błędzie, że pakiet, w którym klasa Powitanie miałaby się znajdować, nie istnieje:

WykorzystanieClasspath.java:1: error: package przykladowypakiet.pl does not exist import przykladowypakiet.pl.Powitanie; ^ WykorzystanieClasspath.java:5: error: cannot find symbol Powitanie.wypiszKomunikat(); ^ symbol: variable Powitanie location: class WykorzystanieClasspath 2 errors

Moglibyśmy utworzyć w lokalizacji D:\programy katalog przykladowypakiet, w którym z kolei utworzylibyśmy katalog pl, do którego skopiowalibyśmy klasę Powitanie, ale mamy lepsze wyjście – możemy wskazać kompilatorowi języka Java (oraz Maszynie Wirtualnej Java) lokalizację, gdzie powinien szukać klas, których nasz program potrzebuje. Jest to tzw. classpath, czyli lista katalogów na dysku komputera, gdzie mogą znajdować się klasy napisane w języka Java.

Aby odpowiednio ustawić classpath, przekazujemy kompilatorowi języka Java parametr o nazwie -classpath, po którym następuje lista lokalizacji na dysku, gdzie ma szukać klas – możemy podać kilka katalogów, rozdzielonych przecinkami. Spójrzmy na ponowną próbę skompilowania klasy WykorzystanieClasspath z odpowiednio ustawioną wartością classpath:

D:\programy> javac -classpath C:\programowanie WykorzystanieClasspath.java

Tym razem klasa kompiluje się bez błędów, ponieważ kompilator znalazł w katalogu C:\programowanie klasę przykladowypakiet.pl.Powitanie, której chcemy użyć w klasie WykorzystanieClasspath.

W wyniku kompilacji, kompilator utworzył plik WykorzystanieClasspath.class. Spróbujmy teraz uruchomić tą klasę w Maszynie Wirtualnej Java:

D:\programy> java WykorzystanieClasspath Exception in thread "main" java.lang.NoClassDefFoundError: przykladowypakiet/pl/Powitanie at WykorzystanieClasspath.main(WykorzystanieClasspath.java:5) Caused by: java.lang.ClassNotFoundException: przykladowypakiet.pl.Powitanie

Uruchomienie klasy się nie powiodło – tym razem to Maszyna Wirtualna Java nie wie, gdzie szukać skompilowanej klasy przykladowypakiet.pl.Powitanie. Ponownie skorzystamy z argumentu -classpath:

D:\programy> java -classpath C:\programowanie WykorzystanieClasspath Error: Could not find or load main class WykorzystanieClasspath Caused by: java.lang.ClassNotFoundException: WykorzystanieClasspath

Ku potencjalnemu zaskoczeniu, tym razem Maszyna Wirtualna Java nie może znaleźć klasy, którą chcemy uruchomić, czyli WykorzystanieClasspath – dlaczego tak się stało?

Ustawiając wartość classpath na C:\programowanie, powiedzieliśmy Maszynie Wirtualnej Java: "Wszystkie klasy, jakie będą potrzebne do uruchomienia klasy WykorzystanieClasspath, jak i klasa WykorzystanieClasspath, znajdują się w lokalizacji wskazywanej przez wartość argumentu classpath".

Nie do końca o to nam chodziło – chcieliśmy, by uruchomiona została klasa, która znajduje się w katalogu D:\programy o nazwie WykorzystanieClasspath, a wszelkie potrzebne jej klasy można było znaleźć w katalogu C:\programowanie. W naszym argumencie classpath zabrakło jednej lokalizacji – aktualnego katalogu, w którym wywołujemy Maszynę Wirtualną Java – jeżeli umieścimy tam tą lokalizację, to Maszyna Wirtualna Java będzie w stanie znaleźć klasę WykorzystanieClasspath.

W lini komend, aktualny katalog, w którym się znajdujemy, oznaczany jest przez jedną kropkę. Możemy więc do argumentu -classpath dodać po średniku kropkę, którą będzie oznaczała "katalog, w którym aktualnie znajdujemy się w linii komend" – w tym przypadku, kropka będzie oznaczała katalog D:\programowanie. Spróbujmy:

D:\programy> java -classpath C:\programowanie;. WykorzystanieClasspath Witaj Swiecie!

Tym razem udało nam się uruchomić naszą klasę – Maszyna Wirtualna Java otrzymała wartość classpath składającą się z dwóch lokalizacji:

  • C:\programowanie – tutaj znajduje się klasa przykladowypakiet.pl.Powitanie, używana przez klasę WykorzystanieClasspath,
  • . (kropka) oddzieloną od lokalizacji C:\programowanie znakiem średnika – ta kropka oznacza aktualny katalog, w którym znajdujemy się w linii komend, więc będzie to D:\programy.

W pierwszej lokalizacji z argumentu classpath Maszyna Wirtualna Java znalazła klasę przykladowypakiet.pl.Powitanie, a w drugiej – klasę, którą chcieliśmy uruchomić, czyli WykorzystanieClasspath.

Kiedy nie trzeba stosować instrukcji import

Wielokrotnie w poprzednich rozdziałach korzystaliśmy z jednej z utworzonych przez nas klas w innej, także utworzonej przez nas klasie, jednak wtedy nie musieliśmy korzystać z instrukcji importu.

Klasy musimy importować tylko w przypadku, gdy są one zawarte w innych pakietach – jeżeli klasy są w tym samym katalogu (lub w tym samym pakiecie), to nie musimy korzystać z instrukcji import – dlatego w poprzednich rozdziałach nie korzystaliśmy z instrukcji import do importowania klas, które sami pisaliśmy – zawsze przetrzymywaliśmy je w tym samym katalogu.

Z drugiej jednak strony, w rozdziale o metodach statycznych korzystaliśmy z klasy Math, ale nie musieliśmy korzystać z instrukcji import, chociaż ta klasa nie znajdowała się w tym samym katalogu, co nasza klasa. Podobnie z używaną od dawna klasą String – nigdy jej nie importowaliśmy. Dlaczego w tych przypadkach błąd braku importu nie występował?

Biblioteka Standardowa Java zawiera pakiet o nazwie java.lang, w którym zdefiniowane są m. in. klasy String oraz Math, oraz wiele innych klas, które są tak często wykorzystywane przez programistów, że kompilator języka Java dodaje import tego pakietu do każdego programu napisanego w języku Java automatycznie, dla naszej wygody, dzięki czemu możemy korzystać m. in. z klas String oraz Math bez potrzeby ich importowania własnoręcznie w naszych programach.

Dostęp domyślny (default access) i klasy niepubliczne

Dostęp domyślny

W jednym z poprzednich podrozdziałów poznaliśmy modyfikatory dostępu private oraz public, oraz dowidzieliśmy się, że istnieją łącznie cztery różne modyfikatory dostępu w języku Java:

  • private
  • dostęp domyślny
  • protected
  • public

Dostęp domyślny (default access) nazywany jest po angielsku także package-private. Ten rodzaj dostępu do pól i metod jest o tyle wyjątkowy, że w jego przypadku po prostu nie stosujemy żadnego modyfikatora dostępu – jeżeli przed definicją metody bądź pola w klasie nie użyjemy ani modyfikatora private, ani public, ani protected, to takie pole (bądź metoda) będzie miało dostęp domyślny.

Dostęp domyślny charakteryzuje się tym, że jest prawie tak restrykcyjny, jak modyfikator private, ale z jednym wyjątkiem. W przypadku modyfikatora private, żadna inna klasa nie może korzystać z pola bądź metody prywatnej. Dostęp domyślny, natomiast, ogranicza dostęp do pól i metod klasy, pozwalając jednak na korzystanie z nich innym klasom, które znajdują się w tym samym pakiecie, tzn. klasom, które mają taki sam pakiet zdefiniowany za pomocą słowa kluczowego package.

Poniżej zaprezentowana jest struktura katalogów, w których umieszczonych zostało kilka klas, które posłużą do zobrazowania, czym dostęp domyślny różni się od modyfikatorów private oraz public:

programowanie
|
|   ObcaKlasa.java
|
`---przykladdomyslny
        Komunikaty.java
        WtajemniczonaKlasa.java

W katalogu programowanie znajduje się klasa ObcaKlasa, natomiast klasy Komunikaty oraz WtajemniczonaKlasa są klasami pakietowymi – zawarte są w pakiecie o nazwie przykladdomyslny. Spójrzmy teraz treść klasy Komunikaty:

Nazwa pliku: przykladdomyslny/Komunikaty.java
package przykladdomyslny;

public class Komunikaty {
  private static void tajnyKomunikat() {
    System.out.println("Cii! Tajny komunikat!");
  }

  static void komunikatDlaWtajemniczonych() {
    System.out.println("Komunikat dla wtajemniczonych!");
  }

  public static void komunikatOgolny() {
    System.out.println("Bedzie padac.");
  }
}

Klasa ta zawiera trzy metody statyczne:

  • metoda tajnyKomunikat jest prywatna, ponieważ zdefiniowana jest z modyfikatorem private,
  • metoda komunikatOgolny jest publiczna – użyty został modyfikator public,
  • nowością jest sposób definicji metody komunikatDlaWtajemniczonychjej sygnatura nie ma żadnego modyfikatora dostępu, więc jej dostęp to dostęp domyślny.

Z prywatnej metody tajnyKomunikat możemy korzystać jedynie we wnętrzu klasy Komunikaty. Z publicznej metody komunikatOgolny mogą korzystać wszystkie klasy, niezależnie od tego, w jakim pakiecie się znajdują. Najciekawsza z tych trzech metod, metoda komunikatDlaWtajemniczonych, może zostać użyta w klasie WtajemniczonaKlasa, ponieważ obie klasy są w tym samym pakiecie. Z kolei klasa ObcaKlasa, będąca poza pakietem przykladdomyslny, nie ma prawa z tej metody korzystać.

Spójrzmy co się stanie, jeżeli spróbujemy użyć metody o domyślnym dostępie z klasy Komunikaty w klasie ObcaKlasa:

Nazwa pliku: ObcaKlasa.java
import przykladdomyslny.Komunikaty;

public class ObcaKlasa {
  public static void main(String[] args) {
    // blad! metoda komunikatDlaWtajemniczonych
    // jest niedostepna spoza pakietu przykladdomyslny!
    Komunikaty.komunikatDlaWtajemniczonych();
  }
}

Zauważmy, że musimy skorzystać z instrukcji importu, ponieważ klasa Komunikaty, z której chcemy skorzystać, znajduje się w pakiecie przykladdomyslny. Próba kompilacji tego programu kończy się następującym błędem:

ObcaKlasa.java:7: error: komunikatDlaWtajemniczonych() is not public in Komunikaty; cannot be accessed from outside package Komunikaty.komunikatDlaWtajemniczonych(); ^ 1 error

Kompilator informuje nas, że metoda komunikatDlaWtajemniczonych jest niedostępna spoza pakietu pakietdomyslny. Podobny błąd zobaczylibyśmy, gdybyśmy spróbowali użyć metody tajnyKomunikat, która jest prywatna w klasie Komunikaty. Jedyna metoda z klasy Komunikaty, z której możemy skorzystać w klasie ObcaKlasa, to publiczna metoda komunikatOgolny:

import przykladdomyslny.Komunikaty;

public class ObcaKlasa {
  public static void main(String[] args) {
    // blad! metoda komunikatDlaWtajemniczonych
    // jest niedostepna spoza pakietu przykladdomyslny!
    //Komunikaty.komunikatDlaWtajemniczonych();

    Komunikaty.komunikatOgolny();
  }
}

Tym razem program kompiluje się bez błędów – na ekranie zobaczymy komunikat:

Bedzie padac.

Przejdźmy teraz do klasy WtajemniczonaKlasa, która znajduje się w tym samym pakiecie, co klasa Komunikaty – spróbujmy skorzystać z metody o domyślnym dostępie, zdefiniowanej w klasie Komunikaty:

Nazwa pliku: przykladdomyslny/WtajemniczonaKlasa.java
public class WtajemniczonaKlasa {
  public static void main(String[] args) {
    Komunikaty.komunikatDlaWtajemniczonych();
    Komunikaty.komunikatOgolny();
  }
}

Zauważmy, że tym razem nie stosujemy instrukcji importu, ponieważ obie klasy: WtajemniczonaKlasa oraz Komunikaty, znajdują się w tym samym pakiecie. Próba kompilacji powyższej klasy kończy się bezbłędnie, a w wyniku jej uruchomienia, na ekranie zobaczymy:

Komunikat dla wtajemniczonych! Bedzie padac.

Tym razem kompilator nie protestował – klasa WtajemniczonaKlasa może korzystać z metody o domyślnym dostępie komunikatDlaWtajemniczonych, ponieważ klasa ta znajduje się w tym samym pakiecie, co klasa, w której ta metoda jest zdefiniowana.

Dostęp domyślny mogą mieć zarówno pola, jak i metody.
Dostęp domyślny dotyczy zarówno pól i metod statycznych, jak i niestatycznych.

Kiedy stosować dostęp domyślny?

Dostęp domyślny przydaje się, gdy projektujemy klasy, które będą wspólnie działały na rzecz wykonania pewnego zadania. Dzięki dostępowi domyślnemu, możemy pozwolić klasom z tego samego pakietu na dostęp do przydatnych metod i pól z innych klas z tego pakietu, nie dającym tym samym tej możliwości jakimkolwiek innym klasom, zdefiniowanym w innych pakietach. Tylko "wtajemniczone" klasy z tego samego pakietu będą mogły odnosić się do tych pól i metod, które dla świata zewnętrznego powinny pozostać prywatne i niedostępne.

Modyfikator dostępu protected

Ostatni modyfikator, protected, poznamy w rozdziale o dziedziczeniu. W tej chwili ważną informacją na temat tego modyfikatora jest to, że tak samo jak dostęp domyślny, pozwala on na dostęp do pól i metod klasom, które zdefiniowane są w tym samym pakiecie – jest to cecha wspólna modyfikatora protected oraz dostępu domyślnego (czyli tego, który występuje, gdy nie skorzystamy z żadnego modyfikatora dostępu).

Niepubliczne klasy

Do tej pory wszystkie klasy, jakie pisaliśmy, były publiczne – zawsze stosowaliśmy modyfikator dostępu public podczas ich definiowania, na przykład:

public class HelloWorld {
  public static void main(String[] args) {
    System.out.println("Witaj Swiecie!");
  }
}

Klasy nie muszą być jednak publiczne – możemy pominąć modyfikator public. W takim przypadku, gdy brak jest modyfikatora dostępu, klasa będzie miała domyślny dostęp – jego znaczenie będzie takie samo, jak w przypadku pól i metod, które mają domyślny dostęp, jak zobaczyliśmy wcześniej w tym podrozdziale.

Klasy z dostępem domyślnym mogą być wykorzystywane jedynie przez klasy znajdujące się w tym samym pakiecie – wszystkie klasy znajdujące się w innych pakietach (bądź w pakiecie domyślnym, gdy nie podamy żadnego pakietu), nie będą mogły z takiej klasy korzystać.

Kiedy stosować klasy niepubliczne? Jeżeli tworzymy zestaw klas, które mają wspólnie rozwiązywać pewien problem, to dobrze jest zdefiniować wszystkie klasy, które nie są potrzebne "światu zewnętrznemu" jako klasy niepubliczne – tylko te klasy, których użytkownicy pakietu będą używać bezpośrednio powinny być publiczne. Dzięki temu ukrywamy przed użytkownikiem docelowym implementację rozwiązania, na które składa się wiele klas. Takie podejście powoduje także, że osoby, które będą analizowały klasy z naszego pakietu, będą musiały zajrzeć tylko do klas publicznych, aby dowiedzieć się, jak korzystać z funkcjonalności, które wszystkie klasy w pakiecie (kolektywnie) udostępniają, ponieważ tylko z tych publicznych klas użytkownicy będą mogli korzystać.

Podsumowanie

Pakiety klas

  • Aby zmniejszyć prawdopodobieństwo kolizji nazw klas, możemy umieszczać je w pakietach.
  • Do oznaczenia, w jakim pakiecie klasa się znajduje, używamy słowa kluczowego package, po którym następuje nazwa pakietu.
  • Nazwa pakietu staje się integralną częścią nazwy klasy – zdefiniowana poniżej klasa Powitanie znajduje się w pakiecie przykladowypakiet.pl. Pełna nazwa tej klasy to przykladowypakiet.pl.Powitanie:
    package przykladowypakiet.pl;
    
    public class Powitanie {
      public static void main(String[] args) {
        wypiszKomunikat();
      }
    
      public static void wypiszKomunikat() {
        System.out.println("Witaj Swiecie!");
      }
    }
    
  • Przynależność klasy do pakietu musi być na początku pliku – jedyne, co może poprzedzać użycie słowa kluczowego package, to komentarze – w przeciwnym razie, klasa w ogóle się nie skompiluje.
  • Najlepiej, by nazwy pakietów zawierały tylko litery i liczby. Dodatkowo, człony nazwy pakietu rozdzielane są kropkami oraz nie stosujemy camelCase – używamy zawsze małych liter.
  • Nazwa pakietu, w którym umieszczamy klasy, powinna odpowiadać strukturze katalogów na dysku, w których ta klasa się znajduje. W przeciwnym razie Maszyna Wirtualna Java nie będzie w stanie znaleźć klasy o danej nazwie. Dla przykładu, powyższa klasa Powitanie powinna być umieszczona w katalogu pl, który z kolei powinien być zawarty w katalogu o nazwie przykladowypakiet:
    programowanie
    |
    `--przykladowypakiet
       |
       `---pl
               Powitanie.java
    
  • Jeżeli chcemy uruchomić klasę pakietową, to musimy znajdować się w katalogu, z którego Maszyna Wirtualna Java będzie mogła odnieść się do klasy, którą ma uruchomić, zgodnie z opisanym powyżej sposobem lokalizowania klas.
  • Jeżeli więc chcielibyśmy uruchomić klasę przykladowypakiet.pl.Powitanie, to w linii poleceń powinniśmy znajdować się w katalogu, w którym znajduje się pierwszy katalog z pakietu klasy – w tym przypadku jest to katalog przykladowypakiet – ten katalog zawarty jest w katalog programowanie – i to jest właśnie miejsce, z którego powinniśmy uruchamiać klasę przykladowypakiet.pl.Powitanie:
    C:\programowanie> javac przykladowypakiet/pl/Powitanie C:\programowanie> java przykladowypakiet.pl.Powitanie Witaj Swiecie!
  • Pełna nazwa klasy to pakiet i nazwa klasy, rozdzielone kropkami, na przykład przykladowypakiet.pl.Powitanie – takiej nazwy klasy oczekuje Maszyna Wirtualna Java jako argumentu.
  • Ścieżka do pliku z klasą do skompilowania, jakiej oczekuje kompilator języka Java (program javac), to nazwa klasy poprzedzona nazwami katalogów, w których klasa ta się znajduje (które mogą być częścią pakietu tej klasy) – w tym przypadku, nazwy katalogów oddzielamy znakiem slash / od nazwy klasy, np.:
    javac przykladowypakiet/eng/Powitanie.java
  • Klasy nie muszą być zawarte w pakietach – do tej pory wszystkie klasy, które pisaliśmy, nie zawierały słowa kluczowego package. Takie klasy są wtedy po prostu w ogólnym, "domyślnym" pakiecie.
  • Istnieje konwencja nazewnicza pakietów klas, wedle której pakiety odzwierciedlają odwróconą domenę Internetową firm bądź osób, które są autorami klas.
  • Dla przykładu, pakiety klas napisane dla tego kursu mógłby być umieszczone w pakiecie, którego nazwa zaczynałaby się od com.kursjava. Kolejnym członem pakietu mogłyby być numery rozdziałów, z których pochodzą przykłady do tego kursu, np. com.kursjava.rozdzial9.
  • Pakiety klas zdefiniowanych przez twórców języka Java znajdują się w pakietach zaczynających się od java oraz javax.

Importowanie klas

  • Aby skorzystać z klas, które zdefiniowane są w innych pakietach, musimy je zaimportować do naszych programów.
  • Klasy musimy importować tylko w przypadku, gdy są one zawarte w innych pakietach.
  • Jeżeli spróbujemy skorzystać z klasy, której nie zaimportujemy, oraz która znajduje się w innym pakiecie, to kompilacja klasy zakończy się błędem:
    public class WczytywanieDanychBezImport {
      public static int getInt() {
        // blad! braku import klasy Scanner
        return new Scanner(System.in).nextInt();
      }
    }
    
    WczytywanieDanychBezImport.java:4: error: cannot find symbol return new Scanner(System.in).nextInt(); ^ symbol: class Scanner location: class WczytywanieDanychBezImport 1 error
  • Aby zaimportować klasę, korzystamy ze słowa kluczowego import, po którym następuje nazwa klasy, którą chcemy zaimportować:
    import przykladowypakiet.pl.Powitanie;
    
    public class PrzykladImportu {
      public static void main(String[] args) {
        // korzystamy z zaimportowanej klasy Powitanie
        // wywolujemy statyczna metoda tej klasy
        Powitanie.wypiszKomunikat();
      }
    }
    
  • Możemy zaimportować wszystkie klasy w pakiecie korzystając z * (gwiazdka), która oznacza "wszystkie klasy", zamiast podawać nazwę klasy z pakietu. Uwaga: gwiazdka * powoduje zaimportowanie wszystkich klas z konkretnego pakietu, ale nie z podpakietów!
    import przykladowypakiet.pl.*;
    
    public class PrzykladImportuZGwiazdka {
      public static void main(String[] args) {
        // korzystamy z zaimportowanej klasy Powitanie
        // wywolujemy statyczna metoda tej klasy
        Powitanie.wypiszKomunikat();
      }
    }
    
  • Instrukcje import muszą występować po (ewentualnej) instrukcji package, a przed definicją klasy – w przeciwnym razie kod klasy się nie skompiluje.
  • Nie możemy importować dwóch klas o takiej samej nazwie – spowodowałoby to błąd kompilacji.
  • Jest jednak sposób, aby skorzystać z klas o takich samych nazwach – importujemy jedną z nich, a do drugiej odnosimy się korzystając z jej pełnej nazwy, czyli wraz z nazwą pakietu:
import przykladowypakiet.pl.Powitanie;

public class KorzystanieZDwochKlasOTejSamejNazwie {
  public static void main(String[] args) {
    // klasa z pakietu przykladowypakiet.pl.Powitanie
    Powitanie.wypiszKomunikat();

    // inna klasa o nazwie Powitanie zdefiniowana w innym pakiecie
    // korzystamy z pelnej nazwy tej klasy, aby sie do niej odniesc
    przykladowypakiet.eng.Powitanie.wypiszKomunikat();
  }
}
  • Nasze programy mogą zawierać dowolną liczbę instrukcji import – możemy zaimportować tyle klas, ile nam potrzeba.
  • Jedną z konwencji odnośnie importowania klas jest by nie korzystać z gwiazdki, a zamiast tego używać konkretnych nazw klas, które chcemy zaimportować.
  • W Javie istnieje także inny rodzaj importu – import statyczny.
  • Statyczny import to zaimportowanie metody bądź pola do naszej klasy w taki sposób, jakby ta metoda bądź stała była częścią naszej klasy, a nie pochodziła z innej klasy – dzięki temu, nie musimy pisać nazwy klasy za każdym razem, gdy z tej metody bądź stałej będziemy korzystać:
    import static java.lang.Math.PI;
    import static java.lang.Math.sqrt;
    
    public class StatycznyImportMath {
      public static void main(String[] args) {
        System.out.println("Liczba PI wynosi: " + PI);
        System.out.println("Liczba E wynosi: " + Math.E);
    
        System.out.println(
            "Sinus 90 stopni wynosi: " + Math.sin(Math.toRadians(90))
        );
        System.out.println(
            "Zaokraglona liczba PI: " + Math.round(PI)
        );
        System.out.println(
            "Pierwiastek liczby 100 to " + sqrt(100)
        );
      }
    }
    
    W tym przykładzie korzystamy ze statycznego importu do zaimportowania do klasy StatycznyImportMath statyczną stałą PI z klasy Math, oraz statyczną metodę sqrt z tej samej klasy. Dzięki temu, w zaznaczonych liniach nie musimy umieszczać nazwy klasy Math przed stałą PI oraz metodą sqrt.
  • Statyczny import różni się tym od zwykłego importowania klas, że po słowie kluczowym import dodajemy słowo kluczowe static:
    import static java.lang.Math.PI;
    import static java.lang.Math.sqrt;
    
  • Korzystając ze statycznego importu, także możemy użyć gwiazdki * zamiast nazwy pola bądź metody, aby zaimportować statycznie wszystkie publiczne pola i metody klasy.
  • Statyczny import przydaje się od czasu do czasu, gdy wielokrotnie korzystamy z pewnych metod bądź pól innych klas, w szczególności z Biblioteki Standardowej Java. Nie powinniśmy jednak nadużywać tej funkcjonalności, ponieważ może to powodować, że nasz kod będzie trudniejszy w zrozumieniu, lub wystąpi kolizja nazwy metody bądź pola z naszej klasy i klasy, z której statycznie importujemy metodą bądź pole.
  • classpath to lista katalogów na dysku komputera, gdzie mogą znajdować się klasy napisane w języka Java, których kompilator języka Java może potrzebować do skompilowania naszego programu, oraz Maszyna Wirtualna Java do jego uruchomienia.
  • Aby odpowiednio ustawić classpath, przekazujemy kompilatorowi języka Java parametr o nazwie -classpath, po którym następuje lista lokalizacji na dysku, gdzie ma szukać klas – możemy podać kilka katalogów, rozdzielonych przecinkami:
    D:\programy> javac -classpath C:\programowanie WykorzystanieClasspath.java
  • W lini komend, aktualny katalog, w którym się znajdujemy, oznaczany jest przez jedną kropkę.
  • Jeżeli uruchamiamy Maszynę Wirtualną Java z parametrem classpath, a klasa, którą chcemy wykonać znajduje się w aktualnym katalogu, to do classpath powinniśmy dodać aktualny katalog, czyli kropkę, aby Maszyna Wirtualna Java była w stanie odnaleźć klasę, którą chcemy uruchomić:
    D:\programy> java -classpath C:\programowanie;. WykorzystanieClasspath Witaj Swiecie!
  • Biblioteka Standardowa Java zawiera pakiet o nazwie java.lang, w którym zdefiniowane są m. in. klasy String oraz Math, oraz wiele innych klas, które są tak często wykorzystywane przez programistów, że kompilator języka Java dodaje import tego pakietu do każdego programu napisanego w języku Java automatycznie, dla naszej wygody.

Dostęp domyślny

  • W Javie, poza modyfikatorami dostępu public oraz private, istnieją jeszcze modyfikatory: domyślny oraz protected.
  • Dostęp domyślny (default access) nazywany jest po angielsku także package-private. W przypadku tego rodzaju dostępu nie stosujemy żadnego modyfikatora dostępu – jeżeli przed definicją metody bądź pola w klasie nie użyjemy ani modyfikatora private, ani public, ani protected, to takie pole (bądź metoda) będzie miało dostęp domyślny.
  • Dostęp domyślny jest prawie tak restrykcyjny, jak modyfikator private, z jednym wyjątkiem. W przypadku modyfikatora private, żadna inna klasa nie może korzystać z pola bądź metody prywatnej. Dostęp domyślny, natomiast, ogranicza dostęp do pól i metod klasy, pozwalając jednak na korzystanie z nich innym klasom, które znajdują się w tym samym pakiecie, tzn. klasom, które mają taki sam pakiet zdefiniowany za pomocą słowa kluczowego package.
  • Przykład dostępu domyślnego – struktura pakietów klas:
    programowanie
    |
    |   ObcaKlasa.java
    |
    `---przykladdomyslny
            Komunikaty.java
            WtajemniczonaKlasa.java
    
    treść klasy Komunikaty:
    package przykladdomyslny;
    
    public class Komunikaty {
      private static void tajnyKomunikat() {
        System.out.println("Cii! Tajny komunikat!");
      }
    
      static void komunikatDlaWtajemniczonych() {
        System.out.println("Komunikat dla wtajemniczonych!");
      }
    
      public static void komunikatOgolny() {
        System.out.println("Bedzie padac.");
      }
    }
    
  • Klasa Komunikaty zawiera metodą komunikatDlaWtajemniczonych, która ma dostęp domyślny, ponieważ nie ma zdefiniowanego żadnego modyfikatora dostępu.
  • Z prywatnej metody tajnyKomunikat możemy korzystać jedynie we wnętrzu klasy Komunikaty.
  • Z metody komunikatOgolny mogą korzystać wszystkie klasy, niezależnie od tego, w jakim pakiecie się znajdują.
  • Metoda o dostępie domyślnym, komunikatDlaWtajemniczonych, może zostać użyta tylko w klasie WtajemniczonaKlasa, ponieważ obie klasy są w tym samym pakiecie. Z kolei klasa ObcaKlasa, będą poza pakietem przykladdomyslny, nie ma prawa z tej metody korzystać.
  • Dostęp domyślny przydaje się, gdy projektujemy klasy, które będą wspólnie działały na rzecz wykonania pewnego zadania. Dzięki dostępowi domyślnemu, możemy pozwolić klasom z tego samego pakietu na dostęp do przydatnych metod i pól z innych klas z tego pakietu, nie dającym tym samym tej możliwości jakimkolwiek innym klasom, zdefiniowanym w innych pakietach. Tylko "wtajemniczone" klasy z tego samego pakietu będą mogły odnosić się do tych pól i metod, które dla świata zewnętrznego powinny pozostać prywatne i niedostępne.
  • Modyfikator protected tak samo, jak dostęp domyślny, pozwala na dostęp do pól i metod klasom, które zdefiniowane są w tym samym pakiecie – jest to cecha wspólna modyfikatora protected oraz dostępu domyślnego. Inne cechy modyfikatora protected poznamy w rozdziale o dziedziczeniu.
  • Klasy nie muszą być publiczne – możemy pominąć modyfikator public. W takim przypadku, gdy brak jest modyfikatora dostępu, klasa będzie miała domyślny dostęp – jego znaczenie będzie takie samo, jak w przypadku pól i metod, które mają domyślny dostęp.
  • Klasy z dostępem domyślnym mogą być wykorzystywane jedynie przez klasy znajdujące się w tym samym pakiecie – wszystkie klasy znajdujące się w innych pakietach (bądź w pakiecie domyślnym, gdy nie podamy żadnego pakietu), nie będą mogły z takiej klasy korzystać.

Pytania

  1. Jaka jest pełna nazwa poniższej klasy?
    package com.kursjava;
    
    public class TestowaKlasa {
      public static void main(String[] args) {
        wypiszKomunikat();
      }
    
      public static void wypiszKomunikat() {
        System.out.println("Witam.");
      }
    }
    
  2. Jaką komendą należy skompilować, a jaką uruchomić, klasę z powyższego zadania?
  3. Mając poniższą strukturę katalogów:
    programy
    |
    `--com
       `---kursjava
               TestowaKlasa.java
    
    Czy poniższa próba uruchomienia klasy TestowaKlasa się powiedzie?
    C:\programy\pewien\pakiet> java com.kursjava.TestowaKlasa
  4. Czy poniższa próba kompilacji klasy TestowaKlasa powiedzie się?
    javac com.kursjava.TestowaKlasa.java
  5. Czy poniższa klasa skompiluje się bez błędów?
    import com.*;
    
    public class WykorzystanieTestowejKlasy {
      public static void main(String[] args) {
        TestowaKlasa.wypiszKomunikat();
      }
    }
    
  6. Czy poniższa klasa skompiluje się i wykona się bez błędów?
    public class WykorzystanieTestowejKlasy {
      public static void main(String[] args) {
        com.kursjava.TestowaKlasa.wypiszKomunikat();
      }
    }
    
  7. Czy poniższa klasa skompiluje się bez błędów?
    import java.util.Scanner;
    
    package pakiet;
    
    public class PytanieImport {
      public static int getInt() {
        return new Scanner(System.in).nextInt();
      }
    }
    
  8. Czy poniższy kod skompiluje się i wykona bez błędów?
    public class TestowaKlasa {
      public static void main(String[] args) {
        System.out.println("Pi wynosi: " + Math.PI);
      }
    }
    
  9. Co zobaczymy na ekranie w wyniku uruchomienia poniższego programu? Czy kod w ogóle się skompiluje?
    import static java.lang.Math.PI;
    
    public class TestowaKlasa {
      private static int PI = 3;
    
      public static void main(String[] args) {
        System.out.println("Pi wynosi: " + PI);
      }
    }
    
  10. Czym jest classpath? Jak ustawić wartość classpath?
  11. Dlaczego w naszych programach nie musimy importować typu String z Biblioteki Standardowej Java?
  12. Czym różni się dostęp domyślny od dostępów definiowanych za pomoc modyfikatorów private oraz public?
  13. Jak zdefiniować, że pole bądź metoda klasy ma mieć dostęp domyślny?
  14. Czy klasy mogą być niepubliczne? Jeżeli tak, to czym takie klasy różnią się od klas publicznych?
  15. Do których pól i metod klasy A oraz klasy B mają klasy C i D, oraz dlaczego?
programy
|
`--com
   |   D.java
   |
   `---kursjava
           A.java
           B.java
           C.java

Klasy A oraz B zdefiniowane są następująco:

package com.kursjava;

public class A {
  private static int x;
  public static int y;
  static int z;

  void pewnaMetoda() {
    System.out.println("Bedzie padac.");
  }
}
package com.kursjava;

class B {
  public static int n;
  static int m;

  public void innaMetoda() {
    System.out.println("Witam");
  }
}

Odpowiedzi do pytań

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Nie musisz podawać swojego imienia, e-mailu, ani strony www, aby opublikować komentarz. Komentarze muszą zostać zatwierdzone, aby były widoczne na stronie.