В предишния ми урок за 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_
).
Трябва да спазвате няколко правила, за да избегнете допълнителни проблеми с изявлението на пакета:
- Можете да декларирате само един оператор на пакет в изходен файл.
- Не можете да предшествате изявлението на пакета с нищо освен коментари.
Първото правило, което е специален случай на второто правило, съществува, защото няма смисъл да се съхранява референтен тип в множество пакети. Въпреки че пакет може да съхранява множество типове, един тип може да принадлежи само на един пакет.
Когато изходен файл не декларира изявление на пакет, типовете на изходния файл се считат за принадлежащи към неназования пакет . Нетривиалните референтни типове обикновено се съхраняват в собствените им пакети и избягват неназования пакет.
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 theAudio
class. This listing begins with a package statement that identifiesca.javajeff.audio
as the class's package. Audio
is declaredpublic
so that it can be referenced from outside of its package. Also, it's declaredfinal
so that it cannot be extended (meaning, subclassed).Audio
declaresprivate
samples
andsampleRate
fields to store audio data. These fields are initialized to the values passed toAudio
's constructor.Audio
's constructor is declared package-private (meaning, the constructor isn't declaredpublic
,private
, orprotected
) so that this class cannot be instantiated from outside of its package.Audio
presentsgetSamples()
andgetSampleRate()
methods for returning an audio clip's samples and sample rate. Each method is declaredpublic
so that it can be called from outside ofAudio
's package.Audio
concludes with apublic
andstatic
newAudio()
factory method for returning anAudio
object corresponding to thefilename
argument. If the audio clip cannot be obtained,null
is returned.newAudio()
comparesfilename
's extension with.wav
(this example only supports WAV audio). If they match, it executesreturn WavReader.read(filename)
to return anAudio
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:
- Select a suitable location in your file system as the current directory.
- Create a
ca/javajeff/audio
subdirectory hierarchy within the current directory. - Copy Listings 1 and 2 to files
Audio.java
andWavReader.java
, respectively; and store these files in theaudio
subdirectory. - Assuming that the current directory contains the
ca
subdirectory, executejavac ca/javajeff/audio/*.java
to compile the two source files inca/javajeff/audio
. If all goes well, you should discoverAudio.class
andWavReader.class
files in theaudio
subdirectory. (Alternatively, for this example, you could switch to theaudio
subdirectory and executejavac *.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
Когато работите по проект за много разработчици, избягвайте да използвате *
заместващ символ, така че другите разработчици да могат лесно да видят кои типове се използват във вашия изходен код.