- package: la unidad de biblioteca
- Organización del código
- Creación de nombres de paquetes unívocos
- Una biblioteca personalizada de herramientas
- Utilización de importaciones para modificar el comportamiento
- Un consejo sobre los nombres de paquete
- Especificadores de acceso Java
- Acceso de paquete
- public: acceso de interfaz
- private: ¡no lo toque!
- protected: acceso de herencia
- Interfaz e implementación
- Acceso de clase
El control de acceso (u ocultación de la implementación) trata acerca de "que no salgan las cosas a la primera".
El trabajo de rediseño consiste en reescribir código que ya funciona para hacerlo más legible, comprensible y mantenible.
En diseño orientado a objetos, hay que separar las cosas que cambian de las cosas que permanecen.
Esto es importante para las bibliotecas. Los consumidores de bibliotecas confían en el elemento que están utilizando y saben que no tendrán que reescribir código si se publica una nueva versión de la biblioteca. El creador de la biblioteca debe poder realizar mejoras sin que el código del cliente se vea afectado por esos cambios. Es decir, por un lado tenemos a los creadores de bibliotecas, que crearán y mejorarán el código de las bibliotecas. Por otro lado tenemos a los consumidores de bibliotecas, que utilizan las bibliotecas creadas por los creadores de bibliotecas. Por tanto, cuando los creadores de bibliotecas modifiquen el código de las bibliotecas, deben tener cuidado de que esos cambios no afecten a los programas de los clientes creados con versiones anteriores de estas bibliotecas. Estos programas deben funcionar correctamente con las nuevas versiones de las bibliotecas.
Esto se puede lograr siguiendo un convenio adecuado. Por ejemplo, el programador de bibliotecas debe aceptar no eliminar los métodos existentes cuando modifica una clase de la biblioteca, ya que podría afectar al código del programador de clientes. La situación inversa es más complicada de resolver. En el caso de un campo, es complicado que el programador de bibliotecas sepa a qué campos han accedido los programadores de clientes. Lo mismo ocurre con métodos que forman parte de la implementación de una clase y que no están para ser usados directamente por el programador de clientes. Parece que el creador de bibliotecas tiene las manos atadas y no puede modificar nada.
Para resolver este problema en Java tenemos los especificadores de acceso, que permiten al creador de bibliotecas decir qué cosas están disponibles para el programa cliente y qué cosas no lo están. Los niveles de control de acceso de mayor a menor acceso son: public, protected, acceso de paquete y private.
Sin embargo, el concepto de biblioteca de componentes y el control acerca de quién puede acceder a los componentes de esa biblioteca no es completo. ¿Cómo se empaquetan los componentes para formar una unidad de biblioteca cohesionada? Esto se realiza mediante la palabra clave package y los especificadores de acceso se verán afectados dependiendo de que una clase se encuentre en el mismo paquete o en otro paquete distinto. Vamos a ver cómo se incluyen los componentes de la biblioteca en los paquetes y así entenderemos el significado de los especificadores de acceso.
PACKAGE: LA UNIDAD DE BIBLIOTECA
Un paquete contiene un grupo de clases, organizadas conjuntamente dentro de un mismo espacio de nombres.
Así, existe una biblioteca en Java llamada java.util. Una de las clases de java.util es ArrayList, una forma de utilizar un objeto ArrayList consistiría en especificar el nombre completo, lo vemos con un ejemplo:
Sin embargo, esto resulta pesado e incluso hará el código más complicado, la solución es utilizar la palabra clave import, así si queremos importar una clase:
Ahora utilizamos ArrayList de forma directa. En este caso sólo tenemos disponible la clase ArrayList de la biblioteca java.util. Si deseamos disponer de todas las clases de esta biblioteca tendremos que importarlas mediante el símbolo '*':
La razón para efectuar las importaciones es proporcionar un mecanismo para gestionar los espacios de los nombres. Los nombres de los miembros de las clases están aislados de las clases restantes. Así un método f() de la clase A no coincidirá con un método f() que tenga la misma signatura en la clase B. El problema está en encontrarnos dos clases con el mismo nombre. Por ello es importante disponer de un control completo de los espacios de nombres en Java y así tener una combinación unívoca para cada clase.
La mayoría de los ejemplos vistos hasta ahora han sido creados para uso local y no han sido incluidos en ningún paquete, en realidad sí que están incluidos en el paquete predeterminado o "innominado". Esta solución es correcta y simple. Sin embargo, si lo que queremos es crear bibliotecas o programas que puedan cooperar con otros programas Java, tendremos que tener en cuenta las posibles colisiones entre los nombres de clases.
Los archivos de código fuente para Java se denominan unidades de compilación o de traducción, tienen la extensión .java y dentro de estas unidades puede haber una clase public con el mismo nombre del archivo (incluyendo mayúsculas y minúsculas, sin la extensión .java). Sólo puede haber una clase public en cada unidad de compilación. Si están incluidas otras clases, éstas están ocultas para el mundo exterior al paquete ya que no son public, se trata de clases soporte para la clase public principal.
Organización del código
Cuando compilamos un archivo .java obtenemos un archivo de salida para cada clase incluida en el archivo .java, estos archivos tendrán el nombre de cada clase y la extensión .class. En Java, un programa funcional está formado por un conjunto de archivos .class que se pueden empaquetar y comprimir en un archivo JAR (Java Archive) utilizando el archivador jar de Java. El intérprete de Java localiza, carga e interpreta estos archivos, (aunque en Java también existen compiladores de código nativo que generan un único archivo ejecutable).
Una biblioteca es un grupo de archivos de clase. Cada archivo fuente suele tener una clase public y un número arbitrario de clases no públicas. Para agrupar todos estos componentes, cada uno con sus propios archivos .java y .class separados, podemos utilizar la palabra clave package.
La instrucción package debe aparecer como la primera línea no de comentario en el archivo.
Con esta instrucción estamos indicando que esta unidad de compilación pertenece a la biblioteca access. Estamos especificando que la clase pública situada dentro de esta unidad de compilación está dentro de la biblioteca access, de modo que quien quiera utilizar ese nombre deberá especificarlo por completo o utilizar la palabra clave import en combinación con access, utilizando las opciones vistas anteriormente. Los nombres de los paquetes van en minúscula, incluso las palabras intermedias.
Si tenemos el archivo MyClass.java, significa que sólo puede haber una clase public en este archivo y que el nombre de esta clase es MyClass:
Ahora para utilizar MyClass o cualquiera de las clases públicas de access, tendremos que utilizar la palabra clave import. Podemos especificar el nombre completamente cualificado (sin utilizar import):
Aunque lo lógico es utilizar la palabra clave import:
Lo que las palabras clave package e import nos permiten hacer, como diseñadores de bibliotecas, es dividir el espacio de nombres global único, para que los nombres no colisionen, independientemente de cuántas personas se conecten a Internet y empiecen a escribir clases en Java.
Creación de nombres de paquete unívocos
Un paquete está formado por varios archivos .class, por lo que el sistema de archivos puede estar un tanto abarrotado. Para evitar el desorden todos los archivos .class pertenecientes a un paquete se colocan dentro de un mismo directorio.
Ubicar los archivos de un paquete dentro de un subdirectorio resuelve otros dos problemas: creación de nombres de paquetes unívocos y localización de aquellas clases que puedan estar perdidas en algún lugar de la estructura de directorios. Esto se hace codificando la ruta correspondiente a la ubicación del archivo .class dentro del nombre del paquete. La primera parte del nombre del paquete es el nombre de dominio de Internet invertido. Como los nombres de los dominios de Internet son únicos, siguiendo este convenio nuestro paquete será únivoco y no se producirá colisión de nombres. Si no disponemos de dominio tendremos que crear una combinación de nombres lo suficientemente improbable para que no se dupliquen.
La segunda parte de la solución consiste en establecer la correspondencia entre los nombres de paquete y los directorios de la máquina, así cuando se necesite ejecutar el programa Java y cargar el .class, se pueda localizar el directorio donde está el archivo.
El intérprete Java actúa de la siguiente forma: primero localiza la variable de entorno CLASSPATH (normalmente se fija a través del sistema operativo, aunque a veces es definida por el programa de instalación Java). Esta variable contiene los directorios raíces en los que intérprete buscará los archivos .class. Comenzando por esa raíz, el intérprete tomará el nombre del paquete y sustituirá cada punto por una barra inclinada generando un nombre de ruta a partir de CLASSPATH. Así el paquete package foo.bar.baz se convertirá en foo\bar\baz o en otra cosa dependiendo del sistema operativo. Esto se concatena con las entradas que hay en la variable CLASSPATH. En ese subdirectorio será donde el intérprete busque el archivo .class que se corresponda con la clase que se está intentando crear, también buscará en algunos directorios donde reside el intérprete Java.
Así, si consideramos el nombre de dominio MindView.net, invirtiéndolo y pasándolo a minúsculas, net.mindview tenemos un nombre global unívoco para las clases. Este espacio de nombres se puede subdividir más creando, por ejemplo, una biblioteca llamada simple, así el nombre del paquete será:
Este nombre de paquete puede utilizarse como espacio de nombres para los siguientes dos archivos:
Como vemos la instrucción package es la primera línea de no comentario. El segundo archivo:
Ambos archivos se ubicarán en el siguiete subdirectorio del sistema (Windows):
En un sistema Linux podría ser:
Si observamos la ruta vemos el nombre del paquete net.mindview.simple, ¿y la primera parte de la ruta? De esa parte se encarga la variable de entorno CLASSPATH. Un ejemplo de esta variable en Windows sería:
En Linux:
Fijaos que en Linux separa las diferentes rutas de búsqueda con dos puntos en lugar de punto y coma.
Como vemos la variable de entorno CLASSPATH puede contener varias rutas de búsqueda.
Si tenemos archivos JAR, debemos poner el nombre real de archivo JAR en la variable de ruta y no simplemente la ruta donde está ubicado. Para un archivo JAR llamado grape.jar, la variable CLASSPATH (en Windows) sería:
Una vez configurada la variable, el siguiente archivo puede ubicarse en cualquier directorio:
Cuando el compilador se encuentra con la instrucción import correspondiente a la biblioteca simple, comienza a buscar en los directorios especificados en CLASSPATH (C:\DOC\JavaT), en este caso el subdirectorio net/mindview/simple y luego los archivos Vector.class y List.class. Las dos clases y los métodos utilizados deben ser de tipo public.
La variable CLASSPATH en la versiones más recientes de Java funciona de forma más inteligente, se pueden compilar y ejecutar programas básicos sin configurar la variable. Para compilar los ejemplos que estamos viendo y que están disponibles en http://www.mindviewinc.com/TIJ4/CodeInstructions.html, debemos añadir a la variable CLASSPATH el directorio base del árbol de código, en esta página os explica cómo hacerlo, (en nuestro caso C:\DOC\JavaT para Windows o bien /home/usuario/Documentos/JavaT en Linux).
Ejercicio 1. Crea una clase dentro de un paquete. Crea una instancia de esa clase fuera de dicho paquete.
Colisiones
¿Qué pasaría si se importaran dos bibliotecas con '*' y ambas incluyeran los mismos nombres? Por ejemplo, un programa hace esto:
Como java.util.* contiene una clase Vector, se podría producir una colisión. Mientras no escribamos el código que produzca la colisión no pasa nada. Sin embargo, si intentamos construir un Vector, tendríamos una colisión:
¿A qué clase Vector nos referimos, a la de net.mindview.simple.* o a java.util.*? El compilador no puede saberlo y generará un error. Para utilizar el Vector de java.util.* tendremos que escribir:
Esto junto con la variable CLASSPATH especifica la ubicación de la clase Vector y no hará falta utilizar la instrucción import a no ser que vayamos a utilizar otra clase de java.util.
Ejercicio 2. Toma los fragmentos de código de esta sección y transfórmelos en un programa para verificar que se producen las colisiones que hemos mencionado.
Una biblioteca personalizada de herramientas
Ahora podemos crear nuestras propias bibliotecas de herramientas. En muchos de los ejercicios hemos estado utilizando un alias para System.out.println(), para escribirlo de forma más breve. Esto puede formar parte de una clase llamada Print, así disponemos de una instrucción de impresión más legible:
Podemos utilizar estas abreviaturas para imprimir cualquier cosa.
Este archivo deberá estar en un directorio que comience con una de las ubicaciones definidas en CLASSPATH y luego continuar con net/mindview/util/Print. Después de compilar los métodos de esta clase, están disponibles para cualquier lugar del sistema utilizando una instrucción import static:
Otro componente de esta biblioteca pueden ser los métodos range() que permiten el uso de la instrucción foreach para secuencias simples de enteros:
Cualquier utilidad que se considere interesante se puede añadir a la biblioteca. Mas adelante añadiremos más componentes a net.mindview.util.
Utilización de importaciones para modificar el comportamiento
Una característica de C que no tiene Java es la compilación condicional, a través de la cual cambiando una variable indicadora tendremos un comportamiento diferente sin variar ninguna otra parte del código. El motivo de no incorporarlo a Java es que en C la mayor parte de las veces se utiliza para resolver problemas interplataforma, dependiendo de la plataforma de destino se compilan diferentes partes del código. Java está pensado para ser interplataforma así que esta característica no debería ser necesaria.
Existen otras aplicaciones de la compilación condicional. Un uso habitual es en la depuración del código. Las características de depuración se activan durante el desarrollo y se desactivan en el momento de lanzar el producto. El mismo efecto se consigue modificando el paquete que se importe dentro de nuestro programa para poder conmutar entre el código utilizado en la versión de depuración y en la versión de producción. Esta técnica puede utilizarse para cualquier código de tipo condicional.
Ejercicio 3. Crea dos paquetes: debug y debugoff, que contengan una clase idéntica con un método debug(). La primera versión debe mostrar su argumento String en la consola, mientras que la segunda no debe hacer nada. Utiliza una línea static import para importar la clase en un programa de prueba y demuestra el efecto de la compilación condicional.
Un consejo sobre los nombres de paquete
Cuando creamos un paquete estamos creando implícitamente una estructura de directorio al dar el nombre al paquete. Éste debe estar en el directorio indicado por su nombre, que deberá ser alcanzable a través de una de las rutas especificadas en la variable CLASSPATH. Cuando empezamos a utilizar package puede ser un poco frustrante encontrarnos errores a la hora de ejecutar nuestro código. Cuando nos ocurra esto, mediante un comentario desactivamos la instrucción package, si el programa funciona ya sabemos dónde está el error.
El código compilado se coloca a menudo en un directorio distinto de aquel en el que reside el código fuente, pero la ruta al código compilado deberá seguir siendo localizable por la JVM a través de la variable CLASSPATH.
ESPECIFICADORES DE ACCESO JAVA
Los especificadores de acceso Java public, protected y private, se colocan delante de la definición de cada miembro de la clase, ya sea éste un campo o un método. Cada especificador controla el acceso para esa definición concreta.
Si no se proporciona un especificador de acceso, ese miembro tendrá "acceso de paquete".
Acceso de paquete
En los ejemplos vistos hasta ahora no hemos utilizado especificadores de acceso. El acceso que no tiene asociada ninguna palabra clave se denomina acceso de paquete. Esto significa que todas las clases del paquete actual tendrán acceso a ese miembro, y para las clases situadas fuera del paquete, ese miembro será private. Las clases dentro de una misma unidad de compilación están disponibles para las otras mediante el acceso de paquete.
El acceso de paquete permite agrupar en un mismo paquete clases relacionadas, para que así puedan interactuar entre ellas. Así garantizamos que el código de ese paquete sea "propiedad" nuestra. Este tipo de acceso hace que tenga sentido agrupar las clases dentro de un paquete. En Java la forma en que se hagan las definiciones en los archivos deben ser organizadas de forma lógica. El acceso de paquete permite excluir a las clases que no queremos que tengan acceso a las clases de nuestro paquete.
Cada clase se encarga de controlar qué código tiene acceso a sus miembros. La única forma de acceder a un miembro consiste en:
1. Hacer dicho miembro public, así todo el mundo podrá acceder a él.
2. Hacer que el miembro tenga acceso de paquete, no incluyendo ningún especificador de acceso y colocando las otras clases que deban acceder a él dentro del mismo paquete. Así las clases del paquete podrán acceder a ese miembro.
3. Más adelante veremos la herencia. Una clase heredada puede acceder a los miembros protected y public, pero no a los private. Esta clase podrá acceder a los miembros con acceso de paquete si ambas pertenecen al mismo paquete.
4. Proporcionar métodos de "acceso/mutadores", denominados también "get/set" que permitan leer y cambiar el valor. Éste es el enfoque más civilizado en términos de programación orientada a objetos, y resulta fundamental en JavaBeans, según veremos en el Tema 22, Interfaces gráficas de usuario.
public: acceso de interfaz
Cuando la palabra clave public hace referencia a algún miembro significa que éste está disponible para todo el mundo. Vamos a definir un paquete dessert:
Este archivo debe colocarse en un subdirectorio llamado dessert, dentro de un directorio access que a su vez deberá estar bajo uno de los directorios CLASSPATH. En CLASSPATH debe incluirse el directorio actual con '.', ya que si no no se examinará el directorio actual.
Creamos un programa que use Cookie:
Podemos crear un objeto Cookie porque su constructor es public, sin embargo no podemos acceder al método bite() ya que tiene acceso de paquete y sólo pueden acceder a él las clases del mismo paquete (dessert).
El paquete predeterminado
Vamos a ver el siguiente código, puede parecer que no cumple las reglas pero veremos que sí:
En un segundo archivo del mismo directorio tenemos:
Podríamos pensar que estos dos archivos no tienen nada que ver, pero a partir de la clase Cake se puede crear un objeto Pie e invocar su método f() (recuerda que en CLASSPATH debemos tener '.' para que busque en el directorio actual). La clase Pie y su método f() tienen acceso de paquete, por tanto, podríamos pensar que no son accesibles desde Cake. Sin embargo, si nos fijamos en las clases, no tienen ningún nombre explícito de paquete, por lo que la clase Pie y f() están disponibles para Cake. Java trata esos archivos como si fueran implícitamente parte del "paquete predeterminado" de ese directorio y por tanto, proporciona acceso de paquete a todos los archivos situados en ese directorio.
private: ¡No lo toque!
La palabra clave private significa que nadie puede acceder a ese miembro salvo la clase que lo contiene. Los miembros private están ocultos para el resto de las clases, sean del mismo paquete o no, están protegidos.
El acceso de paquete predeterminado proporciona un nivel adecuado de ocultación, éste es el acceso que normalmente se utiliza, (recuerda que no tiene ningún especificador de control de acceso). Cuando creemos una clase tendremos que pensar qué miembros definiremos como públicos para que puedan ser utilizados por el resto de las clases. Se puede pensar que private no se utiliza mucho, sin embargo un uso coherente de esta palabra clave tiene gran importancia, especialmente en el caso de programación multihebra (lo veremos en el Tema 21). Veamos un ejemplo de private:
Éste es un ejemplo en el que private resulta muy útil: queremos tener el control sobre la forma de crear un objeto y así evitar que nadie pueda acceder a un constructor concreto. Así, vemos que no podemos crear un objeto Sundae a través de su constructor sino que tenemos que invocar el método makeASundae() para que lo haga por nosotros. Además al definir el constructor como private impedirá que nadie herede esta clase.
Cualquier método que estemos seguros de que sólo funciona como "auxiliar" de la clase puede declararse como private, así evitamos utilizarlo accidentalmente en otro lugar del paquete, e impedimos modificarlo o eliminarlo. Definir un método como privado garantiza que lo podremos modificar libremente en el futuro.
Lo mismo sucede con los campos privados. A menos que tengamos que exponer la implementación subyacente (lo cual es bastante menos habitual de lo que podría pensarse), debemos definir los campos como privados. Sin embargo, el que una referencia a un objeto sea private no significa que algún otro objeto no pueda tener una referencia de tipo public al mismo objeto.
protected: acceso de herencia
La palabra clave protected trata con la herencia, que toma una clase existente (clase base) y añade nuevos miembros a esta clase sin tocar la clase existente. También se puede modificar el comportamiento de los miembros de la clase. Para heredar una clase debemos especificarlo con la palabra extends, lo vemos con un ejemplo:
Si creamos un paquete y heredamos una clase de otro paquete, sólo tendremos acceso a los miembros públicos del paquete original. Si la herencia es dentro del mismo paquete, se podrá acceder a todos los miembros con acceso de paquete. A veces el creador de la clase puede tomar un miembro concreto y garantizar el acceso a las clases derivadas pero no al mundo en general. Esto es lo que hace la palabra clave protected. Esta palabra también proporciona acceso de paquete, así, las restantes clases del mismo paquete podrán acceder a los elementos protegidos.
Vamos con un ejemplo:
Cuando se ejecuta ChocolateChip fijaos que primero entra en el constructor de Cookie y luego en el de ChocolateChip. Eso es por la herencia ya que la clase ChocolateChip hereda la clase Cookie (public class ChocolateChip extends Cookie). La herencia la veremos más adelante.
Esta clase no puede invocar el método bite() que tiene acceso de paquete.
Con la herencia se supone que los métodos de la clase Cookie existirán también para las clases que heredan de Cookie. Pero como bite() tiene acceso de paquete y está situado en un paquete distinto, no estará disponible para nosotros en el nuevo paquete. Si fuese público sí que tendríamos acceso a él. Vamos a modificar la clase Cookie:
Ahora toda la clase que herede de Cookie podrá acceder al método bite():
Aunque bite() tiene acceso de paquete, no es de tipo public.
Ejercicio 4. Demuestra que los métodos protegidos (protected) tienen acceso de paquete pero no son públicos.
Ejercicio 5. Crea una clase con campos y métodos de tipo public, private, protected y con acceso de paquete. Crea un objeto de esa clase y ve los tipos de mensajes de compilación que se obtienen cuando se intenta acceder a todos los miembros de la clase. Tenga en cuenta que las clases que se encuentran en el mismo directorio forman parte del paquete "predeterminado".
Ejercicio 6. Crea una clase con datos protegidos. Crea una segunda clase en el mismo archivo con un método que manipule los datos protegidos de la primera clase.
INTERFAZ E IMPLEMENTACIÓN
El mecanismo de control de acceso se denomina a menudo ocultación de la implementación. Envolver los datos y los métodos dentro de la clase, junto con la ocultación de la implementación se denomina a menudo encapsulación, aunque mucha gente denomina encapsulación a la ocultación de la implementación exclusivamente.
El mecanismo de control de acceso levanta una serie de fronteras dentro de un tipo de datos por dos razones. La primera es establecer qué pueden utilizar los programas cliente y qué no. Diseñamos la clase para no preocuparnos de que los programas cliente utilicen accidentalmente mecanismos internos.
La segunda razón consiste separar la interfaz de la implementación. Tenemos una clase dentro de un conjunto de programas, si los programas cliente sólo pueden enviar mensajes a la interfaz pública, podremos modificar cualquier cosa que no sea pública (miembros con acceso de paquete, protegidos o privados) sin miedo a que el código cliente deje de funcionar.
Para que las cosas resulten más claras, a la hora de crear las clases situaremos los miembros públicos al principio, seguidos de los miembros protegidos, miembros con acceso de paquete y miembros privados. Así, el usuario de la clase podrá ver al principio los miembros públicos que son realmente los que le interesan y dejar de leer en cuanto encuentre miembros no públicos que forman parte de la implementación interna.
Esto sólo facilita la lectura ya que la interfaz y la implementación siguen mezcladas, podemos ver el código fuente (la implementación). Además Javadoc no da mucha importancia a la legibilidad del código por parte de los programadores de clientes. El explorador de clases es una herramienta que examina todas las clases disponibles y muestra lo que se puede hacer con ellas de forma apropiada. En Java, visualizar la documentación del JDK con un explorador web proporciona el mismo resultado que si utilizáramos un explorador de clases.
ACCESO DE CLASE
Los especificadores de acceso también pueden emplearse para determinar qué clases de una biblioteca estarán disponibles para los usuarios de esa biblioteca. Si queremos que una clase esté disponible utilizaremos la palabra clave public. Así se controla si los programas cliente pueden siquiera crear un objeto de esa clase. Para controlar el acceso a una clase, el especificador se sitúa justo antes de la palabra clave class:
Si el nombre de la biblioteca es access, cualquier programa cliente podrá acceder a Widget mediante la instrucción:
O:
Sin embargo, existen una serie de restricciones adicionales:
1. Sólo puede haber una clase public por cada unidad de compilación (archivo). Además de esta clase puede tener tantas clases de soporte con acceso de paquete como deseemos. Si tenemos más de una clase pública el compilador generará un mensaje de error.
2. El nombre de la clase public se debe corresponder exactamente con el nombre del archivo. Para Widget el nombre del archivo deberá ser Widget.java. Si los nombres no concuerdan, tendremos un error de compilación.
3. Es posible, aunque no normal, que exista una unidad de compilación sin ninguna clase public. En este caso podremos dar al archivo el nombre que queramos.
¿Qué pasa si tenemos en access una clase que sólo utilizamos para tareas relacionadas con Widget o alguna otra clase de tipo public de access? Puede que no queramos crear la documentación para el programador de clientes y quizá más adelante queramos modificar o eliminar esta clase. Para disponer de esta flexibilidad necesitamos garantizar que ningún programa cliente dependa de detalles concretos de implementación ocultos dentro de access. Para esto basta con no incluir la palabra clave public en esta clase, así tendrá acceso de paquete.
Ejercicio 7. Crea una biblioteca usando los fragmentos de código con los que hemos descrito access y Widget. Crea un objeto Widget dentro de una clase que no forme parte del paquete access.
En una clase con acceso de paquete es conveniente definir los campos private (los campos deben ser lo más privados posible) y los métodos deben tener el mismo tipo de acceso que la clase (acceso de paquete). Una clase con acceso de paquete sólo se utiliza normalmente dentro del paquete, sólo definiremos como públicos aquellos métodos que sean imprescindibles.
Una clase no puede ser private, ya que sería inaccesible para todo el mundo salvo la propia clase, ni protected. Realmente puede haber clases internas privadas o protegidas, pero son casos especiales, lo veremos en el Tema 10, Clases internas. Sólo tenemos dos opciones para las clases: acceso de paquete o public. Si no queremos que la clase sea accesible, definiremos los constructores como privados, así no se podrá crear ningún objeto de esa clase salvo nosotros, que podremos hacerlo dentro de un miembro de tipo static de esa clase. Veamos un ejemplo:
Hasta ahora los métodos devolvían void o un tipo primitivo, por lo que si observamos:
Nos puede parecer un poco confuso. La palabra Soup1 antes del nombre del método makeSoup dice lo que el método devuelve, en este caso, una referencia a un objeto, Soup1.
Las clases Soup1 y Soup2 muestran cómo impedir la creación directa de objetos definiendo sus constructores como privados. Si no creamos un constructor se creará automáticamente el constructor predeterminado (sin argumentos). Si escribimos el constructor predeterminado garantizamos que no sea escrito automáticamente. Si definimos los constructores como privados, ¿cómo podrá usar alguien esa clase? En el ejemplo anterior hemos visto dos opciones: en Soup1 se crea un método static donde se crea un objeto Soup1 y devuelve una referencia al mismo. Esto puede ser útil si queremos realizar algunas operaciones adicionales con el objeto Soup1 antes de devolverlo, o si queremos llevar la cuenta de cuántos objetos Soup1 se han creado para limitar el número total de objetos.
Soup2 utiliza lo que se llama un patrón de diseño, de esto se habla en Thinking in Patterns (with Java) en http://www.mindview.net. Este patrón se denomina Solitario (singleton) porque sólo permite crear un objeto. En Soup2 se crea un objeto static Soup2 privado (ps1), a continuación se crea un método (access) static público que devuelve el objeto static privado (ps1).
Ejercicio 8. Siguiendo la forma del ejemplo Lunch.java, crea una clase denominada ConnectionManager que gestione una matriz fija de objetos Connection. El programa cliente no debe poder crear explícitamente objetos Connection, sino que sólo debe poder obtenerlos a través de un método estático de ConnectionManager. Cuando ConnectionManager se quede sin objetos, devolverá una referencia null. Prueba las clases con un programa main().
Ejercicio 9. Crea el siguiente archivo en el directorio access/local (dentro de tu ruta CLASSPATH):
A continuación crea el siguiente archivo en un directorio distinto de access/local:
Explica por qué el compilador crea un error. ¿Se resolvería el error si la clase Foreign fuera parte del paquete access.local?
Esto se puede lograr siguiendo un convenio adecuado. Por ejemplo, el programador de bibliotecas debe aceptar no eliminar los métodos existentes cuando modifica una clase de la biblioteca, ya que podría afectar al código del programador de clientes. La situación inversa es más complicada de resolver. En el caso de un campo, es complicado que el programador de bibliotecas sepa a qué campos han accedido los programadores de clientes. Lo mismo ocurre con métodos que forman parte de la implementación de una clase y que no están para ser usados directamente por el programador de clientes. Parece que el creador de bibliotecas tiene las manos atadas y no puede modificar nada.
Para resolver este problema en Java tenemos los especificadores de acceso, que permiten al creador de bibliotecas decir qué cosas están disponibles para el programa cliente y qué cosas no lo están. Los niveles de control de acceso de mayor a menor acceso son: public, protected, acceso de paquete y private.
Sin embargo, el concepto de biblioteca de componentes y el control acerca de quién puede acceder a los componentes de esa biblioteca no es completo. ¿Cómo se empaquetan los componentes para formar una unidad de biblioteca cohesionada? Esto se realiza mediante la palabra clave package y los especificadores de acceso se verán afectados dependiendo de que una clase se encuentre en el mismo paquete o en otro paquete distinto. Vamos a ver cómo se incluyen los componentes de la biblioteca en los paquetes y así entenderemos el significado de los especificadores de acceso.
PACKAGE: LA UNIDAD DE BIBLIOTECA
Un paquete contiene un grupo de clases, organizadas conjuntamente dentro de un mismo espacio de nombres.
Así, existe una biblioteca en Java llamada java.util. Una de las clases de java.util es ArrayList, una forma de utilizar un objeto ArrayList consistiría en especificar el nombre completo, lo vemos con un ejemplo:
//: access/FullQualification.java public class FullQualification { public static void main(String[] args) { java.util.ArrayList list = new java.util.ArrayList(); } } ///:~
Sin embargo, esto resulta pesado e incluso hará el código más complicado, la solución es utilizar la palabra clave import, así si queremos importar una clase:
//: access/SingleImport.java import java.util.ArrayList; public class SingleImport { public static void main(String[] args) { ArrayList list = new java.util.ArrayList(); } } ///:~
Ahora utilizamos ArrayList de forma directa. En este caso sólo tenemos disponible la clase ArrayList de la biblioteca java.util. Si deseamos disponer de todas las clases de esta biblioteca tendremos que importarlas mediante el símbolo '*':
import java.util.*;
La razón para efectuar las importaciones es proporcionar un mecanismo para gestionar los espacios de los nombres. Los nombres de los miembros de las clases están aislados de las clases restantes. Así un método f() de la clase A no coincidirá con un método f() que tenga la misma signatura en la clase B. El problema está en encontrarnos dos clases con el mismo nombre. Por ello es importante disponer de un control completo de los espacios de nombres en Java y así tener una combinación unívoca para cada clase.
La mayoría de los ejemplos vistos hasta ahora han sido creados para uso local y no han sido incluidos en ningún paquete, en realidad sí que están incluidos en el paquete predeterminado o "innominado". Esta solución es correcta y simple. Sin embargo, si lo que queremos es crear bibliotecas o programas que puedan cooperar con otros programas Java, tendremos que tener en cuenta las posibles colisiones entre los nombres de clases.
Los archivos de código fuente para Java se denominan unidades de compilación o de traducción, tienen la extensión .java y dentro de estas unidades puede haber una clase public con el mismo nombre del archivo (incluyendo mayúsculas y minúsculas, sin la extensión .java). Sólo puede haber una clase public en cada unidad de compilación. Si están incluidas otras clases, éstas están ocultas para el mundo exterior al paquete ya que no son public, se trata de clases soporte para la clase public principal.
Organización del código
Cuando compilamos un archivo .java obtenemos un archivo de salida para cada clase incluida en el archivo .java, estos archivos tendrán el nombre de cada clase y la extensión .class. En Java, un programa funcional está formado por un conjunto de archivos .class que se pueden empaquetar y comprimir en un archivo JAR (Java Archive) utilizando el archivador jar de Java. El intérprete de Java localiza, carga e interpreta estos archivos, (aunque en Java también existen compiladores de código nativo que generan un único archivo ejecutable).
Una biblioteca es un grupo de archivos de clase. Cada archivo fuente suele tener una clase public y un número arbitrario de clases no públicas. Para agrupar todos estos componentes, cada uno con sus propios archivos .java y .class separados, podemos utilizar la palabra clave package.
La instrucción package debe aparecer como la primera línea no de comentario en el archivo.
package access;
Con esta instrucción estamos indicando que esta unidad de compilación pertenece a la biblioteca access. Estamos especificando que la clase pública situada dentro de esta unidad de compilación está dentro de la biblioteca access, de modo que quien quiera utilizar ese nombre deberá especificarlo por completo o utilizar la palabra clave import en combinación con access, utilizando las opciones vistas anteriormente. Los nombres de los paquetes van en minúscula, incluso las palabras intermedias.
Si tenemos el archivo MyClass.java, significa que sólo puede haber una clase public en este archivo y que el nombre de esta clase es MyClass:
//: access/mypackage/MyClass.java package access.mypackage; public class MyClass { // ... } ///:~
Ahora para utilizar MyClass o cualquiera de las clases públicas de access, tendremos que utilizar la palabra clave import. Podemos especificar el nombre completamente cualificado (sin utilizar import):
//: access/QualifiedMyClass.java public class QualifiedMyClass { public static void main(String[] args) { access.mypackage.MyClass m = new access.mypackage.MyClass(); } } ///:~
Aunque lo lógico es utilizar la palabra clave import:
//: access/ImportedMyClass.java import access.mypackage.*; public class ImportedMyClass { public static void main(String[] args) { MyClass m = new MyClass(); } } ///:~
Lo que las palabras clave package e import nos permiten hacer, como diseñadores de bibliotecas, es dividir el espacio de nombres global único, para que los nombres no colisionen, independientemente de cuántas personas se conecten a Internet y empiecen a escribir clases en Java.
Creación de nombres de paquete unívocos
Un paquete está formado por varios archivos .class, por lo que el sistema de archivos puede estar un tanto abarrotado. Para evitar el desorden todos los archivos .class pertenecientes a un paquete se colocan dentro de un mismo directorio.
Ubicar los archivos de un paquete dentro de un subdirectorio resuelve otros dos problemas: creación de nombres de paquetes unívocos y localización de aquellas clases que puedan estar perdidas en algún lugar de la estructura de directorios. Esto se hace codificando la ruta correspondiente a la ubicación del archivo .class dentro del nombre del paquete. La primera parte del nombre del paquete es el nombre de dominio de Internet invertido. Como los nombres de los dominios de Internet son únicos, siguiendo este convenio nuestro paquete será únivoco y no se producirá colisión de nombres. Si no disponemos de dominio tendremos que crear una combinación de nombres lo suficientemente improbable para que no se dupliquen.
La segunda parte de la solución consiste en establecer la correspondencia entre los nombres de paquete y los directorios de la máquina, así cuando se necesite ejecutar el programa Java y cargar el .class, se pueda localizar el directorio donde está el archivo.
El intérprete Java actúa de la siguiente forma: primero localiza la variable de entorno CLASSPATH (normalmente se fija a través del sistema operativo, aunque a veces es definida por el programa de instalación Java). Esta variable contiene los directorios raíces en los que intérprete buscará los archivos .class. Comenzando por esa raíz, el intérprete tomará el nombre del paquete y sustituirá cada punto por una barra inclinada generando un nombre de ruta a partir de CLASSPATH. Así el paquete package foo.bar.baz se convertirá en foo\bar\baz o en otra cosa dependiendo del sistema operativo. Esto se concatena con las entradas que hay en la variable CLASSPATH. En ese subdirectorio será donde el intérprete busque el archivo .class que se corresponda con la clase que se está intentando crear, también buscará en algunos directorios donde reside el intérprete Java.
Así, si consideramos el nombre de dominio MindView.net, invirtiéndolo y pasándolo a minúsculas, net.mindview tenemos un nombre global unívoco para las clases. Este espacio de nombres se puede subdividir más creando, por ejemplo, una biblioteca llamada simple, así el nombre del paquete será:
package net.mindview.simple;
Este nombre de paquete puede utilizarse como espacio de nombres para los siguientes dos archivos:
//: net/mindview/simple/Vector.java // Creación de un paquete. package net.mindview.simple; public class Vector{ public Vector(){ System.out.println("net.mindview.simple.Vector"); } } ///:~
Como vemos la instrucción package es la primera línea de no comentario. El segundo archivo:
//: net/mindview/simple/List.java // Creación de un paquete. package net.mindview.simple; public class List{ public List(){ System.out.println("net.mindview.simple.List"); } } ///:~
Ambos archivos se ubicarán en el siguiete subdirectorio del sistema (Windows):
c:\DOC\JavaT\net\mindview\simple
En un sistema Linux podría ser:
/home/usuario/Documentos/JavaT/net/mindview/simple
Si observamos la ruta vemos el nombre del paquete net.mindview.simple, ¿y la primera parte de la ruta? De esa parte se encarga la variable de entorno CLASSPATH. Un ejemplo de esta variable en Windows sería:
CLASSPATH=.;D:\JAVA\LIB;C:\DOC\JavaT
En Linux:
CLASSPATH=.:/home/usuario/Documentos/JavaT:/home/usuario/JAVA/LIB;
Fijaos que en Linux separa las diferentes rutas de búsqueda con dos puntos en lugar de punto y coma.
Como vemos la variable de entorno CLASSPATH puede contener varias rutas de búsqueda.
Si tenemos archivos JAR, debemos poner el nombre real de archivo JAR en la variable de ruta y no simplemente la ruta donde está ubicado. Para un archivo JAR llamado grape.jar, la variable CLASSPATH (en Windows) sería:
CLASSPATH=.;D\JAVA\LIB;C:\flavors\grape.jar
Una vez configurada la variable, el siguiente archivo puede ubicarse en cualquier directorio:
//: access/LibTest.java // Uses the library. import net.mindview.simple.*; public class LibTest { public static void main(String[] args) { Vector v = new Vector(); List l = new List(); } } /* Output: net.mindview.simple.Vector net.mindview.simple.List *///:~
Cuando el compilador se encuentra con la instrucción import correspondiente a la biblioteca simple, comienza a buscar en los directorios especificados en CLASSPATH (C:\DOC\JavaT), en este caso el subdirectorio net/mindview/simple y luego los archivos Vector.class y List.class. Las dos clases y los métodos utilizados deben ser de tipo public.
La variable CLASSPATH en la versiones más recientes de Java funciona de forma más inteligente, se pueden compilar y ejecutar programas básicos sin configurar la variable. Para compilar los ejemplos que estamos viendo y que están disponibles en http://www.mindviewinc.com/TIJ4/CodeInstructions.html, debemos añadir a la variable CLASSPATH el directorio base del árbol de código, en esta página os explica cómo hacerlo, (en nuestro caso C:\DOC\JavaT para Windows o bien /home/usuario/Documentos/JavaT en Linux).
Ejercicio 1. Crea una clase dentro de un paquete. Crea una instancia de esa clase fuera de dicho paquete.
Colisiones
¿Qué pasaría si se importaran dos bibliotecas con '*' y ambas incluyeran los mismos nombres? Por ejemplo, un programa hace esto:
import net.mindview.simple.*; import java.util.*;
Como java.util.* contiene una clase Vector, se podría producir una colisión. Mientras no escribamos el código que produzca la colisión no pasa nada. Sin embargo, si intentamos construir un Vector, tendríamos una colisión:
Vector v=new Vector();
¿A qué clase Vector nos referimos, a la de net.mindview.simple.* o a java.util.*? El compilador no puede saberlo y generará un error. Para utilizar el Vector de java.util.* tendremos que escribir:
java.util.Vector v=new java.util.Vector();
Esto junto con la variable CLASSPATH especifica la ubicación de la clase Vector y no hará falta utilizar la instrucción import a no ser que vayamos a utilizar otra clase de java.util.
Ejercicio 2. Toma los fragmentos de código de esta sección y transfórmelos en un programa para verificar que se producen las colisiones que hemos mencionado.
Una biblioteca personalizada de herramientas
Ahora podemos crear nuestras propias bibliotecas de herramientas. En muchos de los ejercicios hemos estado utilizando un alias para System.out.println(), para escribirlo de forma más breve. Esto puede formar parte de una clase llamada Print, así disponemos de una instrucción de impresión más legible:
//: net/mindview/util/Print.java // Print methods that can be used without // qualifiers, using Java SE5 static imports: package net.mindview.util; import java.io.*; public class Print { // Print with a newline: public static void print(Object obj) { System.out.println(obj); } // Print a newline by itself: public static void print() { System.out.println(); } // Print with no line break: public static void printnb(Object obj) { System.out.print(obj); } // The new Java SE5 printf() (from C): public static PrintStream printf(String format, Object... args) { return System.out.printf(format, args); } } ///:~
Podemos utilizar estas abreviaturas para imprimir cualquier cosa.
Este archivo deberá estar en un directorio que comience con una de las ubicaciones definidas en CLASSPATH y luego continuar con net/mindview/util/Print. Después de compilar los métodos de esta clase, están disponibles para cualquier lugar del sistema utilizando una instrucción import static:
//: access/PrintTest.java // Uses the static printing methods in Print.java. import static net.mindview.util.Print.*; public class PrintTest { public static void main(String[] args) { print("Available from now on!"); print(100); print(100L); print(3.14159); } } /* Output: Available from now on! 100 100 3.14159 *///:~
Otro componente de esta biblioteca pueden ser los métodos range() que permiten el uso de la instrucción foreach para secuencias simples de enteros:
//: net/mindview/util/Range.java // Array creation methods that can be used without // qualifiers, using Java SE5 static imports: package net.mindview.util; public class Range { // Produce a sequence [0..n) public static int[] range(int n) { int[] result = new int[n]; for(int i = 0; i < n; i++) result[i] = i; return result; } // Produce a sequence [start..end) public static int[] range(int start, int end) { int sz = end - start; int[] result = new int[sz]; for(int i = 0; i < sz; i++) result[i] = start + i; return result; } // Produce a sequence [start..end) incrementing by step public static int[] range(int start, int end, int step) { int sz = (end - start)/step; int[] result = new int[sz]; for(int i = 0; i < sz; i++) result[i] = start + (i * step); return result; } } ///:~
Cualquier utilidad que se considere interesante se puede añadir a la biblioteca. Mas adelante añadiremos más componentes a net.mindview.util.
Utilización de importaciones para modificar el comportamiento
Una característica de C que no tiene Java es la compilación condicional, a través de la cual cambiando una variable indicadora tendremos un comportamiento diferente sin variar ninguna otra parte del código. El motivo de no incorporarlo a Java es que en C la mayor parte de las veces se utiliza para resolver problemas interplataforma, dependiendo de la plataforma de destino se compilan diferentes partes del código. Java está pensado para ser interplataforma así que esta característica no debería ser necesaria.
Existen otras aplicaciones de la compilación condicional. Un uso habitual es en la depuración del código. Las características de depuración se activan durante el desarrollo y se desactivan en el momento de lanzar el producto. El mismo efecto se consigue modificando el paquete que se importe dentro de nuestro programa para poder conmutar entre el código utilizado en la versión de depuración y en la versión de producción. Esta técnica puede utilizarse para cualquier código de tipo condicional.
Ejercicio 3. Crea dos paquetes: debug y debugoff, que contengan una clase idéntica con un método debug(). La primera versión debe mostrar su argumento String en la consola, mientras que la segunda no debe hacer nada. Utiliza una línea static import para importar la clase en un programa de prueba y demuestra el efecto de la compilación condicional.
Un consejo sobre los nombres de paquete
Cuando creamos un paquete estamos creando implícitamente una estructura de directorio al dar el nombre al paquete. Éste debe estar en el directorio indicado por su nombre, que deberá ser alcanzable a través de una de las rutas especificadas en la variable CLASSPATH. Cuando empezamos a utilizar package puede ser un poco frustrante encontrarnos errores a la hora de ejecutar nuestro código. Cuando nos ocurra esto, mediante un comentario desactivamos la instrucción package, si el programa funciona ya sabemos dónde está el error.
El código compilado se coloca a menudo en un directorio distinto de aquel en el que reside el código fuente, pero la ruta al código compilado deberá seguir siendo localizable por la JVM a través de la variable CLASSPATH.
ESPECIFICADORES DE ACCESO JAVA
Los especificadores de acceso Java public, protected y private, se colocan delante de la definición de cada miembro de la clase, ya sea éste un campo o un método. Cada especificador controla el acceso para esa definición concreta.
Si no se proporciona un especificador de acceso, ese miembro tendrá "acceso de paquete".
Acceso de paquete
En los ejemplos vistos hasta ahora no hemos utilizado especificadores de acceso. El acceso que no tiene asociada ninguna palabra clave se denomina acceso de paquete. Esto significa que todas las clases del paquete actual tendrán acceso a ese miembro, y para las clases situadas fuera del paquete, ese miembro será private. Las clases dentro de una misma unidad de compilación están disponibles para las otras mediante el acceso de paquete.
El acceso de paquete permite agrupar en un mismo paquete clases relacionadas, para que así puedan interactuar entre ellas. Así garantizamos que el código de ese paquete sea "propiedad" nuestra. Este tipo de acceso hace que tenga sentido agrupar las clases dentro de un paquete. En Java la forma en que se hagan las definiciones en los archivos deben ser organizadas de forma lógica. El acceso de paquete permite excluir a las clases que no queremos que tengan acceso a las clases de nuestro paquete.
Cada clase se encarga de controlar qué código tiene acceso a sus miembros. La única forma de acceder a un miembro consiste en:
1. Hacer dicho miembro public, así todo el mundo podrá acceder a él.
2. Hacer que el miembro tenga acceso de paquete, no incluyendo ningún especificador de acceso y colocando las otras clases que deban acceder a él dentro del mismo paquete. Así las clases del paquete podrán acceder a ese miembro.
3. Más adelante veremos la herencia. Una clase heredada puede acceder a los miembros protected y public, pero no a los private. Esta clase podrá acceder a los miembros con acceso de paquete si ambas pertenecen al mismo paquete.
4. Proporcionar métodos de "acceso/mutadores", denominados también "get/set" que permitan leer y cambiar el valor. Éste es el enfoque más civilizado en términos de programación orientada a objetos, y resulta fundamental en JavaBeans, según veremos en el Tema 22, Interfaces gráficas de usuario.
public: acceso de interfaz
Cuando la palabra clave public hace referencia a algún miembro significa que éste está disponible para todo el mundo. Vamos a definir un paquete dessert:
//: access/dessert/Cookie.java // Creates a library. package access.dessert; public class Cookie { public Cookie() { System.out.println("Cookie constructor"); } void bite() { System.out.println("bite"); } } ///:~
Este archivo debe colocarse en un subdirectorio llamado dessert, dentro de un directorio access que a su vez deberá estar bajo uno de los directorios CLASSPATH. En CLASSPATH debe incluirse el directorio actual con '.', ya que si no no se examinará el directorio actual.
Creamos un programa que use Cookie:
//: access/Dinner.java // Uses the library. import access.dessert.*; public class Dinner { public static void main(String[] args) { Cookie x = new Cookie(); //! x.bite(); // Can't access } } /* Output: Cookie constructor *///:~
Podemos crear un objeto Cookie porque su constructor es public, sin embargo no podemos acceder al método bite() ya que tiene acceso de paquete y sólo pueden acceder a él las clases del mismo paquete (dessert).
El paquete predeterminado
Vamos a ver el siguiente código, puede parecer que no cumple las reglas pero veremos que sí:
//: access/Cake.java // Accesses a class in a separate compilation unit. class Cake { public static void main(String[] args) { Pie x = new Pie(); x.f(); } } /* Output: Pie.f() *///:~
En un segundo archivo del mismo directorio tenemos:
//: access/Pie.java // The other class. class Pie { void f() { System.out.println("Pie.f()"); } } ///:~
Podríamos pensar que estos dos archivos no tienen nada que ver, pero a partir de la clase Cake se puede crear un objeto Pie e invocar su método f() (recuerda que en CLASSPATH debemos tener '.' para que busque en el directorio actual). La clase Pie y su método f() tienen acceso de paquete, por tanto, podríamos pensar que no son accesibles desde Cake. Sin embargo, si nos fijamos en las clases, no tienen ningún nombre explícito de paquete, por lo que la clase Pie y f() están disponibles para Cake. Java trata esos archivos como si fueran implícitamente parte del "paquete predeterminado" de ese directorio y por tanto, proporciona acceso de paquete a todos los archivos situados en ese directorio.
private: ¡No lo toque!
La palabra clave private significa que nadie puede acceder a ese miembro salvo la clase que lo contiene. Los miembros private están ocultos para el resto de las clases, sean del mismo paquete o no, están protegidos.
El acceso de paquete predeterminado proporciona un nivel adecuado de ocultación, éste es el acceso que normalmente se utiliza, (recuerda que no tiene ningún especificador de control de acceso). Cuando creemos una clase tendremos que pensar qué miembros definiremos como públicos para que puedan ser utilizados por el resto de las clases. Se puede pensar que private no se utiliza mucho, sin embargo un uso coherente de esta palabra clave tiene gran importancia, especialmente en el caso de programación multihebra (lo veremos en el Tema 21). Veamos un ejemplo de private:
//: access/IceCream.java // Demonstrates "private" keyword. class Sundae { private Sundae() {} static Sundae makeASundae() { return new Sundae(); } } public class IceCream { public static void main(String[] args) { //! Sundae x = new Sundae(); Sundae x = Sundae.makeASundae(); } } ///:~
Éste es un ejemplo en el que private resulta muy útil: queremos tener el control sobre la forma de crear un objeto y así evitar que nadie pueda acceder a un constructor concreto. Así, vemos que no podemos crear un objeto Sundae a través de su constructor sino que tenemos que invocar el método makeASundae() para que lo haga por nosotros. Además al definir el constructor como private impedirá que nadie herede esta clase.
Cualquier método que estemos seguros de que sólo funciona como "auxiliar" de la clase puede declararse como private, así evitamos utilizarlo accidentalmente en otro lugar del paquete, e impedimos modificarlo o eliminarlo. Definir un método como privado garantiza que lo podremos modificar libremente en el futuro.
Lo mismo sucede con los campos privados. A menos que tengamos que exponer la implementación subyacente (lo cual es bastante menos habitual de lo que podría pensarse), debemos definir los campos como privados. Sin embargo, el que una referencia a un objeto sea private no significa que algún otro objeto no pueda tener una referencia de tipo public al mismo objeto.
protected: acceso de herencia
La palabra clave protected trata con la herencia, que toma una clase existente (clase base) y añade nuevos miembros a esta clase sin tocar la clase existente. También se puede modificar el comportamiento de los miembros de la clase. Para heredar una clase debemos especificarlo con la palabra extends, lo vemos con un ejemplo:
class Foo extends Bar{
Si creamos un paquete y heredamos una clase de otro paquete, sólo tendremos acceso a los miembros públicos del paquete original. Si la herencia es dentro del mismo paquete, se podrá acceder a todos los miembros con acceso de paquete. A veces el creador de la clase puede tomar un miembro concreto y garantizar el acceso a las clases derivadas pero no al mundo en general. Esto es lo que hace la palabra clave protected. Esta palabra también proporciona acceso de paquete, así, las restantes clases del mismo paquete podrán acceder a los elementos protegidos.
Vamos con un ejemplo:
//: access/ChocolateChip.java // Can't use package-access member from another package. import access.dessert.*; public class ChocolateChip extends Cookie { public ChocolateChip() { System.out.println("ChocolateChip constructor"); } public void chomp() { //! bite(); // Can't access bite } public static void main(String[] args) { ChocolateChip x = new ChocolateChip(); x.chomp(); } } /* Output: Cookie constructor ChocolateChip constructor *///:~
Cuando se ejecuta ChocolateChip fijaos que primero entra en el constructor de Cookie y luego en el de ChocolateChip. Eso es por la herencia ya que la clase ChocolateChip hereda la clase Cookie (public class ChocolateChip extends Cookie). La herencia la veremos más adelante.
Esta clase no puede invocar el método bite() que tiene acceso de paquete.
Con la herencia se supone que los métodos de la clase Cookie existirán también para las clases que heredan de Cookie. Pero como bite() tiene acceso de paquete y está situado en un paquete distinto, no estará disponible para nosotros en el nuevo paquete. Si fuese público sí que tendríamos acceso a él. Vamos a modificar la clase Cookie:
//: access/cookie2/Cookie.java package access.cookie2; public class Cookie { public Cookie() { System.out.println("Cookie constructor"); } protected void bite() { System.out.println("bite"); } } ///:~
Ahora toda la clase que herede de Cookie podrá acceder al método bite():
//: access/ChocolateChip2.java import access.cookie2.*; public class ChocolateChip2 extends Cookie { public ChocolateChip2() { System.out.println("ChocolateChip2 constructor"); } public void chomp() { bite(); } // Protected method public static void main(String[] args) { ChocolateChip2 x = new ChocolateChip2(); x.chomp(); } } /* Output: Cookie constructor ChocolateChip2 constructor bite *///:~
Aunque bite() tiene acceso de paquete, no es de tipo public.
Ejercicio 4. Demuestra que los métodos protegidos (protected) tienen acceso de paquete pero no son públicos.
Ejercicio 5. Crea una clase con campos y métodos de tipo public, private, protected y con acceso de paquete. Crea un objeto de esa clase y ve los tipos de mensajes de compilación que se obtienen cuando se intenta acceder a todos los miembros de la clase. Tenga en cuenta que las clases que se encuentran en el mismo directorio forman parte del paquete "predeterminado".
Ejercicio 6. Crea una clase con datos protegidos. Crea una segunda clase en el mismo archivo con un método que manipule los datos protegidos de la primera clase.
INTERFAZ E IMPLEMENTACIÓN
El mecanismo de control de acceso se denomina a menudo ocultación de la implementación. Envolver los datos y los métodos dentro de la clase, junto con la ocultación de la implementación se denomina a menudo encapsulación, aunque mucha gente denomina encapsulación a la ocultación de la implementación exclusivamente.
El mecanismo de control de acceso levanta una serie de fronteras dentro de un tipo de datos por dos razones. La primera es establecer qué pueden utilizar los programas cliente y qué no. Diseñamos la clase para no preocuparnos de que los programas cliente utilicen accidentalmente mecanismos internos.
La segunda razón consiste separar la interfaz de la implementación. Tenemos una clase dentro de un conjunto de programas, si los programas cliente sólo pueden enviar mensajes a la interfaz pública, podremos modificar cualquier cosa que no sea pública (miembros con acceso de paquete, protegidos o privados) sin miedo a que el código cliente deje de funcionar.
Para que las cosas resulten más claras, a la hora de crear las clases situaremos los miembros públicos al principio, seguidos de los miembros protegidos, miembros con acceso de paquete y miembros privados. Así, el usuario de la clase podrá ver al principio los miembros públicos que son realmente los que le interesan y dejar de leer en cuanto encuentre miembros no públicos que forman parte de la implementación interna.
//: access/OrganizedByAccess.java public class OrganizedByAccess { public void pub1() { /* ... */ } public void pub2() { /* ... */ } public void pub3() { /* ... */ } private void priv1() { /* ... */ } private void priv2() { /* ... */ } private void priv3() { /* ... */ } private int i; // ... } ///:~
Esto sólo facilita la lectura ya que la interfaz y la implementación siguen mezcladas, podemos ver el código fuente (la implementación). Además Javadoc no da mucha importancia a la legibilidad del código por parte de los programadores de clientes. El explorador de clases es una herramienta que examina todas las clases disponibles y muestra lo que se puede hacer con ellas de forma apropiada. En Java, visualizar la documentación del JDK con un explorador web proporciona el mismo resultado que si utilizáramos un explorador de clases.
ACCESO DE CLASE
Los especificadores de acceso también pueden emplearse para determinar qué clases de una biblioteca estarán disponibles para los usuarios de esa biblioteca. Si queremos que una clase esté disponible utilizaremos la palabra clave public. Así se controla si los programas cliente pueden siquiera crear un objeto de esa clase. Para controlar el acceso a una clase, el especificador se sitúa justo antes de la palabra clave class:
public class Widget{
Si el nombre de la biblioteca es access, cualquier programa cliente podrá acceder a Widget mediante la instrucción:
import access.Widget;
O:
import access.*;
Sin embargo, existen una serie de restricciones adicionales:
1. Sólo puede haber una clase public por cada unidad de compilación (archivo). Además de esta clase puede tener tantas clases de soporte con acceso de paquete como deseemos. Si tenemos más de una clase pública el compilador generará un mensaje de error.
2. El nombre de la clase public se debe corresponder exactamente con el nombre del archivo. Para Widget el nombre del archivo deberá ser Widget.java. Si los nombres no concuerdan, tendremos un error de compilación.
3. Es posible, aunque no normal, que exista una unidad de compilación sin ninguna clase public. En este caso podremos dar al archivo el nombre que queramos.
¿Qué pasa si tenemos en access una clase que sólo utilizamos para tareas relacionadas con Widget o alguna otra clase de tipo public de access? Puede que no queramos crear la documentación para el programador de clientes y quizá más adelante queramos modificar o eliminar esta clase. Para disponer de esta flexibilidad necesitamos garantizar que ningún programa cliente dependa de detalles concretos de implementación ocultos dentro de access. Para esto basta con no incluir la palabra clave public en esta clase, así tendrá acceso de paquete.
Ejercicio 7. Crea una biblioteca usando los fragmentos de código con los que hemos descrito access y Widget. Crea un objeto Widget dentro de una clase que no forme parte del paquete access.
En una clase con acceso de paquete es conveniente definir los campos private (los campos deben ser lo más privados posible) y los métodos deben tener el mismo tipo de acceso que la clase (acceso de paquete). Una clase con acceso de paquete sólo se utiliza normalmente dentro del paquete, sólo definiremos como públicos aquellos métodos que sean imprescindibles.
Una clase no puede ser private, ya que sería inaccesible para todo el mundo salvo la propia clase, ni protected. Realmente puede haber clases internas privadas o protegidas, pero son casos especiales, lo veremos en el Tema 10, Clases internas. Sólo tenemos dos opciones para las clases: acceso de paquete o public. Si no queremos que la clase sea accesible, definiremos los constructores como privados, así no se podrá crear ningún objeto de esa clase salvo nosotros, que podremos hacerlo dentro de un miembro de tipo static de esa clase. Veamos un ejemplo:
//: access/Lunch.java // Demonstrates class access specifiers. Make a class // effectively private with private constructors: class Soup1 { private Soup1() {} // (1) Allow creation via static method: public static Soup1 makeSoup() { return new Soup1(); } } class Soup2 { private Soup2() {} // (2) Create a static object and return a reference // upon request.(The "Singleton" pattern): private static Soup2 ps1 = new Soup2(); public static Soup2 access() { return ps1; } public void f() {} } // Only one public class allowed per file: public class Lunch { void testPrivate() { // Can't do this! Private constructor: //! Soup1 soup = new Soup1(); } void testStatic() { Soup1 soup = Soup1.makeSoup(); } void testSingleton() { Soup2.access().f(); } } ///:~
Hasta ahora los métodos devolvían void o un tipo primitivo, por lo que si observamos:
public static Soup1 makeSoup(){ return new Soup1(); }
Nos puede parecer un poco confuso. La palabra Soup1 antes del nombre del método makeSoup dice lo que el método devuelve, en este caso, una referencia a un objeto, Soup1.
Las clases Soup1 y Soup2 muestran cómo impedir la creación directa de objetos definiendo sus constructores como privados. Si no creamos un constructor se creará automáticamente el constructor predeterminado (sin argumentos). Si escribimos el constructor predeterminado garantizamos que no sea escrito automáticamente. Si definimos los constructores como privados, ¿cómo podrá usar alguien esa clase? En el ejemplo anterior hemos visto dos opciones: en Soup1 se crea un método static donde se crea un objeto Soup1 y devuelve una referencia al mismo. Esto puede ser útil si queremos realizar algunas operaciones adicionales con el objeto Soup1 antes de devolverlo, o si queremos llevar la cuenta de cuántos objetos Soup1 se han creado para limitar el número total de objetos.
Soup2 utiliza lo que se llama un patrón de diseño, de esto se habla en Thinking in Patterns (with Java) en http://www.mindview.net. Este patrón se denomina Solitario (singleton) porque sólo permite crear un objeto. En Soup2 se crea un objeto static Soup2 privado (ps1), a continuación se crea un método (access) static público que devuelve el objeto static privado (ps1).
Ejercicio 8. Siguiendo la forma del ejemplo Lunch.java, crea una clase denominada ConnectionManager que gestione una matriz fija de objetos Connection. El programa cliente no debe poder crear explícitamente objetos Connection, sino que sólo debe poder obtenerlos a través de un método estático de ConnectionManager. Cuando ConnectionManager se quede sin objetos, devolverá una referencia null. Prueba las clases con un programa main().
Ejercicio 9. Crea el siguiente archivo en el directorio access/local (dentro de tu ruta CLASSPATH):
// access/local/PackagedClass.java package access.local; class PackagedClass{ public PackagedClass(){ System.out.println("Creating a packaged class"); } }
A continuación crea el siguiente archivo en un directorio distinto de access/local:
// access/foreign/Foreign.java package access.foreign; import access.local.*; public class Foreign{ public static void main(String[] args){ PackagedClass pc=new PackagedClass(); } }
Explica por qué el compilador crea un error. ¿Se resolvería el error si la clase Foreign fuera parte del paquete access.local?
En los últimos años los algoritmos de reconocimiento de huella dactilar y biométricos han realizado grandes avances.
ResponderEliminarGracias a ello, a la reducción de los costos y a las enormes ventajas que presentan los lectores de huella dactilar, las empresas cada vez más están optando por confiar en el control de acceso biométrico para sustituir los sistemas tradicionales de control de acceso.