Java – Paso a paso Tema 10
Written by lopezatienza on 05/09/2009 – 21:42 -TEMA10. Conceptos avanzados de Java
En esta Unidad Didáctica intentaremos introducir algunos conceptos más avanzados de Java, para que alumno conozca estos conceptos, su aplicación práctica y despertar la motivación y el interés para profundizar sobre ellos si lo considerase necesario.
Veremos el concepto de paquete, la entrada / salida de ficheros en Java, programación multihilo, clases abstractas e interfaces…
1. Paquetes en Java
2. Clases abstractas e interfaces
3. Entrada y Salida
4. Programación Multihilo
5. Empaquetamiento y despliegue de aplicaciones
1. Paquetes en Java
Los paquetes en Java nos proporcionan una manera de organizar el código en grupos.
Adicionalmente, ayudan a evitar colisiones en los nombres de las clases. Toda clase Java pertenece a un paquete. Si no se especifica nada, pertenece al “paquete por defecto” (que es un paquete raíz sin nombre).
Para especificar el paquete al que pertenece una clase se utiliza la palabra reservada: package.
La sentencia package tiene que ser la primera línea del fichero con el código fuente de la clase.
Declaración: package nombre_del_paquete.
El nombre de una clase no se limita al identificador utilizado en la definición, sino al Nombre de paquete + Identificador de la Clase.
Por tanto, al ir a utilizar una clase debemos conocer siempre el paquete al que pertenece para poder referenciarla porque si no el compilador no va a saber encontrarla.
Existe una convención aceptada por todos los desarrolladores en cuanto a la nomenclatura de los paquetes Java:
- Todas las palabras que componen el nombre del paquete van en minúsculas.
- Se suele utilizar el nombre de dominio invertido para intentar asegurar nombres
unívocos y evitar colisiones.
Para utilizar una clase en nuestro código tenemos que escribir su nombre completo: paquete + clase.
Pero existe otro mecanismo para facilitar la codificación que es el uso de la palabra reservada import.
Declaración de un import:
- import nombre_del_paquete.nombre_de_la_clase;
- import nombre_del_paquete.*;
Los import se ubican entre la sentencia package y la definición de la clase.
Las clases importadas de esta manera pueden ser referenciadas en el código directamente por su nombre de clase sin necesidad de escribir el paquete. Un import genérico (es decir, con el *) importa solo las clases de ese paquete, pero no de los subpaquetes.
Al igual que las clases Java tienen un reflejo en el Sistema de Archivos, lo mismo ocurre con los paquetes Java.
Los paquetes Java equivalen a directorios. Es decir, cada miembro del paquete (separado por puntos) se traduce a un directorio en el Sistema de Archivos.
Ejemplo:
package ejemplo.tema1.inf;
public class Test {…}
Tendremos la estructura ejemplo->tema1->inf->Test.java
Con el ejemplo anterior, para compilar tenemos dos opciones:
1) Desde c:\ hacemos: javac ejemplo\tema1\inf\Test.java
2) Desde c:\ejemplo\tema1\inf hacemos javac Test.java
Para ejecutar solo tenemos una opción: java ejemplo.tema1.inf.Test
NOTA: el directorio C:\ debe estar en el CLASSPATH.
Un import no implica la inclusión de código como ocurre en un #include en otros lenguajes de programación. Simplemente son vías de acceso para buscar el código. El código se va cargando según se necesita.
Las clases System, String, Math, etc.. pertenecen al paquete java.lang.*. ¿Cómo compilaban nuestros ficheros si no conocíamos los paquetes Java? Porque el compilador por defecto siempre añade la siguiente línea a nuestro código: import java.lang.*;
Ejemplo práctico:
package biblioteca;
public class Libro {
static double precio=40.00;
public static void verPrecio( )
{
System.out.println(“Precio = “+precio);
}
}
Dos formas de uso:
1) public class GestionBiblioteca{
public static void main(String[] args)
{
biblioteca.Libro.verPrecio( );
}
}
2) import biblioteca.Libro;
public class GestionBiblioteca{
public static void main(String[] args)
{
Libro.verPrecio( );
}
}
Compilación (Teniendo en cuenta que biblioteca es un directorio de C:\)
C:\> javac Biblioteca\Libro.java
C:\> javac GestionBiblioteca
Ejecución:
C:\> java GestionBiblioteca
2. Clases abstractas e interfaces
A menudo existen clases que sirven para definir un tipo genérico pero que no tiene sentido instanciar (crear objetos de ella).
Pongamos el ejemplo de un mueble. No existe una instancia de mueble. Sin embargo, si existen instancias de mesa de oficina, silla o armario. Esto es así porque el mueble representa un concepto abstracto, no tiene sentido que exista una instancia de mueble.
Estas clases pueden estar siendo usadas simplemente para agrupar bajo un mismo tipo a otras clases, o para contener código reutilizable, o para forzar un API a sus subclases…
Las clases se definen como abstractas mediante la palabra reservada abstract.
Además de clases abstractas, también podemos tener métodos abstractos. Un método abstracto significa que tiene que ser sobrescrito. No está implementado. Una clase con uno o varios métodos abstractos tiene que ser declarada abstracta. No
obstante, una clase abstracta no tiene porque tener métodos abstractos.
Un método abstracto se declara: public abstract void miMetodo( );
El objetivo de un método abstracto es forzar una interfaz (API) pero no una implementación.
Interfaces en Java
Los interfaces en Java nos solucionan en parte la no existencia de la herencia múltiple; habilitando así las posibilidades del polimorfismo en la herencia múltiple sin los permisos que esta conlleva.
Las interfaces son un tipo de clase especial que no implementa ninguno de sus métodos. Todos son abstractos. Por tanto no se pueden instanciar.
Declaración: public interface MiInterface.
Siguen siendo clases Java por lo que su código fuente se guarda en un fichero de texto con extensión .java y al compilarlo se generará un .class.
De los interfaces también se hereda, aunque se suele decir implementa. Y se realiza mediante la palabra reservada implements.
Declaración: public class MiClase implements MiInterface.
Una clase puede heredar de múltiples interfaces. Una clase puede heredar de otra clase y a la vez heredar de múltiples interfaces. Un interface puede también definir constantes. Si una clase que hereda de un interface, no implementa todos
los métodos de este, deberá ser definida como abstracta.
Un interface se trata como un tipo cualquiera. Por tanto, cuando hablamos de polimorfismo, significa que una instancia de una clase puede ser referenciada por un tipo interface siempre y cuando esa clase o una de sus superclases implemente dicho interface.
Un interface no puede implementar ningún método. Una clase puede implementar n interfaces pero solo una clase. Un interface no forma parte de la jerarquía de clases.
Haremos una clase abstracta cuando queramos definir un grupo genérico de clases y además tengamos algunos métodos implementados que reutilizar. También cuando no queramos que nadie instancia dicha clase.
Haremos un interface cuando queramos definir un grupo genérico de clases y no tengamos métodos implementados que reutilizar. O cuando nos veamos forzados por la falta de herencia múltiple en Java.
3. Entrada y Salida
La entrada / salida en Java se gestiona mediante los streams. Un stream tiene un origen y un destino, y por él fluye la información. Se lee de un origen y se escribe en un destino. Se manejan independientemente del origen o del destino, y se encuentran en el paquete java.io.*.
Se encuentran divididos en Streams de bytes o de caracteres.
Byte Streams
Leen o escriben bytes (8 bits). Para leer hay que usar la clase java.io.InputStream o cualquiera de las clases que heredan de ella. Para escribir hay que usar la clase java.io.OutputStream o cualquiera de las clases que heredan de ella.
Pero ambas clases no pueden ser instanciadas puesto que son abstractas. Se reciben como resultado de la ejecución de algún método- Suelen usarse para manejar imágenes, sonidos, …
Character Streams
Leen o escriben caracteres (16 bits). Para leer hay que usar la clase java.io.Reader o cualquiera de las clases que heredan de ella. Para escribir hay que usar la clase java.io.Writer o cualquiera de las clases que heredan de ella.
Pero ambas clases no pueden ser instanciadas puesto que son abstractas. Se reciben como resultado de la ejecución de algún método. Suelen usarse para manejar textos.
Filtros
En el paquete java.io.* tenemos una serie de clases abstractas que definen e implementan como filtrar el contenido que viene o va a un stream. Los filtros se construyen pasando como parámetro otro stream (o filtro) al que van a complementar.
Algunos filtros interesantes son:
java.io.BufferedInputStream y java.io.BufferedReader
java.io.BufferedOutputStream y java.io.BufferedWriter
java.io.DataInputStream
java.io.DataOutputStream
La clase java.lang.System tiene los siguientes atributos estáticos:
System.in: Instancia de la clase java.io.InputStream, representa la entrada estándar (teclado).
System.out: Instancia de la clase java.io.PrintStream, representa la salida estándar (pantalla).
System.err: Instancia de la clase java.io.PrintStream, representa la salida de errores (pantalla).
4. Programación Multihilo
Los threads son flujos de ejecución secuencial dentro de un proceso. Un mismo proceso Java puede tener un único thread (monotarea) o varios threads (por ejemplo el thread principal y otros secundarios, multitarea).
Casi todas las clases referentes al manejo de threads se encuentran en el paquete
java.lang.*.
java.lang.Thread
Nos ofrece el API genérico de los threads así como la implementación de su comportamiento incluyendo: arrancar, dormirse, parar, ejecutarse, esperar, gestión de prioridades.
La lógica que va a ejecutar un thread se introduce en el método: public void run( ).
Cuando termina la ejecución del método run ( ) se termina el thread. La clase java.lang.Thread contiene un método run( ) vacío.
java.lang.Runnable
Se trata de una interfaz. Simplemente fuerza la implementación de un método:
public void run( ).
Implementando un Thread
Existen dos técnicas:
- Heredar de la clase java.lang.Thread y sobrescribir el método run( ).
- Implementar el interfaz java.lang.Runnable (por tanto tenemos que implementar el método run( )) y crear una instancia de la clase
java.lang.Thread pasándole el objeto que implementa
java.lang.Runnable como parámetro.
Normalmente se usará la opción de Runnable cuando la clase que va a contener la lógica del thread ya herede de otra clase (Swing, Applets, …).
¿Cómo se usan estas clases?
public class Carrera
{
public static void main(String[] args)
{
TortugaThread tortuga=new TortugaThread( );
Thread liebre=new Thread(new LiebreThread( ));
}
}
Un thread puede pasar por varios estados durante su vida: ejecutándose, pausado o parado, muerto. Existen distintos métodos que provocan las transiciones entre estos estados.
Para crear un thread hay que instanciarlo llamando al constructor como con el resto de clases Java.
Dependiendo de cómo hayamos implementado el thread se actuará de una forma u otra:
- Si hereda de la clase java.lang.Thread, simplemente se instancia nuestra clase.
- Si implementa el interfaz java.lang.Runnable, se instancia la clase java.lang.Thread pasándole como parámetro del constructor una instancia de nuestra clase.
Para arrancar un thread hay que llamar al método start( ). Este método registra al thread en el planificador de tareas del sistema y llama al método run( ) del thread.
Ejecutar este método no significa que de forma inmediata comience a ejecutarse. Esto ya dependerá del planificador de tareas (Sistema Operativo) y del número de procesadores (Hardware).
Pueden existir distintos motivos por los que un thread puede detener temporalmente su ejecución, o pasar a un estado de pausa. Se llama a su método sleep.
Recibe un long con el número de milisegundos de la pausa. Se llama al método wait, y espera hasta recibir una señal (notify) o cumplirse un timeout definido por un long con el número de milisegundos. Se realiza alguna acción de entrada/salida. Se llama al método yield( ), que saca del procesador al thread hasta que el Sistema Operativo le vuelva a meter.
Existen distintos motivos por los que un thread puede reanudar su ejecución:
- Se consumen los milisegundos establecidos en una llamada al método sleep.
- Se recibe una señal (notify) o se consumen los milisegundos en una llamada al método wait.
- Se termina alguna acción de entrada/salida.
Un thread, por defecto, termina cuando finaliza la ejecución de su método run( ).
La manera correcta de terminar un thread es conseguir que finalice la ejecución del método run( ) mediante la implementación de algún tipo de bucle gobernado por una condición controlable. El método System.exit( ) termina la JVM, terminando también todos los threads.
Se puede conocer el estado de un thread mediante el método isAlive( ) que dirá si el thread está vivo o no.
Cuando existe un único procesador(CPU) no existe multiproceso real. Los distintos threads van compartiendo dicho procesador (CPU) siguiendo las políticas o algoritmos del Sistema Operativo. Pero estas políticas o algoritmos pueden tener en cuenta prioridades cuando realiza sus cálculos. La prioridad de un thread se establece mediante el método setPriority( ) pasándole un int entre: Thread. MAX_PRIORITY y Thread.MIN_PRIORITY.
Sincronización de threads.
Hasta ahora hemos visto threads totalmente independientes. Pero podemos tener el caso de dos threads que ejecuten un mismo método o accedan a un mismo dato. ¿Qué pasa si un thread está trabajando con un dato y llega otro y se lo cambia? Para evitar estos problemas existe la sincronización de threads que regula estas situaciones. Existen dos mecanismos de sincronización: Bloqueo del objeto (Synchronized), uso de señales (wait y notify). El tema de la sincronización de threads es muy delicado y peligroso. Se puede llegar a colgar la aplicación. La depuración de problemas provocados por una mala sincronización es muy compleja.
Para poder bloquear un objeto e impedir que otro thread lo utilice mientras está este, se emplea la palabra synchronized en la definición de los métodos susceptibles de tener problemas de sincronización: public synchronized int getNumero( ).
Cuando un thread está ejecutando un método synchronized en un objeto, se establece un bloqueo en dicho objeto. Cualquier otro thread que quiera ejecutar un método marcado como synchronized en un objeto bloqueado, tendrá que esperar a que
se desbloquee. El objeto se desbloquea cuando el thread actual termina la ejecución del método synchronized. Se creará una lista de espera y se irán ejecutando por orden de llegada. El sistema de bloqueo/desbloqueo es algo gestionado de forma automática por la JVM.
Uso de señales
Este es un sistema mediante el cual un thread puede detener su ejecución a la espera de una señal lanzada por otro thread. Para detener la ejecución y esperar a que otro thread nos envie una señal se utiliza el método: public void wait( ) o public void
wait(long timeout).
Para enviar una señal a los threads que están esperando en el objeto desde donde enviamos la señal se utiliza el método: public void notify o public void notifyAll( ).
5. Empaquetamiento y despliegue de aplicaciones
Ya tenemos nuestra aplicación compilada y funcionando. ¿Cómo se la damos a los usuarios? ¿Qué le damos exactamente a los usuarios? ¿Y si no conocemos a los usuarios? Necesitamos una forma de empaquetar las aplicaciones Java y desplegarlas a
las plataformas cliente. La primera idea es separar el código fuente de los bytecode. El código fuente no es útil para el usuario final, y además, ocupa espacio aumentando el tamaño del aplicativo. Se puede organizar el código mediante una estructura de
directorios.
Existen múltiples posibilidades a la hora de organizar los directorios. Como sugerencia proponemos lo siguiente:
- Un primer directorio raíz. Por ejemplo c:\trabajo.
- Un directorio para el código fuente. Por ejemplo c:\trabajo\src.
- Un directorio para los bytecode. Por ejemplo c:\trabajo\bin.
En el directorio src iremos creando todos los paquetes y código fuente (*.java).
Si compilamos tal cual el código, los bytecode se crearían en la estructura del código fuente. Para evitar esto, existe la opción –d con la que indicaremos el directorio donde queremos que nos genere el bytecode (*.class). java –d ..\bin *.java.
Es muy importante que el directorio de los fuentes (c:\trabajo\src) esté en el CLASSPATH para evitar posibles problemas.
Ejecutamos: java edu.upco.einf.Test. NOTA: recuerda que ambos directorios:
src y bin fueron añadidos al CLASSPATH.
Ya tenemos los fuentes separados de los bytecode. Ahora debemos empaquetar todos los bytecode para que sea más fácil el despliegue. Para ello vamos a usar los ficheros JAR (Java Archive). Están basados en el formato de los ficheros ZIP y nos
permiten empaquetar todas las clases en un único fichero. Para trabajar con estos ficheros, hay una herramienta jar.exe.
Las opciones más comunes son:
· -c: crear un fichero JAR nuevo.
· -t: listar el contenido de un fichero JAR.
· -x: extraer el contenido de un fichero JAR.
· -f: especificar el fichero JAR.
· -m: especificar un fichero “manifest”.
· -v: mostrar información del proceso por pantalla.
Ejemplos:
jar cvf mijar.jar edu
jar –tvf mijar.jar
Tanto la JVM como el compilador de Java saben buscar clases dentro de los ficheros JAR. Para poder ejecutar aplicaciones en ficheros JAR tenemos tres opciones:
- Añadir el fichero JAR al CLASSPATH: set
CLASSPATH=c:\temp\mijar.jar java edu.upco.einf.Test
- Añadir el fichero JAR al CLASSPATH en línea de ejecución: java –cp
c:\temp\mijar.jar edu.upco.einf.Test.
- Crear un fichero JAR ejecutable.
Fichero JAR ejecutable
Los ficheros JAR contienen un fichero de descripción llamado el fichero “manifest”. Al listar el contenido del fichero JAR en el ejemplo pudimos observar la entrada: META-INF/MANIFEST.MF.
Si extraemos el contenido del fichero JAR y editamos el fichero “manifest”
veremos algo como:
Manifest-Version: 1.0
Created-By: 1.4.2 02(Sun Microsystems Inc.)
Para que un fichero JAR sea ejecutable necesitamos añadir la siguiente entrada al fichero “manifest”:
Main-Class: nombre_de_clase_principal (sin .class).
Para añadir dicha línea al fichero “manifest”, crearemos un fichero texto con la línea y al crear el fichero JAR utilizaremos la opción –m para reverenciarlo: jar cvmf
manifest.txt mijar.jar *.class.
Nota: es muy importante añadir un “Intro” al final de la línea “Main-Class” para que se añada correctamente.
Para ejecutarlo, utilizaremos la opción –jar de la JVM: java –jar mijar.jar.
<< Volver al tema anterior || >> Ir al tema siguiente
Referencias:
Tags: Java
Posted in Java | No Comments »