Пакети и статичен импорт в Java

В предишния ми урок за Java 101 научихте как да организирате по-добре своя код, като декларирате референтни типове (известни също като класове и интерфейси) като членове на други референтни типове и блокове. Също така ви показах как да използвате влагане, за да избегнете конфликти на имена между вложени референтни типове и референтни типове от най-високо ниво, които споделят едно и също име.

Наред с влагането, Java използва пакети за разрешаване на проблеми със същото име в референтните типове от най-високо ниво. Използването на статично импортиране също опростява достъпа до статичните членове в пакетирани референтни типове от най-високо ниво. Статичното импортиране ще ви спести натискания на клавиши при достъп до тези членове във вашия код, но има няколко неща, за които трябва да внимавате, когато ги използвате. В този урок ще ви запозная с използването на пакети и статичен импорт във вашите Java програми.

изтегляне Вземете кода Изтеглете изходния код, например приложения в този урок за Java. Създадено от Jeff Friesen за JavaWorld.

Референтни типове опаковки

Разработчиците на Java групират свързани класове и интерфейси в пакети. Използването на пакети улеснява намирането и използването на референтни типове, избягване на конфликти на имена между едноименните типове и контрол на достъпа до типове.

В този раздел ще научите за пакетите. Ще разберете какво представляват пакетите, ще научите за извлеченията packageи importизявленията и ще разгледате допълнителните теми за защитен достъп, JAR файлове и търсене на тип.

Какво представляват пакетите в Java?

При разработването на софтуер обикновено организираме артикулите според техните йерархични взаимоотношения. Например в предишния урок ви показах как да декларирате класове като членове на други класове. Също така можем да използваме файлови системи за влагане на директории в други директории.

Използването на тези йерархични структури ще ви помогне да избегнете конфликти на имена. Например в неиерархична файлова система (една директория) не е възможно да се присвои едно и също име на множество файлове. За разлика от нея, йерархичната файлова система позволява едноименните файлове да съществуват в различни директории. По същия начин два заграждащи класа могат да съдържат едноименни вложени класове. Конфликтите на имена не съществуват, защото елементите са разделени на различни пространства от имена.

Java също ни позволява да разделяме референтни типове от най-високо ниво (несгънати) на множество пространства от имена, за да можем по-добре да организираме тези типове и да предотвратим конфликти на имена. В Java използваме функцията на езика на пакета, за да разделяме референтните типове от най-високо ниво на множество пространства от имена. В този случай пакетът е уникално пространство от имена за съхраняване на референтни типове. Пакетите могат да съхраняват класове и интерфейси, както и подпакети, които са пакети, вложени в други пакети.

Пакетът има име, което трябва да е нерезервиран идентификатор; например java,. Операторът за достъп на член ( .) разделя името на пакета от името на подпакета и разделя името на пакета или подпакета от името на типа. Например двучленните оператори за достъп в java.lang.Systemотделно име на пакета от името javaна langподпакета и отделно име на подпакета от името langна Systemтипа.

Референтните типове трябва да бъдат декларирани publicкато достъпни извън техните пакети. Същото се отнася за всички константи, конструктори, методи или вложени типове, които трябва да бъдат достъпни. Ще видите примери за тях по-късно в урока.

Изявлението за пакета

В Java използваме инструкцията за пакет, за да създадем пакет. Това изявление се появява в горната част на изходния файл и идентифицира пакета, към който принадлежат типовете изходни файлове. Той трябва да отговаря на следния синтаксис:

 package identifier[.identifier]*; 

Изявлението на пакета започва със запазената дума packageи продължава с идентификатор, който по желание е последван от последователност от идентификатори, разделена на период. Запетая ( ;) прекратява това твърдение.

Първият (най-левият) идентификатор назовава пакета, а всеки следващ идентификатор именува подпакет. Например в package a.b;, всички типове, декларирани в изходния файл, принадлежат към bподпакета на aпакета.

Споразумение за именуване на пакети / подпакети

По споразумение изразяваме име на пакет или подпакет с малки букви. Когато името се състои от множество думи, може да искате да пишете с главни букви всяка дума, с изключение на първата; например generalLedger,.

Поредица от имена на пакети трябва да бъде уникална, за да се избегнат проблеми със компилацията. Да предположим например, че създавате два различни graphicsпакета и приемете, че всеки graphicsпакет съдържа Triangleклас с различен интерфейс. Когато Java компилаторът срещне нещо като това, което е долу, той трябва да провери дали Triangle(int, int, int, int)конструкторът съществува:

 Triangle t = new Triangle(1, 20, 30, 40); 

Триъгълник ограничаваща кутия

Помислете за Triangleконструктора като за определяне на ограничаващо поле, в което да нарисувате триъгълника. Първите два параметъра идентифицират горния ляв ъгъл на кутията, а вторите два параметъра определят обхвата на кутията.

Компилаторът ще търси всички достъпни пакети, докато не намери graphicsпакет, който съдържа Triangleклас. Ако намереният пакет включва подходящия Triangleклас с Triangle(int, int, int, int)конструктор, всичко е наред. В противен случай, ако намереният Triangleклас няма Triangle(int, int, int, int)конструктор, компилаторът съобщава за грешка. (Ще кажа повече за алгоритъма за търсене по-късно в този урок.)

Този сценарий илюстрира важността на избора на уникални последователности от имена на пакети. Конвенцията при избора на уникална последователност от имена е да обърнете името на вашия домейн в Интернет и да го използвате като префикс за последователността. Например бих избрал ca.javajeffза свой префикс, защото javajeff.caе името на моя домейн. След това щях да посоча ca.javajeff.graphics.Triangleза достъп Triangle.

Компоненти на имена на домейни и валидни имена на пакети

Компонентите на имена на домейни не винаги са валидни имена на пакети. Едно или повече имена на компоненти могат да започват с цифра ( 3D.com), да съдържат тире ( -) или друг нелегален символ ( ab-z.com) или да са една от запазените думи на Java ( short.com). Конвенцията повелява да поставите цифрата с префикс с долна черта ( com._3D), да замените незаконния знак с долна черта ( com.ab_z) и да добавите запазената дума с долна черта ( com.short_).

Трябва да спазвате няколко правила, за да избегнете допълнителни проблеми с изявлението на пакета:

  1. Можете да декларирате само един оператор на пакет в изходен файл.
  2. Не можете да предшествате изявлението на пакета с нищо освен коментари.

Първото правило, което е специален случай на второто правило, съществува, защото няма смисъл да се съхранява референтен тип в множество пакети. Въпреки че пакет може да съхранява множество типове, един тип може да принадлежи само на един пакет.

Когато изходен файл не декларира изявление на пакет, типовете на изходния файл се считат за принадлежащи към неназования пакет . Нетривиалните референтни типове обикновено се съхраняват в собствените им пакети и избягват неназования пакет.

Java implementations map package and subpackage names to same-named directories. For example, an implementation would map graphics to a directory named graphics. In the case of the package a.b, the first letter, a would map to a directory named a and b would map to a b subdirectory of a. The compiler stores the class files that implement the package's types in the corresponding directory. Note that the unnamed package corresponds to the current directory.

Example: Packaging an audio library in Java

A practical example is helpful for fully grasping the package statement. In this section I demonstrate packages in the context of an audio library that lets you read audio files and obtain audio data. For brevity, I'll only present a skeletal version of the library.

The audio library currently consists of only two classes: Audio and WavReader. Audio describes an audio clip and is the library's main class. Listing 1 presents its source code.

Listing 1. Package statement example (Audio.java)

 package ca.javajeff.audio; public final class Audio { private int[] samples; private int sampleRate; Audio(int[] samples, int sampleRate) { this.samples = samples; this.sampleRate = sampleRate; } public int[] getSamples() { return samples; } public int getSampleRate() { return sampleRate; } public static Audio newAudio(String filename) { if (filename.toLowerCase().endsWith(".wav")) return WavReader.read(filename); else return null; // unsupported format } } 

Let's go through Listing 1 step by step.

  • The Audio.java file in Listing 1 stores the Audio class. This listing begins with a package statement that identifies ca.javajeff.audio as the class's package.
  • Audio is declared public so that it can be referenced from outside of its package. Also, it's declared final so that it cannot be extended (meaning, subclassed).
  • Audio declares privatesamples and sampleRate fields to store audio data. These fields are initialized to the values passed to Audio's constructor.
  • Audio's constructor is declared package-private (meaning, the constructor isn't declared public, private, or protected) so that this class cannot be instantiated from outside of its package.
  • Audio presents getSamples() and getSampleRate() methods for returning an audio clip's samples and sample rate. Each method is declared public so that it can be called from outside of Audio's package.
  • Audio concludes with a public and staticnewAudio() factory method for returning an Audio object corresponding to the filename argument. If the audio clip cannot be obtained, null is returned.
  • newAudio() compares filename's extension with .wav (this example only supports WAV audio). If they match, it executes return WavReader.read(filename) to return an Audio object with WAV-based audio data.

Listing 2 describes WavReader.

Listing 2. The WavReader helper class (WavReader.java)

 package ca.javajeff.audio; final class WavReader { static Audio read(String filename) { // Read the contents of filename's file and process it // into an array of sample values and a sample rate // value. If the file cannot be read, return null. For // brevity (and because I've yet to discuss Java's // file I/O APIs), I present only skeletal code that // always returns an Audio object with default values. return new Audio(new int[0], 0); } } 

WavReader is intended to read a WAV file's contents into an Audio object. (The class will eventually be larger with additional private fields and methods.) Notice that this class isn't declared public, which makes WavReader accessible to Audio but not to code outside of the ca.javajeff.audio package. Think of WavReader as a helper class whose only reason for existence is to serve Audio.

Complete the following steps to build this library:

  1. Select a suitable location in your file system as the current directory.
  2. Create a ca/javajeff/audio subdirectory hierarchy within the current directory.
  3. Copy Listings 1 and 2 to files Audio.java and WavReader.java, respectively; and store these files in the audio subdirectory.
  4. Assuming that the current directory contains the ca subdirectory, execute javac ca/javajeff/audio/*.java to compile the two source files in ca/javajeff/audio. If all goes well, you should discover Audio.class and WavReader.class files in the audio subdirectory. (Alternatively, for this example, you could switch to the audio subdirectory and execute javac *.java.)

Now that you've created the audio library, you'll want to use it. Soon, we'll look at a small Java application that demonstrates this library. First, you need to learn about the import statement.

Java's import statement

Imagine having to specify ca.javajeff.graphics.Triangle for each occurrence of Triangle in source code, repeatedly. Java provides the import statement as a convenient alternative for omitting lengthy package details.

The import statement imports types from a package by telling the compiler where to look for unqualified (no package prefix) type names during compilation. It appears near the top of a source file and must conform to the following syntax:

 import identifier[.identifier]*.(typeName | *); 

An import statement starts with reserved word import and continues with an identifier, which is optionally followed by a period-separated sequence of identifiers. A type name or asterisk (*) follows, and a semicolon terminates this statement.

The syntax reveals two forms of the import statement. First, you can import a single type name, which is identified via typeName. Second, you can import all types, which is identified via the asterisk.

The * symbol is a wildcard that represents all unqualified type names. It tells the compiler to look for such names in the right-most package of the import statement's package sequence unless the type name is found in a previously searched package. Note that using the wildcard doesn't have a performance penalty or lead to code bloat. However, it can lead to name conflicts, which you will see.

For example, import ca.javajeff.graphics.Triangle; tells the compiler that an unqualified Triangle class exists in the ca.javajeff.graphics package. Similarly, something like

 import ca.javajeff.graphics.*; 

tells the compiler to look in this package when it encounters a Triangle name, a Circle name, or even an Account name (if Account has not already been found).

Avoid the * in multi-developer projects

Когато работите по проект за много разработчици, избягвайте да използвате *заместващ символ, така че другите разработчици да могат лесно да видят кои типове се използват във вашия изходен код.