Java Class Design #7

Use package and import statements

Kiedy wraz ze wzrostem funkcjonalności projektowanego systemu, rośnie liczba potencjalnych klas do zaimplementowania, konieczne jest logiczne rozdzielenie tych klas na grupy reprezentujące jeden konkretny obszar aplikacji. Dodatkowo podział na takie grupy pozwala nam na korzystanie z wielu różnych klas o tych samych nazwach, w tym klas dostarczonych przez innych programistów. W Javie takie grupy nazywamy paczkami (ang. package) i to im poświęcony jest ostatni artykuł z tego rozdziału.


Na skróty:

Zasady definiowania paczki [Opis]
Import klas [Opis]
Import paczek [Opis]
Paczka domyślna [Opis]
Static import [Opis]

Repozytorium z przykładami [GitHub]


Czy to konieczne?

Choć możliwe jest stworzenie klasy bez jawnego przypisania jej paczki (o tym za chwilę), to nie jest to zalecana praktyka. Może sprawdzi się w malutkich, kilku klasowych aplikacyjkach do ćwiczeń, lub zaprezentowania jakiegoś konceptu, ale w rzeczywistych projektach komercyjnych klasy powinny być logicznie rozdzielone na paczki reprezentujące atomowe obszary. Przykładowo dostępna w JDK paczka java.sql zawiera klasy przeznaczone do pracy z bazami danych.

Zasady definiowania paczki

Klasę przypisujemy do paczki za pomocą słowa kluczowego package.

package com.javathehat.practise;
public class PractiseClass{
}

Powyższy kod tworzy klasę PractiseClass i przypisuje ją do paczki com.javathehat.practise.

Paczka to nic innego jak katalog, w którym zapisana jest klasa. Znak kropki oznacza podkatalog, to znaczy, że fizycznie klasa PractiseClass zapisana jest w katalogu: ‚root/com/javathehat/practise’ , gdzie root to główny katalog, w którym znajdują się pliki źródłowe projektu.

Żeby to lepiej zobrazować, dla takich czterech klas:

package com.myapp.database;
public class DataBaseConnection{
}
package com.myapp.database;
public class DataBaseUtils{
}
package com.myapp.view;
public class MainWindow{
}
package com.myapp;
public class Main{
}

Tak wyglądała by struktura plików źródłowych w projekcie:

root
 |-com
    |--myapp
         |---Main.java
         |---database
                |----DataBaseConnection.java
                |----DataBaseUtils.java
         |---view
                |----MainWindow.java

Zgodnie z konwencją nazewniczą Javy, nazwy paczki powinny być pisane w całości małą literą. Dodatkowo muszą spełniać standardowe zasady nazewnictwa w Javie.

Deklaracja paczki musi być w pierwszej linii kodu klasy.

Jedynym wyjątkiem od powyższego są komentarze, które mogą być umieszczone przed deklaracją paczki.

W jednym pliku .java może być co najwyżej jedna deklaracja paczki, co oznacza, że wszystkie klasy utworzone w tym pliku będą należały do tej jednej paczki.

Używanie klas z innych paczek

Robiliśmy to już wielokrotnie. Aby móc wykorzystać w kodzie klasę znajdującą się innej paczce, musimy ją zaimportować, korzystając ze słowa kluczowego import.

Przykładowo, chcąc utworzyć obiekt klasy HashMap, która znajduje się w JDK, w paczce java.util, importujemy ją w ten sposób:

package com.myapp;
import java.util.HashMap;

public class Main{
  HashMap map = new HashMap();
}

Nie jest to jednak jedyny sposób na skorzystanie z klasy innej paczki. Możliwe jest pominięcie importu, a następnie w kodzie, odwołanie się do klasy po jej pełnej nazwie, czyli razem z paczką:

package com.myapp;

public class Main{
  java.util.HashMap map = new java.util.HashMap();
}

Zapis jest raczej mniej czytelny i skorzystanie z pierwszej wersji importu słusznie wydaje się lepszą praktyką. Zdarzają się jednak sytuacje, w których musimy skorzystać z dwóch różnych klas, które nieszczęśliwe nazywają się tak samo. Gdybyśmy przykładowo mieli swoją własną wersję HashMapy i z jakiś powodów w jednej klasie musieli skorzystać zarówno ze swojej wersji jak i wersji dostarczonej w JDK, to taki zapis by się nam nie skompilował:

package com.myapp;
import java.util.HashMap;
import com.myapp.collection.HashMap;
public class Main{
  HashMap map1 = new HashMap();
  HashMap map2 = new HashMap();
}

Kompilator nie ma jak rozróżnić tych klas, kiedy deklarujemy zmienne odnosząc się jedynie do nazwy klasy, nawet nie pozwoli na import dwóch klas o tej samej nazwie. Jednak, jak najbardziej prawidłowy będzie taki zapis:

package com.myapp;
import java.util.HashMap;
public class Main{
  HashMap map1 = new HashMap();
  com.myapp.collection.HashMap map2 = new com.myapp.collection.HashMap();
}

Jedną klasę importujemy „normalnie”, a do drugiej odwołujemy się poprzez pełną nazwę.

Niezależnie od tego w jaki sposób importujemy inne klasy, ich kod nie jest w trakcie kompilacji doklejany do naszego kodu, więc realnie importy nie zwiększają rozmiaru tworzonych plików źródłowych.

Importowanie wielu klas

Kiedy chcemy skorzystać z wielu klas zawartych w tej samej paczce, nie musimy pisać oddzielnego importu dla każdej klasy. Możemy wykorzystać znak ‚*‚ , aby zaimportować całą zawartość paczki.

Przykładowo, zarówno HashMap jak i ArrayList należą do tej samej paczki java.util. Jeśli będziemy korzystać w kodzie z obu tych klas, możemy zaimportować je w ten sposób:

import java.util.*;

Należy tylko pamiętać, że taki sposób importu, nie odnosi się do podkatalogów. Zatem zapis:

import java.*;

Nie zaimportuje nam klas z paczki java.util, ani żadnych innych paczek będących podkatalogami katalogu java.

Paczka domyślna

Jak już wspomniałem, możliwe jest nie przypisanie klasy do żadnej paczki. Wtedy jest ona niejawnie przypisywana do paczki domyślnej (ang. default). Wiąże się to jednak z jednym bardzo dużym ograniczeniem:

Żadna klasa, której jawnie przypisaliśmy paczkę NIE MOŻE skorzystać z (importować) klas zawartych w paczce domyślnej.

Natomiast klasy będące w paczce domyślnej mogą korzystać z siebie na wzajem, pod warunkiem, że fizycznie znajdują się w tym samym katalogu.

Static import

W Javie możliwe jest też importowanie wybranych, statycznych, publicznych elementów innych klas. Służy to przede wszystkim poprawie czytelności kodu.

Poniżej prezentuję standardowe wywołanie statycznej metody format, dostępnej w klasie String:

public class Main{
  public static void main(String[] args){
    String.format("Jaka ładna okrągła liczba: %d",64);
  }
}

Z pomocą static import możemy pominąć nazwę klasy w wywołaniu metody format i odwoływać się do niej, jakby była metodą naszej własnej klasy:

import static java.lang.String.format;

public class Main{
  public static void main(String[] args){
    format("Jaka ładna okrągła liczba: %d",64);
  }
}

Oczywiście przy jednym wywołaniu metody format, nie robi nam to wielkiej różnicy, jednak gdybyśmy wykorzystywali ją wielokrotnie w rozbudowanej klasie, ten sposób znacząco podnosi czytelność kodu.

Przy użyciu static import należy mieć na uwadze, że w ten sposób możemy importować tylko publiczne, statyczne elementy klas – zarówno metody jak i zmienne. Ważna jest też kolejność słów w zapisie, mimo, że mechanizm nazywa się ‚static import‚, to w kodzie piszemy ‚import static‚ 🙂

To wszystko w tym temacie, jak również w tym dziale. W następnym artykule otwieramy nowy zbiór zagadnień.