Pacchetti e importazioni statiche in Java

Nel mio precedente tutorial di Java 101 , hai imparato come organizzare meglio il tuo codice dichiarando i tipi di riferimento (noti anche come classi e interfacce) come membri di altri tipi e blocchi di riferimento. Ti ho anche mostrato come utilizzare l'annidamento per evitare conflitti di nome tra tipi di riferimento annidati e tipi di riferimento di primo livello che condividono lo stesso nome.

Insieme alla nidificazione, Java utilizza i pacchetti per risolvere i problemi con lo stesso nome nei tipi di riferimento di primo livello. L'utilizzo di importazioni statiche semplifica anche l'accesso ai membri statici nei tipi di riferimento di primo livello in pacchetto. Le importazioni statiche ti faranno risparmiare le battiture quando accedi a questi membri nel tuo codice, ma ci sono alcune cose a cui prestare attenzione quando li usi. In questo tutorial, ti introdurrò all'uso di pacchetti e importazioni statiche nei tuoi programmi Java.

download Ottieni il codice Scarica il codice sorgente per applicazioni di esempio in questo tutorial Java. Creato da Jeff Friesen per JavaWorld.

Tipi di riferimento per il packaging

Gli sviluppatori Java raggruppano le classi e le interfacce correlate in pacchetti. L'uso dei pacchetti semplifica l'individuazione e l'uso dei tipi di riferimento, evita conflitti di nome tra tipi con lo stesso nome e controlla l'accesso ai tipi.

In questa sezione imparerai a conoscere i pacchetti. Scoprirete quali pacchetti sono, conoscere la packagee importdichiarazioni, ed esplorare i temi aggiuntivi di accesso protetto, file JAR, e le ricerche di tipo.

Cosa sono i pacchetti in Java?

Nello sviluppo del software, organizziamo comunemente gli elementi in base alle loro relazioni gerarchiche. Ad esempio, nel tutorial precedente, ti ho mostrato come dichiarare le classi come membri di altre classi. Possiamo anche utilizzare i file system per annidare le directory in altre directory.

L'utilizzo di queste strutture gerarchiche ti aiuterà a evitare conflitti di nome. Ad esempio, in un file system non gerarchico (una singola directory), non è possibile assegnare lo stesso nome a più file. Al contrario, un file system gerarchico consente la presenza di file con lo stesso nome in directory diverse. Allo stesso modo, due classi di inclusione possono contenere classi nidificate con lo stesso nome. I conflitti di nome non esistono perché gli elementi sono partizionati in spazi dei nomi diversi.

Java ci consente anche di partizionare i tipi di riferimento di primo livello (non annidati) in più spazi dei nomi in modo da poter organizzare meglio questi tipi e prevenire conflitti di nome. In Java, utilizziamo la funzionalità del linguaggio del pacchetto per partizionare i tipi di riferimento di primo livello in più spazi dei nomi. In questo caso, un pacchetto è uno spazio dei nomi univoco per l'archiviazione dei tipi di riferimento. I pacchetti possono memorizzare classi e interfacce, nonché sottopacchetti, che sono pacchetti annidati all'interno di altri pacchetti.

Un pacchetto ha un nome, che deve essere un identificatore non riservato; per esempio java,. L'operatore di accesso ai membri ( .) separa il nome di un pacchetto da un nome di sottopacchetto e separa il nome di un pacchetto o di un sottopacchetto da un nome di tipo. Ad esempio, gli operatori di accesso a due membri nel java.lang.Systemnome del pacchetto separato javadal nome del langsottopacchetto e nel nome del sottopacchetto separato langdal nome del Systemtipo.

I tipi di riferimento devono essere dichiarati publicaccessibili dall'esterno dei loro pacchetti. Lo stesso vale per tutte le costanti, i costruttori, i metodi o i tipi annidati che devono essere accessibili. Vedrai esempi di questi più avanti nel tutorial.

La dichiarazione del pacchetto

In Java, usiamo l' istruzione package per creare un pacchetto. Questa istruzione viene visualizzata all'inizio di un file di origine e identifica il pacchetto a cui appartengono i tipi di file di origine. Deve essere conforme alla seguente sintassi:

 package identifier[.identifier]*; 

Un'istruzione pacchetto inizia con la parola riservata packagee continua con un identificatore, che è facoltativamente seguito da una sequenza di identificatori separati da punti. Un punto e virgola ( ;) termina questa istruzione.

Il primo identificatore (quello più a sinistra) denomina il pacchetto e ogni identificatore successivo nomina un sottopacchetto. Ad esempio, in package a.b;, tutti i tipi dichiarati nel file sorgente appartengono al bsottopacchetto del apacchetto.

Convenzione di denominazione di pacchetti / sottopacchetti

Per convenzione, esprimiamo il nome di un pacchetto o di un sottopacchetto in minuscolo. Quando il nome è composto da più parole, potresti voler scrivere in maiuscolo ogni parola tranne la prima; per esempio generalLedger,.

Una sequenza di nomi di pacchetti deve essere univoca per evitare problemi di compilazione. Ad esempio, supponiamo di creare due graphicspacchetti diversi e presumere che ogni graphicspacchetto contenga una Triangleclasse con un'interfaccia diversa. Quando il compilatore Java incontra qualcosa di simile a quanto riportato di seguito, deve verificare che il Triangle(int, int, int, int)costruttore esista:

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

Riquadro di delimitazione del triangolo

Pensa al Trianglecostruttore come a specificare un riquadro di delimitazione in cui disegnare il triangolo. I primi due parametri identificano l'angolo superiore sinistro della casella, mentre i secondi due parametri definiscono le estensioni della casella.

Il compilatore cercherà tutti i pacchetti accessibili finché non trova un graphicspacchetto che contiene una Triangleclasse. Se il pacchetto trovato include la Triangleclasse appropriata con un Triangle(int, int, int, int)costruttore, va tutto bene. Altrimenti, se la Triangleclasse trovata non ha un Triangle(int, int, int, int)costruttore, il compilatore segnala un errore. (Dirò di più sull'algoritmo di ricerca più avanti in questo tutorial.)

Questo scenario illustra l'importanza di scegliere sequenze di nomi di pacchetto univoci. La convenzione nella selezione di una sequenza di nomi univoca è invertire il nome del dominio Internet e usarlo come prefisso per la sequenza. Ad esempio, sceglierei ca.javajeffcome prefisso perché javajeff.caè il mio nome di dominio. Specificherei quindi ca.javajeff.graphics.Triangledi accedere Triangle.

Componenti del nome di dominio e nomi di pacchetti validi

I componenti del nome di dominio non sono sempre nomi di pacchetto validi. Uno o più nomi di componenti potrebbero iniziare con una cifra ( 3D.com), contenere un trattino ( -) o un altro carattere non valido ( ab-z.com) o essere una delle parole riservate di Java ( short.com). La convenzione impone di anteporre alla cifra un trattino basso ( com._3D), sostituire il carattere non valido con un trattino basso ( com.ab_z) e aggiungere un carattere di sottolineatura alla parola riservata come suffisso ( com.short_).

È necessario seguire un paio di regole per evitare ulteriori problemi con la dichiarazione del pacchetto:

  1. È possibile dichiarare una sola istruzione del pacchetto in un file di origine.
  2. Non è possibile far precedere l'istruzione del pacchetto da nulla oltre ai commenti.

La prima regola, che è un caso speciale della seconda regola, esiste perché non ha senso memorizzare un tipo di riferimento in più pacchetti. Sebbene un pacchetto possa memorizzare più tipi, un tipo può appartenere a un solo pacchetto.

Quando un file di origine non dichiara un'istruzione di pacchetto, i tipi del file di origine si dice che appartengono al pacchetto senza nome . I tipi di riferimento non banali sono in genere archiviati nei propri pacchetti ed evitano il pacchetto senza nome.

Le implementazioni Java associano i nomi di pacchetti e sottopacchetti a directory con lo stesso nome. Ad esempio, un'implementazione verrà mappata graphicsa una directory denominata graphics. Nel caso del pacchetto a.b, la prima lettera, a si associa a una directory denominata ae b a una bsottodirectory di a. Il compilatore memorizza i file di classe che implementano i tipi del pacchetto nella directory corrispondente. Notare che il pacchetto senza nome corrisponde alla directory corrente.

Esempio: creazione del pacchetto di una libreria audio 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

Quando si lavora su un progetto multi-sviluppatore, evitare di utilizzare il *carattere jolly in modo che altri sviluppatori possano vedere facilmente quali tipi sono utilizzati nel codice sorgente.