Paquetes e importaciones estáticas en Java

En mi tutorial anterior de Java 101 , aprendió cómo organizar mejor su código declarando tipos de referencia (también conocidos como clases e interfaces) como miembros de otros tipos y bloques de referencia. También le mostré cómo usar el anidamiento para evitar conflictos de nombres entre tipos de referencia anidados y tipos de referencia de nivel superior que comparten el mismo nombre.

Junto con el anidamiento, Java usa paquetes para resolver problemas con el mismo nombre en tipos de referencia de nivel superior. El uso de importaciones estáticas también simplifica el acceso a los miembros estáticos en tipos de referencia de nivel superior empaquetados. Las importaciones estáticas le ahorrarán pulsaciones de teclas al acceder a estos miembros en su código, pero hay algunas cosas a tener en cuenta cuando los usa. En este tutorial, le presentaré el uso de paquetes e importaciones estáticas en sus programas Java.

descargar Obtener el código Descargar el código fuente, por ejemplo, aplicaciones en este tutorial de Java. Creado por Jeff Friesen para JavaWorld.

Tipos de referencia de empaque

Los desarrolladores de Java agrupan clases e interfaces relacionadas en paquetes. El uso de paquetes facilita la localización y el uso de tipos de referencia, evita conflictos de nombres entre tipos del mismo nombre y controla el acceso a los tipos.

En esta sección, aprenderá sobre los paquetes. Usted se dará cuenta qué paquetes son, aprender sobre el packagey importdeclaraciones, y explorar los temas adicionales de acceso protegido, archivos JAR y búsquedas tipo.

¿Qué son los paquetes en Java?

En el desarrollo de software, normalmente organizamos elementos de acuerdo con sus relaciones jerárquicas. Por ejemplo, en el tutorial anterior, le mostré cómo declarar clases como miembros de otras clases. También podemos usar sistemas de archivos para anidar directorios en otros directorios.

El uso de estas estructuras jerárquicas le ayudará a evitar conflictos de nombres. Por ejemplo, en un sistema de archivos no jerárquico (un solo directorio), no es posible asignar el mismo nombre a varios archivos. Por el contrario, un sistema de archivos jerárquico permite que los archivos con el mismo nombre existan en diferentes directorios. De manera similar, dos clases adjuntas pueden contener clases anidadas del mismo nombre. Los conflictos de nombres no existen porque los elementos están divididos en diferentes espacios de nombres.

Java también nos permite dividir tipos de referencia de nivel superior (no anidados) en múltiples espacios de nombres para que podamos organizar mejor estos tipos y evitar conflictos de nombres. En Java, utilizamos la función de lenguaje de paquetes para dividir tipos de referencia de nivel superior en varios espacios de nombres. En este caso, un paquete es un espacio de nombres exclusivo para almacenar tipos de referencia. Los paquetes pueden almacenar clases e interfaces, así como subpaquetes, que son paquetes anidados dentro de otros paquetes.

Un paquete tiene un nombre, que debe ser un identificador no reservado; por ejemplo java,. El operador de acceso de miembros ( .) separa un nombre de paquete de un nombre de subpaquete y separa un nombre de paquete o subpaquete de un nombre de tipo. Por ejemplo, los operadores de acceso de dos miembros en el java.lang.Systemnombre javadel paquete separado del nombre del langsubpaquete y el nombre langdel subpaquete separado del nombre del Systemtipo.

Los tipos de referencia deben declararse publicaccesibles desde fuera de sus paquetes. Lo mismo se aplica a las constantes, constructores, métodos o tipos anidados que deben ser accesibles. Verá ejemplos de estos más adelante en el tutorial.

La declaración del paquete

En Java, usamos la declaración del paquete para crear un paquete. Esta declaración aparece en la parte superior de un archivo fuente e identifica el paquete al que pertenecen los tipos de archivos fuente. Debe ajustarse a la siguiente sintaxis:

 package identifier[.identifier]*; 

Una declaración de paquete comienza con la palabra reservada packagey continúa con un identificador, que es seguido opcionalmente por una secuencia de identificadores separados por puntos. Un punto y coma ( ;) termina esta declaración.

El primer identificador (el más a la izquierda) nombra el paquete y cada identificador subsiguiente nombra un subpaquete. Por ejemplo, en package a.b;, todos los tipos declarados en el archivo fuente pertenecen al bsubpaquete del apaquete.

Convención de nomenclatura de paquetes / subpaquetes

Por convención, expresamos el nombre de un paquete o subpaquete en minúsculas. Cuando el nombre consta de varias palabras, es posible que desee poner en mayúscula cada palabra excepto la primera; por ejemplo generalLedger,.

Una secuencia de nombres de paquetes debe ser única para evitar problemas de compilación. Por ejemplo, suponga que crea dos graphicspaquetes diferentes y asume que cada graphicspaquete contiene una Triangleclase con una interfaz diferente. Cuando el compilador de Java encuentra algo como lo que se muestra a continuación, debe verificar que el Triangle(int, int, int, int)constructor existe:

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

Cuadro delimitador triangular

Piense en el Triangleconstructor como si especificara un cuadro delimitador en el que dibujar el triángulo. Los primeros dos parámetros identifican la esquina superior izquierda del cuadro y los dos segundos parámetros definen la extensión del cuadro.

El compilador buscará todos los paquetes accesibles hasta que encuentre un graphicspaquete que contenga una Triangleclase. Si el paquete encontrado incluye la Triangleclase apropiada con un Triangle(int, int, int, int)constructor, todo está bien. De lo contrario, si la Triangleclase encontrada no tiene un Triangle(int, int, int, int)constructor, el compilador informa de un error. (Diré más sobre el algoritmo de búsqueda más adelante en este tutorial).

Este escenario ilustra la importancia de elegir secuencias únicas de nombres de paquetes. La convención al seleccionar una secuencia de nombre única es invertir su nombre de dominio de Internet y usarlo como prefijo para la secuencia. Por ejemplo, elegiría ca.javajeffcomo prefijo porque javajeff.caes mi nombre de dominio. Luego, especificaría el ca.javajeff.graphics.Triangleacceso Triangle.

Componentes de nombre de dominio y nombres de paquetes válidos

Los componentes del nombre de dominio no siempre son nombres de paquete válidos. Uno o más nombres de componentes pueden comenzar con un dígito ( 3D.com), contener un guión ( -) u otro carácter ilegal ( ab-z.com), o ser una de las palabras reservadas de Java ( short.com). La convención dicta que debe anteponer el dígito con un guión bajo ( com._3D), reemplazar el carácter ilegal con un guión bajo ( com.ab_z) y agregar un sufijo a la palabra reservada con un guión bajo ( com.short_).

Debe seguir un par de reglas para evitar problemas adicionales con la declaración del paquete:

  1. Puede declarar solo una declaración de paquete en un archivo fuente.
  2. No puede preceder la declaración del paquete con nada más que comentarios.

La primera regla, que es un caso especial de la segunda regla, existe porque no tiene sentido almacenar un tipo de referencia en varios paquetes. Aunque un paquete puede almacenar varios tipos, un tipo puede pertenecer a un solo paquete.

Cuando un archivo fuente no declara una declaración de paquete, se dice que los tipos del archivo fuente pertenecen al paquete sin nombre . Los tipos de referencia no triviales generalmente se almacenan en sus propios paquetes y evitan el paquete sin nombre.

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

Cuando trabaje en un proyecto de varios desarrolladores, evite utilizar el *comodín para que otros desarrolladores puedan ver fácilmente qué tipos se utilizan en su código fuente.