Manual Java - C#
Manual Java - C#
Lenguajes de Programación 2
Temas de Estudio
TERCERA EDICIÓN
Lenguajes de Programación 2:
Temas de Estudio, Segunda Edición
2
3
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
E S P E C I A L I D A D D E I N G E N I E R Í A I N F O R M Á T I C A , P U C P
V
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
E S P E C I A L I D A D D E I N G E N I E R Í A I N F O R M Á T I C A , P U C P
Tabla de Contenido
Introducción a Java y C# ...........................................................................................................................2
Introducción a Java ...................................................................................................................................2
Breve Historia ........................................................................................................................................2
La Concepción de Java...........................................................................................................................2
Entorno de Trabajo de Java ....................................................................................................................3
Ciclo de Desarrollo.................................................................................................................................4
Tipos de Programas en Java ...................................................................................................................4
Compiladores, Intérpretes Puros e Intérpretes JIT .................................................................................4
Paralelo entre Java y C++.......................................................................................................................5
Introducción a la Programación..............................................................................................................7
Aplicaciones Java: Ejemplo 1 .............................................................................................................7
Compilación y Ejecución de una Aplicación Java ..............................................................................7
Aplicaciones Java: Ejemplo 2 .............................................................................................................8
Conclusiones Preliminares .....................................................................................................................9
Elementos de la Programación .............................................................................................................10
Identificadores...................................................................................................................................10
Operadores ...........................................................................................................................................10
Estructuras de Control ..........................................................................................................................11
Constantes Literales .............................................................................................................................11
El API de Java ......................................................................................................................................12
Introducción a C#....................................................................................................................................13
Breve Historia ......................................................................................................................................13
La Concepción de C# ...........................................................................................................................13
Entorno de Trabajo de C# ....................................................................................................................13
Ciclo de Desarrollo...............................................................................................................................13
Primer Programa ..................................................................................................................................16
Comparación con C++ .........................................................................................................................17
El API de .NET ....................................................................................................................................18
Arreglos y Cadenas...................................................................................................................................29
Arreglos en Java......................................................................................................................................29
Arreglos Unidimensionales ..................................................................................................................29
Arreglos Multidimensionales ...............................................................................................................30
Acceso a los Elementos de un Arreglo.................................................................................................31
Paso de un Arreglo a un Método ..........................................................................................................31
Preguntas de Repaso.............................................................................................................................32
VI
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
E S P E C I A L I D A D D E I N G E N I E R Í A I N F O R M Á T I C A , P U C P
Arreglos en C# ........................................................................................................................................ 32
Cadenas de Carácteres en Java.............................................................................................................. 34
Cadenas de Carácteres en C#................................................................................................................. 37
VII
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
E S P E C I A L I D A D D E I N G E N I E R Í A I N F O R M Á T I C A , P U C P
VIII
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
E S P E C I A L I D A D D E I N G E N I E R Í A I N F O R M Á T I C A , P U C P
Excepciones............................................................................................................................................. 144
¿Qué son las excepciones? ................................................................................................................... 144
Implementación..................................................................................................................................... 146
C++ .................................................................................................................................................... 146
Java .................................................................................................................................................... 153
C#....................................................................................................................................................... 158
Ventajas, desventajas y criterios de uso............................................................................................. 159
IX
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
E S P E C I A L I D A D D E I N G E N I E R Í A I N F O R M Á T I C A , P U C P
X
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
E S P E C I A L I D A D D E I N G E N I E R Í A I N F O R M Á T I C A , P U C P
XI
Capítulo
1
Introducción a Java y C#
El presente material presume que el lector conoce los principios de programación estructurada
y modular utilizando lenguaje C++. Este capítulo tiene como objetivo dar las bases para
que el alumno sea capaz de aplicar los mismos principios de programación pero en los
lenguajes Java y C#, dado que estos serán utilizados a lo largo de los siguientes capítulos.
Introducción a Java
Breve Historia
1991: Sun Microsystems, dentro de un proyecto interno (Green) crea un lenguaje Oak (autor:
James Gosling), destinado a lo que ellos pensaban sería el próximo gran desarrollo: dispositivos
electrónicos de consumo inteligentes. Lenguaje basado en C++.
1993: La WWW explota en popularidad. SUN encuentra nuevas posibilidades para su lenguaje,
en el desarrollo de páginas WEB con contenido dinámico. Se revitaliza el proyecto.
1995: Se anuncia JAVA. Generó interés inmediato debido al ya existente interés sobre la WWW.
La Concepción de Java
Mientras que otros lenguajes fueron concebidos para facilitar la investigación científica, la
enseñanza académica o para aplicaciones específicas, JAVA tuvo una motivación comercial.
Esto significaba que debía cumplir con objetivos tales como:
Ser fácil de aprender y de utilizar. Un lenguaje que si bien está orientado a objetos,
debe evitar todas las sofisticaciones que un lenguaje como C++ implementa y lo hacen
difícil de aprender y de utilizar.
Tener un ciclo de desarrollo rápido. El lenguaje deberá proporcionar herramientas
para todos los trabajos más comunes durante la programación, de forma que el
programador sólo se concentre en agregar nueva funcionalidad. Los ciclos de prueba y
depuración también deben ser rápidos.
Ser confiable. El lenguaje deberá llevar al desarrollador a programar de forma tal que el
programa final sea estable (sin caídas). Además, el lenguaje deberá prevenir que cualquier
acción del programador afecte al sistema operativo inestabilizándolo.
Ser seguro. Al estar orientado al desarrollo para Internet, el lenguaje deberá brindar
características de seguridad tanto en la protección del programa distribuido (contra
2
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
I N T R O D U C C I Ó N A J A V A Y C #
3
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
I N T R O D U C C I Ó N A J A V A Y C #
Ciclo de Desarrollo
El desarrollo de un programa en JAVA pasa por 5 fases. Las 2 primeras corresponden a la
creación del programa
4. Edición. Las fuentes de Java son archivos de texto con extensión JAVA. Un
programa en Java puede escribirse utilizando cualquier editor de texto. Si el archivo
contiene una clase pública, su nombre debe corresponder exactamente al nombre
de esta clase.
5. Compilación. Se utiliza un compilador para JAVA (javac.exe para Borland, jvc.exe
para Microsoft). Este programa recibe como entrada el archivo fuente (*.java) y
genera un archivo compilado con el mismo nombre que el anterior, pero con
extensión CLASS (ejemplo: Hola.class). Este archivo compilado contiene juegos de
instrucciones llamadas BYTECODE, no código maquina, por lo que no puede ser
ejecutado directamente por el sistema operativo.
Las 3 últimas corresponden a la ejecución del programa generado
6. Carga de la Clase. Lo realiza un programa Cargador de Clases que se encarga de
colocar en memoria el bytecode del archivo a procesar.
7. Verificación del BYTECODE. Lo realiza un programa Verificador de BYTECODE
que se encarga de revisar que todo el BYTECODE leído y cargado a memoria sea
válido y que no viole las restricciones de seguridad de JAVA.
8. Interpretación. Lo realiza un programa Intérprete, el cual lee el BYTECODE y lo
convierte a instrucciones en lenguaje máquina, de forma que pueda ser ejecutado.
4
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
I N T R O D U C C I Ó N A J A V A Y C #
Java es un lenguaje interpretado. Esto quiere decir que las instrucciones de un programa en Java
son traducidas por otro programa que hace de intermediario (Intérprete) entre el lenguaje Java y
el entorno de ejecución (hardware + sistema operativo). Los intérpretes leen una instrucción
Java y ejecutan su equivalente en instrucciones que pueden ser ejecutadas directamente por el
computador (código nativo).
Al comienzo, del lado del cliente sólo se tenía disponibles programas intérpretes. Sin embargo, el
costo adicional que el proceso de interpretación significa hace que un programa interpretado sea
comparativamente mucho más lento que un programa compilado. Debido a esto se
desarrollaron Compiladores Reales, de BYTECODE a código nativo, orientados a programas
en donde la máquina del cliente es conocida. El programa generado, al igual que cualquier
programa compilado a código nativo, es dependiente de la plataforma de ejecución del cliente.
Dado que la interpretación de un código cuyas instrucciones no están amarradas a ninguna
arquitectura (arquitectura neutra) es esencial para cumplir con el objetivo de portabilidad que se
buscaba con el lenguaje, y que Java fue pensado para funcionar como lenguaje para desarrollo de
aplicaciones en Internet, un ejecutable de Java tendría que ser transferido al computador en
donde se desea ver una página HTML que contenga un applet de Java. Dado que un ejecutable
Java es mucho más extenso que un compilado a BYTECODE, se crea un conflicto entre la
velocidad de ejecución y la velocidad de carga inicial de una pagina HTML que contenga un
programa applet de Java.
Una solución parcial a este problema lo resuelven los llamados Intérpretes JIT (Just-In-Time).
Un JIT funciona de la misma manera que un intérprete normal la primera vez que ejecuta cada
parte de un BYTECODE, pero genera a la par un cuasi-código-nativo de dichas instrucciones
de forma que la siguiente vez que se intente ejecutar las mismas instrucciones se utilizará el
nuevo código generado. Esto hace un poco más lenta la ejecución inicial pero hace rápida la
carga de la página HTML que contiene el applet (dado que lo que se transfiere es BYTECODE)
y acelera las sucesivas ejecuciones del programa.
C++ JAVA
5
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
I N T R O D U C C I Ó N A J A V A Y C #
Los fuentes están formados por archivos Toda la implementación se hace en un sólo
cabecera y un archivo de implementación. archivo.
Algunas semejanzas notorias entre la programación bajo C++ para Windows y JAVA se
muestran en la Tabla 1 - 2.
Tabla 1 - 2 Semejanzas entre Java y C++
C++ y JAVA
Ambos utilizan los mismos operadores aritméticos y lógicos, con las mismas reglas de
precedencia y agrupación.
La implementación de una clase (la herencia, el polimorfismo, etc.) son muy similares.
El juego de palabras reservadas y su uso es muy similar, por ejemplo, los objetos pueden
hacer referencia a si mismos mediante la palabra clave this.
Tienen la misma implementación de las estructuras de control (if, if/else, while, do/while,
switch, for).
Los modificadores de ámbito de acceso son muy similares en su uso y reglas (public, private,
protected, etc.).
6
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
I N T R O D U C C I Ó N A J A V A Y C #
Introducción a la Programación
Las siguientes secciones dan una introducción a la programación en Java y C# mediante
ejemplos de programas básicos.
Aplicaciones Java: Ejemplo 1
Para mostrar los detalles más básicos sobre la programación en Java analizaremos una aplicación
Java simple que muestre un mensaje.
Los fuentes de un programa en Java son archivos de texto con extensión JAVA, por lo que el
siguiente código puede ingresarse y guardarse con cualquier editor de texto disponible.
// Archivo: Aplicacion1.java
/* Descripción:
El programa muestra un mensaje en pantalla
*/
public class Aplicacion1 {
public static void main( String args[ ] ) {
System.out.println( "Primer programa en Java" );
}
}
Los comentarios en un archivo fuente de Java siguen la misma sintaxis y reglas que C/C++.
Al igual que C/C++, Java especifica un juego de palabras reservadas. Las palabras class, public,
void, etc. son reservadas por Java y no deben de utilizarse para definir identificadores. Todas las
palabras reservadas de Java están en minúscula.
En el ejemplo, las palabras Aplicacion1, main, System, out, println, String, etc. son
identificadores.
La sintaxis para especificar literales (numéricos, caracteres y cadenas de caracteres) es la misma
que C/C++. En el ejemplo anterior se especifica un literal tipo cadena.
Las sentencias en Java siguen las mismas reglas que C/C++. Una sentencia siempre debe
finalizar con un ‘;’; sin embargo, note que Java no requiere que se coloque un ‘;’ al final de la
implementación de la clase.
La implementación de una clase en Java es, en términos generales, muy similar a C++.
A continuación analizaremos los aspectos de este programa referentes a la programación en
Java.
Compilación y Ejecución de una Aplicación Java
Para compilar un fuente JAVA se puede utilizar el compilador correspondiente al IDE o
entorno de trabajo con que se cuente, por ejemplo:
JVC.EXE Correspondiente al IDE de Microsoft
JAVAC.EXE Correspondiente al IDE de Borland y al SDK de Sun.
En el caso de contar con un IDE, normalmente es más sencillo dejar que éste se encargue de la
tarea de compilación en vez de realizarla nosotros mismos desde la línea de comandos; sin
embargo, por motivos didácticos, realizaremos esto último.
En nuestro ejemplo, para compilar el archivo Aplicacion1.java se puede utilizar la siguiente
instrucción:
C:\>javac.exe Aplicacion1.java
7
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
I N T R O D U C C I Ó N A J A V A Y C #
Esta instrucción utiliza el compilador de Borland (parte de su IDE para Java, JBuilder) para
compilar el archivo editado. Si la compilación fue exitosa, se genera el correspondiente archivo:
Aplicacion1.class.
Para ejecutar un archivo CLASS obtenido de la compilación de un archivo fuente JAVA
necesitaremos correr el intérprete de Java y pasarle como parámetro nuestro archivo compilado.
Nuevamente, se puede utilizar el intérprete correspondiente al IDE o entorno de trabajo con
que se cuente, por ejemplo:
JVIEW.EXE y WJVIEW.EXE Correspondiente al IDE de Microsoft
JAVA.EXE y APPLETVIEWER.EXE Correspondiente al IDE de Borland y al SDK
de Sun.
En nuestro ejemplo, para ejecutar el archivo Aplicacion1.class se puede utilizar la siguiente
instrucción:
C:\>java.exe Aplicacion1
Note que el ejemplo anterior corresponde a una aplicación de consola. El archivo implementado
contiene un método “main” que es el punto de entrada que utilizará el intérprete cuando lo que
se ejecuta es una aplicación Java. El intérprete llamará a éste método y éste será el encargado de
ejecutar todas las acciones de la aplicación. A las clases para una aplicación Java que contengan el
método “main” se les podrá utilizar como punto de inicio de la aplicación. Debido a esto, a estas
clases se les llama clases ejecutables. El intérprete no crea un objeto Aplicacion1, su única
responsabilidad es llamar al método estático “main” de esta clase (lo que, al igual que C/C++,
no requiere que se cree el objeto para ser llamado). El método “main” será el encargado de crear
los objetos necesarios para realizar las tareas de la aplicación.
Aplicaciones Java: Ejemplo 2
El siguiente ejemplo agrega utiliza algunas características adicionales comunes a los programas
en Java.
// Archivo: Aplicacion2.java
// Descripción:
// Se utiliza una caja de dialogo para mostrar un mensaje.
import javax.swing.JOptionPane;
class Aplicacion2 {
public static void main( String args[ ] ) {
String sMensaje;
8
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
I N T R O D U C C I Ó N A J A V A Y C #
System.exit( 0 );
}
La aplicación anterior utiliza una caja de diálogo para mostrar un mensaje, en lugar de hacerlo
por la salida estándar, la consola.
Note que los operadores, las estructuras de control, la declaración de variables y su inicialización,
la declaración de los métodos y sus parámetros, la llamada a métodos y el paso de parámetros, el
retorno de valores de un método y las operaciones de tipo cast para conversión entre tipos
primitivos son iguales a C++.
El ejemplo utiliza una variable de tipo arreglo. Los detalles acerca del uso de arreglos se verán
más adelante. También utiliza la clase JOptionPane que pertenece a la librería “javax.swing”, por
lo que una sentencia “import” al inicio del archivo debe de ser agregada indicándole al
compilador dónde se define la clase que estoy utilizando. Las librerías en Java se denominan
“paquetes”.
La clase JOptionPane permite mostrar ventanas con mensajes y botones de selección. Siempre
que se utilicen elementos gráficos (como las ventanas) se deberá finalizar una aplicación
llamando al método System.exit, el cual recibe como parámetro un número entero que
representa el valor de retorno de la aplicación. Estos valores de retorno son comúnmente
utilizados en archivos de procesamiento por lotes, como los archivos BAT de Windows, de la
misma forma que el valor de retorno de la función “main” de un programa en C/C++.
Conclusiones Preliminares
Por lo revisado en los 2 simples códigos de ejemplo anteriores, todo programa en Java consiste
básicamente en la definición de una o más clases. Nada está fuera de las clases.
Aunque no se vio en los ejemplos, cada archivo Java puede contener la implementación de más
de una clase, sin embargo sólo una de ellas puede ser pública (y su nombre debe corresponder
exactamente al del archivo en donde está definida), esto es, declarada como public. El uso de
estos modificadores de acceso se verá más adelante.
9
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
I N T R O D U C C I Ó N A J A V A Y C #
Elementos de la Programación
Identificadores
Todo identificador es un conjunto de caracteres, sin espacios, que no pueden comenzar con un
dígito y que pueden contener:
Letras (a-z, A-Z).
Dígitos (0-9).
Los caracteres ‘$’ y ‘_’.
El carácter ‘$’ debe evitarse dado que es usado por el compilador para representar internamente
los identificadores.
Al igual que C/C++, Java es sensitivo a las mayúsculas y minúsculas en el nombre de los
identificadores.
Operadores
Tabla 1 - 3 Operadores de Java
Operadores Aritméticos: Son operadores binarios (actúan sobre 2 operandos). El tipo del
valor resultado depende del tipo de dato de los operandos, siguiendo las reglas de promoción de
tipos.
Operadores Lógicos: Trabajan sobre operandos booleanos o expresiones que al ser evaluadas
devuelvan un booleano. El resultado es un booleano. Siguen las reglas de evaluación de circuito
corto.
&& Y || O inclusivo ! NO
Operadores Lógicos Booleanos: Trabajan a nivel de bits sobre operandos de tipo primitivo e
integral. Si los operandos son de diferente tipo (por ejemplo int y byte) el resultado es de un tipo
promovido (byte se promociona a int y el resultado es de tipo int). Cuando los operandos son
booleanos, estos operadores se comportan como los Operadores Lógicos. Siguen las reglas de
evaluación de circuito largo.
10
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
I N T R O D U C C I Ó N A J A V A Y C #
++ Incrementa en -- Decrementa en
uno el uno el
operando operando
Al igual que en C++, en una expresión con varios operadores, se aplica la misma regla de
precedencia. Cuando se utilizan varios operandos con iguales o distintos operadores, se aplica la
misma regla de agrupación de C++.
Estructuras de Control
Existen las mismas estructuras de control que en C++, con la misma sintaxis y las reglas de
evaluación. Éstas son:
Dentro de los bucles (for, while, do / while) se pueden utilizar los controladores de flujo:
Nótese que a diferencia de C++, existen versiones con etiqueta para cada controlador. El detalle
de estas características de Java escapa del alcance del curso.
Constantes Literales
Se siguen las mismas reglas que C++. Los sufijos para los literales numéricos, al igual que C++,
modifican el tipo por defecto de éstos. La Tabla 1 - 4 muestra ejemplo de declaración de
constantes para diferentes tipos de datos.
Los literales de tipo cadena aceptan los caracteres de escape de C: “\n”, “\t”, “\r”, “\\”, “\””.
Tabla 1 - 4 Ejemplos de constantes literales en Java
178 int
8864L long
37.266 double
37.266D double
87.363F float
26.77e3 double
'c' char
11
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
I N T R O D U C C I Ó N A J A V A Y C #
trae boolean
El API de Java
Java cuenta con una extensa gama de clases predefinidas. A este conjunto de clases, agrupados
en paquetes, se le conoce como el API de Java.
Algunos paquetes más usados del API de JAVA se muestran en la Tabla 1 - 5.
Nombre Descripción
java.awt Java Abstract Window Toolkit. Contiene los componentes básicos para
crear y manejar aplicaciones con interfaz gráfica de usuario (GUI).
java.awt.event Clases e interfaces que permiten manejar eventos para los componentes de
este paquete.
javax.swing Clases, interfaces y otros sub-paquetes que permiten utilizar el nuevo juego
de componentes SWING. Reemplazan a muchos de los elementos
proporcionados por los paquetes (y sub-paquetes bajo éstos) java.applet y
java.awt.
12
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
I N T R O D U C C I Ó N A J A V A Y C #
Introducción a C#
Breve Historia
.Net es una estrategia que desarrolla Microsoft para el fácil desarrollo y manejo de aplicaciones
uso de Web Services. Para el flujo de información a través de sistemas y dispositivos
interconectados.
Los servicios Web son módulos de software autodescriptivo que encapsula funcionalidad que es
encontrada y accedida a través de protocolos estándares de comunicación como SOAP y XML.
La Concepción de C#
C# es un lenguaje orientado a objetos que se basa en la familia de lenguajes basados en C como
C++ y Java.
C# está estandarizado según ECMA e ISO/IEC y sus compiladores también han sido
desarrollados de acuerdo a estos estándares.
A pesar de ser orientado a objetos también tiene soporte para componentes permitiendo
desarrollar ensamblajes auto descriptivos.
Entre sus características incluye un recolector de basura que permite gestionar de manera
automática la memoria. También permite la utilización de excepciones para poder controlar
errores y desarrollar código de recuperación. Finalmente permite el manejo de código seguro
haciendo imposible la utilización de variables no inicializadas, la señalización de índices fuera de
los límites de un arreglo y los casteos no verificados.
Entorno de Trabajo de C#
Aunque el desarrollo de un programa en C#, al igual que en Java, puede realizarse utilizando
únicamente el Kit de desarrollo, existen muchos entornos de desarrollo integrados (IDE) que
facilitan este proceso. Algunos de los IDE’s utilizados en nuestro medio son:
Visual Studio IDE de Microsoft
SharpDevelop IDE de uso libre.
El SDK consiste principalmente en:
Las librerías que implementan el lenguaje
El resto de librerías estándar
Un compilador
Uno o más intérpretes
Programas utilitarios adicionales
Ciclo de Desarrollo
El proceso de ejecución administrada incluye los pasos siguientes:
1. Elegir un compilador.
Para obtener los beneficios que proporciona Common Language Runtime, se deben utilizar uno
o más compiladores de lenguaje orientados al tiempo de ejecución.
13
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
I N T R O D U C C I Ó N A J A V A Y C #
Para aprovechar las ventajas que ofrece Common Language Runtime, se deben utilizar uno o
varios compiladores de lenguaje orientados al tiempo de ejecución, como Visual Basic, C#,
Visual C++, Jscript o uno de los muchos compiladores de otros fabricantes como un
compilador Eiffel, Perl o COBOL.
Como se ejecuta en un entorno multilenguaje, el motor en tiempo de ejecución es compatible
con una gran variedad de tipos de datos y características de lenguajes. El compilador de lenguaje
utilizado determina las características en tiempo de ejecución que están disponibles y el código se
diseña con esas características. El compilador, y no el motor en tiempo de ejecución, es el que
establece la sintaxis que se debe utilizar en el código. Si todos los componentes escritos en otros
lenguajes deben ser totalmente capaces de utilizar un componente, los tipos exportados de ese
componente deben exponer sólo las características del lenguaje incluidas en Common Language
Specification (CLS).
2. Compilar el código a Lenguaje intermedio de Microsoft (MSIL).
Cuando se compila a código administrado, el compilador convierte el código fuente en Lenguaje
intermedio de Microsoft (MSIL), que es un conjunto de instrucciones independiente de la CPU
que se pueden convertir de forma eficaz en código nativo. MSIL incluye instrucciones para
cargar, almacenar, inicializar y llamar a métodos en los objetos, así como instrucciones para
operaciones lógicas y aritméticas, flujo de control, acceso directo a la memoria, control de
excepciones y otras operaciones. Antes de poder ejecutar código, se debe convertir MSIL al
código específico de la CPU, normalmente mediante un compilador Just-In-Time (JIT).
Common Language Runtime proporciona uno o varios compiladores JIT para cada arquitectura
de equipo compatible, por lo que se puede compilar y ejecutar el mismo conjunto de MSIL en
cualquier arquitectura compatible.
Cuando el compilador produce MSIL, también genera metadatos. Los metadatos describen los
tipos que aparecen en el código, incluidas las definiciones de los tipos, las firmas de los
miembros de tipos, los miembros a los que se hace referencia en el código y otros datos que el
motor en tiempo de ejecución utiliza en tiempo de ejecución. El lenguaje intermedio de
Microsoft (MSIL) y los metadatos se incluyen en un archivo ejecutable portable (PE), que se
basa y extiende el PE de Microsoft publicado y el formato Common Object File Format
(COFF) utilizado tradicionalmente para contenido ejecutable. Este formato de archivo, que
contiene código MSIL o código nativo así como metadatos, permite al sistema operativo
reconocer imágenes de Common Language Runtime. La presencia de metadatos junto con el
Lenguaje intermedio de Microsoft (MSIL) permite crear códigos autodescriptivos, con lo cual las
bibliotecas de tipos y el Lenguaje de definición de interfaces (IDL) son innecesarios. El motor en
tiempo de ejecución localiza y extrae los metadatos del archivo cuando son necesarios durante la
ejecución.
3. Compilar MSIL a código nativo.
Para poder ejecutar el lenguaje intermedio de Microsoft (MSIL), primero se debe convertir éste,
mediante un compilador Just-In-Time (JIT) de .NET Framework, a código nativo, que es el
código específico de la CPU que se ejecuta en la misma arquitectura de equipo que el
compilador JIT. Common Language Runtime proporciona un compilador JIT para cada
arquitectura de CPU compatible, por lo que los programadores pueden escribir un conjunto de
MSIL que se puede compilar mediante un compilador JIT y ejecutar en equipos con diferentes
arquitecturas. No obstante, el código administrado sólo se ejecutará en un determinado sistema
operativo si llama a las API nativas específicas de la plataforma o a una biblioteca de clases
específica de la plataforma.
14
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
I N T R O D U C C I Ó N A J A V A Y C #
La compilación JIT tiene en cuenta el hecho de que durante la ejecución nunca se llamará a parte
del código. En vez de utilizar tiempo y memoria para convertir todo el MSIL de un archivo
ejecutable portable (PE) a código nativo, convierte el MSIL necesario durante la ejecución y
almacena el código nativo resultante para que sea accesible en las llamadas posteriores. El
cargador crea y asocia un código auxiliar a cada uno de los métodos del tipo cuando éste se
carga. En la llamada inicial al método, el código auxiliar pasa el control al compilador JIT, el cual
convierte el MSIL del método en código nativo y modifica el código auxiliar para dirigir la
ejecución a la ubicación del código nativo. Las llamadas posteriores al método compilado
mediante un compilador JIT pasan directamente al código nativo generado anteriormente,
reduciendo el tiempo de la compilación JIT y la ejecución del código.
El motor en tiempo de ejecución proporciona otro modo de compilación denominado
generación de código en el momento de la instalación. El modo de generación de código en el
momento de la instalación convierte el MSIL a código nativo, tal y como lo hace el compilador
JIT normal, aunque convierte mayores unidades de código a la vez, almacenando el código
nativo resultante para utilizarlo posteriormente al cargar y ejecutar el ensamblado. Cuando se
utiliza el modo de generación de código durante la instalación, todo el ensamblado que se está
instalando se convierte a código nativo, teniendo en cuenta las características de los otros
ensamblados ya instalados. El archivo resultante se carga e inicia más rápidamente que si se
hubiese convertido en código nativo con la opción JIT estándar.
Como parte de la compilación MSIL en código nativo, el código debe pasar un proceso de
comprobación, a menos que el administrador haya establecido una directiva de seguridad que
permita al código omitir esta comprobación. En esta comprobación se examina el MSIL y los
metadatos para determinar si el código garantiza la seguridad de tipos, lo que significa que el
código sólo tiene acceso a aquellas ubicaciones de la memoria para las que está autorizado. La
seguridad de tipos ayuda a aislar los objetos entre sí y, por tanto, ayuda a protegerlos contra
daños involuntarios o maliciosos. Además, garantiza que las restricciones de seguridad sobre el
código se aplican con toda certeza.
El motor en tiempo de ejecución se basa en el hecho de que se cumplan las siguientes
condiciones para el código con seguridad de tipos comprobable:
La referencia a un tipo es estrictamente compatible con el tipo al que hace referencia.
En un objeto sólo se invocan las operaciones definidas adecuadamente.
Una identidad es precisamente lo que dice ser.
Durante el proceso de comprobación, se examina el código MSIL para intentar confirmar que el
código tiene acceso a las ubicaciones de memoria y puede llamar a los métodos sólo a través de
los tipos definidos correctamente. Por ejemplo, un código no permite el acceso a los campos de
un objeto si esta acción sobrecarga las ubicaciones de memoria. Además, el proceso de
comprobación examina el código para determinar si el MSIL se ha generado correctamente, ya
que un MSIL incorrecto puede llevar a la infracción de las reglas en materia de seguridad de
tipos. El proceso de comprobación pasa un conjunto de código con seguridad de tipos definido
correctamente, y pasa de forma exclusiva código con seguridad de tipos. No obstante, algunos
códigos con seguridad de tipos no pasan la comprobación debido a las limitaciones de este
proceso, y algunos lenguajes no producen código con seguridad de tipos comprobable debido a
su diseño. Si la directiva de seguridad requiere código con seguridad de tipos y el código no pasa
la comprobación, se produce una excepción al ejecutar el código.
4. Ejecutar código.
15
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
I N T R O D U C C I Ó N A J A V A Y C #
Primer Programa
El siguiente es un programa simple, en modo consola, que escribe un mensaje en pantalla.
using System;
class MainClass {
public static void Main(string[ ] args) {
Console.WriteLine("Hello World!");
}
}
Este código se coloca dentro de un archivo de texto, comúnmente con extensión CS, por
ejemplo “Introduccion.cs”. Una vez grabado, se puede compilar desde una ventana de
comandos utilizando el compilador de C#, el programa CSC.EXE instalado como parte de
.NET Framework SDK, de la siguiente forma:
csc Introduccion.cs
16
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
I N T R O D U C C I Ó N A J A V A Y C #
El formato de definición de una clase, si bien muy similar a C++, presenta algunas diferencias
claras:
Todos los miembros de una clase, datos y funciones, se declaran con todos sus
modificadores de manera individual, por lo que estos modificadores sólo afectan a dicho
elemento.
No se finaliza una declaración de clase con un símbolo ‘;’.
El formato general de declaración de un método es:
[modificadores] <tipo de retorno> <nombre del método>( [parámetros] ) {
<cuerpo del método>
}
Los modificadores especifican características de los métodos, por ejemplo los modificadores de
acceso public, private y protected. El uso de los modificadores se verá más adelante.
El método Main es el punto de entrada de todo programa ejecutable en C#. Existen varios
formatos, según se desee retornar un valor o manejar los argumentos ingresados en la línea de
comando. Este método se define como estático, con el modificador static, debido a que el
intérprete de ..NET, el CLR, accederá a él sin instanciar un objeto de la clase dentro de la cual se
define. Dado que este acceso se realiza desde fuera de la clase, el método Main debe definirse
como público, con el modificador public.
El cuerpo del método Main imprime un mensaje en una ventana de consola, utilizando el
método estático WriteLine de la clase Console. Más adelante se verá el tema de la entrada y
salida de datos.
17
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
I N T R O D U C C I Ó N A J A V A Y C #
El operador ‘&’ desaparece, tanto para obtener una dirección de memoria como para
definir una variable tipo referencia al estilo de C++.
El operador ‘*’ para indireccionamiento desaparece.
Se agregan los siguientes operadores:
checked y unchecked: Sirven para verificar errores de desbordamiento.
is: Sirve para verificar si un objeto de datos puede ser interpretado como un tipo de dato
en particular.
Los controles de flujo de C++ se utilizan en C# con la misma sintaxis pero con los
siguientes cambios:
Las expresiones que evalúan una condición deben retornar un tipo de dato booleano.
El control de flujo switch soporta literales de texto.
Se define el control de flujo foreach y using.
Se define el bloque finally, como un bloque de ejecución obligatoria, para el manejo de
excepciones.
El API de .NET
.NET cuenta con una extensa gama de clases predefinidas. A este conjunto de clases, agrupados
en espacios de nombres.
Algunos espacios de nombres más usados se muestran en la Tabla 1 - 6.
Tabla 1 - 6 Espacios de Nombres
Nombre Descripción
System.Collections Contiene interfaces y clases que definen diversas colecciones de objetos, tales
como listas, colas, matrices, tablas hash y diccionarios.
18
1
Capítulo
2
Tipos de Datos
La programación orientada a objetos (POO) lleva al programador a centrar su esfuerzo en
el desarrollo de tipos de datos. Este capítulo es un breve resumen de ideas acerca de la teoría
general de los tipos de datos, lo que dará al lector una base teórica útil antes de entrar al tema
de la POO.
Conceptos Previos
Un lenguaje de programación se implementa construyendo un traductor, el cual traduce los
programas escritos en dicho lenguaje de programación, a programas escritos en lenguaje
máquina o algún otro lenguaje más cercano al lenguaje máquina.
Se distinguen los siguientes tipos de computadoras:
Computadora: Conjunto integrado de algoritmos y estructuras de datos capaz de
almacenar y ejecutar programas.
Computadora real o de hardware: Formada por dispositivos físicos.
Computadora simulada por software: Formada por software que se ejecuta en otra
computadora.
Computadora virtual: Formada por partes de hardware y de software. Es la que ejecuta
los programas traducidos por el traductor.
24
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
T I P O S D E D A T O S
Objetos de Datos
Mientras que los datos almacenados en la memoria del computador tienen una estructura
simple, tratados como bytes, los datos almacenados en una computadora virtual tienen una
estructura compleja, tratados como pilas, arreglos, etc.
Un objeto de datos es un dato o una agrupación de datos que existe en una computadora
virtual durante su ejecución.
Un programa en ejecución utiliza muchos objetos de datos. Estos objetos de datos y sus
interrelaciones cambian dinámicamente durante la ejecución.
Según quién los define, los objetos de datos se pueden clasificar en:
Definidos por el programador: Los crea y manipula explícitamente, a través de
declaraciones y enunciados.
Definidos por el sistema: Los crea la computadora virtual (comúnmente, de manera
automática para el programador, según se requieran) para “mantenimiento” durante la
ejecución del programa, y a los cuales el programador no tiene acceso directo, como por
ejemplo: pilas de almacenamiento en tiempo de ejecución, registros de activación de
subprogramas, memorias intermedias de archivos y listas de espacio libre.
Los objetos de datos funcionan como contenedores de valores de datos, por ejemplo: un
número, un carácter, un apuntador a otro objeto de datos, etc.
Es fácil confundir objeto de datos y valores de datos. La distinción se aprecia mejor por su
implementación: El primero representa un almacenamiento en la memoria del computador;
mientras que el segundo; un patrón de bits.
Los objetos de datos tienen un tiempo de vida durante el cual pueden usarse para guardar y
recuperar valores de datos.
Los objetos de datos se caracterizan por un conjunto de atributos, los que determinan el
número y tipos de valores que pueden contener, así como la organización lógica de estos
valores. Los atributos no varían durante el tiempo de vida de un objeto de datos.
Los objetos de datos participan en enlaces durante su tiempo de vida, algunos de los cuales
pueden cambiar durante este tiempo. Estos enlaces son:
Localidad: Es la posición que ocupa en la memoria.
Valor: Por lo general resulta de una operación de asignación.
Nombre: Se establece mediante declaraciones y se modifica mediante llamadas y
devoluciones de programas.
Componente: Enlace de un objeto de datos a otros de los que forma parte. Se suele
representar a través de un apuntador.
Un objeto de datos es elemental si su valor de datos se manipula como una unidad, y es una
estructura de datos si es un agregado de otros objetos de datos.
25
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
T I P O S D E D A T O S
Variables y Constantes
Una variable es un objeto de datos definido y nombrado explícitamente en un programa. Si el
objeto de datos es elemental, la variable es simple. Por lo común, el(los) valor(es) de una
variable es(son) modificable(s), mediante operaciones de asignación.
Una constante es un objeto de datos, definido y nombrado explícitamente en un programa, y
enlazado permanentemente a un valor de datos durante su tiempo de vida.
Una constante literal es una constante cuyo nombre es la representación por escrito de su
valor.
Una constante definida por el programador es una constante cuyo nombre es elegido por el
programador.
Dada la característica de una constante, el traductor puede utilizar esta información para realizar
optimizaciones.
Tipo de Datos
Un tipo de dato es una clase de objeto de datos ligados a un conjunto de operaciones para
crearlos y manipularlos.
Todo lenguaje tiene un conjunto de tipos primitivos (o tipos predefinidos) de datos que están
integrados al lenguaje. Un lenguaje puede proveer recursos para definir nuevos tipos de datos,
llamados tipos definidos por el programador (o bien tipos definidos por el usuario).
Un sub-tipo de datos se define a partir de un tipo de dato o súper-tipo, donde su conjunto de
valores posibles es un subconjunto del súper-tipo.
Los tipos referencias son aquellos cuyos objetos de datos apuntan a otros objetos de datos.
Cuando lo que guarda una referencia es una dirección de memoria, se le llama puntero.
Los elementos básicos de la especificación de un tipo de datos son:
Los atributos que distinguen objetos de datos de ese tipo.
Los valores que los objetos de datos de ese tipo pueden tener.
Las operaciones que se pueden realizar con los objetos de datos de ese tipo.
Ejemplo: Para un arreglo de enteros tendríamos:
Atributos: Número de dimensiones, rango de los índices de cada dimensión, tipo de
dato de los componentes, es decir, entero.
Valores: El conjunto de los números enteros soportados por el tipo de datos entero.
Operaciones: Sub-indización para seleccionar componentes del arreglo, creación de un
arreglo, modificación de las dimensiones, etc.
Los elementos básicos de la implementación de un tipo de datos son:
La representación de almacenamiento usada para representar los objetos de datos de
ese tipo.
Los algoritmos, procedimientos u operaciones que manipulan la representación de
almacenamiento elegida.
26
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
T I P O S D E D A T O S
Por último, un tipo de datos tiene una representación sintáctica, en la que, tanto la
especificación como la implementación son en gran medida independientes.
Ejemplo: Para un objeto de datos de un tipo en particular:
Los atributos se representan con declaraciones o definiciones de tipo.
Los valores se representan con constantes.
Las operaciones se representan con símbolos especiales, procedimientos integrados o
funciones.
Esta información suele ser utilizada por los traductores para determinar el tiempo de creación de
enlaces, la representación de almacenamiento a utilizar, revisar errores de tipos, etc.
Tipos de Datos Primitivos
Los lenguajes de programación suelen tener un conjunto de tipos de datos primitivos
elementales, como son: entero, real, carácter, booleano, de enumeración y apuntador, entre
otros. La Tabla 2 - 1 muestra algunos de los tipos de datos primitivos elementales más comunes
de los lenguajes de programación utilizados a lo largo del curso.
Tabla 2 - 1 Tipos de datos primitivos elementales
Es interesante notar que si bien los tipos de datos utilizados para representar texto (las clases
string de C++, String de Java y string de C#) forman parte de la librería estándar de cada
lenguaje, se les deben considerar primitivos estructurados, pues son clases y por tanto, contienen
una estructura interna.
27
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
T I P O S D E D A T O S
Note que Java utiliza 2 bytes para representar un carácter. Esto se debe a que Java maneja
carácteres siguiendo el estándar UNICODE.
Cuando en una expresión o una sentencia de asignación se mezclan datos primitivos de distinto
tipo, cada evaluación de un operador requiere una homogenización de sus operandos, lo que se
realiza siguiendo las reglas de Promoción de Tipos que se muestran en la Tabla 2 - . Estas reglas
de promoción se realizan automáticamente dado que no causan pérdida de información. Sin
embargo, una conversión puede ser forzada mediante una operación cast, por ejemplo:
long x = 10;
int y = (int)x;
Tabla 2 - 3 Reglas de promoción de tipos de datos en Java
float double
28
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
T I P O S D E D A T O S
Al igual que C/C++, este tipo de operaciones puede significar una pérdida de información.
Tipos de Datos en C#
C#, al igual que todos los lenguajes de programación compatibles con ..NET, basan sus tipos de
datos en el sistema de tipos comunes de ..NET o Common Type System (CTS). Este sistema
incluye tanto tipos simples (int, char, float, etc.) como complejos (como string y decimal). Todos
estos tipos de datos son realmente clases, las que tienen métodos para acciones comunes, por
ejemplo: conversión a texto, serialización, identificación del tipo en tiempo de ejecución,
conversión a otros tipos, etc.
C# es un lenguaje fuertemente tipificado, a diferencia de C y C++. Por ejemplo, un tipo de dato
booleano no se convertirá automáticamente a un entero, a menos que se indique explícitamente
que se desea esa conversión, mediante una operación cast. También es posible especificar el
comportamiento de un tipo de dato definido por el usuario, cuando se enfrenta a una
conversión de tipos explícita e implícita.
Los objetos de datos en C#, variables y constantes, pueden estar almacenados en el stack o en el
heap. Los objetos de datos del stack pueden ser primitivos y estructuras. Los objetos de datos
del heap corresponden al resto de tipos de datos. A diferencia de C y C++, en C# es el lenguaje
el que escoge la ubicación de un objeto de datos, en base a su tipo. A los tipos de datos en stack
se les llama tipo valor, a los tipos de datos en heap se les llama tipo referencia.
El heap en C# funciona de manera diferente al de C y C++. En C#, el CLR crea y gestiona los
objetos de datos durante la ejecución del programa, teniendo la responsabilidad de liberar la
memoria no utilizada mediante un recolector de basura, el cual es un hilo que corre en paralelo al
resto de hilos del programa pero con una prioridad baja, realizando la revisión de los objetos de
datos del heap y liberando aquellos marcados como no-utilizados. A este heap se le llama heap
gestionado.
La ubicación de almacenamiento de un tipo de dato implica cómo sus objetos se comportarán
en una operación de asignación. Para los tipos valor se creará una copia del valor, teniéndose dos
objetos de datos distintos almacenando el mismo valor. Para los tipos referencia se creará una
copia de la referencia, teniéndose dos objetos de datos referenciando al mismo valor en la misma
posición de memoria. En el siguiente ejemplo se crea una copia del valor de una variable entera,
el cual es un tipo valor:
int a1 = 10;
int a2;
a2 = a1;
En este ejemplo, a1 y a2 son variables de tipo valor que almacenan en posiciones de memoria
distintas, el mismo valor. En el siguiente ejemplo se crea una copia de la referencia de una
variable cadena, la cual es un tipo referencia:
string s1 = "jose";
string s2 = s1;
En este ejemplo, s1 y s2 son variables de tipo referencia, que refieren al mismo objeto de datos
en memoria, el cual almacena el literal “jose”.
29
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
T I P O S D E D A T O S
Luego, para datos primitivos o complejos pero de poco tamaño, es más eficiente trabajarlos en
el stack, como tipos valor, dado que se evita la sobrecarga que implica la creación y manejo de
objetos de datos en el heap. Por ejemplo, no es deseable tener que crear dinámicamente cada
entero que se utilice en un programa. Como contraparte, para datos predefinidos complejos y
otros definidos por el programador, en donde se almacenan un número considerable de datos,
es más eficiente crearlos en el heap y mantener variables que los refieran, eliminando la
sobrecarga de mantener múltiples copias de datos extensos.
TIPOS PREDEFINIDOS
decimal 1.0 × 10-28al 7.9 × 1028 128 bits, 28 a29 dígitos significativos
30
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
T I P O S D E D A T O S
Dado que el tipo string es un tipo referencia, podríamos esperar que sea sencillo cometer errores
de programación al asignar una variable string a otra, y luego al modificar la cadena de la primera
estaríamos modificando, quizá inadvertidamente la segunda. Sin embargo, el tipo string presenta
una característica particular que evita este tipo de error. Cuando se realiza cualquier operación
que modifica el contenido de una variable string, se crea un nuevo objeto de datos, manteniendo
el valor de la variable original sin cambios. Luego, la regla es, una vez creado un objeto string, el
valor que almacena no puede ser modificado. El siguiente código ejemplifica esta característica.
using System;
class MainClass {
public static void Main(string[] args) {
string s1 = "Una cadena";
string s2 = s1;
Console.WriteLine("s1 es " + s1);
Console.WriteLine("s2 es " + s2);
s1 = "Otra cadena";
Console.WriteLine("s1 es ahora " + s1);
Console.WriteLine("s2 es ahora " + s2);
}
}
DEFINIDOS POR EL PROGRAMADOR
El siguiente código muestra un ejemplo simple del uso de estos tres tipos:
using System;
enum Sexo {
Masculino,
Femenino
}
struct Persona {
public string nombre;
public int edad;
public Sexo sexo;
}
class MainClass {
public static void Main(string[] args) {
Persona p = new Persona();
p.nombre = "Jose";
p.edad = 25;
p.sexo = Sexo.Masculino;
string saludo = CrearSaludo(p);
31
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
T I P O S D E D A T O S
Console.WriteLine(saludo);
}
Nótese que los datos miembros de una estructura se deben declarar public para ser accedidos
directamente. Tanto para las clases como para las estructuras cuando uno de sus miembros no
especifica el nivel de acceso, mediante un modificador, se asume por defecto private. C++
asume por defecto private para las clases y public para las estructuras. Aunque para la
inicialización de una estructura se utiliza el operador new, es importante recordar que los datos
de ésta se encuentran en el stack, no en el heap. Existen otras diferencias entre las estructuras de
C++ y C# que van más allá de una introducción al lenguaje.
Nótese que el formato para acceder a los elementos de un enumerado difiere del de C++. C#
no permite definir un tipo enumerado anónimo, como sí lo permite C++. A diferencia de C++,
el nombre de un tipo enumerado define un espacio de nombres. Es por ello que para acceder a
un elemento del enumerado se requiere el formato:
<nombre del enumerado>.<nombre de la constante>
Las interfaces y los delegados son temas avanzados que van más allá de una introducción al
lenguaje.
CONVERSIÓN ENTRE TIPO S
El error es: CS0029: Cannot implicitly convert type ‘int’ to ‘byte’. Esto se debe que, debido a que
la suma de dos números tipo byte, cuyo rango de valores va del cero al 255, puede producir
fácilmente un número mayor a 255, lo que requeriría un tipo de dato entero de por lo menos
dos bytes. Debido a esto, la suma de tipos byte en C# retorna un valor entero tipo int, lo que
origina el error en el código, dado que el resultado de la suma, un int, se intenta asignar a una
variable byte, lo que podría producir una pérdida de información. Luego, para solucionar este
problema requerimos de una conversión de un tipo de dato a otro.
En C# existen dos formas de conversión del valor de una variable: implícita y explícita. La
conversión implícita es realizada en forma automática por el compilador del lenguaje, sin que el
programador lo solicite, solamente en los casos donde es seguro que no se perderá información
o se modificará el valor original. En los demás casos, se requerirá una conversión explícita,
donde el programador solicita la conversión mediante una sintaxis especial.
El siguiente es un ejemplo de conversión implícita:
32
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
T I P O S D E D A T O S
Dado que toda suma de dos valores tipo byte siempre pueden ser almacenados en una variable
tipo long, el compilador realiza una conversión implícita. La Tabla 1 - 8 muestra las conversiones
implícitas para los tipos de datos primitivos.
Tabla 1 - 8 Conversiones implícitas para tipos de datos primitivos en C#
byte short, ushort, int, uint, long, ulong, float, double, o decimal
float double
Para realizar una conversión explícita requerimos realizar una operación cast, al igual que en
C++. El siguiente código realiza una conversión explícita:
long valor1 = 30000;
int valor2 = (int)valor1;
33
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
T I P O S D E D A T O S
Para convertir una cadena a un tipo primitivo es posible utilizar el método estático Parse de
estos. El siguiente código convierte un objeto string a un objeto int.
string s = "123";
int e = int.Parse(s);
Este código refuerza la idea de que “todos” los tipos de datos en C# son objetos, aun los datos
primitivos e inclusive las constantes literales, por lo que el siguiente código sería válido.
string s = 10.ToString();
Sin embargo, lo que ocurre en el fondo en este código es la creación de un objeto temporal en
el heap que encajone un valor entero, de forma que pueda llamarse al método requerido. A este
proceso se llama encajonamiento o boxing. También es posible realizar un boxing
explícitamente, como en el siguiente código.
int i = 123;
object obj = i;
Console.WriteLine("obj = " + obj);
Es interesante notar que al ejecutarse este código se imprime en pantalla la siguiente línea.
obj = 123
La variable tipo referencia o realmente referencia a un objeto tipo int pero en heap. Dado que el
método ToString originario de la clase object es sobreescrito por su clase derivada int, su llamada
se realiza polimórficamente, lo que origina el resultado mostrado. Este proceso se muestra en la
Figura 1 - 1.
Un proceso boxing puede realizarse sobre cualquier objeto tipo valor, como los enteros y las
estructuras. El proceso inverso se llama unboxing. El siguiente código muestra un ejemplo de su
uso.
int i = 20;
object obj = i;
int j = (int)obj;
34
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
T I P O S D E D A T O S
j++;
Console.WriteLine("i=" + i + ", j=" + j);
Una especificación precisa de la acción de una operación requiere más información que
únicamente los tipos de datos de los argumentos. Esta especificación se ve dificultada por:
Operaciones que no están definidas para ciertas entradas.
Argumentos implícitos.
Efectos colaterales, esto es, resultados implícitos.
Auto modificación, esto es, sensibilidad historial.
Ejercicio
1. Encuentre los errores si es que hubiera en los siguientes códigos y corríjalos.
• byte sizeof = 200;
35
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
T I P O S D E D A T O S
36
1
Capítulo
3
Arreglos y Cadenas
Debido a que el tratamiento de cadenas de caracteres (o simplemente cadenas) esta
estrechamente relacionado al de arreglos en los lenguajes de programación que nos competen,
se verá en este capítulo ambos temas.
Arreglos en Java
Los arreglos en Java son objetos especiales. Cuando se crea un arreglo lo que internamente
realiza Java es crear un objeto de tipo arreglo. Se dice que un arreglo es un objeto especial dado
que el programador nunca puede usar directamente una clase Arreglo (no hacemos un “new” de
alguna clase tipo Arreglo), tampoco podemos crear una clase que herede de la clase Arreglo que
internamente implementa Java.
Los arreglos en Java son estáticos en el sentido de qué una vez creados éstos, no pueden
redimensionarse. Un objeto arreglo, una vez creado, conserva su tamaño durante todo su
tiempo de vida.
Arreglos Unidimensionales
El formato de declaración de un arreglo es:
<tipo de los elemento> <nombre del arreglo> [ ];
Por ejemplo, para declarar una referencia a un arreglo de enteros podríamos utilizar:
int ArregloEnteros[ ];
ArregloEnteros es el nombre de una referencia que puede ser utilizada para referenciar a
cualquier objeto arreglo de enteros. La referencia ArregloEnteros se declara pero aún no se
inicializa. Como todo objeto, una referencia no inicializada tiene el valor null.
Para inicializar un arreglo se usa el operador new, indicando entre corchetes el tamaño del
arreglo. Por ejemplo, para referenciar nuestra variable anterior ArregloEnteros a un objeto
arreglo de 10 enteros se puede utilizar:
ArregloEnteros = new int [ 10 ];
Al igual que C++, un arreglo puede inicializarse utilizando un inicializador, por ejemplo:
29
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
A R R E G L O S Y C A D E N A S D E C A R Á C T E R E S
Como en cualquier declaración de variables, puede inicializarse más de una referencia a variables
de tipo arreglo durante su declaración:
int Arreglo1[ ] = new int [ 10 ],
Arreglo2[ ] = { 10, 5, 2, 3 };
Al igual que el resto de datos miembro de una clase, los datos miembros que son referencias a
objetos de tipo arreglo también pueden declarase e inicializarse a la vez. Por ejemplo, el siguiente
código es correcto:
class MiClaseConArreglos {
…
double Arreglo[ ] = new double [ 205 ];
…
}
Arreglos Multidimensionales
Java no soporta originalmente la declaración directa de arreglos de múltiples dimensiones. En
contraparte, se pueden crear arreglos de arreglos. Por ejemplo, para crear un arreglo
bidimensional de enteros usaríamos:
int b [ ] [ ];
b = new int [ 10 ] [ ];
b[ 0 ] = new int [ 5 ];
b[ 1 ] = new int [ 5 ];
30
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
A R R E G L O S Y C A D E N A S D E C A R Á C T E R E S
Recuerde que, como los elementos en este arreglo son referencias, éstas son inicializadas
automáticamente a null cuando se crea el arreglo. Luego, el siguiente código generaría un error
en tiempo de ejecución:
String a[ ] = new String [ 10 ];
System.out.println( a[ 0 ] );
Dado que el elemento de índice cero del arreglo se está utilizando antes de ser inicializado.
31
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
A R R E G L O S Y C A D E N A S D E C A R Á C T E R E S
El método Metodo2 recibe un parámetro tipo arreglo en la referencia Arr. Tanto Arreglo en
Metodo1 como Arr en Metodo2 son referencias al mismo objeto arreglo, lo cual significa que al
modificar los valores de los elementos del arreglo mediante la referencia Arr se estaría también
modificando los valores a los que refiere Arreglo. Sin embargo, si a la variable Arr en Metodo2
se le referencia a otro arreglo, la variable Arreglo en Metodo1 seguirá referenciando al arreglo
original.
Preguntas de Repaso
Cuando se pasa un arreglo a un método, si se modifica el parámetro correspondiente
dentro del método (por ejemplo, se le asigna otro arreglo), ¿se modificará también la
referencia que se usó en la llamada al método?
Cuando asigno a una referencia de un arreglo el valor de otra referencia de otro arreglo,
¿estoy sacando una copia? Si no, ¿cómo se sacaría una copia?
¿Cómo se declara un método que debe recibir como parámetro un elemento de un
arreglo multidimensional?
Si tengo un arreglo multidimensional y paso uno de sus elementos a un método, si
modifico el parámetro referencia correspondiente, ¿modifico el elemento del arreglo
multidimensional también?
Arreglos en C#
Los arreglos en C# son un tipo de clase predefinida especial, dado que su creación y
manipulación difiere de las clases estándar. Aunque los programadores tienen acceso a la clase
base de todos los arreglos, la clase Array, no puede derivar directamente de ésta, sino
indirectamente a través de los formatos de declaración de arreglos.
El siguiente es el formato de declaración de una variable de tipo arreglo unidimensional:
[modificadores] <tipo de los elementos> [ ] <nombre de la variable>;
A diferencia de C++, los corchetes van entre el tipo de los elementos y el nombre de la variable,
no especificándose las dimensiones del arreglo. El siguiente código muestra un ejemplo del uso
de un arreglo unidimensional.
using System;
class MainClass {
public static void Main(string[] args) {
int[] a;
a = new int[10];
a = new int[3];
a[0] = 10;
a[1] = 20;
a[2] = 30;
for(int i = 0; i < a.Length; i++)
a[i] += i;
int suma = 0;
foreach(int elemento in a)
suma += elemento;
Console.WriteLine("Suma total = " + suma);
}
}
Nótese que la variable “a” es de tipo referencia, dado que los arreglos son objetos. En la primera
asignación, “a” recibe una referencia a un objeto de tipo arreglo de enteros de diez elementos.
En la segunda asignación, “a” recibe una referencia a un nuevo objeto arreglo, esta vez de 3
32
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
A R R E G L O S Y C A D E N A S D E C A R Á C T E R E S
elementos, quedando el primer objeto arreglo sin modificación, aunque dado que ya no es
referenciado por ninguna variable será eliminado de memoria por el recolector de basura. Como
se ve, los objetos arreglo, una vez creados, no pueden modificar sus dimensiones.
Dado que un arreglo es un objeto, contiene datos miembro y métodos. El dato miembro
“Length” (más adelante veremos que realmente se trata de una propiedad, un concepto definido
en C#) retorna el número de elementos del arreglo.
Los objetos arreglo se crean en el heap gestionado, a diferencia de los arreglos de C++, que se
crean en el stack. En C++, los arreglos en el heap se crean mediante punteros.
C# permite manejar arreglos de arreglos, así como arreglos multidimensionales. Para el primer
caso la sintaxis es la siguiente:
[modificadores] <tipo de los elementos> [ ][ ] … <nombre de la variable>;
El arreglo “b” se dice que es “ortogonal”, debido a que no todos los arreglos, dentro de una
misma dimensión, tienen el mismo número de elementos. La contraparte es un arreglo
rectangular. Si lo que deseamos es crear un arreglo de arreglos rectangular debemos asegurarnos
que todos los arreglos creados, para una misma dimensión, tengan el mismo número de
elementos.
La sintaxis para el caso de un arreglo unidimensional es la siguiente:
[modificadores] <tipo de los elementos> [<tantas comas como dimensiones menos 1>] <nombre
de la variable>;
33
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
A R R E G L O S Y C A D E N A S D E C A R Á C T E R E S
En el ejemplo, el método “GetLength” reemplaza al uso de Length, dado que si bien Length
también se define para arreglos multidimensionales, retorna el total de elementos del arreglo
multidimensional sumados los elementos de todas sus dimensiones. Para el ejemplo anterior,
Length retorna 24. El método GetLength recibe como parámetro el número de la dimensión de
la que se desea saber su tamaño. Para el ejemplo anterior GetLength(0) retorna 5, GetLength(1)
retorna 10, GetLength(2) retorna 8.
Un objeto tipo arreglo de arreglos se diferencia de un arreglo ortogonal en que el primero tiene
su memoria dispersa, mientras que el segundo, junta. La Figura 3 - 1 muestra esta diferencia.
La segunda diferencia es que todos los arreglos multidimensionales son forzosamente
rectangulares, debido a que el tamaño de todas las dimensiones debe especificarse al momento
de crear el objeto arreglo.
También existe la posibilidad de mezclar ambos tipos de arreglos al declarar una variable. Por
otro lado, la clase base Array ofrece métodos estáticos que permiten realizar operaciones
comunes sobre arreglos, como son ordenamiento, inversión, etc. Estos aspectos escapan de los
alcances de la presente introducción.
Arreglo de arreglos
int[ ][ ] a;
stack
heap
heap
Arreglo multidimensional
int[ ,] a;
stack
heap
34
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
A R R E G L O S Y C A D E N A S D E C A R Á C T E R E S
utilizando el operador “+”. La declaración de un literal de texto provoca que Java cree un objeto
String sin nombre de forma automática, la que comúnmente suele asignarse a una referencia
para su manipulación. Por ejemplo:
String Cadena1 = "Hola";
String Cadena2 = "Mundo";
String Cadena3;
Cadena3 = Cadena1 + " " + Cadena2;
System.out.println( Cadena1 );
System.out.println( Cadena2 );
System.out.println( Cadena3 + "!!!" );
System.out.println( Cadena3 );
Provocará la salida:
Hola
Mundo
Hola Mundo!!!
Hola Mundo
La clase String provee adicionalmente una serie de constructores para la creación de cadenas.
La concatenación de 2 cadenas provoca la creación de un nuevo objeto String, el objeto original
no es modificado. Como ejemplo, el siguiente código:
String Cadena3;
Cadena3 = "Hola";
Cadena3 += " Mundo";
Realizará primera la creación de un objeto String que contiene la cadena “Hola”, luego se creará
un nuevo objeto String, en base al primero y a otro objeto String “ Mundo”, y es asignada a la
referencia Cadena3. Dado que los objetos String “Hola” y “ Mundo” ya no son referenciados,
en algún momento Java los eliminará de memoria (mediante el sistema de Recolección de
Basura que se explicará más adelante).
Se puede acceder a los carácteres individuales de una cadena utilizando el método “charAt”. El
siguiente ejemplo muestra esto:
String s = "Hola";
for(int i = 0; i < s.length(); i++)
System.out.println("caracter " + i + " = " + s.charAt(i));
Es común que en los programas se realicen conversiones entre cualquier tipo de dato a texto.
Para los datos primitivos, esta conversión es automática cuando se concatenan con por lo
menos una referencia a alguna cadena. Por ejemplo el siguiente código:
int Valor = 10;
String Cadena = "Valor = ";
System.out.println( Cadena + Valor );
String Cadena2;
Cadena2 = "Otra concatenación: " + 50.2;
System.out.println( Cadena2 );
Provocará la salida:
Valor = 10
Otra concatenación: 50.2
Sin embargo, cuando no se realiza una concatenación y se desea convertir un número a una
cadena se pueden utilizar las clases especiales de Java para esta labor (también en java.lang), las
que encapsulan a los datos primitivos en clases y brindan también métodos estáticos (podemos
llamarlos sin necesidad de crear un objeto) que nos facilitan acciones comunes como la
conversión desde y hacia cadenas de texto (String). Por ejemplo:
int Valor = Integer.parseInt( "10" );
35
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
A R R E G L O S Y C A D E N A S D E C A R Á C T E R E S
La clase Integer encapsula un dato primitivo int y contiene métodos utilitarios que permiten
realizar acciones comunes. Para el resto de datos primitivos existen clases equivalentes, por
ejemplo la clase Double, Boolean, etc.
La clase String también cuenta con el método estático “valueOf” que permite la conversión de
cualquier dato de tipo primitivo a una cadena, por ejemplo:
String Cadena = String.valueOf( 10 );
La clase String está diseñada para ser eficiente en el manejo de textos no modificables. Si se
desea manejar un texto grande que se modificará repetidamente, es más eficiente el uso de la
clase “StringBuffer”. Ésta provee métodos para la modificación de su cadena. El siguiente
ejemplo muestra el uso de esta clase.
StringBuffer sb = new StringBuffer();
sb.append("hola ");
sb.append("mundo");
System.out.println("sb=" + sb);
Como puede verse en el ejemplo, sólo un objeto StringBuffer es creado y manipulado, lo que es
más eficiente que la creación constante de nuevos objetos cuando se concatenan objetos String.
Al igual que C++, en Java la comparación de dos variables String utilizando el operador “==”
produce la comparación de las referencias mismas, y no de los objetos String referenciados, en
otras palabras, a menos que ambas variables referencien al mismo objeto una comparación así
devolvería false. Si se desea comparar los textos almacenados en los objetos referenciados se
debe utilizar métodos como equals y equalsIgnoreCase (equivalentes a compareTo y
compareToIgnoreCase). El siguiente ejemplo muestra estas diferencias.
String var1 = "Hola";
String var2 = " a todos";
String var3 = var1 + var2;
String var4 = "Hola a todos";
if(var3 == var4)
System.out.println("var3 y var4 SI referencian al mismo objeto");
else
System.out.println("var3 y var4 NO referencian al mismo objeto");
if(var3.equals(var4))
System.out.println("var3 y var4 contienen textos iguales");
else
System.out.println("var3 y var4 contienen textos diferentes");
Sin embargo algunas políticas de optimización del compilador pueden dar la impresión de que lo
anterior no se cumple siempre. Es así como, al ejecutarse:
String var1 = "Hola";
String var2 = "Hola";
if(var1 == var2)
System.out.println("var1 y var2 SI referencian al mismo objeto");
else
System.out.println("var1 y var2 NO referencian al mismo objeto");
Se obtiene:
var3 y var4 SI referencian al mismo objeto
Lo que aquí sucede es que el compilador, tomando en cuenta que los String no son modificables
y que ambas variables apuntarían a objetos equivalentes, decide hacer que ambos apunten al
36
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
A R R E G L O S Y C A D E N A S D E C A R Á C T E R E S
mismo objeto en lugar de crear dos objetos iguales. Por tanto, en este caso ambas variables si
están refenciando al mismo objeto.
Por último, existen varias formas de crear objetos String. A continuación algunos ejemplos:
char [] arr1 = {'H','o','l','a'};
String var1 = new String(arr1);
String var2 = new String(var1);
String var3 = new String(sb); // sb es tipo StringBuffer
Cadenas de Carácteres en C#
Al igual que todos los tipos nativos de C#, el tipo “string” es un alias del tipo CTS
System.String. Un objeto string se crea automáticamente cuando se ejecuta alguna sentencia que
incluya literales de texto. Por ejemplo, el siguiente código crea un objeto string conteniendo la
palabra “hola” y se le asigna su referencia a una variable.
string cad = "hola";
También puede crearse objetos string utilizando cualquiera de sus constructores. Algunos de
estos son:
public string(char[]); // Crea un string en base a un arreglo de carácteres.
public string(char, int); // Crea un string de n caráteres iguales.
// Crea un string en base a un conjunto de valores de un arreglo de carácteres.
public string(char[], int, int);
Una cadena puede ser trabajada como un arreglo, por lo que el siguiente código es válido:
string cad = "Como un arreglo";
for(int i = 0; i < cad.Length; i++)
Console.WriteLine(" cad(" + i + ") = " + cad[i]);
Una vez creada una cadena, esta no es modificable, por lo que la siguiente instrucción no es
válida:
cad[i] = 'P';
37
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
A R R E G L O S Y C A D E N A S D E C A R Á C T E R E S
La clase string implementa adicionalmente otros métodos que permiten crear un texto con
formato (Format), buscar un carácter o conjunto de carácteres (IndexOf, IndexOfAny,
LastIndexOf, LastIndexOfAny), completar una cadena con carácteres de relleno (PadRight,
PadLeft), reemplazar carácteres (Replace), partir una cadena en dos cadenas (Split), obtener una
sección de una cadena (SubString), colocar los carácteres a mayúsculas o minúsculas (ToLower,
ToUpper), eliminar los carácteres de relleno a la izquierda y derecha (Trim).
Debe tenerse siempre en consideración que la clase string esta diseñada para ser eficiente en el
manejo de cadenas de longitud pequeña y media. Cada método de la clase que produciría una
modificación de la cadena original, retorna como resultado una nueva cadena con las
modificaciones, dejando la cadena original intacta. Por esto, se dice que los objetos string no son
modificables luego de su creación. Si se desea manejar textos de gran tamaño, se puede utilizar la
clase
System.Text.StringBuilder
Es común que en los programas se realicen conversiones entre cualquier tipo de dato a texto.
Para los datos primitivos, esta conversión es automática cuando se concatenan con por lo
menos una referencia a alguna cadena. Por ejemplo el siguiente código:
int Valor = 10;
string Cadena = "Valor = ";
Console.WriteLine( Cadena + Valor );
string Cadena2;
Cadena2 = "Otra concatenación: " + 50.2;
Console.WriteLine( Cadena2 );
38
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
A R R E G L O S Y C A D E N A S D E C A R Á C T E R E S
Provocará la salida:
Valor = 10
Otra concatenación: 50.2
Sin embargo, cuando no se realiza una concatenación y se desea convertir un número a una
cadena se pueden utilizar el método ToString. Dado que en C# todos los tipos de datos son
clases, inclusive los tipos primitivos, todos los tipos de datos poseen un método ToString
heredado de la clase base object. Por ejemplo:
string Cadena = 10.ToString( );
Para convertir una cadena a un tipo primitivo se puede utilizar el método Parse incluido en
todos los tipos primitivos. Por ejemplo:
bool valorBool = bool.Parse("true");
39
1
Capítulo
4
Programación Orientada
a Objetos
Este capítulo asume que el lector posee conocimientos básicos de la Programación Orientada
a Objetos (POO) en C++, así como conocimientos de la estructura general y
funcionamiento de programas en Java y C#. Los temas tratados refuerzan dichos
conocimientos básicos de POO y los profundizan. Estos conceptos son vistos bajo la
implementación de los lenguajes de programación C++, Java y C# con el objetivo de que el
lector pueda aprender dichos conceptos más allá de la implementación particular de un
lenguaje, mediante la identificación de las diferencias y similitudes entre éstos.
Conceptos Básicos
En esta sección se busca revisar los conceptos básicos relacionados a las clases y a los objetos,
así como a los miembros que los componen.
2
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
P R O G R A M A C I Ó N O R I E N T A D A A O B J E T O S
La POO va más allá, haciendo que dichas funciones de manipulación formen parte de la
descripción del tipo de dato. Juntos, la declaración de datos y las funciones de manipulación,
forman el concepto de clase, donde un objeto es una instancia de ésta. La POO también agregó
los conceptos de herencia de implementación y comportamiento polimórfico. Los conceptos
de POO fueron llevados a la práctica por lenguajes como Ada, C++ y SmallTalk. Es
importante señalar que existen diferentes opiniones acerca de los conceptos relacionados con los
ADT y las clases, algunos autores incluso los intercambian indistintamente.
Dado que la encapsulación realiza el ocultamiento de información de un objeto, debido a la
inseguridad de su manipulación libre y directa, se hace necesario definir un mecanismo que
permita realizarla de manera segura. Es aquí donde se define el concepto de interfaz de una
clase, la cual es el mecanismo que dicha clase define para comunicarse con su entorno. En la
práctica, la interfaz de una clase está formada principalmente por un conjunto de funciones, que
forman parte de la definición de la clase, a las que se puede acceder y llamar desde fuera de la
clase.
Cada miembro de una clase tiene un modificador de acceso relacionado ya sea implícita o
explícitamente. En el caso implícito se aplica el modificador de acceso por defecto
correspondiente al lenguaje.
Las Clases
En esta sección veremos las diferencias y similitudes entre la implementación de los lenguajes de
programación tratados respecto a la POO.
3
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
P R O G R A M A C I Ó N O R I E N T A D A A O B J E T O S
A diferencia de C++, los lenguajes Java y C# son más estrictos respecto a su implementación
del paradigma orientado a objetos. En estos lenguajes, todo programa consiste en un conjunto
de clases, donde una de dichas clases debe definir un método que será tratado por el intérprete
como el punto de entrada del programa. A una clase que define un punto de entrada de un
programa la llamaremos la “clase ejecutable”.
En Java y C# toda clase debe contener también su implementación completa. Java y C#, a
diferencia de C++, no permiten la declaración de prototipos de métodos dentro de una clase
para su posterior implementación fuera de ésta. No existe por tanto, en estos lenguajes, un
equivalente a la declaración de una clase en un archivo cabecera (*.H) y su implementación en
un archivo de implementación (*.CPP). En Java y C# una clase se declara e implementa en un
mismo archivo. Tampoco permiten la declaración de variables globales ni funciones fuera de las
clases.
final sealed Evitan que una clase sea utilizada como base
para otra clase.
public, de-paquete (*) public, protected, Determinan el acceso a la clase (por ejemplo,
private, internal (*) para crear objetos de ésta) desde fuera de su
ámbito de declaración. Dependiendo del
contexto en que la clase se declare, alguno de
estos modificadores no pueden utilizarse.
4
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
P R O G R A M A C I Ó N O R I E N T A D A A O B J E T O S
5
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
P R O G R A M A C I Ó N O R I E N T A D A A O B J E T O S
}
}
class Principal {
public static void Main(string[] args) {
Usuario p;
p = new Usuario("Jose", TipoUsuario.Administrador);
Console.WriteLine("Buenos dias Sr(a). " + p.ObtenerNombre());
}
}
Java:
6
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
P R O G R A M A C I Ó N O R I E N T A D A A O B J E T O S
class MiClase {
public MiClase( .... ) { .... }
}
C#:
class MiClase {
public MiClase( ....... ) { ....... }
}
Los constructores pueden recibir un conjunto de parámetros, igual que cualquier método, para
permitir establecer un estado inicial consistente. Cuando un constructor no recibe parámetros se
le llama “constructor por defecto”, cuando su parámetro es un objeto del mismo tipo se le llama
“constructor copia”. Dado que la implementación interna de muchos lenguajes de
programación orientados a objetos requiere que siempre se llame a un constructor cuando se
crea un objeto, cuando no se implementa uno explícitamente (sea éste uno “por defecto” o no),
el compilador del lenguaje crea un constructor por defecto implícitamente.
C++ implementa una llamada automática al “constructor copia” de una clase cuando se crea un
objeto en pila y se le inicializa, pasándole otro objeto del mismo tipo, en su misma declaración.
Este manejo especial de los constructores copia no es implementado ni en Java ni en C#, ni
tampoco permiten la sobrecarga del operador de asignación para estos fines, lo que sí permite
C++.
Java y C# permiten llamar a un constructor desde otro constructor de la misma clase. Esto
permite simplificar la definición de varios constructores opcionales para la creación de un objeto.
El siguiente código muestra esta característica en C#.
class Usuario {
private string nombre;
private string contraseña;
public Usuario(string nombre) : this(nombre, nombre) {
// este constructor llama a su vez al segundo constructor pasándole como
// contraseña el mismo nombre
}
public Usuario(string nombre, string contraseña) {
this.nombre = nombre;
this.contraseña = contraseña;
}
}
7
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
P R O G R A M A C I Ó N O R I E N T A D A A O B J E T O S
Los constructores, como cualquier miembro de una clase, pueden también recibir modificadores
de acceso en su declaración. Los modificadores de acceso que pueden utilizarse dependen en
algunos casos de si la clase es anidada o no. Las clases anidadas se verán más adelante.
Los Constructores Estáticos
A los constructores vistos en la sección anterior se les llama también “de instancia”, debido a
que se llaman cuando se instancia una clase. Su finalidad es la de establecer el estado inicial
consistente de un nuevo objeto. Sin embargo, una clase puede requerir inicializar datos cuyo
tiempo de vida abarquen a todos los objetos de la clase, es decir, datos miembros estáticos. Para
este tipo de inicialización se definen constructores estáticos. Java y C# permiten este tipo de
constructores. Ejemplos de su declaración en Java y C# son:
Java:
class MiClase {
static { ....... }
}
C#:
class MiClase {
static MiClase() { ....... }
}
Nótese que el constructor estático no define un nivel de acceso ni tampoco puede tener
parámetros, debido a que nunca es llamado por otro código del programa, sólo por el intérprete
de Java o ..NET cuando la clase es cargada. C++ no tiene un equivalente a un constructor
estático.
Los Destructores
Mientras que la construcción de un objeto sigue esquemas similares de funcionamiento en la
mayoría de lenguajes orientados a objetos, la destrucción de los mismos depende de si el
esquema de manejo de la memoria es determinista o no-determinista, lo que está relacionado
con el momento en que un destructor es llamado. El concepto mismo de destructor de C++ es
determinista, lo que significa que el programador puede determinar el momento exacto en que
un destructor es llamado durante la ejecución de un programa. Java y C# implementan
“finalizadores”, los cuales son no-deterministas.
La sintaxis de declaración de un finalizador en Java es:
<modificador protected o public> finalize ( ) {
8
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
P R O G R A M A C I Ó N O R I E N T A D A A O B J E T O S
9
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
P R O G R A M A C I Ó N O R I E N T A D A A O B J E T O S
la performance final de estos programas respecto a sus contrapartes. Más aún, se ha encontrado
que en muchos casos un gestor automático de memoria puede tomar decisiones más eficientes
en cuanto al manejo de memoria que un programador promedio. Como consecuencia, para
proyectos de mediana y gran envergadura, las empresas requerirían contar con programadores
expertos en C++ para lograr resultados “significativamente” más eficientes que los obtenidos
con programadores “no-expertos” utilizando Java y C#.
Sin embargo, existen indudablemente proyectos cuya naturaleza requiere obtener la mayor
eficiencia que el hardware y el software disponible puede brindar, por lo que lenguajes como
Java y C# no pueden reemplazar completamente a lenguajes como C++. Como en muchos
aspectos del desarrollo de software, el escoger un lenguaje apropiado para el desarrollo de un
programa, pasa por conocer cuáles son los requerimientos del producto final.
Finalización Determinista y No-Determinista
El manejo automático de la memoria ofrecido por Java y C# implica que dichos lenguajes
faciliten el desarrollo de programas no-deterministas. En un esquema no-determinista el
programador no puede determinar en qué momento los objetos que crea, luego de que ya no
son referenciados por el programa, son efectivamente eliminados de la memoria. Esto trae
consecuencias al momento de decidir la forma en que los recursos reservados por un programa
deberán ser eliminados.
Para esto es importante considerar que si bien la memoria es el recurso más comúnmente
manejado por los programas, no es el único, por lo que el resto de recursos deberán seguir
manejándose de una manera determinista.
Por ejemplo, el sistema operativo Windows permite a los programas crear un número limitado
de “manejadores de dispositivos de las ventanas”. Debido a esto, una librería que permita crear
ventanas en Java y C# deberá controlar en forma determinista la liberación de estos recursos.
Un programa que utilice dicha librería puede crear y luego desechar una gran cantidad de
ventanas en un corto período de tiempo. Si el diseñador de esta librería coloca en el finalizador
de su clase “Ventana” la llamada al sistema operativo que libera el recurso mencionado, es
posible que las llamadas a estos finalizadores tarden lo suficiente como para que el programa
trate de crear una nueva ventana y se produzca un error, puesto que ya se crearon todas las que
el sistema operativo permitía y su liberación aún está pendiente.
Problemas como el descrito requieren que el programador implemente manualmente un manejo
determinista de estos recursos, es decir, se requiere tener la certeza de en qué momento se libera
un determinado recurso.
Los recolectores de basura de Java y C# pueden ser controlados hasta cierto punto por el
programador, haciendo uso de clases especiales. Estos recolectores ofrecen métodos para
despertar manualmente el procedimiento de recolección de basura. Sin embargo, esta solución
no es confiable, puesto que el recolector de basura siempre tiene la potestad de decidir si es
conveniente o no iniciar la recolección. El no hacerlo así, podría ocasionar que el programa se
cuelgue debido a, por ejemplo, un finalizador que es ejecutado por el recolector y que nunca
finaliza por un error en la lógica de su código, lo que ocasionaría que el programa se quede
esperando a que el proceso de recolección termine, lo que nunca ocurrirá.
Un esquema comunmente utilizado pasa por colocar el código encargado de la liberación de la
memoria en un método del objeto que la crea. Adicionalmente, el objeto deberá contener un
dato miembro que funcione como bandera y que indique si dicho método ya fue llamado o no.
Si el programador determina que ya no requiere utilizar más un objeto, y sus recursos, deberá
llamar manualmente a dicho método. Adicionalmente, el finalizador del objeto también llamará
10
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
P R O G R A M A C I Ó N O R I E N T A D A A O B J E T O S
a este método. Finalmente, todos los métodos del objeto deberán hacer una verificación de la
bandera de liberación de los recursos antes de realizar su trabajo, de forma que se impida que el
objeto siga siendo utilizado si es que sus recursos ya fueron liberados.
Puede parecer que finalmente la programación no-determinista no es tan buena idea después de
todo. Sin embargo, la mayor parte de los recursos que un programa suele utilizar corresponden
a la memoria, y para aquellos recursos que no son memoria suelen contarse con librerías
especializadas que hacen casi todo el trabajo no-determinista por nosotros.
El Acceso a los Miembros
El acceso a los miembros de una clase desde fuera de la misma se determina mediante los
modificadores de acceso de cada miembro. La tabla 4.2 muestra los modificadores de acceso de
cada lenguaje según los ámbitos desde dónde se desea que un miembro sea accesible.
Tabla 4 - 2 Modificadores de acceso para miembros de una clase
Sólo desde dentro de la clase donde es private (*) private private (*)
declarada.
// Archivo Gamma.java
package PaqueteX;
public class Gamma {
public void accessMethod(Alpha a) {
a.protectedData = 10; // legal
a.protectedMethod(); // legal
}
}
11
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
P R O G R A M A C I Ó N O R I E N T A D A A O B J E T O S
// Archivo Delta.java
package PaqueteY;
import PaqueteX.Alpha;
public class Delta extends Alpha {
public void accessMethod(Alpha a) {
a.protectedData = 10; // ilegal
a.protectedMethod(); // ilegal
protectedData = 10; // legal
protectedMethod(); // legal
}
}
// Archivo PruebaDeProtected.java
import PaqueteX.Alpha;
import PaqueteX.Gamma;
import PaqueteY.Delta;
public class PruebaDeProtected {
public static void main(String[] args) {
Alpha a = new Alpha();
Gamma g = new Gamma();
Delta d = new Delta();
a.protectedData = 10; // ilegal
a.protectedMethod(); // ilegal
g.accessMethod(a);
d.accessMethod(a);
}
}
En el caso de la clase Gamma es posible acceder a los miembros protegidos de Alpha, puesto
que ambas clases pertenecen al mismo paquete. La clase Delta no puede acceder a los
miembros protegidos de Alpha dado que no pertenecen al mismo paquete, excepto sus propios
miembros heredados. La clase PruebaDeProtected no puede acceder a los miembros
protegidos puesto que no pertenece al mismo paquete.
12
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
P R O G R A M A C I Ó N O R I E N T A D A A O B J E T O S
13
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
P R O G R A M A C I Ó N O R I E N T A D A A O B J E T O S
void Prueba()
{
int iValor = 20;
System.out.println( “iValor = ” + iValor );
System.out.println( “this.iValor = ” + this.iValor );
}
…
La variable local iValor oculta a la variable de la clase. Para poder hacer referencia a la variable
miembro de la clase usamos la palabra reservada this, de la misma forma que se realiza en C++.
this representa la referencia de un objeto a sí mismo dentro de uno de sus métodos.
Dentro de un método no se pueden declarar dos variables con el mismo nombre, aún si están
declaradas en bloques distintos. El siguiente programa en Java muestra un ejemplo de esto:
void Prueba()
{
int iValor = 20;
boolean Flag = true;
if( Flag )
{
int iValor;
…
}
}
En el código, definir un argumento o variable local con el mismo nombre de una variable de
clase oculta ésta última, por lo que se requiere utilizar la palabra reservada this, como en C++.
Sin embargo, la definición de la variable “entero” dentro del cuerpo de la estructura de control if
produce un error de compilación, dado que ya existe una variable local con el mismo nombre en
un ámbito padre, el del método. Esto difiere al caso de las variables “real”, cuya declaración no
produce un error dado que el ámbito de una no es padre del otro.
La Inicialización Automática
A diferencia de C++, en Java y C# toda variable puede ser inicializada al momento en su
declaración, inclusive los datos miembros, con excepción de los parámetros de un método, que
son también variables locales, dado que su inicialización corresponde a los valores pasados al
momento de llamarse a dicho método. El siguiente código en C# muestra esta inicialización:
class Usuario {
private string nombre = "Jose";
14
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
P R O G R A M A C I Ó N O R I E N T A D A A O B J E T O S
Las variables locales deben inicializarse antes de ser utilizadas. Por ejemplo, el siguiente código
en Java arrojará un error al momento de compilarse:
String sTexto;
sTexto += “Hola ”;
sTexto += “mundo”;
De la forma:
String sTexto = new String( );
sTexto += “Hola ”;
sTexto += “mundo”;
O bien de la forma:
String sTexto = new String( “Hola ” );
sTexto += “mundo”;
A diferencia de las variables locales, cuando una variable de clase no es inicializada por el
programa durante la creación del objeto al que pertenece, recibe una inicialización por defecto.
Esta inicialización se realiza de la siguiente forma:
Los datos primitivos numéricos se inicializan en cero.
Los datos primitivos lógicos (boolean en Java, bool en C#) se inicializa en false.
Las referencias se inicializan en null.
Los Modificadores
Dentro de un método, los únicos modificadores permitidos para las variables son “final” en
Java, para variables cuyo valor sólo puede asignarse una vez, y “const” en C#, para constantes.
La Tabla 4 - 3 muestra los modificadores permitidos, fuera de los modificadores de acceso, en
Java y C# para variables declaradas en el ámbito de una clase.
Tabla 4 - 3 Modificadores de Variables en Java y C#
C# Java Descripción
<sin final Variable cuyo valor sólo puede asignarse una vez, en la declaración
equivalente> o después.
readonly <sin Variable cuyo valor sólo puede ser asignado durante la creación del
equivalente> objeto.
15
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
P R O G R A M A C I Ó N O R I E N T A D A A O B J E T O S
new <sin Variable que oculta otra variable, con el mismo nombre, en una
equivalente> clase base.
(*) El uso de estos modificadores va más allá del alcance del presente curso.
El acceso a los datos miembros estáticos en C# se diferencia a C++ y Java en que:
No pueden accederse, desde dentro de la clase, mediante la palabra reservada this.
No pueden accederse, desde fuera de la clase, mediante una referencia, debe hacerse
mediante el nombre de la clase.
El siguiente código en C# presenta estos dos casos.
class Usuario {
...
public static int cuenta = 0;
public Usuario(string nom, TipoUsuario tipo) {
this.cuenta++;// ERROR
cuenta++;//CORRECCIÓN
...
}
...
}
class Principal {
public static void Main(string[] args) {
Usuario p;
p = new Usuario("Jose", TipoUsuario.Administrador);
}
}
La palabra reservada readonly define un dato miembro de sólo lectura, el que sólo puede recibir
un valor durante el proceso de creación del objeto, esto es, en la misma declaración de la variable
o dentro de un constructor. El siguiente código muestra un ejemplo de este tipo de variable.
using System;
class PruebaDeSoloLectura {
private readonly int valor = 50;
public PruebaDeSoloLectura() {
valor = 100;
valor += 10;
}
public void modificar(int nuevo) {
valor = nuevo; // ERROR
}
}
class Principal {
public static void Main(string[] args) {
PruebaDeSoloLectura obj = new PruebaDeSoloLectura();
obj.modificar(200);
}
}
16
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
P R O G R A M A C I Ó N O R I E N T A D A A O B J E T O S
En Java y C#, tanto la lista de parámetros como los modificadores son opcionales. El valor de
retorno puede ser cualquier tipo de dato o bien puede ser void, lo que indica que el método no
devuelve ningún valor.
En Java y C#, un método sin parámetros no puede llevar la palabra void entre los paréntesis,
como si se puede hacer en C++. A diferencia de C++, no se pueden declarar prototipos de
métodos para definirlos fuera de la clase. Un método sólo puede ser definido dentro de una
clase.
Un método no puede definirse dentro de otro método, ni fuera de una clase, lo que sí puede
hacerse en C++.
La Tabla 4 - 4 muestra los modificadores permitidos, fuera de los modificadores de acceso, en
Java y C# para los métodos.
Tabla 4 - 4 Modificadores de métodos en Java y C#, fuera de los de acceso
C# Java Descripción
17
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
P R O G R A M A C I Ó N O R I E N T A D A A O B J E T O S
<sin final Método que no puede ser sobrescrito en una clase derivada.
equivalente>
virtual <sin Método que puede ser sobrescrito en una clase derivada.
equivalente>
override <sin Método que sobrescribe otro, declarado como virtual o abstract,
equivalente> en una clase base.
sealed <sin Método que sobrescribe otro, declarado como virtual o abstract,
override equivalente> en una clase base, evitando también que vuelva a ser sobrescrito
en una clase derivada.
A diferencia de C++ y Java, los métodos estáticos en C#, al igual que los datos miembros
estáticos, no pueden ser llamados utilizando “this” desde dentro de la clase a la que pertenecen, y
sólo pueden ser llamados utilizando el nombre de la clase desde fuera de ésta. En los tres
lenguajes, desde un método estático no puede accederse a ningún elemento no-estático de la
clase. Un buen ejemplo es el método Main de una clase ejecutable. El siguiente código en C#
muestra este caso.
using System;
class MetodosEstaticos {
public static void saludar() {
Console.WriteLine("Hola");
}
public void despedir() {
Console.WriteLine("Adios");
}
}
class Principal {
public static void Main(string[] args) {
MetodosEstaticos.saludar();
MetodosEstaticos.despedir();// ERROR
MetodosEstaticos obj = new MetodosEstaticos();// CORRECCIÓN
obj.despedir();
}
}
En el código anterior, desde Main sólo puede llamarse directamente a “saludar”, más aún, si la
clase “Principal” tuviese otros miembros, sólo podrían accederse desde Main a los estáticos,
dado que Main es estático. Para llamar al método “despedir” se requiere contar con una
referencia a un objeto “MetodosEstaticos”. Como contraparte, desde un método no-estático, o
de instancia, sí se puede acceder a los miembros estáticos de la clase.
18
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
P R O G R A M A C I Ó N O R I E N T A D A A O B J E T O S
En Java y C#, un método abstracto no tiene implementación, dado que se espera que ésta sea
dada en las clases que se hereden. Si un método es abstracto, la clase también deberá declararse
como abstracta. Esto es similar a los métodos virtuales puros de C++.
En Java, un método con el modificador “final” imposibilita a las clases que heredan de
sobrescribirlo. El equivalente más cercano en C# es “sealed override”, con la diferencia que éste
no se puede utilizar en la primera implementación del método, sino en una de sus sobrescrituras.
El modificador synchronized es utilizado en la programación concurrente y se verá en capítulos
posteriores. El uso del modificador native va más allá de los alcances del curso.
Los modificadores relacionados con el comportamiento polimórfico de un método (new,
virtual, abstract, override y sealed) se verán más adelante.
A diferencia de C++, no se pueden asignar valores por defecto a los parámetros.
El Paso de Parámetros
Los lenguajes de programación suelen diferenciar entre dos tipos de pasos de parámetros:
Paso por valor: El parámetro recibe una copia del dato original. Toda modificación al
dato del parámetro no modificará al dato original.
Paso por referencia: El parámetro recibe una referencia del dato original. Toda
modificación al dato del parámetro modificará al dato original.
En Java, cuando se llama a un método y se pasan parámetros todas las variables de tipo
primitivo y objetos son pasadas por valor. Los objetos por ser referencias permiten la
modificación de su contenido tal y como ocurre cuando pasamos punteros como parámetros en
C++. Como consecuencia de esto, no es posible modificar el valor de una variable primitiva
llamando a un método al que se le pase dicha variable como parámetro. Tampoco es posible
hacer que una referencia apunte a otro objeto llamando a un método y pasando dicha referencia
como parámetro.
En C# se ofrece mayor control en el paso de parámetros que en Java. Por defecto el paso de
parámetros es como el indicado en Java. Adicionalmente se puede pasar “por referencia” las
variables tipo valor (lo que incluye las variables primitivas) y “por referencia a referencia” las
variables tipo referencia. Pasar “por referencia una referencia” es similar a pasar “un puntero a
un puntero” en C++, sin sus complicaciones sintácticas. Para esta característica adicional de C#
se utilizan las palabras reservadas “ref” y “out”. El siguiente programa muestra el uso de estos
tipos de pasos de argumentos en C#.
using System;
class Principal {
public static void mostrar(string mensaje, int[] arreglo, int indice) {
Console.WriteLine(mensaje);
Console.WriteLine(" arreglo.Length=" + arreglo.Length);
Console.WriteLine(" arreglo[0]=" + arreglo[0]);
Console.WriteLine(" arreglo[1]=" + arreglo[1]);
Console.WriteLine(" arreglo[2]=" + arreglo[2]);
Console.WriteLine(" indice=" + indice);
}
public static void modificar1(int[] arr, int indice) {
arr[indice++] += 100;
arr = new int[10];
}
public static void modificar2(ref int[] arr, ref int indice) {
arr[indice++] += 100;
arr = new int[10];
}
public static void obtener(out string nombre, out string apellido) {
nombre = "Jose";
19
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
P R O G R A M A C I Ó N O R I E N T A D A A O B J E T O S
apellido = "Perez";
}
public static void Main(string[] args) {
int[] arreglo = {1,2,3};
int indice = 0;
mostrar("al inicio", arreglo, indice);
modificar1(arreglo, indice);
mostrar("luego de llamar a 'modificar1'", arreglo, indice);
modificar2(ref arreglo, ref indice);
mostrar("luego de llamar a 'modificar2'", arreglo, indice);
string nombre, apellido;
obtener(out nombre, out apellido);
Console.WriteLine("nombre=" + nombre + ", apellido=" + apellido);
}
}
A diferencia de C++, no es posible ni en Java ni en C# definir valores por defecto para los
parámetros.
C# permite declarar una lista indeterminada de parámetros utilizando la palabra reservada
“params”. El parámetro declarado con “params” debe ser un arreglo y debe ser el último de la
lista de parámetros del método. El siguiente código muestra el uso de params.
using System;
class Principal {
public static void F(params int[] args) {
Console.Write("args contiene {0} elementos:", args.Length);
foreach (int i in args)
Console.Write(" {0}", i);
Console.WriteLine();
}
public static void Main(string[] args) {
int[] arr = {1, 2, 3};
F(arr);
F(10, 20, 30, 40);
F();
}
}
Como puede verse en el ejemplo anterior, la llamada a un método con un argumento “params”
es más flexible que en C++ con los tres puntos “...”. Se puede pasar tanto un arreglo como
parámetro, como una secuencia de parámetros independientes. En este último caso, el lenguaje
agrupa dichos argumentos en un arreglo pasándoselos como tal al método.
Lo que es significativamente menos legible si tuviésemos acceso directo al dato miembro real y
codificáramos:
Complejo c = new Complejo(1,2);
c.real = c.real * 2;
20
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
P R O G R A M A C I Ó N O R I E N T A D A A O B J E T O S
La ilegibilidad aumenta conforme la expresión se haga más compleja. Más aún, si las clases que
utilizamos no guardan un estándar, el programador que hace uso de ellas requerirá encontrar el
nombre de dos funciones para manejar el mismo dato.
Como una manera de simplificar el acceso a los datos de una clase, C# define el concepto de
“propiedad”. Una propiedad representa a uno o dos métodos de acceso a un dato miembro (o
uno calculado) de una clase. La sintaxis de una propiedad es:
[modificadores] <tipo de dato> <nombre de la propiedad> {
get { <cuerpo del bloque get> }
set { <cuerpo del bloque set> }
}
Se puede definir sólo el bloque set (con lo que tendríamos una propiedad de sólo escritura), sólo
el bloque get (propiedad de sólo lectura) o ambos bloques (propiedad de lectura y escritura).
Como puede verse en la sintaxis, las propiedades pueden recibir modificadores al igual que los
datos miembros y los métodos.
Por ejemplo, la siguiente clase en C# implementa el acceso a un archivo sólo para lectura.
Debido a esto, el dato del tamaño del archivo nunca se cambiará, por lo que este dato puede
implementarse como una propiedad de sólo lectura. Por otra parte, la posición actual de lectura
del archivo sí puede desearse que sea leída o modificada por el programa que usa esta clase, por
lo que puede implementarse como una propiedad de lectura y escritura.
class ArchivoSoloLectura {
...
public int Tamano {
get {
// aquí va el código que permite obtener el tamaño del archivo
return TamanoCalculado;
}
}
public int Posicion {
set {
// asumiendo que el método interno (privado) establecerPosicion
// realiza el trabajo de llamar a las librerías del sistema
// operativo para recolocar el puntero a un archivo
establecerPosicion( value );
}
get {
// aqui íría el código que permitiría calcular la posición actual
return PosicionCalculada;
}
}
}
Note que la palabra reservada “value” es utilizada dentro del bloque “set” para hacer referencia
al valor que se le quiere establecer a la propiedad al utilizarla en un programa. El siguiente
código hace uso de esta clase.
ArchivoSoloLectura arch = new ...;
Console.Write(“El tamano del archivo es “ + Arch.Tamano);
// Aquí se realizaría algún trabajo de lectura
Console.Write(“La posición actual en el archivo es “ + Arch.Posicion);
Arch.Posicion = 0; // se retorna al inicio del archivo
La última línea del programa anterior provoca una llamada al bloque set de la propiedad
Posicion, dentro del cual la palabra reservada “value” contendría en valor “0”.
Bajo el mismo concepto de la claridad del código brindado por las propiedades, cuando un
objeto representa una colección de elementos (por ejemplo, como una lista enlazada) sería más
claro su manejo si fuera posible acceder a estos elementos utilizando la misma sintaxis que con
los arreglos. Para esto, C# define los indizadores.
21
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
P R O G R A M A C I Ó N O R I E N T A D A A O B J E T O S
Un indizador es como una propiedad pero para manejar un objeto con la misma sintaxis que la
de un arreglo unidimensional. La sintaxis de declaración de un indizador es como sigue.
[modificadores] <tipo de dato> this[<tipo del indice> <nombre del indice>] {
get { <cuerpo del bloque get> }
set { <cuerpo del bloque set> }
}
Los indizadores no tienen un nombre particular, como las propiedades, puesto que son llamados
automáticamente cuando se utiliza la sintaxis del acceso a los elementos de un arreglo con ellos.
El parámetro entre corchetes sirve para declarar el tipo y nombre de la variable que servirá para
hacer referencia al índice (o lo que equivalga al mismo) dentro de los bloques set y get. Esto
implica que no requerimos necesariamente utilizar un dato de tipo entero como índice, sino
cualquier tipo de dato que deseemos.
Como ejemplo, imaginemos que deseamos crear una clase que maneje una lista enlazada de
objetos Nodo. Adicionalmente, queremos que el programador tenga la posibilidad de recorrer
los elementos de esta lista de la misma forma como lo haría con un arreglo, por simplicidad.
Tendríamos el siguiente esqueleto del código de esta clase.
class ListaEnlazada {
// Acá van los datos miembros de la clase y sus métodos correspondientes al
// manejo de una lista enlazada. Se asume que la clase “Nodo” ya esta
// implementada
Nodo primerNodo;
Note en el ejemplo anterior el uso del parámetro utilizado como índice dentro de los bloques get
y set del indizador. Note además que es posible definir propiedades en una clase con un
indizador. El siguiente programa hace uso de esta clase.
ListaEnlazada lista = ...;
// Aquí se inicializa la lista con valores para sus nodos
for( int i = 0; i < lista.Tamano; i++ )
// Acá la expresión “lista[i]” equivale a una llamada al bloque “get”
// del indizador de la clase ListaEnlazada
Console.WriteLine(“Valor en la posición “ + i + “ = “ + lista[i]);
Note como en el ejemplo anterior se accede a la variable lista como si se tratase de un arreglo.
Otro ejemplo del uso de indizadores y propiedades es la clase “string” de C#, la que implementa
22
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
P R O G R A M A C I Ó N O R I E N T A D A A O B J E T O S
el acceso a los carácteres de la cadena como un indizador de sólo lectura, así como la propiedad
de sólo lectura Length para obtener su longitud.
Las Estructuras
C++ permite definir tipos de datos “estructura” y “clase”. Las estructuras son herencia de C,
donde se utilizaban como forma de crear nuevos tipos de datos compuestos. Sin embargo, la
implementación interna de C++ para las estructuras es casi la misma que para las clases, con la
única diferencia que por defecto los miembros de una estructura son públicos, por lo que la
decisión de mantener la palabra reservada “struct” en C++ responde solo a razones de
compatibilidad.
C# también permite definir estructuras como un tipo especial de clases. Las estructuras en C#
están diseñadas para ser tipos de datos compuestos más limitados que el resto de clases pero
más eficientes que éstas. A continuación se listan las capacidades de las estructuras en C#:
Sus variables son reservadas automáticamente en memoria de pila y se manejan como
tipos de dato valor.
No permiten la herencia de otras estructuras ni clases, pero sí la implementación de
interfaces (lo que se verá más adelante).
Permiten la definición de métodos, pero no el polimorfismo.
Permiten la definición de constructores, menos los constructores por defecto, dado que
el compilador siempre incluye uno.
No permiten la inicialización de sus datos miembros de instancia en la misma
declaración.
No se puede utilizar una variable estructura hasta que todos sus campos hayan sido
inicializados.
Una variable tipo estructura en C# se reserva automáticamente en pila, mientras que en C++ el
programador es quien decide si se almacena en pila o en montón.
El uso de estructuras en C# es recomendado cuando se cumple una o más de las siguientes
condiciones:
Cuando la información que contendrán las estructuras será manipulada como un tipo de
dato primitivo.
Cuando la información que se almacena es pequeña, esto es, menor o igual a 16 bytes.
Cuando no se requiere utilizar herencia y/o polimorfismo.
La sintaxis de declaración de una estructura en C# es:.
[modificadores] struct <nombre> [: <lista de interfaces que implementan>]
{ <datos,tipos,funciones>
}
23
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
P R O G R A M A C I Ó N O R I E N T A D A A O B J E T O S
La Herencia y el Polimorfismo
Cuando un tipo de dato extiende la definición de otro, se dice que el primero hereda del
segundo. En esta sección revisaremos el manejo de la herencia en los tres lenguajes estudiados,
así como las capacidades que cada uno ofrece en cuanto al manejo del polimorfismo.
La Herencia
Un tipo de dato hereda de otro todos sus miembros: Datos, métodos y otros tipos de datos. El
que un tipo de dato herede de otro es una característica que facilita el desarrollo de programas
gradualmente más complejos.
A la clase de la que se hereda se le llama clase base o súperclase. A la clase que hereda de otra se
le llama clase derivada o subclase. Cuando una clase hereda de más de una base, se le conoce
como herencia múltiple. Como veremos más adelante, la herencia múltiple tiene una serie de
beneficios y problemas, por lo que no es soportada en algunos lenguajes orientados a objetos.
Esta herencia puede producir diferentes tipos de conflicto:
Conflicto de espacio de nombres: Dado que los miembros de la clase base pasan a
formar parte del espacio de nombres de la clase derivada, existe el problema potencial de
que un miembro de una clase base coincida en nombre con uno de la clase derivada.
Conflicto en la resolución de las llamadas a los métodos: Dado que es posible
diferenciar a un método de otro por otros elementos además de su nombre, es posible
definir más de un método con el mismo nombre, tanto en la clase base como en la
24
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
P R O G R A M A C I Ó N O R I E N T A D A A O B J E T O S
derivada, por lo que se requiere de una estrategia para determinar a qué método se está
llamando realmente dentro del contexto de la llamada. Esta situación se conoce como
“polimorfismo”.
Problemas de duplicidad de datos: Lo que es un problema cuando se tiene herencia
múltiple de clases que a su vez heredan, directa o indirectamente, de una misma clase
base. En dichos casos, los objetos de la clase más derivada contendrán datos miembros
duplicados, uno por cada rama de la herencia.
La Tabla 4 - 5 muestra con qué tipos de datos es posible utilizar herencia y qué tipo de herencia
es soportada:
Tabla 4 - 5 Tipos de herencia por lenguaje
C++ Java C#
La Declaración de la Herencia
La sintaxis de declaración de la herencia en C++ es:
class <nombre>: [mod. de acceso]<clase base1>, [mod. de acceso]<clase base2>, ...
{ <cuerpo de la clase> };
25
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
P R O G R A M A C I Ó N O R I E N T A D A A O B J E T O S
Cuando las clases, en un árbol de herencia, contienen constructores por defecto, la secuencia de
llamada durante el proceso de construcción de un objeto se realiza automáticamente. Sin
embargo, cuando una clase base no posee un constructor por defecto o se desea ejecutar
determinado constructor con parámetros, se puede especificar dicho paso de parámetros desde
el constructor de la clase derivada, de la siguiente forma:
C++:
class Base {...};
class Derivada: public Base{
public Derivada(...) : Base(...)
{...}
};
Java:
class Base {...}
class Derivada extends Base{
public Derivada(...)
{ super(...); ... }
}
C#:
class Base{...}
class Derivada: Base{
public Derivada(...): base(...)
{...}
}
Note que mientras en C++ se especifica este paso de parámetros, desde el constructor de una
clase derivada al de una clase base, con el nombre mismo de la clase base, en Java y C# se
utilizan las palabras reservadas “super” y “base” respectivamente. Esto se debe a que C++
soporta herencia múltiple, por lo que podría requerirse pasar parámetros a más de un
constructor, mientras que Java y C# soportan sólo herencia simple de clases. La herencia
múltiple se verá más adelante.
Note además que la llamada a “super” en Java debe hacerse obligatoriamente en la primera línea
del constructor.
Acceso a los Miembros Heredados
En la herencia, la clase derivada puede contener miembros que coincidan en nombre con
algunos de la base. Esto no es un error, y es aceptado en los tres lenguajes estudiados. Sin
embargo, es necesario contar con un mecanismo para diferenciar, en estos casos, a qué miembro
se refiere un pedazo de código, tanto dentro como fuera de la clase.
Dentro de los métodos de la clase derivada, cuando se utiliza directamente el nombre del
miembro en conflicto, el compilador asume que nos referimos al de la clase derivada. Para
referirnos al de la clase base debemos utilizar una sintaxis especial:
En C++: <nombre de la clase base> :: <nombre del miembro>
En Java: super.<nombre del miembro>
En C#: base.<nombre del miembro>
Como puede verse, en Java y C# sólo puede accederse al miembro heredado de la clase base. El
siguiente ejemplo en Java muestra esta limitación:
class Base {
void m() { System.out.println("Llamada a Base.m"); }
}
26
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
P R O G R A M A C I Ó N O R I E N T A D A A O B J E T O S
El Polimorfismo
El polimorfismo ocurre cuando la llamada a un método se resuelve sobre la base del contexto de
la llamada, dado que existe más de una implementación para dicho método, es decir, la llamada a
un método puede tomar distintas formas, según como ésta se realice. Veremos tres casos de
polimorfismo:
La sobrecarga de funciones y métodos.
La sobrescritura en la herencia.
La sobrescritura en la implementación de las interfaces.
La Sobrecarga
La forma más simple de polimorfismo es la sobrecarga, en donde dos métodos con el mismo
nombre son diferenciados por el lenguaje basándose en sus argumentos.
El siguiente ejemplo muestra la sobrecarga en C#.
using System;
struct Punto {
public int x, y;
public Punto(int x, int y) {
this.x = x;
this.y = y;
}
}
class Figura {
Punto posicion;
public Figura() : this(new Punto()) {
}
public Figura(Punto p) {
Mover(p);
}
public void Mover(int x, int y) {
Mover(new Punto(x, y));
}
public void Mover(Punto p) {
posicion = p;
}
}
27
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
P R O G R A M A C I Ó N O R I E N T A D A A O B J E T O S
En el ejemplo anterior el método “Mover” es sobrecargado tres veces, dos en la clase “Figura” y
una en la clase “Circulo”. Además, los constructores de ambas clases están sobrecargados.
Cuando se llama al método “Mover” se resuelve dicha llamada sobre la base del tipo de la
variable utilizada y los parámetros pasados. El siguiente código utiliza las clases anteriores.
Figura f = new Figura();
f.Mover(2,3);
Circulo c = new Circulo(10);
c.Mover(new Punto(7,4), 2);
f.Mover(new Punto(7,4), 2); // ERROR
La última línea anterior genera un error debido a que “f” es tipo “Figura” y dentro de esta clase
no existe una sobrecarga apropiada para los argumentos pasados en la llamada.
Es importante notar que dos métodos no pueden diferenciarse por su valor de retorno, por lo
que dos métodos con el mismo nombre y los mismos argumentos no se podrán diferenciar. La
sobrecarga es soportada por C++, C# y Java y no depende de la herencia.
Adicionalmente C++ y C# permiten la sobrecarga de operadores. C++ ofrece una amplia
gama de opciones en cuanto al juego de operadores que pueden ser sobrecargados y la forma en
que puede declararse dichas sobrecargas. C# por el contrario, ofrece un conjunto restringido de
operadores que pueden ser sobrecargados y un único formato de declaración de dicha
sobrecarga. Por ejemplo, la sobrecarga del operador de suma en C# tiene el siguiente formato:
public static Tipo operator+(Tipo1 op1, Tipo2 op2) { ... }
El siguiente código utiliza una sobrecarga del operador “+” para una clase Vector:
class Vector {
private double x, y;
public Vector(double x, double y) {
this.x = x;
this.y = y;
}
public static Vector operator+(Vector op1, Vector op2) {
return new Vector(op1.x + op2.x, op1.y + op2.y);
}
public void Imprimir() {
Console.WriteLine("Vector[x={0}, y={1}]", x, y);
}
}
class PruebaDeSobrecargaDeOperadores {
public static void Main(string[] args) {
Vector a = new Vector(1, 2), b = new Vector(3, 4), c;
c = a + b;
c.Imprimir();
}
}
La declaración de una sobrecarga debe ser pública y estática y el tipo del primer parámetro debe
coincidir con el tipo de la clase dentro de la que se declara la sobrecarga.
28
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
P R O G R A M A C I Ó N O R I E N T A D A A O B J E T O S
El Ocultamiento
La sobrecarga funciona bien cuando podemos diferenciar dos métodos por sus argumentos,
pero es posible tener un método en una clase base con el mismo nombre y los mismos
argumentos en la clase derivada. Esto es deseable cuando lo que deseamos conseguir es un
ocultamiento de la implementación original de dicho método, de forma que en el código que
utilice nuestra clase se llame a esta nueva implementación. El ocultamiento es un tipo de
sobrescritura, y es tan eficiente como la sobrecarga.
El siguiente programa en C++ muestra el uso del ocultamiento.
class Base {
public: void Metodo( ) { cout << "Base::Metodo\n"; }
};
class Derivada : public Base {
public: void Metodo( ) { cout << "Deri::Metodo\n"; }
};
void main() {
Base ObjBase; Derivada ObjDerivada; Base* pBase;
ObjBase.Metodo( );
ObjDerivada.Metodo( );
pBase = &ObjBase;
pBase->Metodo( );
pBase = &ObjDerivada;
pBase->Metodo( );
}
Al ejecutar este programa se ve que las dos primeras llamadas a “Metodo” utilizando las
variables “ObjBase” y “ObjDerivada” ejecutan la implementación respectiva en cada clase. En
estos casos, el compilador ya no ha podido basarse en los argumentos de la llamada al método
para decidir a cuál implementación llamar, sino en el tipo de las variables utilizadas. Las dos
últimas llamadas utilizan un puntero del tipo de la clase base “Base” para, apuntando a cada
objeto, llamar al “Metodo”. Ambas llamadas se resuelven hacia la implementación en la clase
“Base”, dado que, al igual que en las dos primeras llamadas, el compilador se basó en el tipo de
la variable utilizada para decidir a qué implementación llamar.
El ejemplo anterior muestra una clara ventaja y limitación del ocultamiento. Si bien el
ocultamiento es eficiente dado que la resolución de la llamada se realiza en tiempo de
compilación, la capacidad de ocultar es limitada sólo a los casos donde el tipo del objeto creado
coincide con el tipo de la variable que lo referencia o apunta. Este es el caso del puntero de tipo
“Base” utilizado para llamar al método “Metodo” de un objeto de tipo “Derivada”. Aquí la
nueva implementación de “Metodo” no ocultó a la implementación original, lo que
probablemente se desea que ocurra.
El siguiente programa corresponde a una versión en C# del programa anterior.
using System;
class Base {
public void Metodo( ) {
Console.WriteLine("Base.Metodo");
}
}
class Derivada : Base {
public new void Metodo( ) {
Console.WriteLine("Derivada.Metodo");
}
}
class PruebaDeEstructuras {
public static void Main(string[] args) {
Base ObjBase = new Base();
Derivada ObjDerivada = new Derivada();
ObjBase.Metodo( );
ObjDerivada.Metodo( );
29
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
P R O G R A M A C I Ó N O R I E N T A D A A O B J E T O S
Base refBase;
refBase = ObjBase;
refBase.Metodo( );
refBase = ObjDerivada;
refBase.Metodo( );
}
}
Otra consecuencia de la forma cómo funciona el ocultamiento es que no se permite que las
implementaciones en la clase base utilicen las versiones actualizadas de los métodos ocultados.
Los métodos en la base siempre trabajan con “lo conocido” para su clase. El siguiente ejemplo
en C++ muestra este caso.
class Base {
public: void Met1() { cout << "Base::Met1\n"; }
void Met2() { Met1(); }
};
class Derivada : public Base {
public: void Met1() { cout << "Deri::Met1\n"; }
};
void main() {
Base ObjB; ObjB.Met2();
Derivada ObjD; ObjD.Met2();
}
En resumen, la sobrescritura virtual permite que desde cualquier método de la clase base, de la
clase derivada o desde fuera de ellas, una llamada a un método se resuelva sobre la base del tipo
30
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
P R O G R A M A C I Ó N O R I E N T A D A A O B J E T O S
En C#:
[modificadores] interface <nombre> [ : <lista de interfaces> ]
{ <declaración de los métodos> }
Las interfaces soportan la herencia múltiple, debido a que no puede existir conflicto de
implementación (dado que nada se implementa) ni tampoco duplicidad de datos (dado que no se
permiten declarar datos). En C++ no existe el concepto de interfaz, pero puede ser simulado
mediante una clase que contenga sólo métodos virtuales puros públicos.
Una interfaz declara, no implementa, un conjunto de métodos que corresponden a una
funcionalidad que una clase puede exponer. Por ejemplo, la interfaz “dibujable” puede ser
implementada tanto por una clase “marquesina” como una clase “fotografía”. Por tanto, las
interfaces agrupan a un conjunto de métodos públicos que pueden ser implementados por una
clase.
El siguiente programa Java hace uso de una interfaz.
interface Dibujable {
void Dibujar( );
}
class Imagen implements Dibujable {
public void Dibujar( ) { System.out.println("Llamando a Dibujar en Imagen"); }
}
class Texto implements Dibujable {
public void Dibujar( ) { System.out.println("Llamando a Dibujar en Texto"); }
}
public class PruebaDeInterfaces {
public static void main(String[] args) {
Imagen img = new Imagen();
Texto txt = new Texto();
Dibujable dib = img;
dib.Dibujar();
dib = txt;
dib.Dibujar();
}
}
31
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
P R O G R A M A C I Ó N O R I E N T A D A A O B J E T O S
interface Dibujable {
void Dibujar( );
}
class Imagen : Dibujable {
public void Dibujar( ) { Console.WriteLine("Llamando a Dibujar en Imagen"); }
}
class Texto : Dibujable {
public void Dibujar( ) { Console.WriteLine("Llamando a Dibujar en Texto"); }
}
public class PruebaDeInterfaces {
public static void Main() {
Imagen img = new Imagen();
Texto txt = new Texto();
Dibujable dib = img;
dib.Dibujar();
dib = txt;
dib.Dibujar();
}
}
Una clase puede implementar más de una interfaz y en el caso de C#, una estructura también.
Como puede verse, el concepto de herencia múltiple de interfaces en Java y C# reemplaza al de
herencia múltiple de clases en C++, evitando los problemas que ésta última tiene.
El siguiente código de programa en C# muestra una herencia múltiple de interfaces.
interface IArchivo {
void posicion( );
}
interface IArchivoBinario : IArchivo {
byte leerByte( );
void escribirByte(byte b);
}
interface IArchivoTexto : IArchivo {
char leerChar( );
void escribirChar(char b);
}
class ArchivoBinario : IArchivoBinario { ... }
class ArchivoTexto : IArchivoTexto { ... }
class ArchivoBinarioTexto : IArchivoBinario, IArchivoTexto { ... }
Aún cuando no se declare como públicos los métodos de una interfaz, éstos siempre lo son.
Por tanto, las implementaciones de estos métodos en las clases deberán ser declaradas como
públicas.
32
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
P R O G R A M A C I Ó N O R I E N T A D A A O B J E T O S
Note que en C# el método abstracto en la clase base debe de declararse explícitamente como
“abstract”, lo que no requiere Java. Note también que la implementación del método en la clase
derivada se declara como “override”, debido a que los métodos abstractos son implícitamente
virtuales. Como en Java todo polimorfismo es virtual, un equivalente de este programa en Java
no requeriría utilizar ninguna palabra reservada especial en la declaración de la implementación
en la clase derivada.
Todo método declarado dentro de una interfaz en Java y C# es implícitamente abstracto, virtual
y público.
Java permite la declaración de constantes dentro de sus interfaces. C# permite declarar
propiedades e indizadores. La siguiente interfaz en C# declara una propiedad de sólo lectura y
un indizador de lectura y escritura.
class UnaInterface {
int UnaPropiedad { get; }
int this [ int indice ] { get; set; }
}
33
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
P R O G R A M A C I Ó N O R I E N T A D A A O B J E T O S
sobrescribir parte de la básica. Por otro lado, las interfaces son adecuadas cuando no se requiere
contar con una implementación básica común, dado que toda se realizará en las clases que las
implementen. La tabla 4.6 sumariza estas diferencias.
Tabla 4 - 6 Diferencias entre las clases abstractas y las interfaces.
Implementar métodos No Sí
Crear objetos No No
34
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
P R O G R A M A C I Ó N O R I E N T A D A A O B J E T O S
Cuando carezca de sentido crear objetos de esta clase sin que existan previamente
objetos de su clase outer. En este caso, se requiere de un objeto de la clase outer para
poder crear, sobre la base de su información interna y otros datos externos, objetos de la
clase inner.
Cuando la clase outer le da sentido a la inner. En este caso, la clase outer funciona
como un espacio de nombres. Al igual que en el primer caso, si no se cumpliera esta
condición se recomienda utilizar clases en un espacio de nombres independiente en
lugar de clases inner.
El siguiente programa en Java muestra el uso de una clase inner.
class A {
class B {
public void metodo1() {
System.out.println("Llamada a B.metodo1");
metodo2();
}
}
private void metodo2() {
System.out.println("Llamada a A.metodo2");
}
public void metodo3() {
System.out.println("Llamada a A.metodo3");
B objB = new B();
objB.metodo1();
}
}
public class PruebaDeInterfaces {
public static void main(String[] args) {
A objA = new A();
objA.metodo3();
}
}
En el ejemplo anterior, la clase B es inner de la clase A, por lo que puede acceder a todos los
miembros de ésta última, inclusive los privados, como la llamada al método “metodo2” desde su
implementación de “metodo1”. Dado que la clase B es un miembro más de la clase A, al no
habérsele especificado un modificador de acceso posee el modificador de-paquete. Por tanto, es
posible acceder desde “main” a dicha clase, declarar variables y crear objetos con ella.
Es interesante notar como el nombre de la clase outer forma parte del nombre de la clase inner,
es decir, fuera de la clase A, el nombre completo de la clase B es “A.B”. El uso de la palabra
reservada “new” también requiere una sintaxis especial, dado que los objetos de la clase B son
considerados “de instancia”, esto es, se requiere utilizar una instancia de la clase A para crear una
de la B. Piense en esto: Si la instancia del objeto referenciado por “objB” no hubiera sido creada
haciendo referencia a un objeto de la clase A, ¿al método de qué objeto se estaría accediendo en
la llamada que hace “metodo1” a “metodo2”, dado que éste último es igualmente un método de
instancia? Luego, cuando se crea un objeto de una clase inner de instancia, dicho objeto
conserva una referencia al objeto outer en base al que fue creado, de forma que pueda acceder
35
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
P R O G R A M A C I Ó N O R I E N T A D A A O B J E T O S
tanto a sus miembros estáticos como no-estáticos. Sin embargo, si esta característica no se
deseara, la clase B pudo declararse como estática, es decir:
class A {
static class B {
public void metodo1() { ... }
}
private static void metodo2() { ... }
public void metodo3() { ... }
}
public class PruebaDeInterfaces {
public static void main(String[] args) {
A objA = new A();
objA.metodo3();
A.B objB = new A.B();
objB.metodo1();
}
}
En este caso, los métodos de la clase B sólo pueden acceder a los miembros estáticos de la clase
A, dado que no son creados con referencia a un objeto de A, por lo que “metodo2” requiere ser
estático para poder ser llamado desde “metodo2”. La sintaxis de creación de un objeto B desde
fuera de la clase A, en “main”, también cambia.
En el caso de C++ y C#, los objetos de las clases inner no conservan automáticamente una
referencia a un objeto de la clase outer, por lo que se les puede considerar clases inner estáticas
por defecto, por lo que no requieren ni permiten su declaración utilizando el modificador
“static”.
Finalmente las clases inner pueden declararse con los mismos modificadores de acceso de los
demás miembros. En el ejemplo anterior, si la clase “B” hubiera sido declarada como “private”,
no hubiera podido crearse ni declarar objetos de ésta fuera de los métodos de la clase “A”.
C#:
<nombre de la clase outer>.<nombre del miembro>
El siguiente programa en Java muestra este caso para una clase inner de instancia.
class Outer {
class Inner {
void m() {
System.out.println("Llamada a Inner.m");
Outer.this.m();
}
}
36
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
P R O G R A M A C I Ó N O R I E N T A D A A O B J E T O S
void m() {
System.out.println("Llamada a Outer.m");
}
void prueba() {
Inner objInner = new Inner();
objInner.m();
}
}
public class PruebaDeInterfaces {
public static void main(String[] args) {
Outer objOuter = new Outer();
objOuter.prueba();
}
}
En el programa anterior, la clase inner anónima está definida entre los dos corchetes que siguen
a la expresión de creación “new Base( )”. Esta clase anónima hereda de la clase “Base” y su
nombre no es requerido, debido a que el programa sólo requiere crear un objeto de dicha clase.
El objeto de esta clase anónima es manejado siempre utilizando una variable del tipo de la clase
base. El siguiente ejemplo en Java define una clase anónima que implementa una interfaz.
interface UnaInterface { void Imprimir(); }
class Principal {
public static void main(String args[]) {
UnaInterface obj = new UnaInterface() {
public void Imprimir() {
System.out.println(this.getClass().getName()+".Imprimir");
}
};
obj.Imprimir();
}
}
El código del método “Imprimir” muestra en consola el nombre interno que el compilador de
Java le asigna a la clase anónima definida, en este caso “Principal$1”. Si se revisa el directorio
donde se generan los archivos compilados de Java para este programa se verá el archivo
“Principal$1.class” correspondiente a esta clase inner.
Las clases anónimas son útiles cuando:
Sólo se desea crear objetos de esta clase en una sola parte del programa, para extender
una clase base o implementar una interfaz.
37
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
P R O G R A M A C I Ó N O R I E N T A D A A O B J E T O S
La Reflexión
Cuando se trabaja con árboles de clases con método polimórficos o con interfaces es común
tratar los objetos con referencias a las clases base o al tipo de las interfaces. A la asignación de
un objeto de una clase derivada a una variable de una clase base se le conoce como “up-cast” o
“widening”. Este tipo de asignaciones no requiere de una operación de cast explícita y es segura,
dado que un objeto de un tipo derivado “siempre” pueden tratarse como un objeto de un tipo
base.La operación contraria no es segura, es decir, asignar una referencia desde una variable de
un tipo base a una de un tipo derivado “no siempre” es correcta, dado que siempre es posible
que se esté referenciando a un objeto que no puede ser tratado como el tipo de la variable
destino. Este tipo de operación requiere de un cast explícito, y se conoce como “down-cast” o
“narrowing”. El siguiente código es un ejemplo de esto:
ClaseBase ref1 = new ClaseDerivada( ); // Up-cast o widening, siempre es seguro
En el código anterior, la última línea puede ocasionar un error al ser ejecutada, si la variable
“ref1” referenciara a un objeto de la ClaseBase, producto de una operación de asignación
ejecutada en alguna de las “otras líneas de código”.
Dado que este tipo de operaciones cast son requeridas en algunas ocasiones, suele ser necesario
contar con algún mecanismo para poder verificar si el objeto referenciado o apuntado por una
variable puede o no ser tratado como un tipo determinado. Éste es un ejemplo en donde la
técnica conocida como “reflexión” es útil.
Definición y Uso
Los lenguajes de programación que implementan la reflexión guardan información adicional en
los objetos que son creados durante la ejecución de un programa de forma que, en tiempo de
ejecución, sea posible obtener información sobre el tipo con que fue creado dicho objeto. La
identificación de tipos en tiempo de ejecución (o RTTI por sus siglas en inglés) es sólo una de
las capacidades que ofrece la reflexión. La reflexión permite conocer el árbol de herencia de un
tipo de dato, los modificadores con que fue definido, los miembros que incluye así como
información sobre cada uno de estos miembros, entre otros datos.
Algunos ejemplos del uso de la reflexión son:
Verificación de errores en tiempo de ejecución. Al realizar un cast de una referencia de
un tipo base a un tipo derivado, la reflexión puede servir para averiguar si dicho objeto
puede ser o no interpretado como tal o cual tipo, es decir, si su clase es o hereda del tipo
destino. Si el objeto no puede ser tratado como del tipo destino, se produce un error
que puede ser interceptado y tratado por el programa.
Entornos de desarrollo con componentes. Estos entornos requieren exponer al
programador los elementos de dichos componentes, así como su descripción, tipo,
parámetros, etc. Se pueden instalar nuevos componentes y el sistema deberá poder
reconocerlos automáticamente, siempre que cumplan con el estándar pedido por el
entorno, esto es, que dichos componentes expongan la interfaz requerida.
38
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
P R O G R A M A C I Ó N O R I E N T A D A A O B J E T O S
Programas orientados a dar servicios. Estos programas trabajan con otros programas
que deben cumplir con ofrecer cierta interfaz. El programa servidor determina, en
tiempo de ejecución, si otro programa cumple la interfaz necesaria para dar cierto
servicio y si lo hace, puede trabajar con él. Un ejemplo de un programa de servicio es la
misma arquitectura de un sistema operativo, donde los programas ejecutables y las
librerías deben exponer cierta interfaz para que el sistema operativo pueda ejecutarlos.
Otro ejemplo son los servidores Web y en general, cualquier servidor distribuido.
La reflexión no es la única técnica posible para éstos y otros casos donde un sistema requiera
conocimiento de si mismo y de su entorno para adaptarse, pero ofrece la ventaja de ser simple y
uniforme.
El tema de la reflexión es amplio, por lo que la siguiente sección sólo abarca lo que corresponde
a RTTI.
RTTI
Uno de los aspectos de la reflexión es la identificación de tipos en tiempo de ejecución o RTTI.
Esta consiste en determinar si un objeto puede ser manejado como un tipo de dato
determinado.
C++ expone una implementación limitada de RTTI y requiere que los programas que la utilizan
sean compilados con opciones especiales del compilador. El siguiente programa en C++ utiliza
esta técnica.
#include <iostream.h>
#include <typeinfo.h>
class Base {
public: virtual void Imprimir() { cout << "Base::Imprimir" << endl; }
};
class Derivada : public Base {
public: virtual void Imprimir() { cout << "Derivada::Imprimir" << endl; }
};
void Identificar(Base* pObj) {
Derivada* pDer = dynamic_cast<Derivada*>(pObj);
if(pDer != 0)
cout << "Objeto 'Derivada'" << endl;
else
cout << "Objeto 'Base'" << endl;
const type_info& ti = typeid(*pObj);
cout << "typeinfo: name=" << ti.name() << ", raw_name=" << ti.raw_name() << endl;
}
void main() {
Base* pBase = new Base();
Identificar(pBase);
Derivada* pDer = new Derivada();
Identificar(pDer);
}
El operador “tipeid” retorna una referencia de tipo “const type_info &”, que es un objeto que
contiene información acerca del tipo de objeto pasado al operador.
El operador “dynamic_cast” permite obtener un puntero de un tipo derivado, pasándole como
parámetro un tipo base y el puntero original. Sin embargo, requiere que el tipo base contenga
por lo menos un método “virtual”. Si el puntero pasado como parámetro no apunta a un objeto
que puede interpretarse como el tipo indicado entre los símbolos “< >”, el operador retorna
cero “0”.
39
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
P R O G R A M A C I Ó N O R I E N T A D A A O B J E T O S
Java utiliza el operador “instanceof” para determinar si el objeto referenciado por una variable
puede o no ser manejado como un tipo de dato determinado. El siguiente programa muestra su
uso.
class Base { }
class Derivada extends Base { }
class PruebaDeInstanceof {
static void Identificar( Base obj ) {
if ( obj instanceof Derivada ) System.out.println("Objeto 'Derivada'");
else System.out.println("Objeto 'Base'");
}
public static void main( String args[] ) {
Identificar( new Base( ) );
Identificar( new Derivada( ) );
}
}
Si un objeto puede ser tratado como un tipo de dato determinado, la asignación de ésta,
utilizando una operación de cast explícita, a una variable del tipo destino es segura y no arrojará
error durante su ejecución.
C# utiliza el operador “is” en lugar del operador “instanceof” de Java. El siguiente programa
muestra su uso.
using System;
class Base { }
class Derivada : Base { }
class PruebaDeIs {
static void Identificar(Base obj) {
if ( obj is Derivada ) Console.WriteLine("Objeto 'Derivada'");
else Console.WriteLine("Objeto 'Base'");
}
public static void Main() {
Identificar(new Base());
Identificar(new Derivada());
}
}
40
1
Capítulo
5
Espacios de Nombres y
Librerías
En este capítulo revisaremos la organización del código a un nivel distinto y complementario
al propuesto por la POO, las librerías, así como las ventajas y problemas que estas conllevan.
79
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
E S P A C I O S D E N O M B R E S Y L I B R E R Í A S
namespace <nombre> {
<declaración de elementos de programación>
}
Java por su lado, une el concepto de espacio de nombres con el de librería, por lo que los
detalles acerca de su definición los veremos más adelante, en la sección de librerías.
El siguiente ejemplo corresponde al uso de un espacio de nombres en C++:
namespace general {
int a, b;
void fn() {
a = 20;
b = a * 2;
}
}
void main() {
general::a = 10;
general::b = general::a;
}
Nótese que dentro del namespace “general” es posible acceder directamente a todos los
elementos declarados en él, esto es, utilizar su nombre corto. Fuera del namespace “general”
requerimos utilizar su nombre completo o largo. El nombre del namespace forma parte del
nombre de los elementos declarados dentro de él.
El siguiente ejemplo corresponde al uso de un espacio de nombres en C#:
namespace General {
class A {}
class B {}
}
Nótese que mientras en C++ se pueden definir variables, funciones o tipos de datos en un
espacio de nombres en C++, en C# sólo pueden definirse tipos de datos y como se verá más
adelante, otros espacios de nombres.
namespace Espacio1 {
namespace Espacio2 {
const int var = 10;
}
int var = Espacio2::var * 2;
}
float var = Espacio1::Espacio2::var + Espacio1::var;
namespace Espacio3 {
double var = ::var * 2;
}
void main() {
int var = (int)Espacio3::var + 100;
80
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
E S P A C I O S D E N O M B R E S Y L I B R E R Í A S
Nótese cómo, conforme se sale del namespace “Espacio1”, se requiere utilizar de un nombre
cada vez más largo para acceder a sus elementos desde fuera de él. Nótese además como los
nombres de los elementos son cada vez más largos conforme la anidación de los namespace
crece en profundidad.
El siguiente ejemplo corresponde al uso del anidamiento de espacios de nombres en C#:
using System;
namespace Espacio1 {
namespace Espacio2 {
class A {}
}
class A {}
}
class A {}
public class PruebaDeNamespace {
public static void Main() {
Espacio1.Espacio2.A obj1 = new Espacio1.Espacio2.A();
Espacio1.A obj2 = new Espacio1.A();
A obj3 = new A();
}
}
namespace Espacio1 {
int var1 = 10;
int var2 = 20;
}
namespace Espacio2 {
int var3 = 30;
int var4 = 40;
}
void main() {
cout << "var1 = " << var1 << endl;
cout << "var2 = " << var2 << endl;
cout << "var3 = " << var3 << endl;
cout << "var4 = " << var4 << endl; // ERROR: variable no reconocida
// en el presente espacio de nombres
cout << "Espacio2::var4 = " << Espacio2::var4 << endl; // Corrección
81
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
E S P A C I O S D E N O M B R E S Y L I B R E R Í A S
namespace Espacio1 {
namespace Espacio2 {
class A {}
class B {}
}
}
En el caso de C#, la directiva “using” sólo puede utilizarse antes de la declaración de cualquier
espacio de nombres. Note que “System” es realmente un espacio de nombres, no una librería.
Dentro de System están definidos todos los tipos de datos primitivos, tipos para el manejo de la
entrada y salida estándar como la clase Console, entre otros tipos y espacios de nombres que
forman la librería estándar de .NET.
Uso de un Alias
También se puede declarar un alias para hacer referencia a un namespace dentro de otro
namespace. El siguiente programa en C++ muestra un ejemplo de esto.
#include <iostream.h>
namespace Espacio {
const double var = 3.1416;
}
void main() {
cout << "EspacioX::var = " << EspacioX::var << endl;
}
namespace Espacio1 {
namespace Espacio2 {
class A {}
class B {}
}
}
82
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
E S P A C I O S D E N O M B R E S Y L I B R E R Í A S
#include <iostream.h>
namespace Espacio {
int var1 = 10;
}
namespace Espacio {
int var2 = 20;
}
int main(void) {
cout << "Espacio::var1 = " << Espacio::var1 << endl;
cout << "Espacio::var2 = " << Espacio::var2 << endl;
cin.ignore();
return 0;
}
Se dice que el segundo bloque namespace “Espacio” extiende el espacio de nombres definido
por el primer bloque namespace “Espacio”. Nótese que “var1” y “var2” pertenecen al mismo
espacio de nombres, aún cuando son declarados en bloques distintos. Los bloques pudieron
incluso haberse colocado en archivos distintos. Si esto fuese así, el compilador iría completando
el espacio de nombres conforme fuera compilando los archivos fuente de un proyecto.
El siguiente programa en C# muestra esta definición por partes.
namespace Espacio1 {
namespace Espacio2 {
class A {}
}
}
namespace Espacio1.Espacio2 {
class B {}
}
En el ejemplo anterior, las clases “A” y “B” forman parte del espacio de nombres
“Espacio1.Espacio2”.
Las Librerías
En el contexto de los lenguajes de programación, una librería es una unidad de agrupación de los
elementos de programación en una forma tal que puede ser distribuido para su reutilización
desde otros programas. Los elementos del lenguaje que pueden formar parte de una librería, así
como la forma de crearla y utilizarla dependen de cada lenguaje.
Las siguientes secciones describen el enfoque utilizado y la creación de librerías en C/C++, Java
y C#.
Librerías en C/C++
En C/C++ se definen dos tipos de librerías:
Las librerías estáticas.
Las librerías dinámicas.
Las librerías estáticas (comúnmente, con extensión LIB) son archivos con código máquina ya
enlazado (similar a los archivos OBJ, solo que estos últimos no están enlazados) que se utilizan
durante el proceso de enlace de archivos OBJ’s para formar un programa final, ya sea un
83
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
E S P A C I O S D E N O M B R E S Y L I B R E R Í A S
ejecutable u otra librería. En este sentido, utilizar librerías estáticas significa tener los compilados
de un conjunto de archivos fuente, C y CPP, con lo que se ahorra tiempo al momento de
compilar un programa que las utilice, dado que para dichos archivos ya no se requiere pasar por
un proceso de compilación. Si una librería es extensa, este tiempo de compilación ahorrado
puede ser significativo.
Las librerías dinámicas (comúnmente, con extensión DLL, abreviatura de Dinamic Link
Library) son archivos con código máquina ya enlazado y datos que permiten a otro programa, en
tiempo de ejecución, obtener la ubicación de sus funciones, para llamarlas. Los programas que
hacen uso de estas librerías deben seguir un proceso, asistido por el sistema operativo, para
cargar dicho código a memoria, buscar la ubicación en memoria de las funciones y llamar a éstas.
La DLL no forma parte del archivo del programa que las utiliza. Si dos o más programas en
ejecución solicitan una DLL, ésta se carga una sola vez a memoria, en la primera solicitud, con lo
que se ahorra espacio de memoria.
Dado que las librerías no se ejecutan directamente sino a través de otros programas que llaman a
sus funciones, no requieren implementar una función de entrada, como una función main o
WinMain.
Cuando una función de una librería es declarada de manera tal que ésta pueda ser llamada desde
otro programa, se dice que dicha función es exportada por la librería. Sólo las funciones
exportadas por una librería pueden ser accedidas desde un programa que utilice dicha librería.
Las librerías estáticas son más sencillas de utilizar que las dinámicas. En contraparte, las librerías
dinámicas permiten evitar la duplicidad de código. En consecuencia, se suele utilizar librerías
estáticas cuando:
Estas son significativamente pequeñas respecto a la cantidad de memoria disponible en
las computadoras donde correrán los programas que las usen.
Son pocos los programas, instalados en una misma computadora, que las utilizan.
En el resto de casos, se prefiere utilizar librerías dinámicas.
Las librerías pueden ser utilizadas tanto por programas ejecutables, como por otras librerías. Una
librería puede contener, además de las funciones exportadas y no-exportadas, recursos como
imágenes, audio, video, textos, etc.
Librerías Estáticas
La creación y manejo de una librería estática es sencillo:
Se compila como librería estática los archivos fuente que la conforman. Si se está
trabajando con un IDE, comúnmente se deberá crear el proyecto del tipo “para
creación de una librería estática”. Por ejemplo, en Microsoft Visual C++ 6.0, el tipo de
proyecto es “Win32 Static Library”.
El proceso de compilación generará el archivo de la librería, con extensión LIB.
Se incluye el archivo LIB dentro del proyecto del programa que utilizará esta librería. Si
se está utilizando un IDE, comúnmente basta con agregar el LIB como un archivo más
del proyecto, al igual que los archivos fuente.
Librerías Dinámicas
Las DLL son librerías a las que los programas acceden en tiempo de ejecución. Dado que estas
librerías no forman parte de dichos programas, tampoco son cargadas a memoria
84
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
E S P A C I O S D E N O M B R E S Y L I B R E R Í A S
automáticamente al ejecutarse éstos. Por ello, si un programa requiere ejecutar una función
exportada por una DLL, deberá solicitar al sistema operativo que cargue el archivo DLL a
memoria, averigüe la dirección de la función exportada de interés y llamarla. Al proceso de
averiguar la ubicación de una función para luego llamarla, se le conoce como enlace dinámico,
de allí el nombre de este tipo de librería.
Las DLL no tienen un punto de entrada para un hilo primario, como sucede con los archivos
ejecutables (función main o WinMain), debido a que el sistema operativo no crea un hilo de
ejecución para ellas. Son los hilos de los procesos que hacen uso de una librería, los que ejecutan
el código de ésta.
Windows está formado en gran parte por librerías dinámicas, desde donde se comparte la
funcionalidad que otras aplicaciones necesitan importar para interactuar con el sistema
operativo. Como ejemplo tenemos algunas de las DLL típicamente utilizados por las
aplicaciones de Windows:
KRNL386.EXE { Nótese que es un ejecutable}
GDI.EXE
USER.EXE
KEYBOARD.DRV
SOUND.DRV
Winmm.dll
Msvcrt40.dll, Msvcrt20.dll, Msvcrt.dll
Aunque es un uso poco frecuente, un archivo ejecutable también puede ser utilizado como una
DLL, siempre que éste contenga funciones exportadas y datos que permitan a otros programas
ubicarlas. Más adelante veremos cuáles son estos datos y cómo se crean.
Las DLL y los procesos que las llaman se cargan a memoria en tiempos y lugares distintos, por
lo que originalmente tienen espacios de direccionamiento distintos. Sin embargo, el sistema
operativo “mapea” las llamadas a las funciones de las DLL’s dentro de los espacios de
direccionamiento de los procesos de los hilos llamadores. Esto significa que la dirección de la
función exportada obtenida por el hilo llamador corresponde a un valor dentro del espacio de
direccionamiento de su proceso, pero cuando es utilizada para llamar a la función, el sistema
operativo “mapea” dicha dirección de forma que se acceda a la ubicación real de dicha función y
se pueda ejecutar su código. Por tanto, nunca se rompe la regla de que “los hilos de un proceso
no pueden acceder a direcciones en memoria fuera de su espacio de direccionamiento”.
Estructura Interna
Cuando un conjunto de archivos fuente es compilado y enlazado como una DLL, al archivo
resultante se le agrega al inicio, una tabla de exportación. Esta tabla contiene los datos que
permiten a los programas que hacen uso de una DLL, obtener la ubicación de una función
exportada. La estructura de esta tabla es, de manera simplificada, la siguiente:
85
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
E S P A C I O S D E N O M B R E S Y L I B R E R Í A S
1 Nombre 1 Dirección 1
DLL
2 Nombre 2 Dirección 2
Tabla de
exportación . . .
. . .
. . .
Datos y
código de
la librería
N Nombre N Dirección
N
Figura 5 - 1 Estructura interna de una DLL.
Cuando un programa carga a memoria una DLL, mediante funciones del API de Windows,
obtiene un handle a dicha librería. Mediante este handle el programa puede obtener la dirección
mapeada de cualquiera de las funciones exportadas por la DLL, igualmente, mediante funciones
del API de Windows.
Estas funciones del API de Windows realizan una búsqueda sobre la tabla de exportación, según
los parámetros que se le pasen, bien por el nombre de la función o directamente utilizando un
índice. Si se encuentra la función, se retorna su dirección mapeada. La búsqueda por nombre es
más lenta que utilizando un índice, pero ofrece la ventaja de asegurar al programa usuario que la
dirección devuelta corresponde a la función correcta. Como contraparte, la búsqueda por índice
es más rápida pero, dado que la posición en la tabla de exportación para los datos de una
función exportada puede variar entre versiones de una misma DLL, la dirección retornada
puede no ser de la función buscada.
Todas las funciones dentro de una DLL que son declaradas para exportación, estarán incluidas
en esta tabla. El resto de funciones son privadas de la librería, pero pueden ser llamadas desde las
funciones exportadas.
Espacio de Direccionamiento
Cada proceso activo en el sistema tiene un espacio de direccionamiento virtual privado. El
espacio de direccionamiento es un rango de direcciones virtuales, lo cual significa que dos
procesos podrían tener punteros con el mismo valor pero estar realmente apuntando a lugares
de memoria diferentes. La dirección a la que realmente se apunta es determinada por el sistema
operativo mediante tablas de direccionamiento. Este esquema de trabajo permite al sistema
operativo sacar de memoria (copiando sus datos a disco) procesos, o parte de ellos, que no se
estén ejecutando para colocar, en la misma ubicación de memoria real, otros procesos que deban
ejecutarse. De esta forma, el sistema puede simular que se está trabajando con una memoria
mucho mayor de la que realmente existe. A la técnica de bajar y subir a memoria bloques de
datos de procesos se le llama SWAPING. Al archivo en disco que se utiliza para esto se le llama
archivo de SWAP.
En conclusión, el sistema puede trabajar con tantos procesos a la vez como espacio de memoria
tenga sumando la memoria RAM más el espacio disponible en disco para el archivo de SWAP.
86
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
E S P A C I O S D E N O M B R E S Y L I B R E R Í A S
La técnica de mapeado de las direcciones de las funciones exportadas por las DLL’s cargadas a
memoria, permite no violar la regla de que los hilos de un proceso no pueden acceder a
direcciones fuera del espacio de direccionamiento de su proceso.
Creación de una DLL
Se deben seguir los siguientes pasos:
7. Se crea un nuevo proyecto del tipo “quiero crear una DLL”. Por ejemplo, en Microsoft
Visual C++ 6.0, el tipo del proyecto es “Win32 Dynamic-Link Library”.
8. Se agrega los archivos y el código necesario.
9. Se compila y se genera el archivo de la librería con extensión DLL y un archivo para
enlace en modo implícito con extensión LIB. El uso de éste último se verá más adelante.
Una DLL comúnmente contiene los siguientes archivos:
1. Un archivo cabecera (*.H) donde se declaran las funciones exportadas.
2. Los archivos fuentes (*.C o *.CPP) y otros archivos de cabecera. Una de las fuentes
deberá implementar la función DllMain. Las fuentes deberán implementar las funciones
exportadas, junto con el resto de funciones utilizadas por éstas.
3. Un archivo de definición (*.DEF) donde se declaren qué funciones serán las que se
exporten.
Como ejemplo, se explicarán los archivos correspondientes a una DLL que sólo exporte una
función. Llamaremos a esta DLL “MiLibreria.DLL”.
EL ARCHIVO CABECERA MILIBRERIA.H
Contiene los prototipos de las funciones a exportar. Los prototipos deben especificar además el
tipo de convención de llamada que se usará. Cada convención de llamada provoca una
decoración particular del nombre de cada función, de forma que quién llame a dicha función
sepa distinguir a qué convención se refiere. Nosotros podemos dejar que el compilador coloque
dichos adornos internamente (al momento de compilar) por nosotros o colocar dichas
decoraciones manualmente.
La convención de llamada de una función determina la forma en que el código máquina que se
genere, al momento de compilar las fuentes, realice la llamada a dichas funciones.
Si una función exportada por una DLL tiene una determinada convención de llamada, el
programa que utilice dicha función deberá declararla con la misma convención de llamada.
Luego, la manera más sencilla de salvar estos problemas es dejar que el compilador decida la
convención de llamada por nosotros, tanto cuando se compila el DLL como el programa que lo
utilizará.
Sin embargo, el compilador de C++ agrega adornos adicionales al nombre de las funciones
exportadas, lo que no realiza el compilador de C. La manera más sencilla de solucionar este
problema, si el programa y la DLL son compilados con compiladores distintos, es forzar a que
las funciones de exportación sean declaradas de una misma forma, por ejemplo, de C. Para hacer
esto, se debe declarar los prototipos de estas funciones, tanto en el programa como en el DLL,
dentro de la sentencia:
extern “C” {
// Aquí se colocan los prototipos de las funciones exportadas
}
87
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
E S P A C I O S D E N O M B R E S Y L I B R E R Í A S
Todo lo que esté dentro de los corchetes se compila con el compilador de C. Si el fichero es
agregado a un archivo *.CPP, este código se compila igualmente como C, y el resto del archivo
fuente se compila con el compilador de C++.
Como ejemplo, se desea declarar el archivo cabecera de la DLL para exportar una función que
muestre un mensaje de saludo. El archivo contendrá:
///////////////////////////
// Archivo MiLibreria.h
///////////////////////////
#include <windows.h>
#ifdef __cplusplus
extern “C” {
#endif
#ifdef __cplusplus
}
#endif
Las directivas #ifdef y #endif permiten que el código entre ellas sea tomado en cuenta por el
compilador únicamente si éste es el compilador de C++. La macro WINAPI permite especificar
la convención de llamada de la función a la del estándar utilizado por las librerías dinámicas de
Windows.
EL ARCHIVO FUENTE MILIBRERIA.CPP
Donde:
hModule : Es el handle para la instancia del DLL cargada en memoria.
dwReason : Es la razón por la que se llama a la función.
LPVOID lpReserved : Esta reservado para uso del sistema.
Si la función no se define, el entorno de Visual C++ agrega a nuestro código compilado la
declaración de una función DllMain por defecto. Si la función devuelve FALSE significa que la
88
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
E S P A C I O S D E N O M B R E S Y L I B R E R Í A S
carga / descarga falló, por lo que la función, en el hilo del proceso desde dónde se llamó a la
carga / descarga de la librería, recibirá un valor de error como resultado.
El resto del archivo fuente deberá contener la implementación de las funciones exportadas así
como otras de uso interno. Para nuestro ejemplo, el código sería:
///////////////////////////
// Archivo MiLibreria.cpp
///////////////////////////
#include <windows.h>
#include "MiLibreria.h"
LIBRARY SALUDAMEDLL
DESCRIPTION "Implementación de un saludo."
EXPORTS
Saludame @1
89
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
E S P A C I O S D E N O M B R E S Y L I B R E R Í A S
La inclusión de un archivo DEF en el proyecto también permite la creación, por parte de IDE,
de un archivo LIB, el cual podrá agregarse a los demás proyectos C/C++ desde donde
deseemos usar la librería enlazándola en modo implícito.
Cuando se utiliza la palabra-clave __declspec(dllexport), ésta es la que le indica al IDE la
creación del archivo LIB. Si para el ejemplo anterior deseáramos no utilizar un DEF, tendríamos
que declarar el prototipo de la función a exportar de la forma:
__declspec(dllexport) void WINAPI Saludame(char * szNombre);
El enlace en modo implícito se consigue utilizando el archivo LIB generado cuando se compiló
la DLL. Existen además programas utilitarios que permiten obtener un LIB directamente de un
DLL ya generado, como el programa implib.exe de Borland. Este archivo LIB contiene código
compilado que es agregado a nuestro programa. Dicho código se ejecutará al momento de
iniciar el programa, realizando el enlace dinámico por nosotros. Adicionalmente, este código es
sumamente eficiente y nos permite utilizar los prototipos de las funciones como si éstas fueran
codificadas dentro de nuestro programa al momento de compilarlo.
El archivo LIB se utiliza durante el proceso de enlace del programa que hará uso de la librería. Si
se usa un IDE, se tendrá que configurar el proyecto para que utilice los LIB’s de las DLL’s que
deseamos utilizar. Como ejemplo, para agregar un archivo LIB a un proyecto en Microsoft
Visual C++ 6.0, se colocan el nombre de éste en:
Project => Settings => Link => Category:General => Object/Library modules:
Luego, dentro del código de nuestro programa llamador podemos realizar la llamada a la función
exportada de la siguiente manera:
#include <windows.h>
#include "..\MiLibreria\MiLibreria.h"
int WINAPI WinMain( HINSTANCE hInstance, HINSTANCE hInstancePrev, LPSTR lpCmdLine, int
nCmdShow ) {
Saludame("JUAN");
return 0;
}
El enlace en modo explícito no requiere el uso del archivo LIB. Esto es útil cuando no
disponemos de éste o cuando sabemos el nombre del DLL sólo después que la aplicación se
está ejecutando, por ejemplo, siendo ingresado por el usuario.
Para este caso, necesitamos realizar los siguientes pasos:
90
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
E S P A C I O S D E N O M B R E S Y L I B R E R Í A S
BOOL FreeLibrary(
HINSTANCE hInstLib // Handle a la DLL, devuelta por AfxLoadLibrary
);
FARPROC GetProcAddress(
HMODULE hModule, // Handle a la DLL
LPCSTR lpProcName // Nombre de la función
);
La que utilizaremos para ejecutar la función en la DLL. Nuestro código de ejemplo quedaría de
la siguiente forma:
typedef void (WINAPI * PFUNC) (char *);
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hInsPrev, LPSTR lpCLine, int nCShow) {
PFUNC pfnSaludo;
HINSTANCE hDll;
hDll = LoadLibrary(“MyDll.dll”);
if ( hDll != NULL ) {
pfnSaludo = (PFUNC)GetProcAddress( hDll, “Saludame” );
if( pfnSaludo != NULL ) {
// cualquiera de los dos formatos siguientes es válido
( *pfnSaludo ) ( “OTTO” );
pfnSaludo ( “OTTO” );
}
FreeLibrary( hDll );
}
return 0;
}
Las funciones de carga y descarga lo que hacen es manejar un contador, mantenido por el
sistema operativo, del uso de una DLL. Cuando ese contador regresa a cero (una carga lo
aumenta en uno, una descarga lo baja en uno) la librería es descargada de memoria dado que ya
nadie la está utilizando.
Mecanismo de Búsqueda de una DLL
El enlace estático utiliza el siguiente mecanismo de búsqueda en directorios para encontrar el
archivo de la DLL:
4. En el directorio donde se encuentra el ejecutable de la aplicación.
91
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
E S P A C I O S D E N O M B R E S Y L I B R E R Í A S
Librerías en Java
Las librerías en Java se conocen con el nombre de paquetes. Un paquete Java es realmente un
directorio en algún parte de nuestro disco. Todos los archivos CLASS bajo un mismo directorio
pertenecen a un mismo paquete. Los sub-directorios dentro del directorio de un paquete
corresponden a otros paquetes que generalmente están relacionados a éste. Por ejemplo, la
siguiente estructura de directorios corresponde a parte de la librería estándar de Java.
<Directorio raíz de las clases de Java>
│
├──► java
│ │
│ ├──► awt
│ │
│ └──► applet
│ │
│ └──► event
└──► javax
│
├──► swing
│
└──► JoptionPane
La estructura indica que el paquete (directorio) “java” contiene otros 2 paquetes: “awt” y
“applet”. El paquete (directorio) “applet” contiene a su vez al paquete “event”. De igual forma,
el paquete (directorio) “javax” contiene al paquete “swing” el cual contiene la clase
“JOptionPane”.
Uso de un Paquete
El siguiente programa hace uso de la clase JOptionPane.
public class PruebaDeInterfaces {
public static void main(String[] args) {
javax.swing.JOptionPane.showMessageDialog(null, "Hola",
"Un Mensaje", JOptionPane.ERROR_MESSAGE);
System.exit(0);
}
}
Nótese que el nombre completo de una clase indica el directorio donde se encuentra el archivo
CLASS de ésta. Nótese además como Java une el concepto de espacio de nombres con el de
librería. Un paquete define un espacio de nombres y todo espacio de nombres es un paquete.
También es posible publicar el contenido de un paquete, en otro paquete, utilizando la palabra
reservada “import”, de forma que se puedan utilizar los nombres cortos de los elementos del
paquete. El siguiente programa modifica el anterior utilizando “import”.
import javax.swing.*;
public class PruebaDeInterfaces {
public static void main(String[] args) {
JOptionPane.showMessageDialog(null, "Hola", "Un Mensaje",
JOptionPane.ERROR_MESSAGE);
System.exit(0);
92
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
E S P A C I O S D E N O M B R E S Y L I B R E R Í A S
}
}
El uso de “import” es equivalente al uso de “using” en C++ y C#. Adicionalmente Java permite
publicar elementos individuales de un paquete dentro de otro paquete. La primera línea del
programa anterior pudo haberse escrito de la siguiente manera:
import javax.swing.JOptionPane;
Sin embargo, si se utilizan muchos elementos definidos dentro de un paquete, resulta más
conveniente publicar todo el paquete en lugar de publicar cada elemento individualmente.
Java no permite la definición de alias para los paquetes.
Ubicación de un Paquete
El directorio raíz bajo el que se encuentran todos los paquetes (directorios) de Java instalados en
una máquina se indica mediante una variable de entorno guardada en algún archivo de
configuración del sistema operativo. En el caso de Windows, esta variable de entorno se llama
CLASSPATH. Para Windows 95/98/Millenium, ésta y otras variables de entorno se suelen
colocar dentro del archivo autoexec.bat, donde CLASSPATH es inicializado de la siguiente
forma:
set CLASSPATH = <rutas iniciales separadas por “;”>
Por ejemplo, supongamos que el directorio raíz estuviese especificado de la siguiente forma.
set CLASSPATH = C:\CLASES
La instrucción anterior le dirá al compilador que la clase Graphics la podrá encontrar en la ruta
C:\CLASES\JAVA\AWT
Java es sensitivo a las diferencias entre letras capitales y no-capitales, por tanto las siguientes
importaciones son consideradas distintas por el compilador:
import java.awt.Graphics;
import java.awt.graphics;
Nótese que en los programas en Java se hace uso de algunas clases sin haber especificado su
paquete, como la clase String y la clase System. Estas clases pertenecen al paquete “java.lang”.
Este paquete corresponde a la librería básica de java y contiene definiciones que son parte del
mismo lenguaje, siendo importado automáticamente por el compilador por lo que no es
necesario declarar una sentencia “import” para dicho paquete.
Creación de un Paquete
Para definir clases que formen parte de un paquete se requiere utilizar la directiva “package” al
inicio del archivo donde se declaran estas clases. El siguiente programa define la clase A y B
como parte del paquete P.
package P;
public class A {}
class B {}
93
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
E S P A C I O S D E N O M B R E S Y L I B R E R Í A S
modificador de acceso por defecto “de-paquete”, por lo que sólo podrá ser utilizada desde otras
clases que pertenezcan al paquete “P”. Se pueden crear otros archivos que definan clases para el
paquete “P”, por lo que un paquete puede ser definido por partes, y no necesariamente en un
solo archivo.
Cuando un archivo de Java no declara la directiva “package”, las clases que define pertenecen al
paquete global, el cual coincide con el directorio actual de ejecución del programa.
Utilización de un Nuevo Paquete
Para utilizar un paquete nuevo se requiere crear una estructura de directorios que coincida con la
del nuevo paquete. Por ejemplo, si se tiene un paquete A con las clase A1, A2 y A3, y el
subpaquete B con las clases B1 y B2, se requeriría crear un directorio A, colocar dentro los
archivos A1.class, A2.class y A3.class, crear dentro el subdirectorio B y colocar dentro los
archivos B1.class y B2.class.
La nueva estructura de directorios, para ser reconocida por el programa que requiere utilizarla,
puede ir:
Bajo el mismo directorio de los archivos “class” que lo utilizan.
Bajo cualquier otro directorio en el computador, agregando dicho directorio a la lista de
rutas especificadas en la variable de entorno utilizada por el compilador y el intérprete,
CLASSPATH en el caso de Windows.
Si se revisa el contenido por defecto de la variable de entorno de ubicación de paquetes
encontraremos que lo que indican es la ubicación de uno o más archivos con extensión “jar”.
Ésta es una forma alternativa de distribuir un paquete.
Los archivos JAR empaquetan, de la misma forma que un archivo de compresión como el ZIP,
toda una estructura de directorios que forman un paquete, junto con los archivos incluidos en
éstos. De esta forma, cuando el compilador o intérprete de Java busca una clase y encuentra la
especificación de archivos con extensión JAR, continúa la búsqueda dentro de éstos de manera
similar a como lo haría en un directorio.
La ventaja de utilizar archivos JAR es el ahorro de espacio y facilita la distribución de los
paquetes, dado que sólo se requiere copiar un archivo y no crear toda una estructura de
directorios en el computador donde se desea utilizar un paquete.
Para generar un archivo JAR se puede utilizar el programa utilitario, distribuido junto con el
JDK, “jar.exe”. La siguiente sintaxis corresponde a la llamada a este programa:
jar {ctxu}[vfm0Mi] [archivo-jar] [archivo-manifest] [-C dir] archivos
El detalle de lo que cada opción significa se puede obtener ejecutando el programa sin ningún
parámetro. El siguiente comando es un ejemplo de creación de un paquete MiPaquete.jar con las
clases contenidas en el directorio DirectorioRaiz:
jar cvf MiPaquete.jar -C DirectorioRaiz / .
94
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
E S P A C I O S D E N O M B R E S Y L I B R E R Í A S
Librerías en C#
Las DLL’s presentan una carencia en su concepción: El sistema operativo no registra
automáticamente que programas hacen uso de una DLL. Algunos casos típicos de problemas
originados por esta carencia son:
Si al desinstalar un programa, éste elimina una DLL que es utilizada por otro programa,
éste último dejará de funcionar correctamente.
Si un usuario cambia de posición el archivo de la DLL utilizada por un programa, éste
quizá no lo encuentre, por lo que dejará de funcionar correctamente.
Adicionalmente, dado que las DLL’s se ubican en el sistema de archivos, en directorios
específicos, si se intenta copiar una nueva DLL, utilizada por ejemplo por un programa nuevo, y
su nombre coincide con otra preexistente, se reemplazará el archivo de la DLL antigua. Por lo
tanto, todos los programas que utilizaban la DLL reemplazada dejarán de funcionar
correctamente. Aún en el caso que dicho reemplazo sea intencional, por ejemplo al actualizar la
versión de una DLL, si la verificación de la versión de la DLL preexistente versus la nueva no se
realiza correctamente, es posible que se reemplace una DLL más reciente con una más antigua.
Incluso aún en el caso que el reemplazo sea realizado con una correcta verificación de las
versiones, siempre es posible que un error de programación en la nueva versión haga que un
programa que funcionaba correctamente con la antigua, deje de funcionar con la nueva.
Todos estos problemas son demasiado comunes, por lo que ..NET desarrolla un nuevo
concepto orientado a darles solución: Los ensamblajes.
Los Ensamblajes
Un ensamblaje es una unidad de instalación autodescriptiva. Todos los archivos generados en
.NET son parte de algún ensamblaje. Por ejemplo, los ejecutables (*.EXE) son ensamblajes.
Un ensamblaje puede estar formado por uno o más archivos, los que en conjunto contienen los
siguientes elementos:
Metadata del ensamblaje
Metadata de tipos
Código MSIL
Recursos
Los diagramas de la Figura 5 - 2 muestran dos ejemplos de distribución de estos elementos en
los archivos de un ensamblaje. El primero, Ensamblaje1.dll, es un ensamblaje tipo librería (esto
es, no existe un punto de entrada o método Main desde donde ejecutar un hilo primario)
formado por un único archivo. El segundo es un ensamblaje tipo ejecutable (si existe un Main)
formado por tres archivos. La Metadata del Ensamblaje del archivo Ensamblaje2.exe guarda la
descripción exacta de los archivos que forman el ensamblaje. Fuera de la Metadata del
Ensamblaje, el resto de elementos pueden existir en cada uno de los archivos que forman un
ensamblaje.
Un ensamblaje puede estar formado por los siguientes tipos de archivos:
Un archivo principal, EXE o DLL, donde se encuentra la Metadata del Ensamblaje,
entre otros elementos.
95
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
E S P A C I O S D E N O M B R E S Y L I B R E R Í A S
Cero, uno o más archivos de módulo de .NET, con extensión NETMODULE. Los
módulos de .NET no contienen Metadata del Ensamblaje.
Cero, uno o más archivos de recursos, como archivos de imagen, sonido, video, etc.
Si bien el Ensamblaje2 de la figura es formado por tres archivos, es posible compilar el archivo
principal, Ensamblaje2.exe, de forma que los demás archivos se incluyan dentro.
Las características más importantes de un ensamblaje son:
Es autodescriptivo. Toda la información sobre la versión del ensamblaje, la descripción
de los tipos de datos que contiene, los archivos que lo forman, etc., se encuentra dentro
del propio ensamblaje, por lo que instalar un ensamblaje sólo requiere copiarlo. No se
utiliza el registro de Windows.
Registra sus dependencias a otros ensamblajes: Nombre, versión, etc.
Pueden instalarse, en un mismo computador, diferentes versiones de un mismo
ensamblaje, sin causar conflicto.
Instalación sin impactos, es decir, el instalar un ensamblaje sólo puede afectar a los
programas que lo utilizan. No hay posibilidad que un ensamblaje reemplace otro con el
mismo nombre pero para otro uso, o con diferente versión. Los ensamblajes se
identifican de manera única.
Se ejecutan dentro de Dominios de Aplicación de un proceso.
Un ensamblaje en ejecución se le denomina aplicación. Dentro de un mismo proceso se pueden
ejecutar varias aplicaciones, del mismo o distinto ensamblaje, cada uno en un Dominio de
Aplicación distinto. Un Dominio de Aplicación es la frontera que aísla una aplicación del resto
de aplicaciones. De esta manera, las fallas en una aplicación no pueden afectar a otra aplicación,
aún perteneciendo ambas al mismo proceso. Para que un objeto de una aplicación acceda a uno
en otra aplicación, requiere hacer uso de un objeto proxy, cuyo concepto es el mismo que el
Stub de CORBA (ver capítulo 9).
Existen dos tipos de ensamblajes: Los privados y los públicos o compartidos.
96
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
E S P A C I O S D E N O M B R E S Y L I B R E R Í A S
Los ensamblajes privados son aquellos que se instalan junto con el programa que los utiliza, en el
mismo directorio o en un subdirectorio de donde se encuentra este programa. No se manejan
números de versión ni nombres únicos, dado que no se necesitan. Este tipo de ensamblaje
puede causar conflictos (de versiones por ejemplo) pero sólo en la aplicación que lo utiliza, y
como es natural se resuelven durante el proceso de desarrollo de dicho programa. Este tipo de
ensamblaje no puede afectar otros programas.
Los ensamblajes públicos o compartidos pueden ser utilizados por más de un programa, por lo
que se instalan en un lugar común. Estos ensamblajes deben seguir las siguientes reglas:
Tener un nombre único (llamado nombre fuerte). Parte de este nombre es un número
de versión mandatorio.
Mayormente, estar instalado en la Global Assembly Cache, un directorio dentro del
directorio de Windows (por ejemplo, C:\WINNT\assembly para Windows 2000).
El principal componente de la Metadata del Ensamblaje es el Manifiesto. Éste contiene:
El nombre identificatorio para el ensamblaje, la versión, la cultura y una llave pública
(una cadena de carácteres).
Una lista de los archivos que forman el ensamblaje.
Una lista de los ensamblajes referenciados por éste y por tanto, de los que depende para
su ejecución.
Un conjunto de Solicitudes de Permiso, que son los permisos necesarios para correr o
utilizar el ensamblaje.
Metadata de tipos de datos para aquellos tipos dentro de los archivos de módulo del
ensamblaje.
Puede examinarse el contenido de un ensamblaje, incluyendo su manifiesto, utilizando el
programa utilitario ILDASM.EXE.
El Ensamblaje Tipo Librería
Todos los programas hasta ahora generados son ensamblajes de un sólo archivo de tipo
ejecutable. Para crear un ensamblaje tipo librería que sea utilizada por otro ensamblaje se debe de
compilar como tal. Pongamos un ejemplo.
El siguiente código corresponde al archivo Ensamblaje1.cs:
using System;
public class Clase1 {
public void Saludame(string nombre) {
Console.WriteLine("Hola " + nombre);
}
}
Para compilarlo como un ensamblaje tipo librería, utilizamos la opción /target::library del
compilador csc.
csc /target:library Ensamblaje1.cs
97
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
E S P A C I O S D E N O M B R E S Y L I B R E R Í A S
Para compilarlo como un ensamblaje tipo ejecutable, que hace referencia a elementos dentro del
ensamblaje Ensamblaje1.dll utilizamos la opción /reference:Ensamblaje1.dll del compilador csc.
csc /reference:Ensamblaje1.dll Ensamblaje2.cs
Esto genera el archivo Ensamblaje2.exe. Para ejecutar este programa, ambos archivos,
Ensamblaje1.dll y Ensamblaje2.exe, deben de estar en el mismo directorio. Este ejemplo
corresponde a un ensamblaje privado. Los ensamblajes públicos van más allá de los alcances del
presente curso.
Relación con los Espacios de Nombres
C# separa los conceptos de espacios de nombres y el de ensamblaje. Un espacio de nombres
puede estar definido por partes en varios ensamblajes.
98
1
Capítulo
6
Programación genérica
En este capítulo revisaremos la utilización de la Programación Genérica..
Introducción
Es una técnica que permite generalizar un código de forma que pueda ser utilizado con datos de
distintos tipos. Para ejemplificar esta definición, revise la definición en Código 1 de una función
de intercambio Swap en C++.
void Swap(int & a, int & b) { // cuide de usar S mayúscula, para no confundir
int temp = b; // con la versión de esta función en la librería
b = a; // estándar
a = temp;
}
...
int x = 1, y = 2;
Swap(x, y);
2
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
E S P A C I O S D E N O M B R E S Y L I B R E R Í A S
utilizan, siendo especificados estos últimos recién al momento de utilizarse dicho tipo o llamarse
a dicha función.
En otras palabras, los genéricos son declaraciones que no se amarran a tipos de datos
particulares. Es cuando se utiliza un genérico cuando se especifican los tipos de datos con los
que se desea trabajar, dándole así más información al compilador con la cual hacer una mejor
verificación de tipos (y logrando un mejor static type-safe) y generar un código más eficiente. La
programación genérica permite trabajar con conceptos genéricos en lugar de con casos
particulares (ref. 13). En este capítulo veremos el uso de ésta técnica y las similitudes y
diferencias entre los mecanismos de soporte que le dan C++, Java y C#.
Terminología
A los tipos de datos respecto a los que se generaliza la declaración de un genérico se les
denominan parámetros-tipo formales o simplemente parámetros-tipo, mientras que a los tipos
de datos que se indican al utilizar el genérico se les denominan argumentos-tipo actuales. En
consecuencia, a los tipos genéricos se denominan también tipos parametrizados, y las funciones
genéricos (y métodos genéricos) como funciones parametrizadas (y métodos parametrizados).
Es posible generalizar distintos tipos de datos, como las clases, las estructuras y las interfaces.
Por ejemplo, una clase definida así se le denomina clase genérica o clase parametrizada.
En algunas implementaciones el uso de un genérico ocasiona que el compilador (o el intérprete)
construya un nuevo tipo de dato equivalente a la particularización del genérico con
determinados parámetros-tipo. A dichos tipos de datos se les denomina tipos construidos
(constructed type).
3
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
E S P A C I O S D E N O M B R E S Y L I B R E R Í A S
...
int x = 1, y = 2, z = 3;
Swap<int>(x, y); // los argumentos-tipo son pasados explícitamente
Swap(y, z); // aquí el compilador infiere los argumentos-tipo
4
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
E S P A C I O S D E N O M B R E S Y L I B R E R Í A S
para C++ porque permite definir genéricos y utilizarlos sin verse forzado a que los argumentos-
tipo cumplan con todos los requerimientos del genérico.
En Java el equivalente también sería un método genérico, sin embargo una implementación de
Swap en Java requeriría utilizar una clase auxiliar para permitir suplir la carencia de un
mecanismo de paso por referencia. Dejaremos este caso para más adelante. Un ejemplo simple
en Java sería un método que averigüe si un dato existe en un arreglo, para cualquier tipo de
arreglo. Antes de los genéricos, dicho método tendría una implementación como en Código 5.
Usando un método genérico la implementación sería como en Código 6.
public class Ejm2 {
public static boolean Existe(Object dato, Object[] arr) {
for(Object elemento : arr)
if(elemento.equals(dato))
return true;
return false;
}
public static void main(String[] args) {
Integer[] arr = {1, 3, 6, 9};
System.out.println("2 existe?: " + Existe(2, arr));
System.out.println("3 existe?: " + Existe(3, arr));
System.out.println("\"Hola\" existe?: " + Existe("Hola", arr));
}
}
5
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
E S P A C I O S D E N O M B R E S Y L I B R E R Í A S
encontrar un tipo que satisfaga esta condición, el compilador inserta operadores cast de forma
que, al menos en tiempo de ejecución, si ocurre un error sea notificado mediante una excepción.
Para el Código 6, el compilador reemplaza T por Object, quedando el código compilado como
en Código 5, dado que Object es el tipo de dato más genérico que permite que el código de
Existe sea válido. Ese es el motivo por el cual la última llamada a este método es Código 6 no
produce ningún error ni en compilación ni en ejecución. Sin embargo, hay mecanismos en Java
que permiten refinar más el proceso de selección que hace el compilador. Estos mecanismos se
denominan restricciones y actúan sobre los parámetros-tipo. En Java las restricciones son
siempre respecto a la herencia y son de dos tipos: Límite superior (upper bound) y límite inferior
(lower bound). En Código 7 se muestra una versión de Existe donde se especifica que el
parámetro-tipo TD debe ser o igual o uno derivado del parámetro-tipo TA. Por tanto, a TA es el
upper bound de TD.
public class Ejm2 {
public static <TA, TD extends TA> boolean Existe(TD dato, TA[] arr) {
for(TA elemento : arr)
if(elemento.equals(dato))
return true;
return false;
}
Código 8: Segunda versión de Existe como método genérico en Java con restricciones.
Al compilar Código 7 el compilador generará un error para la última llamada a Existe, pues no
podrá encontrar ningún tipo de dato que cumpla la condición impuesta por la restricción del
genérico. Las restricciones del tipo lower bound así como el uso del comodín “?” van más allá
del alcance de este documento.
También es posible definir restricciones en C#. En Código 8 se muestra un ejemplo de
aplicación de una restricción para una función de ordenamiento.
public class Ejm3 {
public static void Ordenar<T>(T[] arreglo) where T : IComparable {
for (int i = 0; i < (arreglo.Length - 1); i++)
for (int j = i + 1; j < arreglo.Length; j++)
if (arreglo[i].CompareTo(arreglo[j]) > 0) {
T temp = arreglo[i];
arreglo[i] = arreglo[j];
arreglo[j] = temp;
}
}
public static void Main() {
String [] arr = {"Jose", "Maria", "Ana"};
Ordenar(arr);
foreach (String s in arr)
Console.WriteLine(s + ",");
}
}
6
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
E S P A C I O S D E N O M B R E S Y L I B R E R Í A S
de los dos-puntos, las restricciones especiales: (1) class, que indica que el parámetro-tipo
restringido debe ser una clase; (2) struct, que indica que el parámetro-tipo restringido debe ser
una estructura; (2) new( ), que indica que el parámetro-tipo restringido debe tener un constructor
sin parámetros. No se puede usar las restricciones class y struct, ni struct y new( ) a la vez.
Adicionalmente es posible utilizar un tipo-construido para restringir un parámetro-tipo. Por
ejemplo, en Código 8 podemos utilizar where T : IComparable<T> en la definición de Ordenar,
utilizando así la versión genérica de IComparable con lo que permitimos que el CLR pueda
generar código más eficiente en tiempo de ejecución. Lo mismo es posible en Java, como se
muestra en Código 9, en donde se usa la interfaz genérica Comparable.
public class Ejm3 {
public static <E extends Comparable<E>> void Ordenar(E[] arreglo) {
for(int i = 0; i < (arreglo.length - 1); i++)
for(int j = i + 1; j < arreglo.length; j++)
if( arreglo[i].compareTo(arreglo[j]) > 0 ) {
E temp = arreglo[i];
arreglo[i] = arreglo[j];
arreglo[j] = temp;
}
}
public static void main(String[] args) {
String [] as = {"Jose", "Maria", "Ana"};
Ordenar(as);
System.out.println(java.util.Arrays.toString(as));
}
}
7
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
E S P A C I O S D E N O M B R E S Y L I B R E R Í A S
public:
ArregloEnteros(int tamanyo) {
this->tamanyo = tamanyo;
pValores = new int[tamanyo];
}
~ArregloEnteros() {
delete [] pValores;
}
void estValor(int indice, int valor) {
if(indice < 0 || tamanyo <= indice) throw -1;
pValores[indice] = valor;
}
int obtValor(int indice) const {
if(indice < 0 || tamanyo <= indice) throw -1;
return pValores[indice];
}
int obtTamanyo() const {
return tamanyo;
}
};
int main() {
ArregloEnteros arr(4);
arr.estValor(0, 3);
arr.estValor(1, 6);
arr.estValor(2, 2);
arr.estValor(3, 9);
return 0;
}
8
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
E S P A C I O S D E N O M B R E S Y L I B R E R Í A S
El problema con el código en Código 11 es que la clase implementada no puede ser reutilizada
para manejar arreglos de otros tipos de datos. Para evitar tener copias casi idénticas de la clase
anterior podemos modificar su definición para generalizarla utilizando punteros void, como se
puede apreciar en Código 12. Note en este código la necesidad de realizar operaciones cast tanto
al ingresar los datos como al extraerlos. Esto evidencia un nuevo problema: Una clase así
definida no me garantiza nada acerca de los valores que se le ingresan. No puedo garantizar que
todos los elementos de la lista sean de un mismo tipo, o por lo menos que hereden de un mismo
tipo. Como ejemplo, a un objeto ArregloPVoid se le podrían agregan punteros a enteros y a
textos y no sucedería ningún error en compilación, pero muy probablemente sí en ejecución al
extraer los valores, pues no sabríamos si todos éstos son del tipo esperado.
class ArregloPVoid {
int tamanyo;
void ** pValores;
public:
ArregloPVoid(int tamanyo) {
this->tamanyo = tamanyo;
pValores = new void *[tamanyo];
}
~ArregloPVoid() {
delete [] pValores;
}
void estValor(int indice, void * valor) {
if(indice < 0 || tamanyo <= indice) throw -1;
pValores[indice] = valor;
}
void * obtValor(int indice) const {
if(indice < 0 || tamanyo <= indice) throw -1;
return pValores[indice];
}
int obtTamanyo() const {
return tamanyo;
}
};
int main() {
ArregloPVoid arr(4);
arr.estValor(0, (void*)3);
arr.estValor(1, (void*)6);
arr.estValor(2, (void*)2);
arr.estValor(3, (void*)9);
return 0;
}
9
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
E S P A C I O S D E N O M B R E S Y L I B R E R Í A S
public:
ArregloGen(int tamanyo) {
this->tamanyo = tamanyo;
pValores = new T[tamanyo];
}
~ArregloGen() {
delete [] pValores;
}
void estValor(int indice, T valor) {
if(indice < 0 || tamanyo <= indice) throw -1;
pValores[indice] = valor;
}
T obtValor(int indice) const {
if(indice < 0 || tamanyo <= indice) throw -1;
return pValores[indice];
}
int obtTamanyo() const {
return tamanyo;
}
};
int main() {
ArregloGen<int> arr(4);
arr.estValor(0, 3);
arr.estValor(1, 6);
arr.estValor(2, 2);
arr.estValor(3, 9);
return 0;
}
class ArregloGen<T> {
int tamanyo;
T [] pValores;
10
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
E S P A C I O S D E N O M B R E S Y L I B R E R Í A S
}
public T obtValor(int indice) {
if (indice < 0 || tamanyo <= indice)
throw new ArgumentException("Indice invalido");
return pValores[indice];
}
public int obtTamanyo() {
return tamanyo;
}
}
class Ejm5 {
public static void Main() {
ArregloGen<int> arr = new ArregloGen<int>(4);
arr.estValor(0, 3);
arr.estValor(1, 6);
arr.estValor(2, 2);
arr.estValor(3, 9);
Console.Write("Arreglo=[");
for(int i = 0; i < (arr.obtTamanyo() - 1); i++)
Console.Write(arr.obtValor(i) + ", ");
Console.WriteLine(arr.obtValor(arr.obtTamanyo() - 1) + "]");
}
}
class Ejm4 {
public static void main(String[] args) {
ListaGen<Integer> lista = new ListaGen<Integer>(3);
lista.agregar(6);
lista.agregar(2);
lista.agregar(9);
11
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
E S P A C I O S D E N O M B R E S Y L I B R E R Í A S
}
System.out.print(")");
}
}
La STL de C++
La librería estándar de C++ para el manejo de colecciones es una librería de genéricos, de donde
deviene su nombre Standard Template Library (o STL). La STL incluye muchos tipos genéricos
que implementan estructuras de datos comúnmente utilizadas, y funciones genéricas que
implementan algoritmos comúnmente utilizados para manejar dichas estructuras de datos. Sus
objetivos de diseño principales incluyen el alto rendimiento y la flexibilidad de uso (ref. 3). Sus
componentes principales son: Contenedores (estructuras de datos populares representadas
como plantillas), iteradores (clases cuyos objetos son utilizados para recorrer los elementos
dentro de los contenedores) y algoritmos (funciones genéricas que implementan algoritmos
comunes de manipulación de datos sobre contenedores). Veremos ejemplos de cada uno.
Los contenedores en STL se agrupan en 5 categorías: Contenedores de secuencia, contenedores
ordenados, contenedores asociativos, adaptadores de contenedores (las dos primeras categorías
también se denominan contenedores de primera clase) y contenedores especializados. Los
contenedores de secuencia representan estructuras de datos lineales (vectores y listas enlazadas).
Los contenedores ordenados suelen utilizar árboles binarios balanceados para mantener sus
elementos ordenados. Los contenedores asociativos representan colecciones de pares llave-
valor, donde una llave es utilizada para recuperar su valor asociado y suelen utilizar estructuras
de datos no lineales (como tablas hash y árboles) para permitir búsquedas rápidas. Los
12
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
E S P A C I O S D E N O M B R E S Y L I B R E R Í A S
adaptadores son clases que derivan de las anteriores, modificando y restringiendo la interfaz que
exponen para darles un uso particular (por ejemplo, las pilas y las colas son adaptadores de la
clase vector). Finalmente, existen los contenedores especializados (o casi-contenedores) que son
sumamente eficientes pero ofrecen una interfaz más restringida que el resto de contenedores
(por ejemplo la clase string y la clase bitset).
Los contenedores son clases genéricas definidas en varios archivos de encabezados. La Tabla 1
describe los contenedores más comúnmente utilizados de la STL.
Por mucho los contenedores más utilizados son vector (una clase genérica) y string (una clase).
En Código 16 se muestra un ejemplo del uso de estos dos contenedores. Note el uso de los
métodos begin( ) y end( ) a lo largo del código. Estos métodos devuelven objetos iteradores del
tipo adecuado al contenedor de donde se obtienen. Un iterador es un objeto que encapsula un
puntero a uno de los elementos de un contenedor y que además sobrecarga los típicos
operadores utilizados con punteros (incremento y decremento, sumas y restas, asignación,
indireccionamiento y comparación) de forma que se les pueda utilizar como si de punteros se
tratase.
Tabla 6 - 1: Algunos contenedores de la STL.
deque deque Igual que vector, pero expandible por ambos extremos, al
inicio y al final.
13
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
E S P A C I O S D E N O M B R E S Y L I B R E R Í A S
Lo importante sobre los iteradores es que: (1) A diferencia de un puntero, un objeto iterador
verifica que no nos salgamos a apuntar a posiciones inválidas de memoria, esto es, que no nos
vayamos más allá de los elementos que forman la colección; (2) un iterador es más general que
un puntero, permitiendo de forma transparente y uniforme desplazarnos por cualquier
contenedor sin importar como internamente éste organice sus elementos (sea un arreglo con
elementos contiguos, una lista enlazada, un árbol, un grafo, etc.) pues cada contenedor define su
propia clase iterador; (3) permite definir los algoritmos de manipulación de elementos de un
contenedor de forma independiente a la definición de los contenedores mismos, como veremos
más adelante.
#include <iostream>
#include <vector> // para la clase genérica "vector"
#include <string> // para la clase "string"
#include <sstream> // para las clases "ostringstream" y "istringstream"
void ejemploVectores() {
cout << "***** Ejemplo de vectores ******" << endl;
reportar("Vector1", vtorInt1);
reportar("Vector2", vtorInt2);
reportar("Vector3", vtorInt3);
14
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
E S P A C I O S D E N O M B R E S Y L I B R E R Í A S
reportar("Vector1", vtorInt1);
reportar("Vector2", vtorInt2);
reportar("Vector3", vtorInt3);
reportar("Vector1", vtorInt1);
reportar("Vector2", vtorInt2);
reportar("Vector3", vtorInt3);
}
void ejemploStrings() {
cout << endl << "***** Ejemplo de string's ******" << endl;
string cad1("Texto de cadena 1"), cad2 = "Texto de cadena 2", cad3(8, 'x');
//string cad4 = 'c', cad5 = 34; // Error: "string" no ofrece estas conversiones
cout << "cad1=" << cad1 << ", cad2=" << cad2 << ", cad3=" << cad3 << endl;
int main() {
ejemploVectores();
ejemploStrings();
return 0;
}
15
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
E S P A C I O S D E N O M B R E S Y L I B R E R Í A S
Para declarar una variable de tipo iterador se utiliza el tipo inner iterator definido dentro de cada
clase contenedor. Eso se puede apreciar en la implementación de la función genérica reportar.
En Código 16 también se muestra un ejemplo del uso de la clase string. Esta clase ofrece varios
constructores, sobrecargas del operador '+' (que sirve para concatenar datos de tipo string y/o
datos de tipo char*), de los operadores relacionales ('==', '>=', '<', etc.), del operador de acceso
a arreglo ('[ ]', para acceder a cada carácter individual cómodamente), y de los operadores '<<' y
'>>' (para escribir/leer un string hacia/desde un flujo). Si se desea concatenar texto con otros
tipos de datos se debe utilizar la clase ostringstream, y si se desea procesar un texto en memoria
(igual como se hace al leer de la entrada estándar con cin) se debe usar istringstream.
Finalmente, la STL incluye un conjunto de funciones genéricas que implementan algoritmos
para procesar datos de un contenedor. Algunas de estas funciones modifican los elementos del
contenedor (por ejemplo copy, remove, rotate) y otras no (por ejemplo, count, find, search). En
Código 17 se muestra un ejemplo de uso de estas funciones. Lo interesante aquí es el enfoque de
diseño seguido. Normalmente las librerías de clases para manipular contenedores definen los
algoritmos de manipulación de los mismos mediante métodos dentro de las clases. La STL
utiliza un enfoque distinto, la definición de los algoritmos se separa de la definición de los
contenedores y operan sobre los elementos de éstos sólo indirectamente, a través de los
iteradores. Esta separación facilita la escritura de algoritmos genéricos que se apliquen a muchas
clases de contenedores (ref. 3). Sin embargo, los algoritmos dependen de los iteradores y de las
características de estos. Por ejemplo, algunos algoritmos solo requieren leer o escribir
unidireccionalmente los elementos de un contenedor, otros en ambas direcciones (hacia adelante
y hacia atrás) y otros acceder a cualquier elemento aleatoriamente. Debido a esto hay una
clasificación de los iteradores según estas funcionalidades esperadas y cada algoritmo define con
que tipo de iterador requiere trabajar. Por tanto, solo se puede aplicar un algoritmo a un
contenedor si su iterador soporta las funcionalidades que dicho algoritmo requiere.
#include <iostream> // cin, cout
#include <vector> // vector
#include <algorithm> // sort, copy, random_shuffle
#include <iterator> // ostream_iterator
int main() {
ostream_iterator<int> salida(cout, ", ");
vector<int> v;
int val;
cout << "Ingrese una secuencia de enteros y finalice con CTRL+Z: ";
while(cin >> val) // mientras no es fin de archivo
v.push_back(val); // agrego al vector
16
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
E S P A C I O S D E N O M B R E S Y L I B R E R Í A S
return 0;
}
17
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
E S P A C I O S D E N O M B R E S Y L I B R E R Í A S
completamente con los tipos de las interfaces, y solo referir a las implementaciones al momento
de crear un objeto para manejar una nueva colección. Todas las implementaciones de una
misma interfaz ofrecen lo mismo, y por tanto la elección de la implementación a utilizar solo
afectará el desempeño final del programa, según el uso que se les den a los objetos de dicha
implementación.
Interfaces Implementaciones
Queue LinkedList
SortedSet TreeSet
SortedMap TreeMap
No todas las implementaciones de las interfaces para colecciones soportan todos los métodos
(lo que no es el caso de ninguna de las implementaciones de la Tabla 2). Cuando un método no
es soportado y es llamado, la implementación dispara una excepción
(UnsupportedOperationException). Esto permite mantener un número reducido de interfaces
para colecciones en esta librería, facilitando su aprendizaje y uso.
Una de las implementaciones más comúnmente utilizada es ArrayList. En Código 18 se muestra
un ejemplo del uso de este genérico.
import java.util.*;
class Ejm6 {
public static <E> void reportar(String nombre, List<E> l) {
System.out.print(nombre + ": size=" + l.size()
+ ", isEmpty=" + l.isEmpty()
+ ", valores=["
);
Iterator<E> it = l.iterator();
while(it.hasNext())
System.out.print(it.next() + ", ");
System.out.print("]\n");
}
public static void main(String[] args) {
List<Integer> l1 = new ArrayList<Integer>();
List<Integer> l2 = new LinkedList<Integer>();
List<Integer> l3 = new Vector<Integer>();
18
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
E S P A C I O S D E N O M B R E S Y L I B R E R Í A S
System.out.println("Listas iniciales:");
reportar("Lista1", l1);
reportar("Lista2", l2);
reportar("Lista3", l3);
l2.addAll(Collections.nCopies(15, 0));
for(int i = 3; i < l2.size() - 3; i++)
l2.set(i, i - 2);
l2.subList(0, 5).clear();
System.out.println("\nBuscando elementos:");
System.out.println("Lista1 contiene '5': " + l1.contains(5));
System.out.println("Lista2 posicion de '0': " + l2.indexOf(0));
System.out.println("Lista3 elemento en posicion '4': " + l3.get(4));
System.out.println("\nRemoviendo elementos:");
l1.remove(2); // remuevo el elemento en posicion 2
l2.remove(new Integer(0)); // remuevo el 1er cero
l3.removeAll(l1); // remuevo de l3 todos los que hay en l1
reportar("Lista1", l1);
reportar("Lista2", l2);
reportar("Lista3", l3);
}
}
class Ejm7 {
public static <E> void imprimir(List<E> v) {
Iterator<E> it = v.iterator();
System.out.print("[");
while(it.hasNext())
System.out.print(it.next() + ", ");
System.out.print("]");
}
public static void main(String[] args) {
List<Integer> v = new ArrayList<Integer>();
Scanner input = new Scanner(System.in);
Collections.reverse(v); // invierto
System.out.print("\nLa secuencia de enteros invertida es: ");
imprimir(v);
19
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
E S P A C I O S D E N O M B R E S Y L I B R E R Í A S
Collections.shuffle(v); // desordenada
System.out.print("\nLa secuencia de enteros desordenada es: ");
imprimir(v);
Collections.sort(v); // ordeno
System.out.print("\nLa secuencia de enteros ordenada es: ");
imprimir(v);
Código 20: Ejemplo de uso de los algoritmos para colecciones de la librería de Java.
20
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
E S P A C I O S D E N O M B R E S Y L I B R E R Í A S
Ilustración 6 - 2: Diagrama de clases de los tipos genéricos para manejo de colecciones de .NET
Framework.
Una de las implementaciones más comúnmente utilizada es List. En Código 20 se muestra un
ejemplo del uso de este genérico.
using System;
using System.Collections.Generic;
class Ejm6 {
public static void Reportar<E>(string nombre, List<E> lista) {
Console.Write(nombre + ": Count=" + lista.Count
+ ", Capacity=[" + lista.Capacity
+ ", valores=["
);
foreach(E elemento in lista)
Console.Write(elemento + ", ");
Console.Write("]\n");
}
public static void Main() {
List<int> lista1 = new List<int>();
List<int> lista2 = new List<int>();
List<int> lista3 = new List<int>();
Console.WriteLine("Listas iniciales:");
Reportar("Lista1", lista1);
Reportar("Lista2", lista2);
Reportar("Lista3", lista3);
lista2.AddRange(new int[15]);
for (int i = 3; i < lista2.Count - 3; i++)
lista2[i] = i - 2;
lista2.RemoveRange(0, 5);
Console.WriteLine("\nBuscando elementos:");
Console.WriteLine("Lista1 contiene '5': " + lista1.Contains(5));
21
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
E S P A C I O S D E N O M B R E S Y L I B R E R Í A S
Console.WriteLine("\nRemoviendo elementos:");
lista1.RemoveAt(2); // remuevo el elemento en posicion 2
lista2.Remove(0); // remuevo el 1er cero
foreach(int elemento in lista1) // remuevo lista1 de lista3
lista3.Remove(elemento);
// la siguiente es una forma equivalente de remover lista1 de lista3,
// utilizando un predicado
//lista3.RemoveAll(delegate(int elemento) { return lista1.Contains(elemento);
});
Reportar("Lista1", lista1);
Reportar("Lista2", lista2);
Reportar("Lista3", lista3);
}
}
class Ejm7 {
public static void Imprimir<E>(List<E> lista) {
Console.Write("[");
foreach (E elemento in lista)
Console.Write(elemento + ", ");
Console.Write("]\n");
}
static bool LeeEntero(out int valor) {
try
{
string linea = Console.ReadLine();
valor = Int32.Parse(linea);
return true;
}
catch (FormatException)
{
valor = 0;
return false;
}
}
public static void Main() {
List<int> lista = new List<int>();
lista.Reverse(); // invierto
Console.Write("La lista de enteros invertida es: ");
Imprimir(lista);
22
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
E S P A C I O S D E N O M B R E S Y L I B R E R Í A S
lista.Sort(); // ordeno
Console.Write("La lista de enteros ordenada es: ");
Imprimir(lista);
Código 22: Ejemplo de uso de métodos de manipulación de los elementos de una colección con
la clase List de la librería de .NET y el uso de un predicado.
23
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
E S P A C I O S D E N O M B R E S Y L I B R E R Í A S
reemplazadas por los tipos que cumplen con las restricciones (según sean estas upper bound o
lower bound). Donde no se indica restricción alguna, el compilador utiliza el tipo Object para
realizar los reemplazos. Por último, a todas las conversiones riesgosas (down-cast) se les inserta
una operación cast explícita. Así, la declaración de un tipo genérico es compilada una sola vez,
como un tipo ordinario. La técnica erasure obliga a tener algunas restricciones importantes: Solo
es posible utilizar tipos referencia como argumentos-tipo al usar un genérico; no se permite
utilizar los parámetros-tipo en contextos estáticos; no es posible utilizar el operador instanceOf
para verificar el tipo actual de un parámetro-tipo; no se permite hacer una operación cast con un
parámetro-tipo; no se permite la declaración de arreglos de un parámetro-tipo; no se permite la
creación de objetos de un parámetro-tipo.
Las características citadas traen como consecuencia: (1) La máquina virtual de Java no tuvo que
ser modificada para soportar genéricos, contando así con una compatibilidad completa hacia
atrás; (2) dado que toda la información sobre los parámetros-tipo es removida en compilación y
dichos tipos son reemplazados mayormente con Object, insertando operaciones cast en cada
lugar donde sea necesario, el desempeño final del programa es prácticamente tan buena como si
se hubiera programado sin genéricos; (3) dado que los tipos primitivos no son soportados como
argumentos-tipo, se hace un uso extensivo del boxing, con el correspondiente costo en
desempeño; (4) el uso de la técnica erasure provoca que la representación que se tiene en tiempo
de compilación sea distinta a la que se tiene en tiempo de ejecución, dificultando
significativamente la aplicación de la reflexión sobre los genéricos.
C# compila sus genéricos como si de cualquier otra clase se tratase, agregando al IL resultante
metadata que indique que el tipo compilado es un genérico, así como sus parámetros tipo. Por
tanto, existe una versión binaria de cada genérico con una descripción completa del mismo. En
ejecución, cuando el programa hace su primera referencia al genérico, el sistema busca si la
especialización requerida ya fue instanciada, y si no la hay, pasa al compilador JIT el IL y la
metadata del genérico, así como los argumentos-tipo. Con dicha información se genera la
versión en código nativo del genérico en el lugar y momento que se necesite. Por tanto, se esta
instanciando el genérico, pero a diferencia de C++, esta instanciación se produce en ejecución y
no produce la misma duplicidad de código. El compilador JIT realmente no genera una versión
diferente por cada tipo construido, sino que sigue la siguiente estrategia: Cuando se utilizan
argumentos-tipo por valor, se genera una copia única de código nativo ejecutable para cada
combinación distinta de dichos argumentos; cuando se utilizan argumentos-tipo por referencia,
se genera solo una copia de código nativo ejecutable, la cual es compartida por todos los tipos
por referencia, dado que son estructuralmente idénticas, cambiándose únicamente la tabla virtual
utilizada en cada caso. Esta instanciación de los genéricos evita la necesidad de introducir
operaciones cast. Por otro lado, los requerimientos esperados por un genérico para sus
parámetros-tipo son especificados mediante restricciones. Finalmente, es posible utilizar
cualquier tipo de dato como argumento-tipo de un genérico.
Las características citadas traen como consecuencia: (1) C# plantea un balance entre facilidad de
reutilización de código (evitando duplicidad cuando no es necesaria) y buen desempeño
(creando copias de código nativo ejecutable para los tipos valor, evitando en todo momento la
inserción de operaciones cast, y evitando las operaciones de boxing); (2) dado que la
instanciación del genérico se hace en tiempo de compilación, los genéricos han requerido una
actualización mayor del CLR, lo que ocasiona incompatibilidad con versiones anteriores a esta
máquina virtual, esto es, no es posible ejecutar un ensamblaje con genéricos en un computador
con CLR 1.0 o 1.1; (3) el contar con información completa de los genéricos en binario hace
posible que cualquier genérico pueda ser instanciado dinámicamente utilizando reflexión.
24
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
E S P A C I O S D E N O M B R E S Y L I B R E R Í A S
En resumen, C++ busca el mejor desempeño y flexibilidad posible, Java busca una perfecta
compatibilidad hacia atrás, y C# busca el mejor balance entre flexibilidad-desempeño y eficiencia
(no duplicación de código).
25
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
E S P A C I O S D E N O M B R E S Y L I B R E R Í A S
26
1
Capítulo
7
Archivos, Flujos y
Persistencia de Objetos
El objetivo de este capítulo es presentar el enfoque orientado a objetos del manejo de archivos, y
flujos en general, que ofrecen los lenguajes Java y C#, dentro de un conjunto de casos
frecuentes de uso.
Archivos y Flujos
La unidad mínima de representación de un valor en una computadora es el bit. La unidad
mínima de procesamiento de datos es el byte. Uno o más bytes pueden representar un carácter
(un dígito, una letra o un símbolo) o un número (integral o de punto flotante). Ésta es la
plataforma base sobre la cual los lenguajes de programación dan un valor agregado a las
capacidades brindadas al programador para manejar estructuras de datos. Una descripción
clásica de la forma de organización de la información por los programas es:
“Un conjunto de bytes con significado agregado y que se operan como una unidad
se le denomina campo. Un conjunto de campos con significado agregado y que se
operan como una unidad se denomina registro. Un conjunto de registros pueden
ser almacenados de forma persistente en un archivo. Un conjunto de archivos de
registros forman una base de datos.”
210
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
A R C H I V O S , F L U J O S Y P E R S I S T E N C I A D E O B J E T O S
Existen dos formas de acceder a los datos persistentes: de forma secuencial y aleatoria. Dicho
acceso puede tener un tipo de permiso asignado: Sólo lectura, sólo escritura o lectura-escritura.
Si es factible que más de un programa acceda a dichos datos, pueden establecerse permisos para
compartir dichos datos. Las formas y permisos dependen de las capacidades de la memoria que
almacena dichos datos.
En general, el acceso a un dato involucra el traspaso de éste de una memoria a otra. Esta
transferencia puede hacerse físicamente por bloques de bytes o de byte en byte. Esta
transferencia puede involucrar una sola operación de copia (todos los datos transferidos están
disponibles a la vez) o varias operaciones secuenciales. En este último caso, los datos están
disponibles conforme van llegando. Es aquí donde el concepto de flujo es útil.
Un flujo es una secuencia de bytes que son accedidos en el mismo orden en que fueron creados.
Un flujo opera de manera similar a una cola: Los datos son leídos desde un flujo en el mismo
orden en que fueron escritos en él. Dado que cualquier memoria permite por lo menos un
acceso secuencial a sus datos, ya sea para lectura o escritura, ya sea que los datos estén
disponibles a la vez o secuencialmente, éstas pueden trabajarse siempre como flujos. Si bien por
definición un flujo permite un acceso secuencial a los datos de la memoria que maneja, también
puede permitir un acceso aleatorio, dependiendo del tipo de memoria.
Objetos Persistentes
En la POO, el concepto de objeto reemplaza sobremanera al de registro. Un objeto que es
almacenado en memoria persistente se le conoce como objeto persistente. Una forma de
lograr esta persistencia es mediante una técnica llamada serialización.
Se dice que un objeto es serializado cuando es escrito hacia un flujo de una forma tal que pueda
ser leído luego y reconstruido. El proceso de lectura y reconstrucción de un objeto se conoce
como deserialización. La serialización implica mucho más que sólo escribir los datos de un
objeto en un orden y leerlos en el mismo orden. Existen dos problemas principales en la
serialización de un objeto:
Determinar quién es responsable de serializar y deserealizar un objeto: El mismo objeto
o el mecanismo de serialización, ya sea éste implementado a nivel del lenguaje de
programación o con una librería.
Cómo manejar los diagramas de clases.
Encargar la responsabilidad de serializar/deserealizar un objeto al mismo objeto tiene la ventaja
de darle al programador de la clase de dicho objeto el control completo del proceso. La clara
contraparte es el mayor trabajo de programación requerido y por consiguiente, el aumento de la
tasa de error. Encargar la responsabilidad al mecanismo de serialización tiene la ventaja de
simplificar la programación, pero la desventaja de requerirse un mecanismo de reflexión de
apoyo, dado que el mecanismo de serialización deberá poder reconocer en tiempo de ejecución
a qué tipo de objeto corresponde los datos leídos y crearlo, todo de manera automática.
Los diagramas de clases se forman cuando los datos miembros de un objeto refieren a otros
objetos, formando un diagrama de conexiones entre objetos. Cuando uno de los objetos de este
diagrama debe serializarse, también deberá serializarse todos los objetos a los que refiere, directa
o indirectamente. Esto no sólo implica un rastreo recursivo de todas las referencias del objeto
serializado, sino además la resolución de conflictos tales como: ¿Qué sucede cuando al rastrear
dicho diagrama durante la serialización se llega a un mismo objeto más de una vez?, ¿Cómo
reconocer que ya se serializó un objeto previamente en el mismo flujo? Para el manejo de los
211
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
A R C H I V O S , F L U J O S Y P E R S I S T E N C I A D E O B J E T O S
Manejo desde C#
Descripción General de las Capacidades
C# permite el manejo de archivos de texto y binarios, secuenciales y aleatorios. Dichos archivos
son manejados como flujos. Cuando un archivo es abierto, C# crea un objeto y lo relaciona con
el flujo de dicho archivo.
Existen tres flujos estándar con objetos relacionados que son creados automáticamente para
todo programa en C#:
El flujo de entrada estándar, mediante la referencia System.Console.In.
El flujo de salida estándar, mediante la referencia System.Console.Out.
El flujo de error estándar, mediante la referencia System.Console.Error.
Los métodos de la clase Console de lectura tales como Read y ReadLine, y de escritura tales
como Write y WriteLine hacen uso de Console.In y Console.Out respectivamente.
Todas las clases para el manejo de las operaciones de entrada y salida se encuentran en el espacio
de nombres System.IO. Algunas de las más usuales se muestran en la Figura 10 - 1.
TextReader TextWriter
StreamReader StreamWriter
StringReader StringWriter
System.CodeDom.Compiler.IndentedTextWriter
Stream
System.Web.HttpWriter
FileStream
System.Web.UI.HtmlTextWriter
MemoryStream
BufferedStream
System.Data.OracleClient.OracleBFile BinaryReader
System.Data.OracleClient.OracleLob BinaryWriter
System.Net.Sockets.NetworkStream System.Runtime.Serialization.
Formatters.Binary.BinaryFormatter
System.Security.Cryptography.CryptoStream
Las clases TextReader, TextWriter y Stream son clases abstractas para la lectura y escritura de
archivos de texto y binarios. Los objetos referenciados por Console.In y Console.Out son de
tipo TextReader y TextWriter respectivamente
212
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
A R C H I V O S , F L U J O S Y P E R S I S T E N C I A D E O B J E T O S
Las clases StreamReader y StringReader permiten manejar como flujos de salida archivos de
texto y cadenas de carácteres respectivamente. Su contraparte para flujos de entrada son
StreamWriter y StringWriter.
Las clases FileStream, MemoryStream y BufferedStream son para manejo de flujos binarios, para
lectura y escritura. A dichos flujos se pueden serializar objetos, para lo que se utiliza la clase
BinaryFormatter. Esta clase puede serializar y deserializar cualquier objeto, junto con su
diagrama de objetos, marcado como serializable. Dicha marca se realiza mediante el atributo
estándar serializable, como se verá más adelante.
Finalmente, C# permite el acceso al sistema de archivos del sistema operativo, mediante las
clases File y Directory, como se verá en la siguiente sección.
Ambas clases proveen métodos estáticos para manejar archivos y directorios. Las Tabla 10 - 1
muestra los métodos de File más comunes.
Tabla 10 - 1 Métodos de la clase File de .NET.
Manipulación de archivos
Nombre Descripción
Información de un archivo
Nombre Descripción
Archivos de texto
Nombre Descripción
213
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
A R C H I V O S , F L U J O S Y P E R S I S T E N C I A D E O B J E T O S
Archivos binarios
Nombre Descripción
Manipulación de directorios
Nombre Descripción
Información de un directorio
Nombre Descripción
214
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
A R C H I V O S , F L U J O S Y P E R S I S T E N C I A D E O B J E T O S
Manejo de Consola
La entrada, salida y error estándar se manejan mediante las propiedades estáticas públicas de sólo
lectura In, Out y Error de la clase Console, las que retornan referencias de tipo TextReader,
TextWriter y TextWriter respectivamente. Luego, las siguientes sentencias son equivalentes:
Console.Write(dato); equivale a Console.Out.Write(dato);
Console.WriteLine(dato); equivale a Console.Out.WriteLine(dato);
dato = Console.Read(); equivale a dato = Console.In.Read();
dato = Console.ReadLine(); equivale a dato = Console.In.ReadLine();
La clase abstracta TextReader permite la lectura de carácteres, uno a uno o por líneas, desde un
flujo de texto. La clase abstracta TextWriter permite la escritura de carácteres, uno a uno o por
líneas, hacia un flujo de texto. Los objetos referidos por las propiedades de Console realmente
son de un tipo concreto que hereda de estas clases. Esto puede comprobarse fácilmente con el
siguiente código:
Type tipo = Console.In.GetType();
Type tipoBase = tipo.BaseType;
Console.WriteLine("Console.In es de tipo = " + tipo.Name);
Console.WriteLine("El que hereda del tipo = " + tipoBase.Name);
class Consola1 {
public static void Main(string[] args) {
Console.Write( "Ingrese el texto a Leer." );
Console.WriteLine( " Finalizar con la combinacion CTRL+Z." );
while(true) {
int Entero = Console.In.Read();
if( Entero == -1 )
break;
char Caracter = (char)Entero;
Console.Write( "Leido Entero=" + Entero );
Console.WriteLine( ", correspondiente al carácter=" + Caracter );
}
Console.WriteLine( "Fin del ingreso" );
}
}
class Consola2 {
public static void Main(string[] args) {
Console.WriteLine( "Ingrese una secuencia de enteros." );
Console.WriteLine( "Finalice el ingreso con '*'." );
int Contador = 0;
int Suma = 0;
while(true) {
215
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
A R C H I V O S , F L U J O S Y P E R S I S T E N C I A D E O B J E T O S
216
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
A R C H I V O S , F L U J O S Y P E R S I S T E N C I A D E O B J E T O S
using System;
class Consola3 {
public static void Main(string[] args) {
Console.WriteLine( "Ingrese una secuencia de palabras." );
string Linea = Console.In.ReadLine();
char[] separadores = {' ', ',', '.', ':'};
string[] Tokens = Linea.Split( separadores );
for(int i = 0; i < Tokens.Length; i++)
Console.WriteLine("Token " + (i + 1) + " = " + Tokens[i]);
}
}
class ArchivoTexto {
public static void Main( string[] args ) {
FileStream Archivo;
Archivo = new FileStream( args[0], FileMode.Create, FileAccess.Write );
StreamWriter Escritor = new StreamWriter( Archivo );
Para crear un archivo se crea un flujo desde un archivo utilizando la clase FileStream. Esta clase
provee métodos para leer y escribir únicamente bytes, sin ninguna interpretación. Para escribir o
leer los datos de este flujo como texto, se crea otro flujo utilizando la clase StringWriter y
217
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
A R C H I V O S , F L U J O S Y P E R S I S T E N C I A D E O B J E T O S
[Serializable]
class Direccion {
int numero;
string calle;
string distrito;
public Direccion(int numero, string calle, string distrito) {
this.numero = numero;
this.calle = calle;
this.distrito = distrito;
}
public override string ToString() {
return calle + " " + numero + ", " + distrito;
}
}
[Serializable]
class Persona {
private string nombre;
private int edad;
private Direccion dir;
public Persona(string nombre, int edad, Direccion dir) {
this.nombre = nombre;
this.edad = edad;
this.dir = dir;
}
public override string ToString() {
return "Sr(a). " + nombre + ", " + edad + " años, direccion = " + dir;
}
}
class ArchivoBinarioSecuencial {
public static void Main(string[] args) {
if(args.Length < 2) {
Console.WriteLine("Error en argumentos.");
return;
}
if(args[0] == "/e") {
FileStream Output = new FileStream(args[1], FileMode.Create,
FileAccess.Write);
BinaryFormatter Formateador = new BinaryFormatter();
Persona[] ListaPersonas = {
new Persona("Jose", 25, new Direccion(123, "Jose Leal", "San
Juan")),
218
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
A R C H I V O S , F L U J O S Y P E R S I S T E N C I A D E O B J E T O S
El programa utiliza los argumentos de la línea de comandos para determinar si debe crear un
archivo o si debe de leerlo.
Al crear el archivo se inicializa un arreglo de objetos Persona, los que serializan utilizando el
método Serialize de la clase BinaryFormatter. Tome en cuenta que cuando se serializa un objeto,
se serializa también todos los objetos a los que hace referencia directa o indirectamente. Si se
intenta serializar un objeto no marcado con el atributo Serializable, el método Serialize dispara
una excepción.
En forma similar al leer el archivo, se utiliza el método Deserialize, el cual retorna una referencia
de tipo object a la que debe aplicársele una operación cast para obtener una referencia al tipo
real. Hay dos cosas que pueden fallar durante una deserialización: Que se haya llegado al final del
archivo sin haberse podido leer todos los datos de un objeto o bien que la operación de cast
falle, es decir, se leyó un objeto diferente al que se esperaba. En ambos casos se produce una
excepción.
Es importante recordar que la serialización no requiere que el flujo reciba objetos del mismo
tipo. Se puede serializar a un mismo flujo objetos de distintos tipos, en cuyo caso deberá
asegurarse que el proceso de deserilización sea realizado en el mismo orden que el proceso de
serialización sobre el flujo tratado.
219
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
A R C H I V O S , F L U J O S Y P E R S I S T E N C I A D E O B J E T O S
using System;
using System.IO;
using System.Runtime.Serialization;
class Combo {
public const int LONG_NOMBRE = 10;
public const int LONG_DESCRIPCION = 30;
public const int LONG_CARACTER = 1; // longitud de un char escrito en un archivo TXT
public const int LONG_REGISTRO =
(LONG_NOMBRE + LONG_DESCRIPCION) * LONG_CARACTER + sizeof(double);
}
public override string ToString() {
string Nombre = new string(this.Nombre);
string Descripcion = new string(this.Descripcion);
return "Combo " + Nombre + ": " + Descripcion + ", a S/. " + Precio;
}
public void SetPrecio(double Precio) {
this.Precio = Precio;
}
public void Serialize(BinaryWriter Output) {
Output.Write(Nombre);
Output.Write(Descripcion);
Output.Write(Precio);
}
public static Combo Deserialize(BinaryReader Input) {
char[] Nombre = Input.ReadChars(LONG_NOMBRE);
char[] Descripcion = Input.ReadChars(LONG_DESCRIPCION);
double Precio = Input.ReadDouble();
return new Combo(new string(Nombre), new string(Descripcion), Precio);
}
}
class ArchvoBinarioAleatorio {
public static void Main(string[] args) {
if(args.Length != 1) {
Console.WriteLine("Error en parametros.");
return;
}
220
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
A R C H I V O S , F L U J O S Y P E R S I S T E N C I A D E O B J E T O S
}
public static void Opcion1(Stream Flujo, BinaryReader Entrada) {
Console.WriteLine("Número de registro a leer:");
int Registro = Int32.Parse(Console.In.ReadLine());
int TotalRegistros = (int)Flujo.Length / Combo.LONG_REGISTRO;
if(Registro >= TotalRegistros)
Console.WriteLine("Número de registro inválido.");
else {
Flujo.Position = Registro * Combo.LONG_REGISTRO;
Console.WriteLine("Puntero del archivo en la pos:" + Flujo.Position);
Combo C = Combo.Deserialize(Entrada);
Console.WriteLine("Leido = " + C);
}
}
public static void Opcion2(Stream Flujo, BinaryReader Entrada, BinaryWriter Salida){
Console.WriteLine("Número de registro a leer:");
int Registro = Int32.Parse(Console.In.ReadLine());
int TotalRegistros = (int)Flujo.Length / Combo.LONG_REGISTRO;
if(Registro >= TotalRegistros)
Console.WriteLine("Número de registro inválido.");
221
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
A R C H I V O S , F L U J O S Y P E R S I S T E N C I A D E O B J E T O S
else {
Flujo.Position = Registro * Combo.LONG_REGISTRO;
Combo C = Combo.Deserialize(Entrada);
Console.WriteLine("Leido = " + C);
Console.WriteLine("Nuevo precio:");
C.SetPrecio(Double.Parse(Console.ReadLine()));
Flujo.Position = Registro * Combo.LONG_REGISTRO;
C.Serialize(Salida);
}
}
}
La clase Combo utiliza arreglos de carácteres en lugar de objetos string de manera que se
controle el tamaño de los datos escritos y leídos desde el flujo. Es interesante notar el hecho de
que se utiliza, para el cálculo del tamaño de los datos escritos al flujo, el tamaño de un carácter
como de 1 byte, cuando realmente un dato tipo char en .NET ocupa 2 bytes. Esto se debe a la
forma al sistema de codificación de carácteres utilizados por defecto por los flujos creados. Bajo
el sistema de codificación de carácteres por defecto, se escribe en el flujo la representación
ASCII, de 1 byte por char, de cada carácter UNICODE utilizada por los tipos char, de 2 bytes
por carácter.
Para moverse por el archivo de manera aleatoria, se modifica el valor de la propiedad pública de
lectura y escritura Position de la clase FileStream. Esta clase también ofrece un método
alternativo que provee de mayor funcionalidad para desplazarse por el archivo.
222
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
A R C H I V O S , F L U J O S Y P E R S I S T E N C I A D E O B J E T O S
223
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
A R C H I V O S , F L U J O S Y P E R S I S T E N C I A D E O B J E T O S
InputStream OutputStream
ByteArrayInputStream ByteArrayOutputStream
FileInputStream FileOutputStream
FilterInputStream FilterOutputStream
BufferedInputStream BufferedOutpuStream
DataInputStream DataOutputStream
PushbackInputStream
PrintStream
ObjectInputStream ObjectOutputStream
PipedInputStream PipedOutputStream
SequenceInputStream
RandomAccessFile
Reader Writer
BufferedReader BufferedWriter
LineNumberReader
CharArrayReader CharArrayWriter
FilterReader FilterWriter
PushbackReader
InputStreamReader OutputStreamWriter
FileReader FileWriter
PipedReader PipedWriter
StringReader StringWriter
PrintWriter
224
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
A R C H I V O S , F L U J O S Y P E R S I S T E N C I A D E O B J E T O S
225
225
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
A R C H I V O S , F L U J O S Y P E R S I S T E N C I A D E O B J E T O S
Manejo de Consola
La salida desde consola se suele realizar utilizando directamente el objeto referenciado por el
dato miembro estático out de la clase System. Este objeto es del tipo PrintWriter, el cual ofrece
la funcionalidad suficiente para escribir datos primitivos y objetos convirtiendo estos a cadenas
de texto. Los métodos más utilizados de esta clase son print y println.
Como contraparte, la clase System provee un dato miembro estático in del tipo
BufferedInputStream. Si se desea trabajar el flujo leyendo byte a byte, este flujo es suficiente. El
siguiente ejemplo muestra el uso de esta clase directamente.
import java.io.*;
class Consola1 {
public static void main( String args[] ) throws IOException {
System.out.print( "Ingrese el texto a Leer." );
System.out.println( " Finalizar con la combinacion CTRL+Z." );
while(true) {
int Entero = System.in.read();
if( Entero == -1 )
break;
char Caracter = (char)Entero;
System.out.println( "leido: numero=" + Entero +
", correspondiente al caracter=" + Caracter );
}
System.out.println( "Fin del ingreso" );
}
}
226
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
A R C H I V O S , F L U J O S Y P E R S I S T E N C I A D E O B J E T O S
Sin embargo, en la mayoría de los casos, lo que se desea leer son líneas de texto que luego serán
procesadas. Lo más común en este caso es combinar System.in con las clases
InputStreamReader y BufferedReader. El siguiente ejemplo muestra el uso de esta combinación.
import java.io.*;
class Consola2 {
public static void main(String args[]) throws IOException {
System.out.println( "Ingrese una secuencia de enteros." );
System.out.println( "Finalice el ingreso con '*'." );
227
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
A R C H I V O S , F L U J O S Y P E R S I S T E N C I A D E O B J E T O S
En otros casos es necesario procesar una línea de texto de forma que se particione ésta en
elementos, cada elemento puede ser una palabra, un número, etc. Los elementos son
reconocidos dado que comúnmente los separamos unos de otros mediante delimitadores, como
espacios en blanco o comas. A este tipo de trabajo se llama tokenización. Un token es una sub-
cadena de carácteres dentro de una cadena, delimitados por carácteres especiales denominados
“delimitadores”. Para realizar este trabajo se puede utilizar la clase StringTokenizer del paquete
java.util. El siguiente ejemplo muestra el uso de esta clase.
import java.io.*;
import java.util.StringTokenizer;
class Consola3 {
public static void main(String args[]) throws IOException {
System.out.println( "Ingrese una secuencia de palabras." );
228
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
A R C H I V O S , F L U J O S Y P E R S I S T E N C I A D E O B J E T O S
El constructor utilizado para crear el objeto StringTokenizer recibe como parámetro la cadena a
tokenizar. Este constructor utiliza como delimitadores por defecto la siguiente cadena: “
\t\n\r\f”. Es posible utilizar un segundo constructor que permite pasar como segundo
parámetro una cadena con los carácteres que deseamos funcionen como delimitadores.
Si se desea un manejo más complejo de un flujo de texto, reconociéndose cadenas de texto,
números, comentarios, etc. (a la manera como un compilador procesa un archivo fuente), se
puede utilizar la clase StreamTokenizer del paquete java.io. El uso de esta clase escapa de los
alcances del curso.
Por último, al igual que en la programación en C y C++, son 3 los flujos estándar que ofrece
Java mediante datos miembro (referencias a objetos de flujo) de la clase System:
System.in Entrada estándar, por defecto direccionado al teclado desde la consola.
System.out Salida estándar, por defecto direccionado a la pantalla de la consola.
System.err Salida de error estándar, por defecto direccionado a la pantalla de la consola.
Cualquiera de éstos puede ser redireccionado por el programa hacia, por ejemplo, un archivo. El
proceso de redireccionamiento consiste simplemente es asignar un nuevo objeto a estas
referencias.
Es importante recordar que todo programa en Java, sea de consola o gráfico, crea
automáticamente una consola, por lo que a ésta siempre es posible utilizarla.
Manejo de Archivos
Cuando un programa accede a un archivo podría, dependiendo del modo de acceso solicitado,
leer cualquier parte de el directamente, sin necesidad de seguir un orden, cosa muy diferente al
concepto de un flujo el cual funciona como una cola o FIFO (First In First Out). Si bien los
archivos pueden trabajarse lógicamente como un flujo, lo que equivale al conocido modo de
acceso secuencial, también es posible, dado las características propias de éste, trabajarlo
accediendo aleatoriamente.
Se examinaran los siguientes casos de manejo de archivos:
Manejo de archivos de texto.
Manejo de archivos binarios secuencialmente.
Manejo de archivos binarios aleatoriamente.
Archivos de Texto
Los archivos de texto se manejan comúnmente utilizando las clases FileWriter y FileReader. El
siguiente programa muestra en uso de estas clases.
import java.io.*;
class ArchivoTexto {
public static void main(String args[]) throws IOException {
FileWriter Escritor = new FileWriter( args[0] );
System.out.println( "Ingrese el texto a almacenar: " );
229
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
A R C H I V O S , F L U J O S Y P E R S I S T E N C I A D E O B J E T O S
Escritor.close();
Entrada.close();
De igual forma, si deseamos utilizar las capacidades de la clase PrintWriter, como con el objeto
referenciado por System.out, podríamos utilizar un código como el siguiente:
PrintWriter Escritor = new PrintWriter( new FileWriter( args[0] ) );
BufferedReader Entrada = new BufferedReader( new InputStreamReader( System.in ) );
while(true) {
String Linea = Entrada.readLine();
if( Linea == null )
break;
Escritor.println( Linea );
}
Escritor.close();
230
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
A R C H I V O S , F L U J O S Y P E R S I S T E N C I A D E O B J E T O S
Para leer y escribir datos de tipo primitivo se suele utilizar las clases DataInputStream y
DataOutputStream. Luego, la creación de los streams de lectura y escritura para estos casos sería:
DataInputStream input = new DataInputStream(
new BufferedInputStream( new FileInputStream( “MiArchivo.dat” ) ) );
Para los programas que sólo requieren almacenar y recuperar algunos datos simples el uso de
estas clases puede ser suficiente, para programas complejos la información que se maneja
provienen de objetos con datos de configuración o bien arreglos de objetos, es decir, más que
datos primitivos lo que se desea es almacenar y recuperar objetos completos. Aquí entran a tallar
dos conceptos importantes: Persistencia y serialización.
Se dice que un objeto es persistente si su existencia trasciende el de su creador y sus usuarios.
Para nuestro punto de vista como programadores, quien crea y usa un objeto es un programa.
La serialización es una forma de implementación del concepto de persistencia, utilizando para
esto un medio de almacenamiento también persistente, como el disco duro (su información se
conserva mas allá de que cada instancia de ejecución de los programas que crean o utilizan la
información, inclusive del sistema operativo mismo y de que la maquina se apague o prenda). La
serialización consiste en la lectura y escritura de objetos a y desde un flujo amarrado a dicho
medio persistente.
Para serializar un objeto es necesario marcar su clase como serializable. Esto consiste en hacer
que la clase a la que pertenece el objeto que deseamos serializar implemente la interfaz
Serializable. Dicha interfaz no define ningún método ni constante, sólo sirve para marcar la
clase. Si la clase contiene datos miembro que sean referencias, los tipos de dichas referencias
también deben marcarse como serializables.
Las clases que permiten la serialización de objetos son ObjectInputStream y
ObjectOutputStream.
ObjectOutputStream define un método writeObject el cual recibe como parámetro un objeto
mediante una referencia de tipo Object. Este método se encarga de averiguar si la clase fue
marcada como Serializable. Si no fue marcada se produce una excepción en tiempo de ejecución.
Si fue marcada, se serializa tanto la información concerniente al tipo de objeto marcado (de
forma que después pueda recrearce el mismo tipo de objeto), como los datos del objeto.
ObjectInputStream define un método readObject, el cual no recibe parámetros y retorna una
referencia de tipo Object del objeto leído y recreado. El método utiliza la información
almacenada sobre el tipo del objeto original para recrear uno del mismo tipo, y la información
sobre sus datos miembro para inicializar los del objeto.
El siguiente programa muestra el uso de estas clases.
import java.io.*;
231
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
A R C H I V O S , F L U J O S Y P E R S I S T E N C I A D E O B J E T O S
class ArchivoBinarioSecuencial {
public static void main(String args[]) throws IOException, ClassNotFoundException {
if(args.length < 2) {
System.out.println("Error en argumentos.");
return;
}
if(args[0].equals("/e")) {
ObjectOutputStream Escritor = new ObjectOutputStream(
new BufferedOutputStream(new FileOutputStream(args[1])));
Persona[] ListaPersonas = {
new Persona("Jose", 25, new Direccion(123, "Jose Leal", "San
Juan")),
new Persona("Mara", 26, new Direccion(456, "Carrión", "Barranco")),
new Persona("Ana", 35, new Direccion(789, "Fresnos", "Lince"))
};
for(int i = 0; i < ListaPersonas.length; i++)
Escritor.writeObject( ListaPersonas[i] );
Escritor.close();
} else if(args[0].equals("/l")) {
ObjectInputStream Lector = new ObjectInputStream(
new BufferedInputStream(new FileInputStream(args[1])));
while(true) {
Persona p;
try { p = (Persona)Lector.readObject(); }
catch(EOFException ex){ break; }
System.out.println("Persona : " + p);
}
Lector.close();
}
}
}
232
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
A R C H I V O S , F L U J O S Y P E R S I S T E N C I A D E O B J E T O S
manejar un archivo como un arreglo de bytes, pudiendo posicionarnos en cualquier lugar del
arreglo para realizar una lectura o escritura. El siguiente programa muestra el uso de esta clase:
import java.io.*;
class ArchivoBinarioAleatorio {
public static void main(String args[]) throws IOException {
RandomAccessFile Lector = new RandomAccessFile( "datos.dat", "rw" );
Lector.writeInt( 100 );
Lector.writeBoolean( false );
Lector.writeDouble( 6.54 );
Lector.writeUTF( "Hola mundo" );
System.out.println( "Archivo llenado" );
System.out.println( "Tamano del archivo = " + Lector.length() );
Lector.close();
}
}
Debido a que la extensa librería de clases de Java está pensada para solucionar los problemas
más comunes de programación, es de esperar que exista alguna clase o paquete que permita
233
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
A R C H I V O S , F L U J O S Y P E R S I S T E N C I A D E O B J E T O S
poder manejar un juego de registros en archivos y acceder a estos registros directamente, esto es,
de manera aleatoria, sin necesidad de tener que recorrer toda la información desde el inicio cada
vez, hasta llegar a dicha información. Este tipo de trabajo es en mucho lo que permite un
sistema de manejo de base de datos. Luego, la solución común cuando se desea realizar este tipo
de trabajo es utilizar los paquetes de la librería JDBC para crear y administrar base de datos. Otra
solución posible es extender la clase RandomAccessFile para permitir escribir y leer objetos y
proveer alguna funcionalidad adicional para poder utilizar llaves únicas a cada objeto/registro
guardado, de forma que se pueda acceder directamente a dicha información. Estos aspectos van
más allá del alcance del curso.
234
1
Capítulo
6
Programación con GUI
El presente capítulo se centra en el trabajo con Interfaces Gráficas de Usuario (IGU por sus
siglas en español, GUI por sus siglas en inglés) utilizando ventanas y otros elementos para el
diseño de las mismas, dentro del sistema operativo Windows. Para el caso particular de Java,
dada su característica multiplataforma, los conceptos dados aquí son aplicables a cualquier
plataforma que soporte Java.
99
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
P R O G R A M A C I Ó N C O N G U I
sistema operativo (resolución de la pantalla, idioma del teclado, fecha, cambio de usuario, etc.),
los relacionados a la escasez de recursos, etc.
Este sistema de mensajería, con distintas variantes, es utilizado por los sistemas operativos con
soporte GUI. En particular veremos el caso del sistema operativo Windows y cómo su sistema
de mensajería es manejado desde diferentes lenguajes de programación.
En la actualidad, existen dos tipos de programas GUI con ventanas comunmente utilizados: Los
programas Stand-Alone y los programas dentro de navegadores de Internet (browsers).
Programas Stand-Alone
Los programas Stand-Alone crean una o más ventanas con las que el usuario interactúa.
Comunmente estos programas poseen una ventana principal y una o más ventanas
especializadas en alguna labor específica. Estos programas pueden ejecutar directamente (en
caso de ser programas ejecutables) o mediante un programa intérprete.
Ejemplos de estos programas son los creados con C o C++ utilizando directamente el API de
Windows, así como las Aplicaciones de Java y las Aplicaciones de Formularios de Ventanas de
..NET.
100
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
P R O G R A M A C I Ó N C O N G U I
101
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
P R O G R A M A C I Ó N C O N G U I
return 0;
// Muestro la ventana.
ShowWindow( hWnd, iShowCmd ); // Establece el estado de 'mostrado' de la ventana.
UpdateWindow( hWnd ); // Genera un mensaje de pintado, el que es
// ejecutado por la función de ventana respectiva.
En este programa existen dos funciones importantes: El punto de entrada del programa, la
función “WinMain”, y la función de ventana “FuncionVentana”.
La función WinMain es el equivalente, para un programa en Windows, a la función “main” en
programas en C y C++ en modo consola. El principal parámetro de esta función es el primero,
el manejador de la instancia del programa. Dicho manejador es, en esencia, un número entero
utilizado por Windows para ubicar los recursos relacionados al programa. Algunos de los
recursos que un programa en Windows puede tener son: Registros de ventanas, textos,
imágenes, audio, video, meta-archivos, otros manejadores (a archivos, puertos, impresoras, etc.),
etc. El manejador de la instancia es utilizado como parámetro de las funciones del API de
Windows donde se involucren, directa o indirectamente, los recursos de una aplicación. El
segundo parámetro no es utilizado en programas de 32-bits (Windows 95 y Windows NT en
adelante).
La función de ventana (que puede tener cualquier nombre pero con el mismo formato de
declaración que el mostrado) es dónde se realiza toda la lógica relacionada a las acciones que una
ventana toma en respuesta a los mensajes recibidos, como por ejemplo, los producidos por la
interacción del usuario con la ventana. La dirección de esta función de ventana es el principal
dato que forma parte del registro, con la estructura WNDCLASS, de una clase de ventana.
Todos los mensajes enviados a ventanas, creados con una misma clase, son procesados por la
misma función de ventana, la indicada en la estructura WNDCLASS al registrarse dicha clase de
ventana.
Todo programa con ventanas en Windows tiene 3 etapas principales:
10. Registro de las clases de ventana que se utilizarán en el programa para crear
ventanas.
11. Creación de la ventana principal del programa.
12. Ejecución del bucle de mensajes del programa.
Para crear una ventana se utiliza la función CreateWindow. Los parámetros de esta función
indican, en este orden:
9. El nombre de la clase de ventana en base a la que se crea la nueva ventana.
10. El título a mostrarse en la barra superior de la ventana.
102
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
P R O G R A M A C I Ó N C O N G U I
103
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
P R O G R A M A C I Ó N C O N G U I
que utiliza, crear dichas ventanas y tener un bucle de procesamiento de mensajes. Para ilustrar
mejor ésto revisemos ejemplos equivalentes en Java y C#.
El siguiente programa es el equivalente en Java al programa anterior.
import javax.swing.*;
import java.awt.event.*;
class MiVentana extends JFrame {
public MiVentana() {
setSize(400, 400);
setTitle("Título de la Ventana");
setVisible(true);
addWindowListener(new WindowAdapter() {
public void windowClosing(WindowEvent e) {
dispose();
System.exit(0);
}
});
}
}
class Aplicacion_Minima {
public static void main(String[] args) {
MiVentana ventana = new MiVentana();
}
}
En este código, el punto de entrada es el método estático “main”, dentro del cual lo único que se
realiza es la creación de un objeto de la clase MiVentana que hereda de la clase JFrame (cuyo
nombre completo es javax.swing.JFrame), que es la clase base para la creación de ventanas
utilizando el paquete (el término utilizado en Java para referirse a una librería) SWING, cuyas
clases son accesibles mediante la sentencia “import javax.swing.*”. Dentro del constructor de la
clase “MiVentana” se configuran las características de la ventana y se establece “qué objeto” será
al que se le llame su método “windowClosing” cuando el usuario del programa indique que
desea cerrar la ventana.
Ahora bien, relacionemos todo esto con lo visto anteriormente en el programa en C o C++ con
API de Windows.
Como es de esperarse, la clase JFrame debe crear en su constructor (o en algún constructor de
sus clases base) una ventana llamando a “CreateWindow” (u otra función equivalente). Como ya
hemos visto, no se debe poder crear una ventana antes de registrar la clase de ventana en base a
la que se creará, por lo que es de esperarse (y realmente sucede así) que el programa intérprete de
Java realice este registro antes de llamar a nuestro método “main”. Siguiendo el orden de
ejecución de los constructores, luego de completada la ejecución del constructor de JFrame se
llamará al de nuestra clase “MiVentana”, donde modificamos los valores por defecto con los que
se creó inicialmente la ventana (por defecto, JFrame crea una ventana en la coordenada [X,Y] =
[0,0], con cero píxeles de ancho y cero de alto y con su atributo de visibilidad puesto en “no-
visible”). El constructor termina indicando, mediante el método de JFrame
“addWindowListener”, a qué método de qué clase se llamará cuando la función de ventana, de la
ventana creada por JFrame, reciba el mensaje WM_CLOSE. El procesamiento por defecto,
realizado por “DefWindowProc”, para este mensaje es llamar a la función de API de Windows
“DestroyWindow”, que es la que realmente destruye la ventana generando, de paso, el mensaje
WM_DESTROY. Como puede deducirse, el método “dispose” de JFrame debería estar
llamando a DestroyWindow y el método “System.exit” a PostQuitMessage.
Finalmente queda algo muy importante que no es directamente visible en nuestro código:
¿Dónde se ejecuta el bucle de procesamiento de mensajes? Para explicar ésto, debe aclararse que
el programa intérprete de Java realiza algunas acciones luego de llamar a nuestro método
“main”, entre ellas está la de verificar si después de ejecutarse este método se creó o no alguna
104
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
P R O G R A M A C I Ó N C O N G U I
ventana. Si se creó entonces se entra a un bucle de procesamiento de mensajes del que, como
puede esperarse, el programa no sale hasta haberse llamado al método “System.exit”. Si no se
creó ninguna ventana, el programa intérprete finaliza. Es por esto que la literatura sobre
programación con ventanas en Java indica, sin dar mayor detalle, que si se creara alguna ventana
en un programa Java Stand-Alone (a éstos se les llama “Aplicaciones Java”), debe de llamarse en
algún momento al método “System.exit”.
Ahora bien, el siguiente programa es el equivalente en C# del programa anterior.
using System;
using System.Windows.Forms;
105
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
P R O G R A M A C I Ó N C O N G U I
Menú
Barras de
Herramientas
Borde
Barra de
Estado
Área Cliente
Figura 6 - 1 Elementos de una ventana
En Windows, como se aprecia en la Figura 6 - 1, las ventanas tienen los siguientes elementos:
Un borde.
Una barra de título.
Un ícono de sistema.
Un conjunto de botones de sistema.
Un menú.
Una o más barras de herramientas.
Una barra de estado.
Un área de dibujo o “área cliente”.
De estos elementos, sólo el último es obligatorio, y si bien los demás son comunes, algunas
ventanas pueden no tenerlos.
106
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
P R O G R A M A C I Ó N C O N G U I
Eventos
Windows
Mensajes
Msg K Msg K
App. N App. O
Aplicación M
Hilo X
Cola de Mensajes
Función de Ventana
Msg K
Bucle
F.V F.V
Hilo Y Hilo Z
107
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
P R O G R A M A C I Ó N C O N G U I
La Figura 6 - 2 muestra el ciclo que siguen los eventos procesados en Windows para las
aplicaciones que utilizan ventanas.
La secuencia de pasos seguida es:
Windows detecta un evento.
Crea el mensaje respectivo y lo envía a la aplicación involucrada.
El bucle de procesamiento de mensajes detecta dicho mensaje y solicita a Windows que
lo envíe a la ventana adecuada.
Windows determina la ventana destinataria y averigua a qué clase pertenece.
En base a la clase determina la función de ventana que le corresponde y envía el mensaje
a dicho procedimiento.
La función de ventana actúa según el mensaje recibido.
Para el procesamiento de los mensajes, toda ventana tiene una “función de procesamiento de
mensajes” relacionada, a la que se le llama “función de ventana”. Dicha “relación” entre una
ventana y su función de ventana se origina al crearse la ventana utilizando una plantilla de
creación llamada “clase de ventana”, la cual debe haberse registrado previamente. Uno de los
datos de dicha plantilla es la dirección de la función de ventana que deberá llamarse para
procesar los mensajes de toda ventana que se cree utilizando dicha plantilla.
La función de ventana es pues, el área central de trabajo de todo programa desarrollado
utilizando el API de Windows. El resto del código suele ser casi siempre el mismo. La función
de ventana es la que determina cómo se comportará nuestro programa, nuestra ventana para el
usuario, ante cada evento.
Existe un conjunto de mensajes estándar reconocidos por el sistema operativo y que nuestras
funciones de ventana pueden manejar. Para cada uno de estos mensajes existe una constante
relacionada. En el programa básico mostrado en la sección 3.1, se utilizó una de estas constantes:
WM_DESTROY. Al igual que esta constante, definida dentro del archivo de cabecera
Windows.h, existe una constante WM_XXX para cada mensaje reconocido por el sistema
operativo. Es posible que un programador defina sus propios mensajes simplemente escogiendo
un valor fuera del rango que Windows reserva para los desarrolladores de su sistema operativo.
El manejo de mensajes definidos por el usuario cae fuera del alcance de esta introducción.
Cuando la función de ventana es llamada para procesar un mensaje, recibe los siguientes datos:
El handle de la ventana a la que corresponde el mensaje, dado que, como hemos visto,
una función de ventana puede utilizarse para procesar los mensajes de más de una
ventana, cuando todas éstas fueron creadas utilizando la misma clase de ventana.
La constante que identifica al mensaje.
Dos parámetros que contienen información sobre dicho mensaje, o bien contienen
direcciones de estructuras reservadas en memoria con dicha información.
El hacer que una ventana reconozca y reaccione a un nuevo mensaje suele consistir en agregar el
“case” (al “switch” principal de la función de ventana) para la constante de dicho mensaje con el
código que realice el comportamiento deseado. El siguiente código de una función de ventana
muestra el manejo de un mensaje correspondiente al ratón y al teclado. El resto del programa no
varía respecto al ejemplo anterior.
108
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
P R O G R A M A C I Ó N C O N G U I
LRESULT CALLBACK FuncionVentana(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
int PosX, PosY;
char Mensaje[100];
int CtrlPres, ShiftPres;
int CodigoTecla, EsTeclaExtendida;
switch( uMsg )
{
case WM_LBUTTONUP:
PosX = LOWORD(lParam); // el word menos significativo
PosY = HIWORD(lParam); // el word más significativo
CtrlPres = wParam & MK_CONTROL;
ShiftPres = wParam & MK_SHIFT;
sprintf(Mensaje, "X=%d, Y=%d, CtrlPres=%d, ShiftPres=%d",
PosX, PosY, CtrlPres, ShiftPres);
MessageBox(hWnd, Mensaje, "Posición del mouse", MB_OK);
break;
case WM_KEYUP:
CodigoTecla = (int)wParam;
EsTeclaExtendida = ( lParam & ( 1 << 24 ) ) != 0;
if( EsTeclaExtendida == 1 )
sprintf(Mensaje, "CodigoTecla=%d (extendida)", CodigoTecla);
else
if( ( '0' <= CodigoTecla && CodigoTecla <= '9' ) ||
( 'A' <= CodigoTecla && CodigoTecla <= 'Z' ) )
sprintf(Mensaje, "CodigoTecla=%d (%c)", CodigoTecla,
(char)CodigoTecla);
else
sprintf(Mensaje, "CodigoTecla=%d", CodigoTecla);
MessageBox(hWnd, Mensaje, "Tecla presionada", MB_OK);
break;
case WM_DESTROY:
PostQuitMessage( 0 );
break;
default:
return DefWindowProc( hWnd, uMsg, wParam, lParam );
}
return 0;
}
109
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
P R O G R A M A C I Ó N C O N G U I
La constante WM_KEYUP corresponde al mensaje producido por el evento de soltar (UP) una
tecla del teclado (KEY). Para este mensaje, el parámetro wParam contiene el código de la tecla
presionada. Sólo los códigos correspondientes a los números (0x30 al 0x39, ‘0’ al ‘9’) y las letras
mayúsculas (0x41 a 0x5A, ‘A’ al ‘Z’) coinciden con la tabla ASCII. Toda tecla del teclado tiene
un código distinto, existiendo constantes en Windows.h para cada una de ellas. Como ejemplo,
podríamos reconocer si la tecla presionada fue F1 utilizando:
if ( wParam == VK_F1 ) { ... }
El parámetro lParam se comporta como un bit-flag con información como “es una tecla
correspondiente al conjunto de teclas extendidas”. Hay 15 mensajes relacionados con el teclado,
de los cuales los más comúnmente procesados son:
WM_KEYDOWN
WM_KEYUP
WM_CHAR
WM_DEADCHAR
class Observador {
public void notificar(String infoDelEvento) {
System.out.println("Sucedió el siguiente evento: " + infoDelEvento);
}
}
class Sujeto {
private Vector listaObservadores = new Vector();
public void registrarObservador(Observador ob) {
listaObservadores.add(ob);
}
public void simularEvento(String infoDelEvento) {
for(int i = 0; i < listaObservadores.size(); i++) {
Observador ob = (Observador)listaObservadores.get(i);
ob.notificar(infoDelEvento);
}
}
}
class PatronObservador {
public static void main(String args[]) {
Observador ob = new Observador();
Sujeto suj = new Sujeto();
suj.registrarObservador(ob);
suj.simularEvento("Evento1");
suj.simularEvento("Evento2");
}
}
110
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
P R O G R A M A C I Ó N C O N G U I
El programa crea un objeto observador y un sujeto observable, para luego simular que dos
eventos ocurren. Note que el proceso de registro consiste simplemente en agregar la referencia
al objeto observador a una lista del sujeto. Note además que al ocurrir el evento simulado, lo que
hace el sujeto es recorrer la lista de referencias a objetos que se registraron como observadores,
llamando a un método para cada uno de estos. Ésta es la forma en que el sujeto notifica al
observador, llamando a un método de este último. Finalmente note que el método “notificar” de
la clase observador es el acuerdo entre ambas partes, sujeto y observador, para que la
notificación sea posible, es decir, todo observador de un objeto de mi clase Sujeto debe ser un
objeto de la clase Observador o bien heredar de él, de forma que se garantice que dicho método
existe.
El ejemplo anterior tiene un problema: Sólo podemos hacer que los objetos de una ClaseX
observen los eventos de mi clase Sujeto, si mi ClaseX hereda de Observador. Esto es indeseable
si pensamos que lenguajes como Java y C# no soportan herencia múltiple y, muy
probablemente, deseáramos que un objeto, cuya clase padre no puedo modificar, escuche los
eventos de otro. La solución a éste es aislar los métodos que forman parte del acuerdo en una
interfaz. El siguiente programa modifica el anterior de forma que se utilice una interfaz en lugar
de una clase:
import java.util.Vector;
interface IObservador {
void notificar(String infoDelEvento);
}
class Sujeto {
private Vector listaObservadores = new Vector();
public void registrarObservador(IObservador ob) {
listaObservadores.add(ob);
}
public void simularEvento(String infoDelEvento) {
for(int i = 0; i < listaObservadores.size(); i++) {
IObservador ob = (IObservador)listaObservadores.get(i);
ob.notificar(infoDelEvento);
}
}
}
class PatronObservador2 {
public static void main(String args[]) {
ObservadorTipo1 ob1 = new ObservadorTipo1();
ObservadorTipo2 ob2 = new ObservadorTipo2();
Sujeto suj = new Sujeto();
suj.registrarObservador(ob1);
suj.registrarObservador(ob2);
suj.simularEvento("Evento1");
suj.simularEvento("Evento2");
}
}
En el ejemplo anterior se tienen dos objetos, cada uno de una clase distinta pero que
implementan la interfaz IObservador, que se registran para escuchar los eventos de un tercer
objeto, uno de la clase Sujeto. Note que la implementación de la clase Sujeto se basa en la
111
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
P R O G R A M A C I Ó N C O N G U I
interfaz IObservador y no en una clase. De esta forma se permite que los objetos de cualquier
clase que implementen la interfaz IObservador sean utilizados como observadores de un objeto
de la clase Sujeto. Ésta última es la estrategia que utiliza Java para sus eventos en ventanas.
En Java, el sujeto es un objeto ventana, una instancia de cualquier clase que herede de JFrame.
Esta clase se comunica con una función de ventana internamente, la que procesa un
subconjunto de todos los mensajes que se pueden generar con una ventana, llamando a los
métodos adecuados de los objetos observadores registrados para dicho objeto ventana. Para
esto, JFrame contiene listas de observadores, como datos miembros, para los distintos grupos de
mensajes: Una lista para los mensajes de manipulación de la ventana, otra para los mensajes del
ratón, otra para los mensajes del teclado, etc. De esta manera, cuando ocurre un evento, el
mensaje es tratado por la función de ventana de JFrame, la que a su vez llama al método
adecuado de cada objeto observador registrado para dicho mensaje.
En Java, las interfaces como IObservador en nuestro ejemplo, se llaman Listeners, y existe una
definida para cada grupo de mensajes. Toda clase cuyos objetos se desea que puedan escuchar
un evento de una ventana, debe de implementar la interfaz Listener adecuada. Veamos cómo
ésto se refleja, en el caso de los mensajes del ratón y del teclado, en el siguiente código.
import javax.swing.*;
import java.awt.event.*;
112
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
P R O G R A M A C I Ó N C O N G U I
char c = e.getKeyChar();
int keyCode = e.getKeyCode();
int modifiers = e.getModifiers();
if (Character.isISOControl(c)) {
charString = "key character = "
+ "(an unprintable control character)";
} else {
charString = "key character = '"
+ c + "'";
}
System.out.println(s + "\n"
+ " " + charString + "\n"
+ " " + keyCodeString + "\n"
+ " " + modString);
}
}
class EventosDeVentanas {
public static void main(String args[]) {
MiVentana ventana = new MiVentana();
}
}
En el código anterior el sujeto observable es el objeto de clase MiVentana creado dentro del
método main, y los observadores son dos: Un objeto de la clase MiObservadorVentana,
implementando la interfaz WindowListener, y un objeto de la clase MiObservadorTeclado,
implementando la interfaz KeyListener. Ambos observadores serán notificados de los eventos
de la ventana y del teclado, respectivamente, que es capaz de detectar y/o producir el sujeto.
Note además que el sujeto, conceptualmente y en la práctica, no requiere saber sobre la
implementación interna de los objetos observadores, lo único que le concierne es que dichos
objetos poseen los métodos adecuados para, mediante éstos, poderles notificar que ocurrió un
evento del tipo para el que se registraron. Es por ello que Java utiliza interfaces para definir
dicho contrato, los métodos que el objeto observador debe implementar y el objeto observable
debe llamar. Note también que los métodos de registro siguen un formato común y forman
parte de la interfaz que ofrece el sujeto, en este caso, los métodos addWindowListener y
addKeyListener. Como es de esperarse, estos métodos reciben como parámetros referencias a
objetos que implementen las interfaces respectivas.
La implementación de la interfaz de un evento particular requiere ser completa, si no lo fuera, la
clase sería abstracta y no podríamos pasarle un objeto instanciado de dicha clase al método de
registro respectivo. Sin embargo, es posible que no se requiera utilizar todos los métodos de la
interfaz para un programa en particular, como es el caso, en el código anterior, de la clase
“MiObservadorVentana”. Debido a esto, muchas interfaces relacionadas a eventos en Java
tienen una clase que las implementa, a la que se le llama “Adaptador”. La siguiente clase es el
113
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
P R O G R A M A C I Ó N C O N G U I
Los adaptadores le dan una implementación vacía a las interfaces que implementan y son
declarados como abstractos únicamente porque se espera que sirvan como clases base para otras
clases que sobrescriban los métodos de las interfaces que requieran. Un ejemplo del uso de un
adaptador puede verse en el primer código de ejemplo de Java, en la sección 3.1. Creación de
una Ventana. En dicho código se utiliza el adaptador WindowAdapter como base de una clase
anidada anónima.
Es importante señalar que no existe ningún impedimento para que el sujeto sea a su vez
observador de otros sujetos o de sí mismo (como en el ejemplo de la sección 3.1), que un mismo
observador pueda observar varios sujetos a la vez (del mismo tipo o de diferente tipo, de uno o
más sujetos) y que un mismo evento sea observado por muchos observadores. Para este último
caso podríamos, en el ejemplo anterior, haber registrado otros objetos para los mismos eventos
(llamando más de una vez a addWindowListener y addKeyListener respectivamente) de manera
que cuando dichos eventos ocurran, el sujeto, uno de la clase “MiVentana”, llamará en secuencia
a los métodos correspondientes de todas los objetos registrados para dicho evento, en el orden
en que se registraron.
Así como un objeto se registra para escuchar un evento, también se puede desregistrar. Para ésto
existen los correspondientes métodos removeXXXListener, como son removeWindowListener
y removeKeyListener.
Note que todos los métodos de una interfaz Listener reciben como parámetro una referencia de
una clase XXXEvent, la que encapsula los datos del mensaje y provee de una interfaz para su
fácil uso. En el caso de la interfaz WindowListener es WindowEvent, en el caso de la interfaz
KeyListener es KeyEvent.
La Tabla 6 - 1 sumariza las clases involucradas en tres tipos de eventos comúnmente manejados
en Java:
114
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
P R O G R A M A C I Ó N C O N G U I
class OtraClase {
public static int Metodo3(string sMensaje) {
Console.WriteLine(" OtraClase.Metodo3 : " + sMensaje);
return 3;
}
public int Metodo4(string sMensaje) {
Console.WriteLine(" OtraClase.Metodo4 : " + sMensaje);
return 4;
}
}
class Principal {
115
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
P R O G R A M A C I Ó N C O N G U I
MiDelegado delegado;
/////////////////////////////
// Prueba con eventos
116
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
P R O G R A M A C I Ó N C O N G U I
Luego de inicializar una variable de tipo delegado, su uso es igual al de un puntero a función de
C o C++, utilizando el nombre de la variable como si se tratara del nombre de un método.
Los objetos delegados utilizados en el código anterior, sólo nos permiten llamar a un único
método a la vez. Sin embargo, es posible utilizar un objeto delegado enlazado con otros objetos
delegados, de forma que se pueda llamar a más de una función. A este tipo de delegado se le
llama MulticastDelegate. El uso de este tipo de delegados cae fuera del alcance del presente
curso.
Sin embargo, como puede verse en el código anterior, una forma simple de llamar a un grupo de
métodos es utilizando la palabra clave event. El formato de declaración de una variable event es:
[Modificadores] event <Nombre del Delegado> <Nombre>;
Esta declaración sólo puede ir en el ámbito de una clase, no dentro de un método. Esto se debe
a que, a pesar de parecerse a la declaración de una variable, al agregarle la palabra event a la
declaración, la sentencia se expande al ser compilado el código, generándose la declaración de
una variable delegado, del tipo puesto en la declaración, y dos métodos, add_XXX y
remove_XXX (donde XXX es el nombre del tipo del delegado). Para el código anterior, esta
expansión sería de la forma:
private static MiDelegado evento = null;
private static MiDelegado add_MiDelegado(...) {}
private static MiDelegado remove_MiDelegado(...) {}
Estos métodos add y remove son llamados automáticamente cuando se utilizan los operadores
‘+’ y ‘-‘, respectivamente, con la variable evento. Debido a esto, la variable declarada con la
palabra event no requiere ser inicializada.
En .NET se utilizan los conceptos de delegado y evento para manejar la respuesta a la
interacción del usuario con las ventanas del programa. Para cada tipo de evento que el usuario
pueda generar, existen en las clases de .NET propiedades que encapsulan variables event, así
como los tipos de delegados correspondientes. El siguiente código muestra un ejemplo del uso
de estos eventos y delegados:
using System;
// Al siguiente espacio de nombres pertenecen:
// Form, KeyPressEventHandler, KeyPressEventArgs,
// MouseEventHandler, MouseEventArgs, PaintEventHandler, PaintEventArgs.
using System.Windows.Forms;
class Eventos_Ventana {
public static void Main(string[] args) {
Ventana refVentana = new Ventana();
117
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
P R O G R A M A C I Ó N C O N G U I
Application.Run(refVentana);
}
}
En el código anterior, se hace uso de las propiedades MouseUp, MouseLeave, KeyPress y Paint,
las que nos permiten acceder a datos miembros internos declarados como event para los tipos
de delegado MouseEventHandler, EventHandler, KeyPressEventHandler y PaintEventHandler
respectivamente.
Es importante tener en cuenta que las variables declaradas como event son únicamente una
forma de simplificar el trabajo con los delegados, cuando se desea tener la posibilidad de llamar a
más de un método cuando un evento sucede.
Aunque enfocado en forma distinta a Java, el uso de delegados es realmente la implementación
del mismo patrón de diseño, el patrón Observador. La única diferencia está en que, mientras en
Java el objeto observador implementa una interfaz para que el sujeto observable le notifique de
un evento llamando a los métodos de ésta, en .NET se utiliza un puntero a función encapsulado
en una clase especial.
Tipos de Eventos
Los eventos con los que interactúa un programa son comunmente los producidos como
consecuencia de un mensaje del sistema operativo, en particular los relacionados con el ratón, el
teclado y con el manejo de la ventana.
Un programa también puede definir sus propios eventos o simular los ya existentes como una
manera de independizar el programa de las capacidades del sistema operativo subyacente (como
es el caso de Java para muchas de sus clases, del paquete Swing, con representación visual).
Un programa también puede disparar eventos al detectar que sucesos no visuales se producen
en su entorno, como por ejemplo: El arribo de un paquete de datos por red, la recepción de un
mensaje enviado desde otro programa, la baja del nivel de algún recurso por debajo del límite
crítico (como la memoria), etc.
Gráficos en 2D
El manejo de los dispositivos gráficos en Windows se realiza mediante la librería GDI32
(Graphics Device Interface). Esta librería aísla las aplicaciones de las diferencias que existen
entre los dispositivos gráficos con los que puede interactuar un computador.
El siguiente diagrama muestra el flujo de comunicación entre las aplicaciones en ejecución, la
librería gráfica de Windows, las librerías por cada dispositivo y los dispositivos mismos.
118
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
P R O G R A M A C I Ó N C O N G U I
App1 App2
GDI
Como se puede apreciar, las aplicaciones interactúan únicamente con la librería GDI. Una vez
que una aplicación le indica a GDI con qué tipo de dispositivo desea interactuar, dicha
interacción se realiza en forma independiente al dispositivo elegido. Las librerías por cada
dispositivo se conocen como Drivers, y deben cumplir la especificación requerida por GDI para
que puedan interactuar con él. De esta forma, cada fabricante de un nuevo dispositivo, que
desee que éste sea utilizado desde Windows deberá proveer un Driver que sepa cómo manejar
su dispositivo y que cumpla las especificaciones de GDI.
Un punto importante es ¿qué sucede cuando una aplicación le indica a GDI con qué tipo de
dispositivo desea interactuar? La GDI busca si dicho dispositivo (es decir, su driver) existe, lo
carga a memoria si no estuviese ya cargado, crea una entrada en la tabla de recursos para el
nuevo recurso a utilizar (el dispositivo), llena dicha entrada y retorna al programa un handle al
nuevo dispositivo creado. Dicha entrada en la tabla de recursos contiene:
Información sobre el dispositivo mismo, su tipo, sus capacidades, etc.
Información sobre su estado actual, lo que puede incluir referencias a otros recursos
utilizados cuando la aplicación desea interactuar con el dispositivo.
A dicha información en conjunto se le llama Contexto del Dispositivo (Device Context), por lo
que el tipo de su handle relacionado es HDC (Handle Device Context).
De lo anterior se resume que, para que una aplicación pueda interactuar con un dispositivo debe
de obtener un HDC adecuado para dicho dispositivo. Al igual que con otros handles, la
aplicación deberá liberar dicho HDC cuando ya no requiera trabajar más con él.
Existen 5 formas de dibujo bastante comúnes (no son las únicas):
Síncrono, donde las acciones de dibujo se realizan en cualquier lugar de la aplicación.
Asíncrono, donde las acciones de dibujo se realizan en un lugar bien definido de la
aplicación.
Sincronizado, cuando el dibujo asíncrono se “sincroniza” con otras acciones en
cualquier lugar de la aplicación.
En Memoria, donde las acciones de dibujo se realizan en un lugar de la memoria distinta
a la memoria de video.
119
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
P R O G R A M A C I Ó N C O N G U I
El código anterior dibujaría la cadena “hola” sobre el DC referido con el handle “hDC”.
Utilizamos la opción DT_NOCLIP dado que no nos interesa restringir la salida del texto a un
rectángulo específico. Por el mismo motivo sólo especificamos la posición X e Y inicial del texto
en la variable “rc”. La estructura RECT se define como:
struct RECT { long left, top, right, bottom; };
La función MoveToEx establece el punto inicial de dibujo de líneas sobre un DC. A partir de
dicha posición se realizará dibujos de líneas hacia otras posiciones, con funciones como LineTo.
Cada nueva línea dibujada actualiza la posición actual de dibujo de líneas. Los prototipos de
MoveToEx y LineTo son:
BOOL MoveToEx(
HDC hdc, // handle al DC
int Xinicial, // coordenada-x de la nueva posición
// (la que se convertirá en la actual)
int Yinicial, // coordenada-y de la nueva posición
// (la que se convertirá en la actual)
POINT* PosAntigua // recibe los datos de la antigua posición actual
);
BOOL LineTo(
HDC hdc, // handle al DC
int Xfinal, // coordenada-x del punto final
int Yfinal // coordenada-y del punto final
);
120
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
P R O G R A M A C I Ó N C O N G U I
El código anterior dibujaría un triángulo con vértices (10,10), (40,10) y (40,40). La posición
actual de dibujo, para subsiguientes dibujos de líneas, quedaría en la coordenada (10,10).
Es importante señalar que todas las acciones de dibujo sobre un DC se realizan en base al
sistema de coordenadas del mismo. En el caso de un DC relativo al área cliente de una ventana,
el origen de su sistema de coordenadas en la esquina superior izquierda del área cliente, con el eje
positivo X avanzando hacia la derecha, y el eje positivo Y avanzando hacia abajo.
Dibujo Asíncrono
Las acciones de dibujo se centralizan en el procesamiento del mensaje WM_PAINT. El
siguiente código muestra un esquema típico de procesamiento de este mensaje:
case WM_PAINT:
hDC = BeginPaint(hWnd, &ps);
// Aquí van las acciones de dibujo
EndPaint(hWnd, &ps);
break;
La función BeginPaint retorna un HDC adecuado para dibujar sobre el área cliente invalidada de
una ventana. La función EndPaint libera el HDC obtenido. Para entender el concepto de
invalidación, imagine el siguiente caso:
Se tiene en un momento dado dos ventanas mostradas en pantalla. La primera oculta
parte de la segunda, como se muestra en la siguiente figura:
Luego se mueve la primera ventana de forma que descubre parte o toda el área ocultada
de la segunda ventana, como se muestra en la siguiente figura:
121
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
P R O G R A M A C I Ó N C O N G U I
El dibujo actual del área descubierta ya no es válido y debe de ser redibujado por el
código de la aplicación correspondiente a la segunda ventana, dado que Windows no
tiene forma de saber cómo se debe dibujar coda ventana de cada aplicación, sólo las
aplicaciones mismas lo saben. Sin embargo, Windows sí reconoce que esta invalidación
ha ocurrido, dado que sabe dónde se encuentra cada ventana y cuál está frente a cual,
por lo que genera un mensaje WM_PAINT, con la información acerca del rectángulo
invalidado, y lo deposita en la cola de mensajes de la aplicación a la que le corresponde
dicha ventana invalidada.
Finalmente, cuando el bucle de procesamiento de mensajes correspondiente extraiga y
mande a procesar dicho mensaje WM_PAINT, la función de ventana de la ventana
invalidada repintará el área de dibujo inválido.
La estructura PAINTSTRUCT es llenada por BeginPaint con información acerca del área
invalidada. Esta información podría ser utilizada por el programa para aumentar la eficiencia del
código de dibujo, dado que podría repintar solamente el área invalidada y no repintar toda el área
cliente. Uno de los datos miembros de dicha estructura es el mismo valor retornado por
BeginPaint. La función EndPaint utiliza dicho dato para eliminar el DC.
Los mensajes WM_PAINT son generados en forma automática por el sistema operativo
cuando éste sabe que el área cliente de una ventana requiere repintarse. Si al generarse un
mensaje WM_PAINT para una ventana, ya existe en la cola de mensajes otro WM_PAINT para
la misma ventana, se juntan ambos mensajes en uno solo para un área invalidada igual a la
combinación de las áreas invalidadas de ambos mensajes.
También es posible generar un mensaje WM_PAINT manualmente y colocarlo en la cola de
mensajes respectiva de forma que se repinte la ventana. La función que hace ésto es:
BOOL InvalidateRect(
HWND hWnd, // handle de la ventana
CONST RECT* lpRect, // rectángulo a invalidar
BOOL bErase // flag de limpiado
);
El segundo parámetro es el rectángulo, dentro del área cliente, que deseamos invalidar. El tercer
parámetro le sirve a la función BeginPaint. Si dicho parámetro es 1, BeginPaint pinta toda el área
invalidada utilizando la brocha con la que se creó la ventana (dato miembro hbrBackground de
la estructura WNDCLASS) antes de finalizar y retornar el HDC (de forma que se comience con
un área de dibujo limpia). Si dicho parámetro es 0, BeginPaint no realiza este limpiado.
122
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
P R O G R A M A C I Ó N C O N G U I
El fragmento de código anterior pertenece a una función de ventana que utiliza InvalidateRect.
Cuando se presiona con el botón izquierdo del ratón se modifica un contador de forma que el
dibujo actual ya no es correcto y debe repintarse. Para el botón derecho se desea que se muestre,
como una secuencia animada, cómo se va modificando el contador. Sin embargo, esta secuencia
no se muestra, y sólo se ve el valor final del contador en la ventana. Esto se debe al hecho que
InvalidateRect “no es” una llamada directa al código en WM_PAINT, sino la colocación de un
mensaje WM_PAINT en la cola de mensajes, por lo que sólo al retornar del procesamiento del
mensaje actual, WM_RBUTTONDOWN en este caso, el bucle de procesamiento de mensajes
podrá retirar el WM_PAINT de la cola de mensajes y procesarlo.
Como puede apreciarse, el dibujo realizado en WM_PAINT es asíncrono respecto a la solicitud
de dibujo realizada en WM_RBUTTONDOWN. Este tipo de dibujo es adecuado para dibujos
estáticos o de fondo, pero no para secuencias de animación.
Dibujo Síncrono
Para realizar dibujos desde cualquier otro lugar fuera del procesamiento del mensaje
WM_PAINT se utiliza la combinación de funciones:
// Obtener un DC para el área cliente de la ventana referida con el handle hWnd
HDC GetDC( HWND hWnd );
// Liberar el DC obtenido con GetDC
int ReleaseDC( HWND hWnd, HDC hDC );
Para obtener un HDC relativo al área cliente de una ventana se utiliza la función GetDC. Una
vez que se han finalizado las acciones de dibujo sobre la ventana, se debe liberar el HDC
obtenido llamando a ReleaseDC.
El siguiente segmento de un programa muestra el uso de esta técnica:
...
case WM_LBUTTONDOWN:
hDC = GetDC(hWnd);
iContador++;
sprintf(szMensaje, "Contador=%d", iContador);
DrawText(hDC, szMensaje, -1, &rc, DT_SINGLELINE);
ReleaseDC(hWnd, hDC);
break;
case WM_RBUTTONDOWN:
hDC = GetDC(hWnd);
for(iBucle = 0; iBucle < 10; iBucle++) {
iContador++;
sprintf(szMensaje, "Contador=%d", iContador);
DrawText(hDC, szMensaje, -1, &rc, DT_SINGLELINE);
Sleep(100);
123
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
P R O G R A M A C I Ó N C O N G U I
}
ReleaseDC(hWnd, hDC);
break;
...
A diferencia del caso anterior, el HDC creado puede utilizarse en cualquier lugar del programa.
Este HDC debe ser liberado cuando ya no sea requerido.
Dibujo Sincronizado
El dibujo sincronizado permite realizar modificaciones al estado del dibujo en cualquier parte del
programa, concentrando el trabajo de dibujo dentro del mensaje de pintado WM_PAINT.
El siguiente segmento de un programa muestra el uso de esta técnica:
...
case WM_LBUTTONDOWN:
iContador++;
InvalidateRect(hWnd, NULL, TRUE);
UpdateWindow(hWnd); // acá se fuerza el procesamiento del mensaje WM_PAINT
// colocado en la cola de mensajes por
InvalidateRect
break;
case WM_RBUTTONDOWN:
for(iBucle = 0; iBucle < 10; iBucle++) {
iContador++;
InvalidateRect(hWnd, NULL, TRUE);
UpdateWindow(hWnd);
Sleep(100); }
break;
case WM_PAINT:
hDC = BeginPaint(hWnd, &ps);
sprintf(szMensaje, "Contador=%d", iContador);
DrawText(hDC, szMensaje, -1, &rc, DT_SINGLELINE);
EndPaint(hWnd, &ps);
break;
...
Dibujo en Memoria
Cuando el dibujo se debe realizar en varios pasos, el realizarlo directamente sobre la ventana
provoca que el usuario del programa vea todo el proceso de dibujo, produciéndose en algunos
casos el parpadeo de la imagen. En estos casos es posible realizar la composición del dibujo en
memoria, para luego pasar la imagen final a la ventana en una sola acción.
El siguiente segmento de un programa muestra el uso de esta técnica:
// Luego de creada la ventana
SelectObject(hDC_Fondo, hBM_Original);
DeleteDC(hDC_Fondo);
DeleteObject(hBM_Fondo);
// En la función de ventana
HBITMAP hBM_Fondo = 0;
HDC hDC_Fondo = 0;
124
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
P R O G R A M A C I Ó N C O N G U I
switch( uMsg ) {
case WM_PAINT:
hDC = BeginPaint(hWnd, &ps);
if(hBM_Fondo != 0 && hDC_Fondo != 0)
BitBlt(hDC, 0, 0, 800, 600, hDC_Fondo, 0, 0, SRCCOPY);
else {
szMensaje = "Error al cargar la imagen";
TextOut(hDC_Fondo, 20, 20, szMensaje, strlen(szMensaje));
}
EndPaint(hWnd, &ps);
break;
...
}
return 0;
}
Dibujo en Java
Java provee, a partir de su versión 1.2, el paquete SWING que simplifica significativamente el
trabajo con ventanas. Esta librería está formada en su mayoría por componentes ligeros, de
forma que se obtenga la máxima portabilidad posible de las aplicaciones con ventanas hacia las
diferentes plataformas que soportan Java.
El paquete SWING se basa en el paquete AWT que fue desarrollado con las primeras versiones
de Java. Para los ejemplos en las siguientes secciones, se realizará dibujo en 2D sobre la clase
base para ventanas JFrame de SWING.
125
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
P R O G R A M A C I Ó N C O N G U I
Dibujo Asíncrono
El dibujo en una ventana requiere sobrescribir el método “paint” de la clase “JFrame”. Dentro
de este método se llama a la implementación de la clase base de paint y luego se realizan acciones
de dibujo utilizando la referencia al objeto Graphics recibida. La clase Graphics contiene
métodos adecuados para realizar:
Dibujo de texto.
Dibujo de figuras geométricas con y sin relleno.
Dibujo de imágenes.
El escribir instrucciones de dibujo dentro del método paint equivale a hacerlo dentro del “case
WM_PAINT” de la función de ventana en API de Windows. En su implementación para
Windows, es de esperarse que la clase Graphics maneje internamente un HDC, obtenido
mediante una llamada a “BeginPaint”. El siguiente programa muestra el dibujo asíncrono.
import ...
class Ventana extends JFrame {
Point clicPos;
public Ventana() {
...
addMouseListener(new MouseAdapter() {
public void mouseClicked(MouseEvent e) {
clicPos = e.getPoint();
repaint();
}
});
}
public void paint(Graphics g) {
super.paint(g);
if(clicPos != null)
g.drawString("Clic en " + clicPos, 100, 100);
}
}
...
Dibujo Síncrono
Para el dibujo síncrono se obtiene un objeto Graphics utilizando el método getGraphics de
JFrame. Es importante que, si dicho método es llamado muy seguido, se libere los recursos del
objeto Graphics obtenido (por ejemplo, el HDC obtenido mediante un GetDC para la
implementación en Windows de esta clase) llamando al método “dispose” (que es de suponer
debería llamar a ReleaseDC). Éste es un claro ejemplo donde el usuario debe preocuparse por
liberar explícitamente los recursos dado que el recolector de basura puede no hacerlo a tiempo.
El siguiente programa muestra el dibujo síncrono.
import javax.swing.*;
import java.awt.event.*;
import java.awt.*;
126
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
P R O G R A M A C I Ó N C O N G U I
En el programa anterior, cada vez que se realice un click con el botón del mouse, estando el
mouse sobre el área cliente de la ventana, se dibujará el texto “HOLA” en dicha posición de la
ventana.
Dibujo Sincronizado
Para sincronizar un dibujo se llama al método “update” de la clase JFrame y se concentra todo el
dibujo en la sobrescritura del método “paint” de la ventana. El siguiente programa muestra el
uso de “update”.
import ...
class Ventana extends JFrame {
Point clicPos;
public Ventana() {
...
addMouseListener(new MouseAdapter() {
public void mouseClicked(MouseEvent e) {
Graphics g = getGraphics();
clicPos = e.getPoint();
update(g);
}
});
}
public void paint(Graphics g) {
super.paint(g);
if(clicPos != null)
g.drawString("Clic en " + clicPos, 100, 100);
}
}
...
En el programa anterior, cada vez que se realiza un click sobre la ventana se actualiza la variable
de clase “clicPos” y se invalida la ventana llamando a “update”.
Dibujo en Memoria
Existen varias estrategias para realizar dibujo en memoria en Java, para cada cual un conjunto de
clases adecuadas. Una de estas estrategias consiste en utilizar un objeto BufferedImage, el cual
crea un espacio en la memoria sobre la cual se puede realizar un dibujo. Esta clase provee un
método “getGraphics” que permite obtener un objeto Graphics adecuado para dibujar en esta
memoria. Luego de compuesta la imagen en memoria, se puede utilizar el método “drawImage”
del objeto Graphics en el método “paint” para dibujar dicha imagen en la ventana.
El siguiente programa muestra esta estrategia de dibujo.
import ...
import java.awt.image.*;
class Ventana extends JFrame implements ImageObserver {
BufferedImage img;
public Ventana() {
...
img = javax.imageio.ImageIO.read(new File("Fondo.gif"));
if(img != null) {
Graphics g = img.createGraphics();
g.fillOval(0, 0, 100, 100);
g.dispose();
}
}
public void paint(Graphics g) {
...
g.drawImage(img, 0, 0, this);
}
}
...
127
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
P R O G R A M A C I Ó N C O N G U I
Dibujo en C#
En .NET las clases relacionadas con el dibujo sobre ventanas se encuentran dentro del espacio
de nombres System.Drawing.
Dibujo Asíncrono
Realizar un dibujo síncrono sobre una ventana consiste en agregar un nuevo delegado al evento
“Paint” de la clase Form. Dicho delegado deberá hacer referencia a un método que reciba como
parámetro una referencia “object” y una referencia “PaintEventArgs”. Ésta última contiene las
propiedades y métodos necesarios para realizar un dibujo sobre la ventana.
El siguiente programa muestra un ejemplo de este tipo de dibujo.
using System;
using System.Windows.Forms;
using System.Drawing;
class DibujoSincronizado {
public static void Main(string[] args) {
Ventana refVentana = new Ventana();
Application.Run(refVentana);
}
}
Dibujo Síncrono
Para el dibujo síncrono se utiliza el método CreateGraphics de la clase Form desde cualquier
punto del programa. Este método retorna un objeto Graphics (podemos suponer que
internamente llama a GetDC) con el cual se puede dibujar sobre la ventana. Cuando ya no se
requiera utilizar este objeto, el programa debe llamar al método “Dispose” del mismo, de forma
que se liberen los recursos reservados por éste en su creación (podemos suponer que libera el
HDC interno que maneja mediante un RealeaseDC).
El siguiente programa muestra el uso del dibujo síncrono.
using System;
using System.Windows.Forms;
using System.Drawing;
128
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
P R O G R A M A C I Ó N C O N G U I
class DibujoSincrono {
public static void Main(string[] args) {
Ventana refVentana = new Ventana();
Application.Run(refVentana);
}
}
Dibujo Sincronizado
Para sincronizar un dibujo se llama al método “Invalidate” de la clase Form y se concentra todo
el dibujo en la sobrescritura del método donde se concentra el trabajo de dibujo asíncrono. El
siguiente programa muestra el uso de “Invalidate”.El siguiente programa muestra el uso del
dibujo sincronizado.
using System;
using System.Windows.Forms;
using System.Drawing;
class DibujoSincronizado {
public static void Main(string[] args) {
Ventana refVentana = new Ventana();
Application.Run(refVentana);
}
}
Dibujo en Memoria
Al igual que en Java, existen muchas estrategias de dibujo en memoria. El siguiente ejemplo crea
un objeto de la clase “Image” que inicialmente contiene un dibujo guardado en un archivo.
Dicho objeto crea un área en memoria, inicializada con la imagen leída del archivo, a la que
puede accederse mediante un objeto Graphics creado mediante el método estático
“FromImage” de la misma clase Graphics. Cuando dicho objeto Graphics ya no se requiera, el
programa debe liberar sus recursos llamando al método “Dispose”. El siguiente programa
muestra un ejemplo de este tipo de dibujo.
129
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
P R O G R A M A C I Ó N C O N G U I
using System;
using System.Windows.Forms;
using System.Drawing;
public Ventana() {
this.Size = new System.Drawing.Size(400, 400);
this.Text = "Título de la Ventana";
this.Visible = true;
this.Paint += new PaintEventHandler(Ventana_Paint);
img = Image.FromFile("Fondo.bmp");
Graphics g = Graphics.FromImage(img);
Font f = this.Font;
Brush b = Brushes.Black;
g.DrawString("Es injusto que el coyote nunca alcance al correcaminos",
f, b, 50, 30);
g.Dispose();
}
public void Ventana_Paint(object sender, PaintEventArgs args) {
Graphics g = args.Graphics;
g.DrawImage(img, 0, 0);
}
}
class DibujoEnMemoria {
public static void Main() {
Application.Run(new Ventana());
}
}
LRESULT CALLBACK FuncionVentana( HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam ) {
switch( uMsg ) {
case WM_COMMAND:
if( LOWORD( wParam ) == ID_BOTON ) {
char szNombre[ 100 ];
HWND hWndBoton = ( HWND )lParam;
GetDlgItemText(hWnd, ID_TEXTO, szNombre, 100);
MessageBox(hWnd, szNombre, "Hola", MB_OK );
}
break;
// Aquí va el resto del switch
...
130
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
P R O G R A M A C I Ó N C O N G U I
}
return 0;
}
int WINAPI WinMain( HINSTANCE hIns, HINSTANCE hInsPrev, LPSTR lpCmdLine, int iShowCmd ) {
// Creo y registro la ventana principal.
...
// Muestro la ventana.
...
return 0;
}
A los elementos GUI del API de Windows se les llama controles. Como puede observarse, los
controles no son más que ventanas cuyos nombres de clase (STATIC, EDIT y BUTTON)
corresponden a clases de ventana preregistradas. Dado que estas ventanas deben dibujarse
dentro del área cliente de nuestra ventana principal, tenemos que asignarles la constante de estilo
WS_CHILD y el octavo parámetro debe ser el identificador de esta ventana padre. El estilo
WS_VISIBLE evita que tengamos que ejecutar ShowWindow para cada una de estas ventanas
hijas.
El noveno parámetro de CreateWindow puede, opcionalmente, ser un número identificador que
distinga dicha ventana hija de sus ventanas hermanas (hijas de la misma ventana padre). Este
parámetro es aprovechado en la función de ventana de nuestra ventana principal. En dicha
función se agrega una sentencia CASE para el mensaje WM_COMMAND, el cual es generado
por diferentes objetos visibles cuando el usuario interactúa con ellos. En particular, cuando
presionamos el botón creado, se agrega a la cola de mensajes del programa, un mensaje
WM_COMMAND con los dos bytes menos significativos del parámetro wParam iguales al
identificador del botón, ID_BOTON. También utilizamos el identificador de la caja de texto,
ID_TEXTO, para poder obtener el texto ingresado llamando a la función GetDlgItemText.
En general, la interacción con los controles estándar del API de Windows, así como otros
objetos visuales de una ventana, generan mensajes que son enviados a la ventana padre para su
procesamiento. De igual forma, dicho procesamiento suele incluir el envío de nuevos mensajes a
los objetos visibles, para obtener más información o para reflejar el cambio de estado del
programa (internamente, GetDlgItemText envía un mensaje directamente a la función de
ventana del control hijo, de manera que ésta devuelva el texto ingresado), en otras palabras, todo
131
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
P R O G R A M A C I Ó N C O N G U I
se realiza enviando y recibiendo mensajes. Esto tiene la ventaja de unificar la forma de trabajo
con ventanas a un modelo simple de envío-recepción de mensajes, pero con la desventaja de
limitar el procesamiento a una sola ventana (comunmente, solo la ventana padre). Esto tiene
sentido bajo el enfoque de que, todos los elementos manejados son ventanas, por tanto es de
esperarse que sólo la ventana padre esté interesada en los mensajes de sus hijas. El problema
sucede cuando queremos encapsular la funcionalidad de una ventana a sí misma para ciertos
trabajos, es decir, ¿cómo hacer para que una caja de texto maneje por sí mismo los mensajes que
sólo le competen a él, y envíe a la ventana padre el resto?. Veremos que Java y C# solucionan,
de diferente forma, estas carencias del enfoque del API de Windows.
txt.getText(),"Hola",JOptionPane.INFORMATION_MESSAGE);
}
});
Container cp = getContentPane();
cp.setLayout(new FlowLayout());
cp.add(lbl);
cp.add(txt);
cp.add(btn);
}
}
En el programa anterior, JTextField, Jlabel y JButton son componentes que representan una caja
de texto, una etiqueta y un botón respectivamente, al igual que las clases de ventana EDIT,
STATIC y BUTTON del API de Windows.
Los elementos GUI de Java se denominan componentes. Como puede observarse, los
componentes en Java son clases que se agregan a una ventana para ser visualizados. Es
importante en este punto hacer una distinción entre lo que son internamente estos
componentes, contra lo que son los controles del API de Windows. Mientras que los controles
132
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
P R O G R A M A C I Ó N C O N G U I
del API de Windows son ventanas, los componentes de Java (a partir de la versión 1.2 del JDK,
denominada Java 2) se dividen en dos categorías:
Los componentes pesados. Su comportamiento está supeditado a las capacidades de la
plataforma subyacente, en este caso particular, Windows. Estos componentes son
JFrame, JDialog y JApplet. Estos componentes crean ventanas de Windows (o utilizan
directamente una ya creada) y las administran internamente, ofreciendo al programador
una interfaz más amigable. En otras palabras, por ejemplo, cuando realizamos click
sobre una ventana creada con un objeto que deriva de JFrame, el evento que se genera
es un mensaje WM_LBUTTONDOWN, el cual es sacado de la cola de mensajes y
enviado a una función de ventana que llama a un método de nuestro objeto JFrame, el
cual se encarga de llamar al método respectivo de MouseListener para todos los objetos
registrados con un llamado a addMouseListener. Además de lo anterior, el dibujo de la
ventana, el efecto de maximizado y minimizado, la capacidad de redimensionar la
ventana y todos los efectos visuales posibles, son gestionados por las funciones del API
de Windows, así como las capacidades ofrecidas por la propia clase JFrame, por
ejemplo, al llamar al método setVisible se estaría llamando internamente a ShowWindow
del API de Windows. Debido a esta dependencia, estos componentes son denominados
pesados.
Los componentes ligeros. Su comportamiento está supeditado a las capacidades
ofrecidas por un componente pesado, del cual heredan o dentro del cual se dibujan, y no
de la plataforma subyacente, en este caso particular, Windows. Si bien los mensajes
producidos por la interacción del usuario siguen siendo producidos por el sistema
operativo, el manejo de éstos (en forma de eventos), el dibujo de los componentes y de
sus efectos visuales relacionados, están codificados completamente en Java. Estos
componentes definen además sus propios eventos. Debido a esta independencia, estos
componentes son denominados ligeros. Ejemplos de estos componentes son JButton,
JLabel y JTextField. Los componentes ligeros se dibujan sobre el área cliente de
componentes pesados y simulan el “Look And Feel” correspondiente, es decir, no crean
una ventana child. Este tipo de ventanas, child, se verá mas adelante (sección “Tipos de
Ventana”)
Por otro lado, la clase JFrame no administra directamente el área cliente de su ventana, sino que
delega dicho trabajo a un objeto Contenedor, derivado de la clase Container. Un Contenedor es
básicamente un Componente Java con la capacidad adicional de poder mostrar otros
Componentes dentro de su área de dibujo. Es por esto que es necesario obtener una referencia a
dicho contenedor de la ventana, llamando al método getContentPane, dado que es a dicho
contenedor al que deberemos agregarle los componentes que deseamos visualizar.
Los componentes, al igual que una ventana, pueden generar mensajes como respuesta a la
interacción del usuario con ellos. En el código anterior, un botón creado con la clase JButton
genera el evento Action cuando el usuario, con el ratón o el teclado, presiona dicho botón. Para
procesar dicho evento, definimos una clase inner anónima que implementa la interfaz
ActionListener, instanciando dicha clase y pasándole la referencia a esta instancia al método
addActionListener del botón.
Es interesante notar el hecho de que una clase inner anónima puede ser creada realmente a partir
de una clase o de una interfaz, siempre que en éste último caso se implementen todos sus
métodos, que para este caso, es uno sólo, actionPerformed. De igual manera, es interesante
notar que se ha escogido configurar el objeto observador del evento de cerrado de la ventana
desde el método main, no desde el constructor de la ventana. Ambos enfoques son equivalentes.
133
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
P R O G R A M A C I Ó N C O N G U I
Dentro del método actionPerformed se hace uso del dato miembro “txt” de tipo JTextField
para poder mostrar el mensaje respectivo mediante el método estático showMessageDialog de la
clase JOptionPane. Note que el primer parámetro de este método debe ser una referencia a la
ventana padre de la ventana que se mostrará, y que dicho parámetro se pasa utilizando la
expresión “Ventana.this”. Esto se debe a que si pasáramos únicamente this, nos estaríamos
refiriendo al objeto anónimo que implementa la interfaz ActionListener.
Finalmente para este código, note que antes de agregar los componentes al content pane, se
llama al método setLayout. Esto permite determinar la forma en que los componentes son
distribuidos dentro del área cliente del contenedor. El manejo del diseño (layout) de una ventana
se tratará más adelante.
Sumarizando las diferencias entre API de Windows y Java, mientras los objetos visuales
comunes llamados controles, preimplementados en dicha librería, son básicamente ventanas y
las acciones son manejadas mediante mensajes enviados por estos controles a sus ventanas
padres, en Java se trabajan con clases llamadas componentes, las cuales se agregan al contenedor
ContentPane de la ventana, y sus acciones son manejadas mediante interfaces. Mientras que los
controles del API de Windows tienen una posición fija respecto a la esquina superior izquierda
del área cliente de su ventana padre y sus dimensiones son fijas, los componentes de Java no
tienen una posición ni dimensión fija, y esto es manejado mediante objetos Layout que asisten
en el diseño de la ventana. Mientras que en API de Windows los mensajes sólo pueden ser
recibidos por ventanas y sólo una ventana, generalmente la ventana padre, es la que recibe los
mensajes de los controles, en Java cualquier objeto de cualquier clase que implemente la interfaz
correspondiente a un evento puede recibir la notificación del mismo.
Elementos GUI de C#
Los elementos GUI en C# se denominan controles. El siguiente código muestra una ventana
equivalente en C# al código anterior en API de Windows.
using System;
using System.Windows.Forms;
using System.Drawing;
public Ventana() {
this.Text = "Prueba de Controles";
this.Size = new Size(300, 300);
this.Controls.Add(lbl);
this.Controls.Add(txt);
this.Controls.Add(btn);
}
134
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
P R O G R A M A C I Ó N C O N G U I
MessageBox.Show(txt.Text, "Hola");
}
}
class Principal {
public static void Main(string[] args) {
Application.Run(new Ventana());
}
}
135
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
P R O G R A M A C I Ó N C O N G U I
En C# el manejo del diseño se realiza mediante anclas (anchors) y muelles (docks). Todo
control posee las siguientes propiedades:
Anchor: Determina a qué borde del contenedor se anclará el control. Por ejemplo, si el
control se coloca inicialmente a una distancia de 100 píxeles del borde inferior de su
ventana, al redimensionarse el anchor modificará automáticamente la posición del
control de forma que conserve dicha distancia.
Dock: Determina a qué borde del contenedor se adosará el control. Por ejemplo, si el
control se adosa al borde izquierdo de su ventana, su ancho inicial se conserva pero su
alto se modifica de forma que coincida con el alto del área cliente de su ventana. Su
posición también se modifica de forma que su esquina superior izquierda coincida con la
del área cliente de su ventana.
DockPadding: Se establece en el contenedor, por ejemplo, una clase que hereda de
Form. Determina la distancia a la que los componentes, adosados a sus bordes, estarán
de los mismos.
El siguiente código muestra el uso de estas propiedades sobre un botón que es agregado a una
ventana.
Boton = new Button();
Boton.Text = "Boton1";
Boton.Dock = DockStyle.Top;
Controls.Add( Boton );
Tipos de Ventana
En un sistema gráfico con ventanas, dichas ventanas pueden estar relacionadas. Estas relaciones
determinan los tipos de ventanas que se pueden crear.
En Windows, existen dos tipos de relaciones entre ventanas:
La relación de pertenencia. Cuando dos ventanas tienen esta relación, una ventana
(llamada owned) le pertenece a la otra ventana (llamada owner), lo que significa que:
⇒ La ventana owned siempre se dibujará sobre su ventana owner. A la ubicación de
una ventana con respecto a otra en un eje imaginario Z que sale de la pantalla del
computador, se le conoce como orden-z.
⇒ La ventana owned es minimizada y restaurada cuando su ventana owner es
minimizada y restaurada.
⇒ La ventana owned es destruida cuando su ventana owner es destruida.
La relación padre-hijo. Cuando dos ventanas tienen esta relación, una ventana (llamada
hija) se dibujará dentro del área cliente de otra (llamada padre).
136
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
P R O G R A M A C I Ó N C O N G U I
137
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
P R O G R A M A C I Ó N C O N G U I
hWndPopupOwner = CreateWindow(
"ClaseVentanaPopup",
"Título de la Ventana Owner",
WS_POPUP | WS_CAPTION,
100, 100, 200, 200,
NULL, // no tiene ventana owner
NULL, hIns, NULL
);
Para crear una ventana owned se utiliza el estilo WS_POPUP y se pasa un manejador válido de
su ventana owner. El siguiente código crea una ventana owned:
hWndPopupOwned = CreateWindow(
"ClaseVentanaPopup",
"Título de la Ventana Popup Owned",
WS_POPUP | WS_CAPTION,
100, 100, 200, 200,
hWndPopupOwner, // ventana owner
NULL, hIns, NULL
);
Para crear una ventana child se utiliza el estilo WS_CHILD y se pasa un manejador válido de su
ventana owner. El siguiente código crea una ventana owned:
hWndChild = CreateWindow(
"ClaseVentanaHija",
"Título de la Ventana Hija",
WS_CHILD | WS_BORDER,
100, 100, 200, 200,
hWnd, // debe tener una ventana owner
NULL,hIns,NULL
);
La creación y manejo de cajas de diálogo y ventanas MDI con API de Windows va más allá de
los alcances del presente documento.
Ventanas en Java
En Java cada nueva herencia de las clases base para la creación de ventanas (JFrame y JDialog)
determina una nueva clase de ventana. Un programa puede definir y crear una o más ventanas,
de igual o distinto tipo. Sin embargo, al crear una nueva ventana no se establece una relación de
parentesco entre ellas, todas se comportan como popups owner.
El siguiente código muestra la creación de una ventana popup en Java desde la ventana principal
del programa.
class VentanaPopup extends JFrame { ... }
class VentanaPrincipal extends JFrame {
public VentanaPrincipal() {
JButton boton = new JButton("Crear Ventana");
boton.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
VentanaPopup vp = new VentanaPopup();
}
}); ...
} ...
}
Java soporta la creación de aplicaciones MDI. La ventana MDI es llamada “backing window” y
consiste en una ventana popup con un “Content Pane” del tipo “JDesktopPane”. Las “pseudo-
ventanas child” son implementadas con la clase “JInternalFrame”, la que hereda de
“JComponent” por lo que, como puede deducirse, no son realmente ventanas. El siguiente
código muestra un ejemplo de este uso:
import java.awt.*;
import java.awt.event.*;
138
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
P R O G R A M A C I Ó N C O N G U I
import javax.swing.*;
Las cajas de diálogo en Java se crean mediante clases que heredan de JDialog. El siguiente
código muestra el uso de esta clase:
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
Container cp = getContentPane();
cp.setLayout(new GridLayout(3,1));
cp.add(new JLabel("Ingrese un texto"));
cp.add(texto);
cp.add(boton);
}
public String ObtenerResultado() {
return texto.getText();
}
}
public Ventanas2() {
addWindowListener(new WindowAdapter() {
public void windowClosing(WindowEvent e) {
dispose();
139
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
P R O G R A M A C I Ó N C O N G U I
System.exit(0);
}
});
Container cp = getContentPane();
cp.setLayout(new GridLayout(3,1));
cp.add(boton1);
cp.add(boton2);
cp.add(etiqueta);
}
El programa anterior muestra la diferencia entre utilizar una caja de diálogo modal y una amodal.
También muestra una forma de pasar datos desde la caja de diálogo y la ventana que la crea.
Las cajas de diálogo de Java también son llamadas “ventanas secundarias”, mientras que las
pseudo-ventanas hijas creadas con JInternalFrame son llamadas “ventanas primarias”.
Adicionalmente Java provee la clase JOptionPane, la que permite crear cajas de diálogo con
funcionalidad común, como por ejemplo, cajas de diálogo con un texto como mensaje y
botones YES, NO y CANCEL.
Ventanas en C#
Al igual que en Java, cada nueva herencia de las clases base para la creación de ventanas, Form,
determina una nueva clase de ventana. Un programa puede definir y crear una o más ventanas,
de igual o distinto tipo. A diferencia de Java, se pueden crear ventanas popups owner y owned.
El siguiente código muestra la creación de dos ventanas popup, una owner y la otra owned.
class VentanaPopup : Form {
public VentanaPopup( Form OwnerForm ) {
...
this.Owner = OwnerForm;
this.Visible = true;
}
}
class VentanaPrincipal : Form {
public VentanaPrincipal() {
140
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
P R O G R A M A C I Ó N C O N G U I
...
VentanaPopup vp1 = new VentanaPopup( null );
VentanaPopup vp2 = new VentanaPopup( this );
}
...
}
C# maneja ventajas child sólo como ventanas hijas de una ventanas MDI. La ventana MDI
consiste en una ventana con la propiedad IsMDIContainer puesta en “true”. Para que una
ventana sea child de otra, se establece su propiedad MdiParent con la referencia de una ventana
MDI. El siguiente código muestra un ejemplo de este uso:
using System;
using System.Windows.Forms;
void InitializeComponent()
{
this.SuspendLayout();
this.Name = "MainForm";
this.Text = "Esta es la Ventana Principal";
this.Size = new System.Drawing.Size(300, 300);
this.ResumeLayout(false);
}
141
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
P R O G R A M A C I Ó N C O N G U I
Controls.Add(etiqueta);
Controls.Add(texto);
Controls.Add(boton);
}
void buttonClick(object sender, System.EventArgs e)
{
//Visible = false;
Close();
}
public string ObtenerResultado() {
return texto.Text;
}
}
void InitializeComponent() {
this.button = new System.Windows.Forms.Button();
this.button2 = new System.Windows.Forms.Button();
this.label = new System.Windows.Forms.Label();
this.SuspendLayout();
//
// Primer botón
//
this.button.Location = new System.Drawing.Point(24, 16);
this.button.Name = "button";
this.button.Size = new System.Drawing.Size(128, 32);
this.button.TabIndex = 0;
this.button.Text = "Mostrar Como Modal";
this.button.Click += new System.EventHandler(this.buttonClick);
//
// Segundo botón
//
this.button2.Location = new System.Drawing.Point(24, 64);
this.button2.Name = "button2";
this.button2.Size = new System.Drawing.Size(128, 32);
142
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
P R O G R A M A C I Ó N C O N G U I
this.button2.TabIndex = 1;
this.button2.Text = "Mostrar Como Amodal";
this.button2.Click += new System.EventHandler(this.button2Click);
//
// Etiqueta
//
this.label.Location = new System.Drawing.Point(24, 112);
this.label.Name = "label";
this.label.Size = new System.Drawing.Size(128, 24);
this.label.TabIndex = 2;
this.label.Text = "Resultado = ...";
//
// Agrego los controles
//
this.ClientSize = new System.Drawing.Size(248, 165);
this.Controls.AddRange(new System.Windows.Forms.Control[] {
this.label,
this.button2,
this.button});
this.Text = "Prueba con Cajas de Diálogo";
this.ResumeLayout(false);
}
El programa anterior muestra la diferencia entre utilizar una caja de diálogo modal, con
ShowDialog, y una amodal, con Show. También muestra una forma de pasar datos desde la caja
de diálogo y la ventana que la crea.
Dado que es común validar la forma en que fue respondida una caja de diálogo modal, se
provee la propiedad DialogResult, cuyo tipo es el enumerado DialogResult con los siguientes
valores: Abort, Cancel, Ignore, No, None, OK, Retry, Yes. Esta propiedad se establece
automáticamente en algunos casos (cuando se cierra la ventana, se establece a Cancel) o
manualmente desde eventos programados.
143
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
P R O G R A M A C I Ó N C O N G U I
Es fácil ver cual es el directorio de trabajo actual del shell (en Windows lo indica el mismo
prompt, en Linux se puede consultar con un comando, por ejemplo pwd) y cambiarlo
(utilizando un comando como cd). De igual forma, utilizando las llamadas a las funciones
adecuadas del API del sistema operativo, cualquier proceso puede modificar su directorio de
trabajo durante su ejecución.
Para programas diferentes a los shells, desde donde también es posible arrancar otros
programas, el directorio de trabajo actual puede no ser claro, por lo que heredarlo puede no ser
lo que el usuario espera. Por ejemplo, al arrancar un programa desde el explorador de Windows
haciendo un doble-click sobre el nombre del archivo ejecutable, el usuario espera que se inicie
dicho programa teniendo como directorio de trabajo inicial el mismo directorio donde se
encuentra el archivo ejecutable, independientemente de cual sea actualmente el directorio de
trabajo del explorador de Windows. Este comportamiento puede modificarse creando accesos
directos a dichos programas ejecutables y configurándolos para especificar un directorio distinto
como directorio de trabajo inicial.
No todos los archivos a los que un proceso requiere acceder se ubican utilizando el directorio de
trabajo como referencia. Por ejemplo, en el caso de las librerías, se suelen utilizar variables de
entorno para definir conjuntos de directorios de búsqueda. Por ejemplo, para las DLL nativas de
Windows, se utiliza la variable de entorno PATH, mientras el compilador e intérprete de Java
utilizan la variable de entorno CLASSPATH.
144
1
Capítulo
9
Excepciones
El objetivo de este capítulo es dar una base teórica y práctica al lector sobre la creación y
manejo de excepciones, base que será utilizada en los temas siguientes.
144
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
E X C E P C I O N E S
145
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
E X C E P C I O N E S
en la idea de separar el código principal del programa (el código propio de la tarea que el
programa desea realizar, un código limpio de verificación de excepciones), del código de manejo
de las excepciones. Como puede entenderse, este código limpio seguirá conteniendo el código
de manejo de los errores frecuentes, bajo las técnicas tradicionales.
En las siguientes secciones veremos cómo se implementa el tratamiento de excepciones en
C++, Java y C#, así como las ventajas y desventajas de su uso.
Implementación
Tanto Java como C# se basan en el modelo de C++ para el tratamiento de excepciones, tema
que revisaremos a continuación para después tratar las diferencias que existen entre los tres
lenguajes.
C++
Para manejar excepciones que pueden ocurrir dentro de un bloque de código, es necesario
“delimitar” dicho bloque. Para ésto, se utiliza la palabra reservada try. La sintaxis a utilizar es:
try {
// Aquí va el código del que se desea controlar las excepciones
// que produzca.
}
Dentro del bloque try se colocará el código del que se espera monitorear las excepciones que
produzca. Cuando se produce una excepción, se ejecutará un determinado bloque de código
llamado manejador de excepción. Estos bloques de código deben de ir inmediatamente después
del bloque try e igualmente deben ser “delimitados” para cada tipo en particular de excepción.
Para ésto, se utiliza la palabra reservada catch. La sintaxis a utilizar será:
catch( TipoDeExcepcion e ) {
// Aquí va el código que se ejecutara en caso que se produzca
// una excepción del tipo “TipoDeExcepcion”.
}
Un bloque try puede ir seguido por tantos bloques catch como tipos de excepciones se desee
manejar. El tipo de variable TipoDeExcepcion determina el tipo de excepción que manejara un
bloque catch.
Dentro del bloque try, una excepción puede ser producida por:
Una sentencia throw que explícitamente dispare la excepción.
Una llamada a una función que dispare la excepción. Dicha función podría disparar la
excepción explícitamente o llamar a otra función (y ésta a otra y así sucesivamente) la
cual sea quien realmente dispare la excepción.
La creación de un objeto, debido a un error dentro del constructor utilizado.
Un error del sistema operativo durante la ejecución de cualquier sentencia dentro del
bloque try.
Una interrupción capturada por el sistema operativo y notificada al programa en
ejecución a manera de una excepción.
El siguiente código muestra un ejemplo simple de un código en C++ que produce y maneja una
excepción.
#include <stdio.h>
146
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
E X C E P C I O N E S
class Fecha {
int dia, mes, anho;
public:
Fecha(char* pszFecha) {
if(sscanf(pszFecha, "%d-%d-%d", &dia, &mes, &anho) != 3)
throw -1;
if(anho < 0 || dia < 1 || 31 < dia || mes < 1 || 12 < mes)
throw -2;
}
void Imprimir() {
printf("[%d/%d/%d]", dia, mes, anho);
}
};
void main() {
printf("Inicio\n");
try {
char szFecha[20];
puts("Ingrese una fecha :");
scanf("%s", szFecha);
Fecha fecha(szFecha);
puts("La fecha ingresada es :");
fecha.Imprimir();
}
catch(int ex) {
printf("Excepción: código = %d\n", ex);
}
printf("Fin\n");
}
147
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
E X C E P C I O N E S
void main() {
try {
char szFecha[20];
puts("Ingrese una fecha :");
scanf("%s", szFecha);
Fecha fecha(szFecha);
puts("La fecha ingresada es :");
fecha.Imprimir();
}
catch(ErrorEnFormato ex) {
printf("Error: el formato es incorrecto\n");
}
catch(ErrorFechaInvalida ex) {
printf("Error: la fecha es inválida\n");
}
}
El utilizar clases en lugar de datos primitivos permite la posibilidad de guardar más información
acerca de la excepción ocurrida (como en qué archivo fuente ocurrió y dentro de éste, en qué
línea) y pasar esta información al manejador de excepción correspondiente. El siguiente código
modifica el ejemplo anterior para utilizar información adicional dentro de las clases de
excepciones.
#include <stdio.h>
class ErrorEnFormato {
char* pszLugar;
public:
ErrorEnFormato(char* psz) : pszLugar(psz) {}
char* Lugar() { return pszLugar; }
};
class ErrorFechaInvalida {
char* pszLugar;
char* pszFecha;
public:
ErrorFechaInvalida(char* pszL, char* pszF) : pszLugar(pszL), pszFecha(pszF) {}
char* Lugar() { return pszLugar; }
char* LaFecha() { return pszFecha; }
};
148
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
E X C E P C I O N E S
class Fecha {
int dia, mes, anho;
public:
Fecha(char* pszFecha) {
if(sscanf(pszFecha, "%d-%d-%d", &dia, &mes, &anho) != 3)
throw ErrorEnFormato("Fecha::Fecha");
if(anho < 0 || dia < 1 || 31 < dia || mes < 1 || 12 < mes)
throw ErrorFechaInvalida("Fecha::Fecha", pszFecha);
}
void Imprimir() {
printf("[%d/%d/%d]", dia, mes, anho);
}
};
void main() {
try {
char szFecha[20];
puts("Ingrese una fecha :");
scanf("%s", szFecha);
Fecha fecha(szFecha);
puts("La fecha ingresada es :");
fecha.Imprimir();
}
catch(ErrorEnFormato ex) {
printf("Error: el formato es incorrecto en %s\n", ex.Lugar());
}
catch(ErrorFechaInvalida ex) {
printf("Error: el formato de la fecha [%s] es incorrecto en %s\n",
ex.LaFecha(), ex.Lugar());
}
}
Note que en el código anterior, existe información común entre ambos tipos de excepciones.
Este aspecto es explotado utilizando el polimorfismo en la implementación en C++. El
siguiente código modifica el ejemplo anterior para utilizar una clase base para ambas
excepciones.
#include <stdio.h>
class Excepcion {
char* pszLugar;
public:
Excepcion(char* psz) : pszLugar(psz) {}
char* Lugar() { return pszLugar; }
};
class Fecha {
int dia, mes, anho;
public:
Fecha(char* pszFecha) {
if(pszFecha == 0)
throw Excepcion("Fecha::Fecha");
if(sscanf(pszFecha, "%d-%d-%d", &dia, &mes, &anho) != 3)
throw ErrorEnFormato("Fecha::Fecha");
if(anho < 0 || dia < 1 || 31 < dia || mes < 1 || 12 < mes)
throw ErrorFechaInvalida("Fecha::Fecha", pszFecha);
}
void Imprimir() {
printf("[%d/%d/%d]", dia, mes, anho);
149
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
E X C E P C I O N E S
}
};
void main() {
try {
char szFecha[20];
puts("Ingrese una fecha :");
scanf("%s", szFecha);
Fecha fecha(szFecha);
puts("La fecha ingresada es :");
fecha.Imprimir();
}
catch(ErrorEnFormato ex) {
printf("Error: el formato es incorrecto en %s\n", ex.Lugar());
}
catch(ErrorFechaInvalida ex) {
printf("Error: el formato de la fecha [%s] es incorrecto en %s\n",
ex.LaFecha(), ex.Lugar());
}
catch(Excepcion ex) {
printf("Error: en %s\n", ex.Lugar());
}
}
Note, en el código anterior, el uso de una clase base para las excepciones anteriormente
definidas. En el constructor se realiza la verificación de un nuevo tipo de error, que la cadena
pasada sea un puntero nulo, para lo cual se usa la clase base Exception, dado que las otras no
son adecuadas. Para esta clase base se le agrega un bloque catch.
Para mostrar porqué se dice que el comportamiento es polimórfico, modifique este código de
forma que el bloque catch Excepcion esté antes del bloque catch ErrorFechaInvalida. Corra el
programa y notará que las excepciones del tipo ErrorFechaInvalida son ahora capturadas por el
bloque catch Excepcion. Si el bloque catch Excepcion lo colocara antes del bloque catch
ErrorEnFormato, todas las excepciones serían capturadas por el bloque catch Excepcion. La
regla de selección del bloque catch que capturará una excepción en C++ es:
En el código anterior, pudo utilizarse únicamente el bloque catch Excepcion para capturar todas
las excepciones. Sin embargo, como puede entenderse, hay información extra que cada tipo de
excepción podría manejar y ser útil para el adecuado manejo de ésta.
C++ permite definir además un bloque catch que capture todas las excepciones, sin importar su
tipo, con el siguiente formato:
catch(...) {
// Aquí va el manejador de la excepción
}
Este bloque catch debe ser el último de todos, sino se produce un error de compilación.
Aunque en todos los ejemplos anteriores, las excepciones generadas eran capturadas y todas se
manejaban en un mismo bloque try, no siempre es así. El siguiente código muestra los diferentes
casos que pueden ocurrir al generarse una excepción.
#include <iostream.h>
150
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
E X C E P C I O N E S
void NoGeneraExcepcion() {
cout << "Inicio de 'NoGeneraExcepcion'" << endl;
try {
cout << "Dentro del try de 'NoGeneraExcepcion'" << endl;
}
catch(Excepcion ex) {
cout << "Dentro del catch de 'NoGeneraExcepcion'" << endl;
}
cout << "Fin de 'NoGeneraExcepcion'" << endl;
}
void GeneraCapturaExcepcion() {
cout << "Inicio de 'GeneraCapturaExcepcion'" << endl;
try {
cout << "Dentro del try de 'GeneraCapturaExcepcion'" << endl;
throw Excepcion();
cout << "Este saludo nunca se muestra" << endl;
}
catch(Excepcion ex) {
cout << "Dentro del catch de 'GeneraCapturaExcepcion'" << endl;
}
cout << "Fin de 'GeneraCapturaExcepcion'" << endl;
}
void GeneraNoCapturaExcepcion() {
cout << "Inicio de 'GeneraNoCapturaExcepcion'" << endl;
try {
cout << "Dentro del try de 'GeneraNoCapturaExcepcion'" << endl;
throw Excepcion();
cout << "Este saludo nunca se muestra" << endl;
}
catch(int ex) {
cout << "Dentro del catch de 'GeneraNoCapturaExcepcion'" << endl;
}
cout << "Fin de 'GeneraNoCapturaExcepcion'" << endl;
}
void GeneraCapturaRedisparaExcepcion() {
cout << "Inicio de 'GeneraCapturaRedisparaExcepcion'" << endl;
try {
cout << "Dentro del try de 'GeneraCapturaRedisparaExcepcion'" << endl;
throw Excepcion();
cout << "Este saludo nunca se muestra" << endl;
}
catch(Excepcion ex) {
cout << "Dentro del catch de 'GeneraCapturaRedisparaExcepcion'" << endl;
throw ex;
}
cout << "Fin de 'GeneraCapturaRedisparaExcepcion'" << endl;
}
void main() {
cout << "Llamando a 'NoGeneraExcepcion'" << endl;
NoGeneraExcepcion();
try {
cout << "Llamando a 'GeneraNoCapturaExcepcion'" << endl;
GeneraNoCapturaExcepcion();
}
catch(Excepcion ex) {
cout << "Excepcion capturada desde 'GeneraNoCapturaExcepcion' en"
<<" 'main'" << endl;
}
try {
cout << "Llamando a 'GeneraCapturaRedisparaExcepcion'" << endl;
GeneraCapturaRedisparaExcepcion();
}
151
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
E X C E P C I O N E S
catch(Excepcion ex) {
cout << "Excepcion capturada desde 'GeneraCapturaRedisparaExcepcion'"
<<" en 'main'" << endl;
}
throw ex;
cout << "Fin del programa";
}
Al ejecutarse se mostrará una salida en una ventana de comandos, como la mostrada en la Figura
7 - 1. Inmediatamente después se muestra una ventana con el mensaje de la Figura 7 - 2.
Como puede entenderse por la ejecución del programa, las excepciones que suceden en la
llamada a una función son disparadas fuera de éste cuando la función no la captura. Luego,
desde donde se llamó a la función que generó la excepción (la llamada a
GeneraNoCapturaExcepcion desde main) se considera que dicha función generó una
excepción, por lo que es factible capturarla. En el caso de la última excepción, al final de main,
ésta no es capturada por nadie por lo que sale fuera del método main, siendo capturada por el
propio sistema operativo que se encarga de mostrar la ventana mostrada en la figura 7.2 o una
similar dependiendo la forma en que se compiló el programa y las capacidades de manejo de
excepciones del sistema operativo.
152
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
E X C E P C I O N E S
void main() {
try {
char* pCadena = new char[10];
throw Excepcion();
delete [] pCadena;
}
catch(Excepcion ex) {
}
}
En el código anterior, debido a que ocurre una excepción antes de poder liberar el recurso
reservado, esta liberación nunca podrá realizarse. Tampoco es posible liberar el recurso en el
bloque catch debido a que la variable pObj es local al bloque try, por lo que para hacer esto
posible debería definirse dicha variable como local a main. De esta forma, la liberación de
memoria se debería hacer al final de todos los bloques catch del bloque try. Sin embargo, si la
excepción producida no es capturada por ninguno de los bloques catch, entonces no habrá
manera de liberar el recurso, a menos que la variable pObj se declare como global de forma que
pueda ser liberado por algún otro manejador de excepción.
Como puede verse, C++ no ofrece una solución estándar a este tipo de pérdida de recursos.
Más adelante veremos cómo es que Java y C# tratan este aspecto.
Por último, es importante señalar que el modelo de manejo de excepciones en C++ sólo
permite manejar como excepciones, errores síncronos. Un ejemplo de esto y de cómo afecta en
la decisión de si un error es buen candidato para ser tratado como excepción, se verá en el
siguiente capítulo de Programación Concurrente.
Java
Al igual que C++, Java comparte el mismo modelo de manejo de excepciones que C++, pero a
diferencia de éste, Java cuenta con un soporte más completo. A continuación se detallan las
principales diferencias:
TODOS LOS TIPOS DE EXCEPCIONES HEREDAN DE UNA CLASE BASE
153
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
E X C E P C I O N E S
La clase Throwable es la clase base de todos los errores y excepciones de Java manejados por el
mecanismo de manejo de excepciones. El intérprete de Java y el programador sólo pueden
disparar excepciones de un tipo que derive de Throwable.
La clase Error es la clase base de excepciones generadas por el intérprete de Java, por lo que no
deben ser manejadas por el programador, dado que son errores para los que sólo el intérprete
puede dar una solución satisfactoria.
La clase Exception es la clase base de todas excepciones con las que el programador sí debe
lidiar, a excepción de las que derivan de RuntimeException, las que tienen un tratamiento
especial.
Si un programa deseara crear sus propias excepciones, heredaría de la clase Exception.
LAS EXCEPCIONES DEBEN SER TRATADAS POR EL PROGRAMA
El programador debe capturar explícitamente las excepciones de dichos tipos, o bien indicar
explícitamente que no desea manejarlas. Las excepciones que derivan de RuntimeException
corresponden a errores de lógica en la programación, por lo que el lenguaje acepta que no sean
explícitamente tratadas, dado que sólo son usadas para depurar los programas y eliminar dichos
errores. Una vez eliminados estos errores, esas excepciones ya no se presentarán, y el código
escrito para manejarlas dejaría de ser útil. A continuación un ejemplo ilustra este aspecto:
public class Ejemplo1 {
public static void main(String args[]) {
// sentencias fuera del bloque de control de excepciones
int Arreglo[] = { 1, 2, 3, 4 };
Por tanto, una vez hecho ésto, dicha excepción nunca volverá a ocurrir, por lo que todo el
código escrito para el manejo de ésta pasa a ser inútil. Todo el resto de excepciones que heredan
de Exception y no de RuntimeException deben ser manejadas explícitamente por el
programador. El siguiente programa muestra este caso:
import java.io.InputStreamReader;
154
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
E X C E P C I O N E S
import java.io.BufferedReader;
import java.io.IOException;
class Ejemplo3 {
El código anterior utiliza el método readLine de la clase BufferedReader para leer el texto
ingresado por teclado desde la ventana de comandos del programa. Dicho método puede
producir una excepción del tipo IOExcepcion, la cual no deriva de RuntimeException, por lo
que tenemos la obligación de capturarla. Otra opción es indicar explícitamente que no deseamos
manejar dicha excepción mediante la clausula throws. Esta cláusula se coloca luego de la
declaración de parámetros de un método. El siguiente código modifica el anterior para utilizar
una cláusula throws.
import java.io.InputStreamReader;
import java.io.BufferedReader;
import java.io.IOException;
class Ejemplo3 {
La cláusula thows puede ir precedida de una lista de tipos de excepciones, separadas por comas.
Esta cláusula le dice al compilador de Java “sé que el código en éste método produce estas
excepciones, pero no deseo manejarlas aquí, sino en el método que llame a éste”.
Si el código en nuestro programa produce excepciones que deben ser manejadas explícitamente,
ya sea mediante una secuencia throw-catch o con una cláusula throws, y no lo son, se generarán
errores durante la compilación indicando que esto debe hacerse.
SE DEFINE UN BLOQUE ESPECIAL QUE SIEMPRE SE EJECUTA
155
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
E X C E P C I O N E S
class Ejemplo3 {
static public void NoDisparaExcepciones() {
System.out.println("Inicio de 'NoDisparaExcepciones'");
try {
System.out.println("Dentro del bloque try de 'NoDisparaExcepciones'");
}
finally {
System.out.println("Dentro del bloque finally de 'NoDisparaExcepciones'");
}
System.out.println("Fin de 'NoDisparaExcepciones'");
}
156
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
E X C E P C I O N E S
System.out.println("\nLlamando 'DisparaNoCapturaExcepcion'");
try {
DisparaNoCapturaExcepcion();
}
catch(Exception ex) {
System.out.println("Dentro del bloque catch 1 de 'main', excepcion="
+
ex.getMessage());
}
finally {
System.out.println("Dentro del bloque finally 1 de 'main'");
}
System.out.println("\nLlamando 'DisparaCapturaRedisparaExcepcion'");
try {
DisparaCapturaRedisparaExcepcion();
}
catch(Exception ex) {
System.out.println("Dentro del bloque catch 2 de 'main', excepcion="
+ex.getMessage());
}
finally {
System.out.println("Dentro del bloque finally 2 de 'main'");
}
}
}
157
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
E X C E P C I O N E S
C#
C# comparte el mismo modelo de manejo de excepciones que C++, pero a diferencia de éste,
C# cuenta con un soporte más completo, muy similar al de Java.
El árbol de herencia de las clases para excepciones es:
System.Object
└──► System.Exception
├─────► System.ApplicationException
└─────► System.SystemException
Al igual que Java, la clase Exception es la clase base de todos los tipos de excepciones. La clase
SystemException es la clase base que utiliza el intérprete de ..NET para todas las excepciones
que puede generar. La clase ApplicationException es la clase base que debe utilizar el
programador para definir sus propias excepciones.
A diferencia de Java, C# no diferencia entre excepciones que deben tratarse y las que sólo sirven
para depuración, es decir, si un determinado código puede generar una excepción, no es
obligatorio colocarlo dentro de un bloque try seguido de un catch que lo capture. Por el mismo
motivo, tampoco existe una cláusula throws con la que se indique que una excepción no se
desea tratar en el método donde ocurre.
Fuera de las diferencias antes indicadas, la implementación en Java y C# es igual. Un código de
ejemplo de excepciones en C# es:.
using System;
158
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
E X C E P C I O N E S
}
Console.WriteLine("Fin");
}
}
159
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
E X C E P C I O N E S
excepciones son utilizadas para tipos de errores que pueden producirse en diferentes lugares a lo
largo de un programa y que se desea sean procesados de manera centralizada. Sin embargo, es
importante tener en cuenta que cuanto más lejos esté el manejador de excepción del punto de
excepción, más difícil podría ser manejar adecuadamente dicha excepción, dado que la misma
podría ser generada por más de un punto de excepción y podría requerirse código especial para
cada caso.
Por último, dado que las técnicas tradicionales de tratamiento de errores se realizan con las
mismas estructuras utilizadas para el control del flujo de un programa, es frecuente la tendencia a
querer utilizar las técnicas de manejo de excepciones para tareas diferentes al tratamiento de
excepciones. Ésto debe evitarse, dado que reduciría la eficiencia del programa y la claridad de su
código.
El tema de la sincronización se verá en el siguiente capítulo, Programación Concurrente.
160
1
Capítulo
10
Programación con GUI
El presente capítulo se centra en el trabajo con Interfaces Gráficas de Usuario (IGU por sus
siglas en español, GUI por sus siglas en inglés) utilizando ventanas y otros elementos para el
diseño de las mismas, dentro del sistema operativo Windows. Para el caso particular de Java,
dada su característica multiplataforma, los conceptos dados aquí son aplicables a cualquier
plataforma que soporte Java.
99
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
P R O G R A M A C I Ó N C O N G U I
sistema operativo (resolución de la pantalla, idioma del teclado, fecha, cambio de usuario, etc.),
los relacionados a la escasez de recursos, etc.
Este sistema de mensajería, con distintas variantes, es utilizado por los sistemas operativos con
soporte GUI. En particular veremos el caso del sistema operativo Windows y cómo su sistema
de mensajería es manejado desde diferentes lenguajes de programación.
En la actualidad, existen dos tipos de programas GUI con ventanas comunmente utilizados: Los
programas Stand-Alone y los programas dentro de navegadores de Internet (browsers).
Programas Stand-Alone
Los programas Stand-Alone crean una o más ventanas con las que el usuario interactúa.
Comunmente estos programas poseen una ventana principal y una o más ventanas
especializadas en alguna labor específica. Estos programas pueden ejecutar directamente (en
caso de ser programas ejecutables) o mediante un programa intérprete.
Ejemplos de estos programas son los creados con C o C++ utilizando directamente el API de
Windows, así como las Aplicaciones de Java y las Aplicaciones de Formularios de Ventanas de
..NET.
100
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
P R O G R A M A C I Ó N C O N G U I
101
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
P R O G R A M A C I Ó N C O N G U I
return 0;
// Muestro la ventana.
ShowWindow( hWnd, iShowCmd ); // Establece el estado de 'mostrado' de la ventana.
UpdateWindow( hWnd ); // Genera un mensaje de pintado, el que es
// ejecutado por la función de ventana respectiva.
En este programa existen dos funciones importantes: El punto de entrada del programa, la
función “WinMain”, y la función de ventana “FuncionVentana”.
La función WinMain es el equivalente, para un programa en Windows, a la función “main” en
programas en C y C++ en modo consola. El principal parámetro de esta función es el primero,
el manejador de la instancia del programa. Dicho manejador es, en esencia, un número entero
utilizado por Windows para ubicar los recursos relacionados al programa. Algunos de los
recursos que un programa en Windows puede tener son: Registros de ventanas, textos,
imágenes, audio, video, meta-archivos, otros manejadores (a archivos, puertos, impresoras, etc.),
etc. El manejador de la instancia es utilizado como parámetro de las funciones del API de
Windows donde se involucren, directa o indirectamente, los recursos de una aplicación. El
segundo parámetro no es utilizado en programas de 32-bits (Windows 95 y Windows NT en
adelante).
La función de ventana (que puede tener cualquier nombre pero con el mismo formato de
declaración que el mostrado) es dónde se realiza toda la lógica relacionada a las acciones que una
ventana toma en respuesta a los mensajes recibidos, como por ejemplo, los producidos por la
interacción del usuario con la ventana. La dirección de esta función de ventana es el principal
dato que forma parte del registro, con la estructura WNDCLASS, de una clase de ventana.
Todos los mensajes enviados a ventanas, creados con una misma clase, son procesados por la
misma función de ventana, la indicada en la estructura WNDCLASS al registrarse dicha clase de
ventana.
Todo programa con ventanas en Windows tiene 3 etapas principales:
13. Registro de las clases de ventana que se utilizarán en el programa para crear
ventanas.
14. Creación de la ventana principal del programa.
15. Ejecución del bucle de mensajes del programa.
Para crear una ventana se utiliza la función CreateWindow. Los parámetros de esta función
indican, en este orden:
22. El nombre de la clase de ventana en base a la que se crea la nueva ventana.
23. El título a mostrarse en la barra superior de la ventana.
102
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
P R O G R A M A C I Ó N C O N G U I
103
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
P R O G R A M A C I Ó N C O N G U I
que utiliza, crear dichas ventanas y tener un bucle de procesamiento de mensajes. Para ilustrar
mejor ésto revisemos ejemplos equivalentes en Java y C#.
El siguiente programa es el equivalente en Java al programa anterior.
import javax.swing.*;
import java.awt.event.*;
class MiVentana extends JFrame {
public MiVentana() {
setSize(400, 400);
setTitle("Título de la Ventana");
setVisible(true);
addWindowListener(new WindowAdapter() {
public void windowClosing(WindowEvent e) {
dispose();
System.exit(0);
}
});
}
}
class Aplicacion_Minima {
public static void main(String[] args) {
MiVentana ventana = new MiVentana();
}
}
En este código, el punto de entrada es el método estático “main”, dentro del cual lo único que se
realiza es la creación de un objeto de la clase MiVentana que hereda de la clase JFrame (cuyo
nombre completo es javax.swing.JFrame), que es la clase base para la creación de ventanas
utilizando el paquete (el término utilizado en Java para referirse a una librería) SWING, cuyas
clases son accesibles mediante la sentencia “import javax.swing.*”. Dentro del constructor de la
clase “MiVentana” se configuran las características de la ventana y se establece “qué objeto” será
al que se le llame su método “windowClosing” cuando el usuario del programa indique que
desea cerrar la ventana.
Ahora bien, relacionemos todo esto con lo visto anteriormente en el programa en C o C++ con
API de Windows.
Como es de esperarse, la clase JFrame debe crear en su constructor (o en algún constructor de
sus clases base) una ventana llamando a “CreateWindow” (u otra función equivalente). Como ya
hemos visto, no se debe poder crear una ventana antes de registrar la clase de ventana en base a
la que se creará, por lo que es de esperarse (y realmente sucede así) que el programa intérprete de
Java realice este registro antes de llamar a nuestro método “main”. Siguiendo el orden de
ejecución de los constructores, luego de completada la ejecución del constructor de JFrame se
llamará al de nuestra clase “MiVentana”, donde modificamos los valores por defecto con los que
se creó inicialmente la ventana (por defecto, JFrame crea una ventana en la coordenada [X,Y] =
[0,0], con cero píxeles de ancho y cero de alto y con su atributo de visibilidad puesto en “no-
visible”). El constructor termina indicando, mediante el método de JFrame
“addWindowListener”, a qué método de qué clase se llamará cuando la función de ventana, de la
ventana creada por JFrame, reciba el mensaje WM_CLOSE. El procesamiento por defecto,
realizado por “DefWindowProc”, para este mensaje es llamar a la función de API de Windows
“DestroyWindow”, que es la que realmente destruye la ventana generando, de paso, el mensaje
WM_DESTROY. Como puede deducirse, el método “dispose” de JFrame debería estar
llamando a DestroyWindow y el método “System.exit” a PostQuitMessage.
Finalmente queda algo muy importante que no es directamente visible en nuestro código:
¿Dónde se ejecuta el bucle de procesamiento de mensajes? Para explicar ésto, debe aclararse que
el programa intérprete de Java realiza algunas acciones luego de llamar a nuestro método
“main”, entre ellas está la de verificar si después de ejecutarse este método se creó o no alguna
104
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
P R O G R A M A C I Ó N C O N G U I
ventana. Si se creó entonces se entra a un bucle de procesamiento de mensajes del que, como
puede esperarse, el programa no sale hasta haberse llamado al método “System.exit”. Si no se
creó ninguna ventana, el programa intérprete finaliza. Es por esto que la literatura sobre
programación con ventanas en Java indica, sin dar mayor detalle, que si se creara alguna ventana
en un programa Java Stand-Alone (a éstos se les llama “Aplicaciones Java”), debe de llamarse en
algún momento al método “System.exit”.
Ahora bien, el siguiente programa es el equivalente en C# del programa anterior.
using System;
using System.Windows.Forms;
105
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
P R O G R A M A C I Ó N C O N G U I
Menú
Barras de
Herramientas
Borde
Barra de
Estado
Área Cliente
Figura 6 - 6 Elementos de una ventana
En Windows, como se aprecia en la Figura 6 - 1, las ventanas tienen los siguientes elementos:
Un borde.
Una barra de título.
Un ícono de sistema.
Un conjunto de botones de sistema.
Un menú.
Una o más barras de herramientas.
Una barra de estado.
Un área de dibujo o “área cliente”.
De estos elementos, sólo el último es obligatorio, y si bien los demás son comunes, algunas
ventanas pueden no tenerlos.
106
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
P R O G R A M A C I Ó N C O N G U I
Eventos
Windows
Mensajes
Msg K Msg K
App. N App. O
Aplicación M
Hilo X
Cola de Mensajes
Función de Ventana
Msg K
Bucle
F.V F.V
Hilo Y Hilo Z
107
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
P R O G R A M A C I Ó N C O N G U I
La Figura 6 - 2 muestra el ciclo que siguen los eventos procesados en Windows para las
aplicaciones que utilizan ventanas.
La secuencia de pasos seguida es:
Windows detecta un evento.
Crea el mensaje respectivo y lo envía a la aplicación involucrada.
El bucle de procesamiento de mensajes detecta dicho mensaje y solicita a Windows que
lo envíe a la ventana adecuada.
Windows determina la ventana destinataria y averigua a qué clase pertenece.
En base a la clase determina la función de ventana que le corresponde y envía el mensaje
a dicho procedimiento.
La función de ventana actúa según el mensaje recibido.
Para el procesamiento de los mensajes, toda ventana tiene una “función de procesamiento de
mensajes” relacionada, a la que se le llama “función de ventana”. Dicha “relación” entre una
ventana y su función de ventana se origina al crearse la ventana utilizando una plantilla de
creación llamada “clase de ventana”, la cual debe haberse registrado previamente. Uno de los
datos de dicha plantilla es la dirección de la función de ventana que deberá llamarse para
procesar los mensajes de toda ventana que se cree utilizando dicha plantilla.
La función de ventana es pues, el área central de trabajo de todo programa desarrollado
utilizando el API de Windows. El resto del código suele ser casi siempre el mismo. La función
de ventana es la que determina cómo se comportará nuestro programa, nuestra ventana para el
usuario, ante cada evento.
Existe un conjunto de mensajes estándar reconocidos por el sistema operativo y que nuestras
funciones de ventana pueden manejar. Para cada uno de estos mensajes existe una constante
relacionada. En el programa básico mostrado en la sección 3.1, se utilizó una de estas constantes:
WM_DESTROY. Al igual que esta constante, definida dentro del archivo de cabecera
Windows.h, existe una constante WM_XXX para cada mensaje reconocido por el sistema
operativo. Es posible que un programador defina sus propios mensajes simplemente escogiendo
un valor fuera del rango que Windows reserva para los desarrolladores de su sistema operativo.
El manejo de mensajes definidos por el usuario cae fuera del alcance de esta introducción.
Cuando la función de ventana es llamada para procesar un mensaje, recibe los siguientes datos:
El handle de la ventana a la que corresponde el mensaje, dado que, como hemos visto,
una función de ventana puede utilizarse para procesar los mensajes de más de una
ventana, cuando todas éstas fueron creadas utilizando la misma clase de ventana.
La constante que identifica al mensaje.
Dos parámetros que contienen información sobre dicho mensaje, o bien contienen
direcciones de estructuras reservadas en memoria con dicha información.
El hacer que una ventana reconozca y reaccione a un nuevo mensaje suele consistir en agregar el
“case” (al “switch” principal de la función de ventana) para la constante de dicho mensaje con el
código que realice el comportamiento deseado. El siguiente código de una función de ventana
muestra el manejo de un mensaje correspondiente al ratón y al teclado. El resto del programa no
varía respecto al ejemplo anterior.
LRESULT CALLBACK FuncionVentana(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
108
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
P R O G R A M A C I Ó N C O N G U I
{
int PosX, PosY;
char Mensaje[100];
int CtrlPres, ShiftPres;
int CodigoTecla, EsTeclaExtendida;
switch( uMsg )
{
case WM_LBUTTONUP:
PosX = LOWORD(lParam); // el word menos significativo
PosY = HIWORD(lParam); // el word más significativo
CtrlPres = wParam & MK_CONTROL;
ShiftPres = wParam & MK_SHIFT;
sprintf(Mensaje, "X=%d, Y=%d, CtrlPres=%d, ShiftPres=%d",
PosX, PosY, CtrlPres, ShiftPres);
MessageBox(hWnd, Mensaje, "Posición del mouse", MB_OK);
break;
case WM_KEYUP:
CodigoTecla = (int)wParam;
EsTeclaExtendida = ( lParam & ( 1 << 24 ) ) != 0;
if( EsTeclaExtendida == 1 )
sprintf(Mensaje, "CodigoTecla=%d (extendida)", CodigoTecla);
else
if( ( '0' <= CodigoTecla && CodigoTecla <= '9' ) ||
( 'A' <= CodigoTecla && CodigoTecla <= 'Z' ) )
sprintf(Mensaje, "CodigoTecla=%d (%c)", CodigoTecla,
(char)CodigoTecla);
else
sprintf(Mensaje, "CodigoTecla=%d", CodigoTecla);
MessageBox(hWnd, Mensaje, "Tecla presionada", MB_OK);
break;
case WM_DESTROY:
PostQuitMessage( 0 );
break;
default:
return DefWindowProc( hWnd, uMsg, wParam, lParam );
}
return 0;
}
La constante WM_KEYUP corresponde al mensaje producido por el evento de soltar (UP) una
tecla del teclado (KEY). Para este mensaje, el parámetro wParam contiene el código de la tecla
109
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
P R O G R A M A C I Ó N C O N G U I
presionada. Sólo los códigos correspondientes a los números (0x30 al 0x39, ‘0’ al ‘9’) y las letras
mayúsculas (0x41 a 0x5A, ‘A’ al ‘Z’) coinciden con la tabla ASCII. Toda tecla del teclado tiene
un código distinto, existiendo constantes en Windows.h para cada una de ellas. Como ejemplo,
podríamos reconocer si la tecla presionada fue F1 utilizando:
if ( wParam == VK_F1 ) { ... }
El parámetro lParam se comporta como un bit-flag con información como “es una tecla
correspondiente al conjunto de teclas extendidas”. Hay 15 mensajes relacionados con el teclado,
de los cuales los más comúnmente procesados son:
WM_KEYDOWN
WM_KEYUP
WM_CHAR
WM_DEADCHAR
class Observador {
public void notificar(String infoDelEvento) {
System.out.println("Sucedió el siguiente evento: " + infoDelEvento);
}
}
class Sujeto {
private Vector listaObservadores = new Vector();
public void registrarObservador(Observador ob) {
listaObservadores.add(ob);
}
public void simularEvento(String infoDelEvento) {
for(int i = 0; i < listaObservadores.size(); i++) {
Observador ob = (Observador)listaObservadores.get(i);
ob.notificar(infoDelEvento);
}
}
}
class PatronObservador {
public static void main(String args[]) {
Observador ob = new Observador();
Sujeto suj = new Sujeto();
suj.registrarObservador(ob);
suj.simularEvento("Evento1");
suj.simularEvento("Evento2");
}
}
110
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
P R O G R A M A C I Ó N C O N G U I
El programa crea un objeto observador y un sujeto observable, para luego simular que dos
eventos ocurren. Note que el proceso de registro consiste simplemente en agregar la referencia
al objeto observador a una lista del sujeto. Note además que al ocurrir el evento simulado, lo que
hace el sujeto es recorrer la lista de referencias a objetos que se registraron como observadores,
llamando a un método para cada uno de estos. Ésta es la forma en que el sujeto notifica al
observador, llamando a un método de este último. Finalmente note que el método “notificar” de
la clase observador es el acuerdo entre ambas partes, sujeto y observador, para que la
notificación sea posible, es decir, todo observador de un objeto de mi clase Sujeto debe ser un
objeto de la clase Observador o bien heredar de él, de forma que se garantice que dicho método
existe.
El ejemplo anterior tiene un problema: Sólo podemos hacer que los objetos de una ClaseX
observen los eventos de mi clase Sujeto, si mi ClaseX hereda de Observador. Esto es indeseable
si pensamos que lenguajes como Java y C# no soportan herencia múltiple y, muy
probablemente, deseáramos que un objeto, cuya clase padre no puedo modificar, escuche los
eventos de otro. La solución a éste es aislar los métodos que forman parte del acuerdo en una
interfaz. El siguiente programa modifica el anterior de forma que se utilice una interfaz en lugar
de una clase:
import java.util.Vector;
interface IObservador {
void notificar(String infoDelEvento);
}
class Sujeto {
private Vector listaObservadores = new Vector();
public void registrarObservador(IObservador ob) {
listaObservadores.add(ob);
}
public void simularEvento(String infoDelEvento) {
for(int i = 0; i < listaObservadores.size(); i++) {
IObservador ob = (IObservador)listaObservadores.get(i);
ob.notificar(infoDelEvento);
}
}
}
class PatronObservador2 {
public static void main(String args[]) {
ObservadorTipo1 ob1 = new ObservadorTipo1();
ObservadorTipo2 ob2 = new ObservadorTipo2();
Sujeto suj = new Sujeto();
suj.registrarObservador(ob1);
suj.registrarObservador(ob2);
suj.simularEvento("Evento1");
suj.simularEvento("Evento2");
}
}
En el ejemplo anterior se tienen dos objetos, cada uno de una clase distinta pero que
implementan la interfaz IObservador, que se registran para escuchar los eventos de un tercer
objeto, uno de la clase Sujeto. Note que la implementación de la clase Sujeto se basa en la
111
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
P R O G R A M A C I Ó N C O N G U I
interfaz IObservador y no en una clase. De esta forma se permite que los objetos de cualquier
clase que implementen la interfaz IObservador sean utilizados como observadores de un objeto
de la clase Sujeto. Ésta última es la estrategia que utiliza Java para sus eventos en ventanas.
En Java, el sujeto es un objeto ventana, una instancia de cualquier clase que herede de JFrame.
Esta clase se comunica con una función de ventana internamente, la que procesa un
subconjunto de todos los mensajes que se pueden generar con una ventana, llamando a los
métodos adecuados de los objetos observadores registrados para dicho objeto ventana. Para
esto, JFrame contiene listas de observadores, como datos miembros, para los distintos grupos de
mensajes: Una lista para los mensajes de manipulación de la ventana, otra para los mensajes del
ratón, otra para los mensajes del teclado, etc. De esta manera, cuando ocurre un evento, el
mensaje es tratado por la función de ventana de JFrame, la que a su vez llama al método
adecuado de cada objeto observador registrado para dicho mensaje.
En Java, las interfaces como IObservador en nuestro ejemplo, se llaman Listeners, y existe una
definida para cada grupo de mensajes. Toda clase cuyos objetos se desea que puedan escuchar
un evento de una ventana, debe de implementar la interfaz Listener adecuada. Veamos cómo
ésto se refleja, en el caso de los mensajes del ratón y del teclado, en el siguiente código.
import javax.swing.*;
import java.awt.event.*;
112
112
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
P R O G R A M A C I Ó N C O N G U I
char c = e.getKeyChar();
int keyCode = e.getKeyCode();
int modifiers = e.getModifiers();
if (Character.isISOControl(c)) {
charString = "key character = "
+ "(an unprintable control character)";
} else {
charString = "key character = '"
+ c + "'";
}
System.out.println(s + "\n"
+ " " + charString + "\n"
+ " " + keyCodeString + "\n"
+ " " + modString);
}
}
class EventosDeVentanas {
public static void main(String args[]) {
MiVentana ventana = new MiVentana();
}
}
En el código anterior el sujeto observable es el objeto de clase MiVentana creado dentro del
método main, y los observadores son dos: Un objeto de la clase MiObservadorVentana,
implementando la interfaz WindowListener, y un objeto de la clase MiObservadorTeclado,
implementando la interfaz KeyListener. Ambos observadores serán notificados de los eventos
de la ventana y del teclado, respectivamente, que es capaz de detectar y/o producir el sujeto.
Note además que el sujeto, conceptualmente y en la práctica, no requiere saber sobre la
implementación interna de los objetos observadores, lo único que le concierne es que dichos
objetos poseen los métodos adecuados para, mediante éstos, poderles notificar que ocurrió un
evento del tipo para el que se registraron. Es por ello que Java utiliza interfaces para definir
dicho contrato, los métodos que el objeto observador debe implementar y el objeto observable
debe llamar. Note también que los métodos de registro siguen un formato común y forman
parte de la interfaz que ofrece el sujeto, en este caso, los métodos addWindowListener y
addKeyListener. Como es de esperarse, estos métodos reciben como parámetros referencias a
objetos que implementen las interfaces respectivas.
La implementación de la interfaz de un evento particular requiere ser completa, si no lo fuera, la
clase sería abstracta y no podríamos pasarle un objeto instanciado de dicha clase al método de
registro respectivo. Sin embargo, es posible que no se requiera utilizar todos los métodos de la
interfaz para un programa en particular, como es el caso, en el código anterior, de la clase
“MiObservadorVentana”. Debido a esto, muchas interfaces relacionadas a eventos en Java
tienen una clase que las implementa, a la que se le llama “Adaptador”. La siguiente clase es el
113
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
P R O G R A M A C I Ó N C O N G U I
Los adaptadores le dan una implementación vacía a las interfaces que implementan y son
declarados como abstractos únicamente porque se espera que sirvan como clases base para otras
clases que sobrescriban los métodos de las interfaces que requieran. Un ejemplo del uso de un
adaptador puede verse en el primer código de ejemplo de Java, en la sección 3.1. Creación de
una Ventana. En dicho código se utiliza el adaptador WindowAdapter como base de una clase
anidada anónima.
Es importante señalar que no existe ningún impedimento para que el sujeto sea a su vez
observador de otros sujetos o de sí mismo (como en el ejemplo de la sección 3.1), que un mismo
observador pueda observar varios sujetos a la vez (del mismo tipo o de diferente tipo, de uno o
más sujetos) y que un mismo evento sea observado por muchos observadores. Para este último
caso podríamos, en el ejemplo anterior, haber registrado otros objetos para los mismos eventos
(llamando más de una vez a addWindowListener y addKeyListener respectivamente) de manera
que cuando dichos eventos ocurran, el sujeto, uno de la clase “MiVentana”, llamará en secuencia
a los métodos correspondientes de todas los objetos registrados para dicho evento, en el orden
en que se registraron.
Así como un objeto se registra para escuchar un evento, también se puede desregistrar. Para ésto
existen los correspondientes métodos removeXXXListener, como son removeWindowListener
y removeKeyListener.
Note que todos los métodos de una interfaz Listener reciben como parámetro una referencia de
una clase XXXEvent, la que encapsula los datos del mensaje y provee de una interfaz para su
fácil uso. En el caso de la interfaz WindowListener es WindowEvent, en el caso de la interfaz
KeyListener es KeyEvent.
La Tabla 6 - 1 sumariza las clases involucradas en tres tipos de eventos comúnmente manejados
en Java:
Tabla 6 - 2 Clases relacionadas con eventos en Java
114
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
P R O G R A M A C I Ó N C O N G U I
interfaz
class OtraClase {
public static int Metodo3(string sMensaje) {
Console.WriteLine(" OtraClase.Metodo3 : " + sMensaje);
return 3;
}
public int Metodo4(string sMensaje) {
Console.WriteLine(" OtraClase.Metodo4 : " + sMensaje);
return 4;
}
}
class Principal {
115
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
P R O G R A M A C I Ó N C O N G U I
/////////////////////////////
// Prueba con eventos
116
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
P R O G R A M A C I Ó N C O N G U I
llama MulticastDelegate. El uso de este tipo de delegados cae fuera del alcance del presente
curso.
Sin embargo, como puede verse en el código anterior, una forma simple de llamar a un grupo de
métodos es utilizando la palabra clave event. El formato de declaración de una variable event es:
[Modificadores] event <Nombre del Delegado> <Nombre>;
Esta declaración sólo puede ir en el ámbito de una clase, no dentro de un método. Esto se debe
a que, a pesar de parecerse a la declaración de una variable, al agregarle la palabra event a la
declaración, la sentencia se expande al ser compilado el código, generándose la declaración de
una variable delegado, del tipo puesto en la declaración, y dos métodos, add_XXX y
remove_XXX (donde XXX es el nombre del tipo del delegado). Para el código anterior, esta
expansión sería de la forma:
private static MiDelegado evento = null;
private static MiDelegado add_MiDelegado(...) {}
private static MiDelegado remove_MiDelegado(...) {}
Estos métodos add y remove son llamados automáticamente cuando se utilizan los operadores
‘+’ y ‘-‘, respectivamente, con la variable evento. Debido a esto, la variable declarada con la
palabra event no requiere ser inicializada.
En .NET se utilizan los conceptos de delegado y evento para manejar la respuesta a la
interacción del usuario con las ventanas del programa. Para cada tipo de evento que el usuario
pueda generar, existen en las clases de .NET propiedades que encapsulan variables event, así
como los tipos de delegados correspondientes. El siguiente código muestra un ejemplo del uso
de estos eventos y delegados:
using System;
// Al siguiente espacio de nombres pertenecen:
// Form, KeyPressEventHandler, KeyPressEventArgs,
// MouseEventHandler, MouseEventArgs, PaintEventHandler, PaintEventArgs.
using System.Windows.Forms;
class Eventos_Ventana {
public static void Main(string[] args) {
Ventana refVentana = new Ventana();
Application.Run(refVentana);
}
}
En el código anterior, se hace uso de las propiedades MouseUp, MouseLeave, KeyPress y Paint,
las que nos permiten acceder a datos miembros internos declarados como event para los tipos
117
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
P R O G R A M A C I Ó N C O N G U I
Tipos de Eventos
Los eventos con los que interactúa un programa son comunmente los producidos como
consecuencia de un mensaje del sistema operativo, en particular los relacionados con el ratón, el
teclado y con el manejo de la ventana.
Un programa también puede definir sus propios eventos o simular los ya existentes como una
manera de independizar el programa de las capacidades del sistema operativo subyacente (como
es el caso de Java para muchas de sus clases, del paquete Swing, con representación visual).
Un programa también puede disparar eventos al detectar que sucesos no visuales se producen
en su entorno, como por ejemplo: El arribo de un paquete de datos por red, la recepción de un
mensaje enviado desde otro programa, la baja del nivel de algún recurso por debajo del límite
crítico (como la memoria), etc.
Gráficos en 2D
El manejo de los dispositivos gráficos en Windows se realiza mediante la librería GDI32
(Graphics Device Interface). Esta librería aísla las aplicaciones de las diferencias que existen
entre los dispositivos gráficos con los que puede interactuar un computador.
El siguiente diagrama muestra el flujo de comunicación entre las aplicaciones en ejecución, la
librería gráfica de Windows, las librerías por cada dispositivo y los dispositivos mismos.
App1 App2
GDI
118
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
P R O G R A M A C I Ó N C O N G U I
Como se puede apreciar, las aplicaciones interactúan únicamente con la librería GDI. Una vez
que una aplicación le indica a GDI con qué tipo de dispositivo desea interactuar, dicha
interacción se realiza en forma independiente al dispositivo elegido. Las librerías por cada
dispositivo se conocen como Drivers, y deben cumplir la especificación requerida por GDI para
que puedan interactuar con él. De esta forma, cada fabricante de un nuevo dispositivo, que
desee que éste sea utilizado desde Windows deberá proveer un Driver que sepa cómo manejar
su dispositivo y que cumpla las especificaciones de GDI.
Un punto importante es ¿qué sucede cuando una aplicación le indica a GDI con qué tipo de
dispositivo desea interactuar? La GDI busca si dicho dispositivo (es decir, su driver) existe, lo
carga a memoria si no estuviese ya cargado, crea una entrada en la tabla de recursos para el
nuevo recurso a utilizar (el dispositivo), llena dicha entrada y retorna al programa un handle al
nuevo dispositivo creado. Dicha entrada en la tabla de recursos contiene:
Información sobre el dispositivo mismo, su tipo, sus capacidades, etc.
Información sobre su estado actual, lo que puede incluir referencias a otros recursos
utilizados cuando la aplicación desea interactuar con el dispositivo.
A dicha información en conjunto se le llama Contexto del Dispositivo (Device Context), por lo
que el tipo de su handle relacionado es HDC (Handle Device Context).
De lo anterior se resume que, para que una aplicación pueda interactuar con un dispositivo debe
de obtener un HDC adecuado para dicho dispositivo. Al igual que con otros handles, la
aplicación deberá liberar dicho HDC cuando ya no requiera trabajar más con él.
Existen 5 formas de dibujo bastante comúnes (no son las únicas):
Síncrono, donde las acciones de dibujo se realizan en cualquier lugar de la aplicación.
Asíncrono, donde las acciones de dibujo se realizan en un lugar bien definido de la
aplicación.
Sincronizado, cuando el dibujo asíncrono se “sincroniza” con otras acciones en
cualquier lugar de la aplicación.
En Memoria, donde las acciones de dibujo se realizan en un lugar de la memoria distinta
a la memoria de video.
En Impresora, donde las acciones de dibujo se traducen en comandos enviados a una
impresora.
En los tres primeros casos, los HDC que se obtendrían corresponden al área cliente de una
ventana, siendo el dispositivo gráfico un monitor de computadora. En el penúltimo caso, el
dispositivo gráfico es un espacio de memoria fuera de la memoria de video.
A continuación veremos cómo se realizan los distintos tipos de dibujo para los diferentes
lenguajes utilizados. Es importante mantener siempre presente la idea de que, sin importar en
qué lenguaje se trabaje, siempre se debe utilizar un HDC para dibujar sobre un dispositivo
gráfico.
119
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
P R O G R A M A C I Ó N C O N G U I
Funciones de Dibujo
A continuación se explica algunas de las funciones de dibujo de la librería GDI:
La función “DrawText” utiliza la fuente de letra, color de texto y color de fondo actual del DC,
para dibujar un texto. Su prototipo es:
int DrawText(
HDC hDC, // handle al DC
char* texto, // texto a dibujar
int longitud, // longitud del texto
RECT* area, // rectángulo dentro del que se dibujará el texto
UINT opciones // opciones de dibujo del texto
);
El código anterior dibujaría la cadena “hola” sobre el DC referido con el handle “hDC”.
Utilizamos la opción DT_NOCLIP dado que no nos interesa restringir la salida del texto a un
rectángulo específico. Por el mismo motivo sólo especificamos la posición X e Y inicial del texto
en la variable “rc”. La estructura RECT se define como:
struct RECT { long left, top, right, bottom; };
La función MoveToEx establece el punto inicial de dibujo de líneas sobre un DC. A partir de
dicha posición se realizará dibujos de líneas hacia otras posiciones, con funciones como LineTo.
Cada nueva línea dibujada actualiza la posición actual de dibujo de líneas. Los prototipos de
MoveToEx y LineTo son:
BOOL MoveToEx(
HDC hdc, // handle al DC
int Xinicial, // coordenada-x de la nueva posición
// (la que se convertirá en la actual)
int Yinicial, // coordenada-y de la nueva posición
// (la que se convertirá en la actual)
POINT* PosAntigua // recibe los datos de la antigua posición actual
);
BOOL LineTo(
HDC hdc, // handle al DC
int Xfinal, // coordenada-x del punto final
int Yfinal // coordenada-y del punto final
);
El código anterior dibujaría un triángulo con vértices (10,10), (40,10) y (40,40). La posición
actual de dibujo, para subsiguientes dibujos de líneas, quedaría en la coordenada (10,10).
Es importante señalar que todas las acciones de dibujo sobre un DC se realizan en base al
sistema de coordenadas del mismo. En el caso de un DC relativo al área cliente de una ventana,
el origen de su sistema de coordenadas en la esquina superior izquierda del área cliente, con el eje
positivo X avanzando hacia la derecha, y el eje positivo Y avanzando hacia abajo.
120
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
P R O G R A M A C I Ó N C O N G U I
Dibujo Asíncrono
Las acciones de dibujo se centralizan en el procesamiento del mensaje WM_PAINT. El
siguiente código muestra un esquema típico de procesamiento de este mensaje:
case WM_PAINT:
hDC = BeginPaint(hWnd, &ps);
// Aquí van las acciones de dibujo
EndPaint(hWnd, &ps);
break;
La función BeginPaint retorna un HDC adecuado para dibujar sobre el área cliente invalidada de
una ventana. La función EndPaint libera el HDC obtenido. Para entender el concepto de
invalidación, imagine el siguiente caso:
Se tiene en un momento dado dos ventanas mostradas en pantalla. La primera oculta
parte de la segunda, como se muestra en la siguiente figura:
Luego se mueve la primera ventana de forma que descubre parte o toda el área ocultada
de la segunda ventana, como se muestra en la siguiente figura:
El dibujo actual del área descubierta ya no es válido y debe de ser redibujado por el
código de la aplicación correspondiente a la segunda ventana, dado que Windows no
tiene forma de saber cómo se debe dibujar coda ventana de cada aplicación, sólo las
121
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
P R O G R A M A C I Ó N C O N G U I
aplicaciones mismas lo saben. Sin embargo, Windows sí reconoce que esta invalidación
ha ocurrido, dado que sabe dónde se encuentra cada ventana y cuál está frente a cual,
por lo que genera un mensaje WM_PAINT, con la información acerca del rectángulo
invalidado, y lo deposita en la cola de mensajes de la aplicación a la que le corresponde
dicha ventana invalidada.
Finalmente, cuando el bucle de procesamiento de mensajes correspondiente extraiga y
mande a procesar dicho mensaje WM_PAINT, la función de ventana de la ventana
invalidada repintará el área de dibujo inválido.
La estructura PAINTSTRUCT es llenada por BeginPaint con información acerca del área
invalidada. Esta información podría ser utilizada por el programa para aumentar la eficiencia del
código de dibujo, dado que podría repintar solamente el área invalidada y no repintar toda el área
cliente. Uno de los datos miembros de dicha estructura es el mismo valor retornado por
BeginPaint. La función EndPaint utiliza dicho dato para eliminar el DC.
Los mensajes WM_PAINT son generados en forma automática por el sistema operativo
cuando éste sabe que el área cliente de una ventana requiere repintarse. Si al generarse un
mensaje WM_PAINT para una ventana, ya existe en la cola de mensajes otro WM_PAINT para
la misma ventana, se juntan ambos mensajes en uno solo para un área invalidada igual a la
combinación de las áreas invalidadas de ambos mensajes.
También es posible generar un mensaje WM_PAINT manualmente y colocarlo en la cola de
mensajes respectiva de forma que se repinte la ventana. La función que hace ésto es:
BOOL InvalidateRect(
HWND hWnd, // handle de la ventana
CONST RECT* lpRect, // rectángulo a invalidar
BOOL bErase // flag de limpiado
);
El segundo parámetro es el rectángulo, dentro del área cliente, que deseamos invalidar. El tercer
parámetro le sirve a la función BeginPaint. Si dicho parámetro es 1, BeginPaint pinta toda el área
invalidada utilizando la brocha con la que se creó la ventana (dato miembro hbrBackground de
la estructura WNDCLASS) antes de finalizar y retornar el HDC (de forma que se comience con
un área de dibujo limpia). Si dicho parámetro es 0, BeginPaint no realiza este limpiado.
El siguiente código muestra el uso de esta función:
...
case WM_LBUTTONDOWN:
iContador++;
InvalidateRect(hWnd, NULL, TRUE);
break;
case WM_RBUTTONDOWN:
for(iBucle = 0; iBucle < 10; iBucle++) {
iContador++;
InvalidateRect(hWnd, NULL, TRUE);
}
break;
case WM_PAINT:
hDC = BeginPaint(hWnd, &ps);
sprintf(szMensaje, "Contador=%d", iContador);
DrawText(hDC, szMensaje, -1, &rc, DT_NOCLIP);
EndPaint(hWnd, &ps);
break;
...
El fragmento de código anterior pertenece a una función de ventana que utiliza InvalidateRect.
Cuando se presiona con el botón izquierdo del ratón se modifica un contador de forma que el
dibujo actual ya no es correcto y debe repintarse. Para el botón derecho se desea que se muestre,
122
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
P R O G R A M A C I Ó N C O N G U I
como una secuencia animada, cómo se va modificando el contador. Sin embargo, esta secuencia
no se muestra, y sólo se ve el valor final del contador en la ventana. Esto se debe al hecho que
InvalidateRect “no es” una llamada directa al código en WM_PAINT, sino la colocación de un
mensaje WM_PAINT en la cola de mensajes, por lo que sólo al retornar del procesamiento del
mensaje actual, WM_RBUTTONDOWN en este caso, el bucle de procesamiento de mensajes
podrá retirar el WM_PAINT de la cola de mensajes y procesarlo.
Como puede apreciarse, el dibujo realizado en WM_PAINT es asíncrono respecto a la solicitud
de dibujo realizada en WM_RBUTTONDOWN. Este tipo de dibujo es adecuado para dibujos
estáticos o de fondo, pero no para secuencias de animación.
Dibujo Síncrono
Para realizar dibujos desde cualquier otro lugar fuera del procesamiento del mensaje
WM_PAINT se utiliza la combinación de funciones:
// Obtener un DC para el área cliente de la ventana referida con el handle hWnd
HDC GetDC( HWND hWnd );
// Liberar el DC obtenido con GetDC
int ReleaseDC( HWND hWnd, HDC hDC );
Para obtener un HDC relativo al área cliente de una ventana se utiliza la función GetDC. Una
vez que se han finalizado las acciones de dibujo sobre la ventana, se debe liberar el HDC
obtenido llamando a ReleaseDC.
El siguiente segmento de un programa muestra el uso de esta técnica:
...
case WM_LBUTTONDOWN:
hDC = GetDC(hWnd);
iContador++;
sprintf(szMensaje, "Contador=%d", iContador);
DrawText(hDC, szMensaje, -1, &rc, DT_SINGLELINE);
ReleaseDC(hWnd, hDC);
break;
case WM_RBUTTONDOWN:
hDC = GetDC(hWnd);
for(iBucle = 0; iBucle < 10; iBucle++) {
iContador++;
sprintf(szMensaje, "Contador=%d", iContador);
DrawText(hDC, szMensaje, -1, &rc, DT_SINGLELINE);
Sleep(100);
}
ReleaseDC(hWnd, hDC);
break;
...
A diferencia del caso anterior, el HDC creado puede utilizarse en cualquier lugar del programa.
Este HDC debe ser liberado cuando ya no sea requerido.
Dibujo Sincronizado
El dibujo sincronizado permite realizar modificaciones al estado del dibujo en cualquier parte del
programa, concentrando el trabajo de dibujo dentro del mensaje de pintado WM_PAINT.
El siguiente segmento de un programa muestra el uso de esta técnica:
...
case WM_LBUTTONDOWN:
iContador++;
InvalidateRect(hWnd, NULL, TRUE);
UpdateWindow(hWnd); // acá se fuerza el procesamiento del mensaje WM_PAINT
// colocado en la cola de mensajes por InvalidateRect
break;
case WM_RBUTTONDOWN:
for(iBucle = 0; iBucle < 10; iBucle++) {
123
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
P R O G R A M A C I Ó N C O N G U I
iContador++;
InvalidateRect(hWnd, NULL, TRUE);
UpdateWindow(hWnd);
Sleep(100); }
break;
case WM_PAINT:
hDC = BeginPaint(hWnd, &ps);
sprintf(szMensaje, "Contador=%d", iContador);
DrawText(hDC, szMensaje, -1, &rc, DT_SINGLELINE);
EndPaint(hWnd, &ps);
break;
...
Dibujo en Memoria
Cuando el dibujo se debe realizar en varios pasos, el realizarlo directamente sobre la ventana
provoca que el usuario del programa vea todo el proceso de dibujo, produciéndose en algunos
casos el parpadeo de la imagen. En estos casos es posible realizar la composición del dibujo en
memoria, para luego pasar la imagen final a la ventana en una sola acción.
El siguiente segmento de un programa muestra el uso de esta técnica:
// Luego de creada la ventana
SelectObject(hDC_Fondo, hBM_Original);
DeleteDC(hDC_Fondo);
DeleteObject(hBM_Fondo);
// En la función de ventana
HBITMAP hBM_Fondo = 0;
HDC hDC_Fondo = 0;
switch( uMsg ) {
case WM_PAINT:
hDC = BeginPaint(hWnd, &ps);
if(hBM_Fondo != 0 && hDC_Fondo != 0)
BitBlt(hDC, 0, 0, 800, 600, hDC_Fondo, 0, 0, SRCCOPY);
else {
szMensaje = "Error al cargar la imagen";
TextOut(hDC_Fondo, 20, 20, szMensaje, strlen(szMensaje));
}
EndPaint(hWnd, &ps);
break;
...
}
return 0;
}
124
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
P R O G R A M A C I Ó N C O N G U I
Dibujo en Java
Java provee, a partir de su versión 1.2, el paquete SWING que simplifica significativamente el
trabajo con ventanas. Esta librería está formada en su mayoría por componentes ligeros, de
forma que se obtenga la máxima portabilidad posible de las aplicaciones con ventanas hacia las
diferentes plataformas que soportan Java.
El paquete SWING se basa en el paquete AWT que fue desarrollado con las primeras versiones
de Java. Para los ejemplos en las siguientes secciones, se realizará dibujo en 2D sobre la clase
base para ventanas JFrame de SWING.
Dibujo Asíncrono
El dibujo en una ventana requiere únicamente sobrescribir el método “paint” de la clase
“JFrame”. Dentro de este método se llama a la implementación de la clase base de paint y luego
se realizan acciones de dibujo utilizando la referencia al objeto Graphics recibida. La clase
Graphics contiene métodos adecuados para realizar:
Dibujo de texto.
Dibujo de figuras geométricas con y sin relleno.
Dibujo de imágenes.
El escribir instrucciones de dibujo dentro del método paint equivale a hacerlo dentro del “case
WM_PAINT” de la función de ventana en API de Windows. En su implementación para
Windows, es de esperarse que la clase Graphics maneje internamente un HDC, obtenido
mediante una llamada a “BeginPaint”. El siguiente programa muestra el dibujo asíncrono.
class Ventana extends JFrame {
int iContador =0;
public Ventana() {
. . .
addMouseListener(new MouseAdapter() {
public void mousePressed(MouseEvent evt) {
if(evt.getButton() == 1){
iContador++;
125
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
P R O G R A M A C I Ó N C O N G U I
repaint();
}
if(evt.getButton() == 3)
for(int iBucle = 0; iBucle < 10; iBucle++) {
iContador++;
repaint();
}
}
});
}
public void paint(Graphics g) {
super.paint(g);
g.drawString("Contador=" + iContador, 100, 100);
System.out.println(iContador);
g.dispose();
}
}
. . .
Dibujo Síncrono
Para el dibujo síncrono se obtiene un objeto Graphics utilizando el método getGraphics de
JFrame. Es importante que, si dicho método es llamado muy seguido, se libere los recursos del
objeto Graphics obtenido (por ejemplo, el HDC obtenido mediante un GetDC para la
implementación en Windows de esta clase) llamando al método “dispose” (que es de suponer
debería llamar a ReleaseDC). Éste es un claro ejemplo donde el usuario debe preocuparse por
liberar explícitamente los recursos dado que el recolector de basura puede no hacerlo a tiempo.
El siguiente programa muestra el dibujo síncrono.
class Ventana extends JFrame {
int iContador =0;
public Ventana() {
. . .
addMouseListener(new MouseAdapter(){
public void mousePressed(java.awt.event.MouseEvent evt){
Graphics g = getGraphics();
if(evt.getButton() == 1) {
iContador++;
g.clearRect(0, 0, 400, 400);
g.drawString("Contador=" + iContador, 100,100);
}
if(evt.getButton() == 3)
for(int iBucle = 0; iBucle < 10; iBucle++) {
iContador++;
g.clearRect(0, 0, 400, 400);
g.drawString("Contador=" + iContador, 100,100);
System.out.println(iContador);
try{ Thread.sleep(800); } catch(Exception e) {}
}
g.dispose();
}
});
}
}
...
Dibujo Sincronizado
Para sincronizar un dibujo se llama al método “update” de la clase JFrame y se concentra todo el
dibujo en la sobrescritura del método “paint” de la ventana. El siguiente programa muestra el
uso de “update”.
class Ventana extends JFrame {
int iContador =0;
public Ventana() {
. . .
126
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
P R O G R A M A C I Ó N C O N G U I
addMouseListener(new MouseAdapter() {
public void mousePressed(MouseEvent evt) {
Graphics g = getGraphics();
if(evt.getButton() == 1) {
iContador++;
update(g);
}
if(evt.getButton() == 3)
for(int iBucle = 0; iBucle < 10; iBucle++) {
iContador++;
update(g);
try{ Thread.sleep(1000); } catch(Exception e) {}
}
g.dispose();
}
});
}
public void paint(Graphics g) {
super.paint(g);
g.drawString("Contador=" + iContador, 100, 100);
System.out.println("Dibujando " + iContador);
g.dispose();
}
}
. . .
Dibujo en Memoria
Existen varias estrategias para realizar dibujo en memoria en Java, para cada cual un conjunto de
clases adecuadas. Una de estas estrategias consiste en utilizar un objeto BufferedImage, el cual
crea un espacio en la memoria sobre la cual se puede realizar un dibujo. Esta clase provee un
método “getGraphics” que permite obtener un objeto Graphics adecuado para dibujar en esta
memoria. Luego de compuesta la imagen en memoria, se puede utilizar el método “drawImage”
del objeto Graphics en el método “paint” para dibujar dicha imagen en la ventana.
El siguiente programa muestra esta estrategia de dibujo.
class Ventana extends JFrame{
Image imgDibujar, imgFondo;
boolean dibujoListo;
String mensaje = "Cargando imagen ...";
public Ventana() {
. . .
Toolkit tk = getToolkit();
imgFondo = tk.createImage("Fondo.gif");
dibujoListo = true;
}
Dibujo en C#
En .NET las clases relacionadas con el dibujo sobre ventanas se encuentran dentro del espacio
de nombres System.Drawing.
127
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
P R O G R A M A C I Ó N C O N G U I
Dibujo Asíncrono
Realizar un dibujo síncrono sobre una ventana consiste en agregar un nuevo delegado al evento
“Paint” de la clase Form. Dicho delegado deberá hacer referencia a un método que reciba como
parámetro una referencia “object” y una referencia “PaintEventArgs”. Ésta última contiene las
propiedades y métodos necesarios para realizar un dibujo sobre la ventana.
El siguiente programa muestra un ejemplo de este tipo de dibujo.
class Ventana : Form {
int iContador =0;
public Ventana() {
. . .
this.Paint += new PaintEventHandler(this.Ventana_Paint);
}
private void Ventana_MouseDown(object sender, MouseEventArgs e) {
if(e.Button == MouseButtons.Left) {
iContador++;
Invalidate();
}
if(e.Button == MouseButtons.Right)
for(int iBucle = 0; iBucle < 10; iBucle++) {
iContador++;
Invalidate();
}
}
private void Ventana_Paint(object sender, PaintEventArgs e) {
Graphics g = e.Graphics;
Font f = this.Font;
Brush b = Brushes.Black;
Console.WriteLine(iContador);
g.DrawString("Contador=" + iContador, f, b, 100, 100);
g.Dispose();
}
}
. . .
Dibujo Síncrono
Para el dibujo síncrono se utiliza el método CreateGraphics de la clase Form desde cualquier
punto del programa. Este método retorna un objeto Graphics (podemos suponer que
internamente llama a GetDC) con el cual se puede dibujar sobre la ventana. Cuando ya no se
requiera utilizar este objeto, el programa debe llamar al método “Dispose” del mismo, de forma
que se liberen los recursos reservados por éste en su creación (podemos suponer que libera el
HDC interno que maneja mediante un RealeaseDC).
El siguiente programa muestra el uso del dibujo síncrono.
class Ventana : Form {
int iContador =0;
public Ventana() {
. . .
}
private void Ventana_MouseDown(object sender, MouseEventArgs e) {
Graphics g = CreateGraphics();
Font f = this.Font;
Brush b = Brushes.Black;
if(e.Button == MouseButtons.Left) {
iContador++;
g.Clear(Color.LightGray);
g.DrawString("Contador=" + iContador, f, b, 100, 100);
}
if(e.Button == MouseButtons.Right)
for(int iBucle = 0; iBucle < 10; iBucle++) {
iContador++;
128
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
P R O G R A M A C I Ó N C O N G U I
g.Clear(Color.LightGray);
System.Console.WriteLine(iContador);
g.DrawString("Contador=" + iContador, f, b, 100, 100);
Thread.Sleep(800);
}
g.Dispose();
}
}
. . .
Dibujo Sincronizado
Para sincronizar un dibujo se llama al método “Refresh” de la clase Form y se concentra todo el
dibujo en la sobrescritura del método donde se concentra el trabajo de dibujo asíncrono. El
siguiente programa muestra el uso de “Refresh”.El siguiente programa muestra el uso del dibujo
sincronizado.
class Ventana : Form {
int iContador =0;
public Ventana() {
. . .
Paint += new PaintEventHandler(this.Ventana_Paint);
}
private void Ventana_MouseDown(object sender, MouseEventArgs e) {
if(e.Button == MouseButtons.Left){
iContador++;
Refresh();
}
if(e.Button == MouseButtons.Right)
for(int iBucle = 0; iBucle < 10; iBucle++) {
iContador++;
Refresh();
Thread.Sleep(100);
}
}
private void Ventana_Paint(object sender, PaintEventArgs e) {
Graphics g = e.Graphics;
Font f = this.Font;
Brush b = Brushes.Black;
g.DrawString("Contador=" + iContador, f, b, 100, 100);
}
}
. . .
Dibujo en Memoria
Al igual que en Java, existen muchas estrategias de dibujo en memoria. El siguiente ejemplo crea
un objeto de la clase “Image” que inicialmente contiene un dibujo guardado en un archivo.
Dicho objeto crea un área en memoria, inicializada con la imagen leída del archivo, a la que
puede accederse mediante un objeto Graphics creado mediante el método estático
“FromImage” de la misma clase Graphics. Cuando dicho objeto Graphics ya no se requiera, el
programa debe liberar sus recursos llamando al método “Dispose”. El siguiente programa
muestra un ejemplo de este tipo de dibujo.
class Ventana : Form {
Image img;
public Ventana() {
this.Size = new System.Drawing.Size(400, 400);
this.Text = "Título de la Ventana";
this.Visible = true;
this.Paint += new PaintEventHandler(Ventana_Paint);
img = Image.FromFile("Fondo.bmp");
Graphics g = Graphics.FromImage(img);
Font f = this.Font;
Brush b = Brushes.Black;
129
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
P R O G R A M A C I Ó N C O N G U I
LRESULT CALLBACK FuncionVentana( HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam ) {
switch( uMsg ) {
case WM_COMMAND:
if( LOWORD( wParam ) == ID_BOTON ) {
char szNombre[ 100 ];
HWND hWndBoton = ( HWND )lParam;
GetDlgItemText(hWnd, ID_TEXTO, szNombre, 100);
MessageBox(hWnd, szNombre, "Hola", MB_OK );
}
break;
// Aquí va el resto del switch
...
}
return 0;
}
int WINAPI WinMain( HINSTANCE hIns, HINSTANCE hInsPrev, LPSTR lpCmdLine, int iShowCmd ) {
// Creo y registro la ventana principal.
...
130
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
P R O G R A M A C I Ó N C O N G U I
// Muestro la ventana.
...
return 0;
}
A los elementos GUI del API de Windows se les llama controles. Como puede observarse, los
controles no son más que ventanas cuyos nombres de clase (STATIC, EDIT y BUTTON)
corresponden a clases de ventana preregistradas. Dado que estas ventanas deben dibujarse
dentro del área cliente de nuestra ventana principal, tenemos que asignarles la constante de estilo
WS_CHILD y el octavo parámetro debe ser el identificador de esta ventana padre. El estilo
WS_VISIBLE evita que tengamos que ejecutar ShowWindow para cada una de estas ventanas
hijas.
El noveno parámetro de CreateWindow puede, opcionalmente, ser un número identificador que
distinga dicha ventana hija de sus ventanas hermanas (hijas de la misma ventana padre). Este
parámetro es aprovechado en la función de ventana de nuestra ventana principal. En dicha
función se agrega una sentencia CASE para el mensaje WM_COMMAND, el cual es generado
por diferentes objetos visibles cuando el usuario interactúa con ellos. En particular, cuando
presionamos el botón creado, se agrega a la cola de mensajes del programa, un mensaje
WM_COMMAND con los dos bytes menos significativos del parámetro wParam iguales al
identificador del botón, ID_BOTON. También utilizamos el identificador de la caja de texto,
ID_TEXTO, para poder obtener el texto ingresado llamando a la función GetDlgItemText.
En general, la interacción con los controles estándar del API de Windows, así como otros
objetos visuales de una ventana, generan mensajes que son enviados a la ventana padre para su
procesamiento. De igual forma, dicho procesamiento suele incluir el envío de nuevos mensajes a
los objetos visibles, para obtener más información o para reflejar el cambio de estado del
programa (internamente, GetDlgItemText envía un mensaje directamente a la función de
ventana del control hijo, de manera que ésta devuelva el texto ingresado), en otras palabras, todo
se realiza enviando y recibiendo mensajes. Esto tiene la ventaja de unificar la forma de trabajo
con ventanas a un modelo simple de envío-recepción de mensajes, pero con la desventaja de
limitar el procesamiento a una sola ventana (comunmente, solo la ventana padre). Esto tiene
sentido bajo el enfoque de que, todos los elementos manejados son ventanas, por tanto es de
esperarse que sólo la ventana padre esté interesada en los mensajes de sus hijas. El problema
sucede cuando queremos encapsular la funcionalidad de una ventana a sí misma para ciertos
trabajos, es decir, ¿cómo hacer para que una caja de texto maneje por sí mismo los mensajes que
sólo le competen a él, y envíe a la ventana padre el resto?. Veremos que Java y C# solucionan,
de diferente forma, estas carencias del enfoque del API de Windows.
131
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
P R O G R A M A C I Ó N C O N G U I
import javax.swing.*;
import java.awt.event.*;
import java.awt.*;
txt.getText(),"Hola",JOptionPane.INFORMATION_MESSAGE);
}
});
Container cp = getContentPane();
cp.setLayout(new FlowLayout());
cp.add(lbl);
cp.add(txt);
cp.add(btn);
}
}
En el programa anterior, JTextField, Jlabel y JButton son componentes que representan una caja
de texto, una etiqueta y un botón respectivamente, al igual que las clases de ventana EDIT,
STATIC y BUTTON del API de Windows.
Los elementos GUI de Java se denominan componentes. Como puede observarse, los
componentes en Java son clases que se agregan a una ventana para ser visualizados. Es
importante en este punto hacer una distinción entre lo que son internamente estos
componentes, contra lo que son los controles del API de Windows. Mientras que los controles
del API de Windows son ventanas, los componentes de Java (a partir de la versión 1.2 del JDK,
denominada Java 2) se dividen en dos categorías:
Los componentes pesados. Su comportamiento está supeditado a las capacidades de la
plataforma subyacente, en este caso particular, Windows. Estos componentes son
JFrame, JDialog y JApplet. Estos componentes crean ventanas de Windows (o utilizan
directamente una ya creada) y las administran internamente, ofreciendo al programador
una interfaz más amigable. En otras palabras, por ejemplo, cuando realizamos click
sobre una ventana creada con un objeto que deriva de JFrame, el evento que se genera
es un mensaje WM_LBUTTONDOWN, el cual es sacado de la cola de mensajes y
enviado a una función de ventana que llama a un método de nuestro objeto JFrame, el
cual se encarga de llamar al método respectivo de MouseListener para todos los objetos
registrados con un llamado a addMouseListener. Además de lo anterior, el dibujo de la
ventana, el efecto de maximizado y minimizado, la capacidad de redimensionar la
132
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
P R O G R A M A C I Ó N C O N G U I
ventana y todos los efectos visuales posibles, son gestionados por las funciones del API
de Windows, así como las capacidades ofrecidas por la propia clase JFrame, por
ejemplo, al llamar al método setVisible se estaría llamando internamente a ShowWindow
del API de Windows. Debido a esta dependencia, estos componentes son denominados
pesados.
Los componentes ligeros. Su comportamiento está supeditado a las capacidades
ofrecidas por un componente pesado, del cual heredan o dentro del cual se dibujan, y no
de la plataforma subyacente, en este caso particular, Windows. Si bien los mensajes
producidos por la interacción del usuario siguen siendo producidos por el sistema
operativo, el manejo de éstos (en forma de eventos), el dibujo de los componentes y de
sus efectos visuales relacionados, están codificados completamente en Java. Estos
componentes definen además sus propios eventos. Debido a esta independencia, estos
componentes son denominados ligeros. Ejemplos de estos componentes son JButton,
JLabel y JTextField. Los componentes ligeros se dibujan sobre el área cliente de
componentes pesados y simulan el “Look And Feel” correspondiente, es decir, no crean
una ventana child. Este tipo de ventanas, child, se verá mas adelante (sección “Tipos de
Ventana”)
Por otro lado, la clase JFrame no administra directamente el área cliente de su ventana, sino que
delega dicho trabajo a un objeto Contenedor, derivado de la clase Container. Un Contenedor es
básicamente un Componente Java con la capacidad adicional de poder mostrar otros
Componentes dentro de su área de dibujo. Es por esto que es necesario obtener una referencia a
dicho contenedor de la ventana, llamando al método getContentPane, dado que es a dicho
contenedor al que deberemos agregarle los componentes que deseamos visualizar.
Los componentes, al igual que una ventana, pueden generar mensajes como respuesta a la
interacción del usuario con ellos. En el código anterior, un botón creado con la clase JButton
genera el evento Action cuando el usuario, con el ratón o el teclado, presiona dicho botón. Para
procesar dicho evento, definimos una clase inner anónima que implementa la interfaz
ActionListener, instanciando dicha clase y pasándole la referencia a esta instancia al método
addActionListener del botón.
Es interesante notar el hecho de que una clase inner anónima puede ser creada realmente a partir
de una clase o de una interfaz, siempre que en éste último caso se implementen todos sus
métodos, que para este caso, es uno sólo, actionPerformed. De igual manera, es interesante
notar que se ha escogido configurar el objeto observador del evento de cerrado de la ventana
desde el método main, no desde el constructor de la ventana. Ambos enfoques son equivalentes.
Dentro del método actionPerformed se hace uso del dato miembro “txt” de tipo JTextField
para poder mostrar el mensaje respectivo mediante el método estático showMessageDialog de la
clase JOptionPane. Note que el primer parámetro de este método debe ser una referencia a la
ventana padre de la ventana que se mostrará, y que dicho parámetro se pasa utilizando la
expresión “Ventana.this”. Esto se debe a que si pasáramos únicamente this, nos estaríamos
refiriendo al objeto anónimo que implementa la interfaz ActionListener.
Finalmente para este código, note que antes de agregar los componentes al content pane, se
llama al método setLayout. Esto permite determinar la forma en que los componentes son
distribuidos dentro del área cliente del contenedor. El manejo del diseño (layout) de una ventana
se tratará más adelante.
Sumarizando las diferencias entre API de Windows y Java, mientras los objetos visuales
comunes llamados controles, preimplementados en dicha librería, son básicamente ventanas y
133
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
P R O G R A M A C I Ó N C O N G U I
las acciones son manejadas mediante mensajes enviados por estos controles a sus ventanas
padres, en Java se trabajan con clases llamadas componentes, las cuales se agregan al contenedor
ContentPane de la ventana, y sus acciones son manejadas mediante interfaces. Mientras que los
controles del API de Windows tienen una posición fija respecto a la esquina superior izquierda
del área cliente de su ventana padre y sus dimensiones son fijas, los componentes de Java no
tienen una posición ni dimensión fija, y esto es manejado mediante objetos Layout que asisten
en el diseño de la ventana. Mientras que en API de Windows los mensajes sólo pueden ser
recibidos por ventanas y sólo una ventana, generalmente la ventana padre, es la que recibe los
mensajes de los controles, en Java cualquier objeto de cualquier clase que implemente la interfaz
correspondiente a un evento puede recibir la notificación del mismo.
Elementos GUI de C#
Los elementos GUI en C# se denominan controles. El siguiente código muestra una ventana
equivalente en C# al código anterior en API de Windows.
using System;
using System.Windows.Forms;
using System.Drawing;
public Ventana() {
this.Text = "Prueba de Controles";
this.Size = new Size(300, 300);
this.Controls.Add(lbl);
this.Controls.Add(txt);
this.Controls.Add(btn);
}
class Principal {
public static void Main(string[] args) {
Application.Run(new Ventana());
}
}
134
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
P R O G R A M A C I Ó N C O N G U I
En C# el manejo del diseño se realiza mediante anclas (anchors) y muelles (docks). Todo
control posee las siguientes propiedades:
Anchor: Determina a qué borde del contenedor se anclará el control. Por ejemplo, si el
control se coloca inicialmente a una distancia de 100 píxeles del borde inferior de su
ventana, al redimensionarse el anchor modificará automáticamente la posición del
control de forma que conserve dicha distancia.
135
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
P R O G R A M A C I Ó N C O N G U I
Dock: Determina a qué borde del contenedor se adosará el control. Por ejemplo, si el
control se adosa al borde izquierdo de su ventana, su ancho inicial se conserva pero su
alto se modifica de forma que coincida con el alto del área cliente de su ventana. Su
posición también se modifica de forma que su esquina superior izquierda coincida con la
del área cliente de su ventana.
DockPadding: Se establece en el contenedor, por ejemplo, una clase que hereda de
Form. Determina la distancia a la que los componentes, adosados a sus bordes, estarán
de los mismos.
El siguiente código muestra el uso de estas propiedades sobre un botón que es agregado a una
ventana.
Boton = new Button();
Boton.Text = "Boton1";
Boton.Dock = DockStyle.Top;
Controls.Add( Boton );
Tipos de Ventana
En un sistema gráfico con ventanas, dichas ventanas pueden estar relacionadas. Estas relaciones
determinan los tipos de ventanas que se pueden crear.
En Windows, existen dos tipos de relaciones entre ventanas:
La relación de pertenencia. Cuando dos ventanas tienen esta relación, una ventana
(llamada owned) le pertenece a la otra ventana (llamada owner), lo que significa que:
⇒ La ventana owned siempre se dibujará sobre su ventana owner. A la ubicación de
una ventana con respecto a otra en un eje imaginario Z que sale de la pantalla del
computador, se le conoce como orden-z.
⇒ La ventana owned es minimizada y restaurada cuando su ventana owner es
minimizada y restaurada.
⇒ La ventana owned es destruida cuando su ventana owner es destruida.
La relación padre-hijo. Cuando dos ventanas tienen esta relación, una ventana (llamada
hija) se dibujará dentro del área cliente de otra (llamada padre).
La relación de pertenencia se establece con el octavo parámetro de la función CreateWindow,
hWndParent. La relación padre-hijo se establece con el tercer parámetrode la función
CreateWindow, escogiendo como bit-flag WS_CHILD o WS_POPUP.
Una ventana popup tiene como área de dibujo el escritorio de Windows (Windows Desktop) y
puede tener un botón relacionado en la barra de tareas (Windows TaskBar). Como contraparte,
una ventana Child tiene como área de dibujo el área cliente de otra ventana, la que puede ser de
tipo Popup o Child, y no puede tener un botón relacionado en la barra de tareas.
Las ventanas popup pueden o no tener una ventana owner. Cuando no la tienen se les llama
OVERLAPPED. Un ejemplo de una ventana OVERLAPPED es la ventana principal de todo
programa con ventanas de Windows. Las ventanas child siempre tienen una ventana owner.
136
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
P R O G R A M A C I Ó N C O N G U I
En resumen, las ventanas popup pueden ser owner o owned, mientras que las ventanas child
son siempre owned.
Una ventana que contiene una o más ventanas child del mismo tipo, cada una con su propia
barra de título y botones de sistema, se le conoce como ventana MDI (Multiple Document
Interface), donde cada ventana hija suele ser utilizada para manipular un documento. Ejemplos
de estas ventanas son los programas editores de texto como Word. Las ventanas no-MDI son
conocidas como ventanas SDI (Single Document Interface).
A las ventanas cuyos elementos permiten mostrar e ingresar información, ésto es, establecer un
diálogo con el usuario se les conoce como Cajas De Dialogo. Las cajas de diálogo no son
estrictamente un tipo de ventana, su tipo real es Popup, más bien su concepto corresponde a
“una forma de manejo” de una ventana. Existen dos formas de mostrar una caja de diálogo:
Modal y amodalmente. Una caja de diálogo modal “detiene”, por así decirlo, la ejecución del
código desde donde se le muestra, una caja de diálogo amodal no. Por ello las cajas de diálogo
modales son adecuadas cuando, dentro de un bloque de instrucciones, se requiere pedir al
usuario que ingrese alguna información necesaria para seguir con la ejecución del algoritmo
implementado por el bloque. Un ejemplo son las ventanas mostradas por los programas al
momento de imprimir. En estos casos “no es conveniente” que el usuario del programa pueda
interactuar con la ventana principal de forma que modifique los datos que se imprimirán
mientras se están imprimiendo, por lo que resulta imprescindible que el procesamiento de los
eventos de la ventana principal sea bloqueado. Las cajas de diálogo amodales son adecuadas para
mostrar e ingresar información mientras se sigue interactuando con otra ventana, comunmente
la ventana principal del programa. Un ejemplo son las barras de herramientas de algunos
programas gráficos como CorelDraw, el editor ortográfico de Word, etc.
El API de Windows implementa un conjunto de cajas de diálogo para acciones comunes como
seleccionar un color, abrir un archivo, imprimir, etc. A estas cajas de diálogo se le conoce como
Cajas de Diálogo Comunes.
En las siguientes secciones se detallará las capacidades del API de Windows manejado desde
C/C++, de Java y de la plataforma .NET programada desde C#, para crear los diferentes tipos
de ventanas.
Para crear una ventana owned se utiliza el estilo WS_POPUP y se pasa un manejador válido de
su ventana owner. El siguiente código crea una ventana owned:
hWndPopupOwned = CreateWindow(
"ClaseVentanaPopup",
"Título de la Ventana Popup Owned",
WS_POPUP | WS_CAPTION,
100, 100, 200, 200,
137
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
P R O G R A M A C I Ó N C O N G U I
Para crear una ventana child se utiliza el estilo WS_CHILD y se pasa un manejador válido de su
ventana owner. El siguiente código crea una ventana owned:
hWndChild = CreateWindow(
"ClaseVentanaHija",
"Título de la Ventana Hija",
WS_CHILD | WS_BORDER,
100, 100, 200, 200,
hWnd, // debe tener una ventana owner
NULL,hIns,NULL
);
La creación y manejo de cajas de diálogo y ventanas MDI con API de Windows va más allá de
los alcances del presente documento.
Ventanas en Java
En Java cada nueva herencia de las clases base para la creación de ventanas (JFrame y JDialog)
determina una nueva clase de ventana. Un programa puede definir y crear una o más ventanas,
de igual o distinto tipo. Sin embargo, al crear una nueva ventana no se establece una relación de
parentesco entre ellas, todas se comportan como popups owner.
El siguiente código muestra la creación de una ventana popup en Java desde la ventana principal
del programa.
class VentanaPopup extends JFrame { ... }
class VentanaPrincipal extends JFrame {
public VentanaPrincipal() {
JButton boton = new JButton("Crear Ventana");
boton.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
VentanaPopup vp = new VentanaPopup();
}
}); ...
} ...
}
Java soporta la creación de aplicaciones MDI. La ventana MDI es llamada “backing window” y
consiste en una ventana popup con un “Content Pane” del tipo “JDesktopPane”. Las “pseudo-
ventanas child” son implementadas con la clase “JInternalFrame”, la que hereda de
“JComponent” por lo que, como puede deducirse, no son realmente ventanas. El siguiente
código muestra un ejemplo de este uso:
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
138
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
P R O G R A M A C I Ó N C O N G U I
child.setResizable(true);
child.setClosable(true);
child.setMaximizable(true);
child.setIconifiable(true);
desktop.add(child);
}
Las cajas de diálogo en Java se crean mediante clases que heredan de JDialog. El siguiente
código muestra el uso de esta clase:
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
Container cp = getContentPane();
cp.setLayout(new GridLayout(3,1));
cp.add(new JLabel("Ingrese un texto"));
cp.add(texto);
cp.add(boton);
}
public String ObtenerResultado() {
return texto.getText();
}
}
public Ventanas2() {
addWindowListener(new WindowAdapter() {
public void windowClosing(WindowEvent e) {
dispose();
System.exit(0);
}
});
139
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
P R O G R A M A C I Ó N C O N G U I
Container cp = getContentPane();
cp.setLayout(new GridLayout(3,1));
cp.add(boton1);
cp.add(boton2);
cp.add(etiqueta);
}
El programa anterior muestra la diferencia entre utilizar una caja de diálogo modal y una amodal.
También muestra una forma de pasar datos desde la caja de diálogo y la ventana que la crea.
Las cajas de diálogo de Java también son llamadas “ventanas secundarias”, mientras que las
pseudo-ventanas hijas creadas con JInternalFrame son llamadas “ventanas primarias”.
Adicionalmente Java provee la clase JOptionPane, la que permite crear cajas de diálogo con
funcionalidad común, como por ejemplo, cajas de diálogo con un texto como mensaje y
botones YES, NO y CANCEL.
Ventanas en C#
Al igual que en Java, cada nueva herencia de las clases base para la creación de ventanas, Form,
determina una nueva clase de ventana. Un programa puede definir y crear una o más ventanas,
de igual o distinto tipo. A diferencia de Java, se pueden crear ventanas popups owner y owned.
El siguiente código muestra la creación de dos ventanas popup, una owner y la otra owned.
class VentanaPopup : Form {
public VentanaPopup( Form OwnerForm ) {
...
this.Owner = OwnerForm;
this.Visible = true;
}
}
class VentanaPrincipal : Form {
public VentanaPrincipal() {
...
VentanaPopup vp1 = new VentanaPopup( null );
VentanaPopup vp2 = new VentanaPopup( this );
}
...
}
C# maneja ventajas child sólo como ventanas hijas de una ventanas MDI. La ventana MDI
consiste en una ventana con la propiedad IsMDIContainer puesta en “true”. Para que una
ventana sea child de otra, se establece su propiedad MdiParent con la referencia de una ventana
MDI. El siguiente código muestra un ejemplo de este uso:
using System;
using System.Windows.Forms;
140
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
P R O G R A M A C I Ó N C O N G U I
void InitializeComponent()
{
this.SuspendLayout();
this.Name = "MainForm";
this.Text = "Esta es la Ventana Principal";
this.Size = new System.Drawing.Size(300, 300);
this.ResumeLayout(false);
}
Controls.Add(etiqueta);
Controls.Add(texto);
Controls.Add(boton);
}
void buttonClick(object sender, System.EventArgs e)
{
//Visible = false;
Close();
}
public string ObtenerResultado() {
return texto.Text;
141
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
P R O G R A M A C I Ó N C O N G U I
}
}
void InitializeComponent() {
this.button = new System.Windows.Forms.Button();
this.button2 = new System.Windows.Forms.Button();
this.label = new System.Windows.Forms.Label();
this.SuspendLayout();
//
// Primer botón
//
this.button.Location = new System.Drawing.Point(24, 16);
this.button.Name = "button";
this.button.Size = new System.Drawing.Size(128, 32);
this.button.TabIndex = 0;
this.button.Text = "Mostrar Como Modal";
this.button.Click += new System.EventHandler(this.buttonClick);
//
// Segundo botón
//
this.button2.Location = new System.Drawing.Point(24, 64);
this.button2.Name = "button2";
this.button2.Size = new System.Drawing.Size(128, 32);
this.button2.TabIndex = 1;
this.button2.Text = "Mostrar Como Amodal";
this.button2.Click += new System.EventHandler(this.button2Click);
//
// Etiqueta
//
this.label.Location = new System.Drawing.Point(24, 112);
this.label.Name = "label";
this.label.Size = new System.Drawing.Size(128, 24);
this.label.TabIndex = 2;
this.label.Text = "Resultado = ...";
//
// Agrego los controles
//
this.ClientSize = new System.Drawing.Size(248, 165);
this.Controls.AddRange(new System.Windows.Forms.Control[] {
this.label,
this.button2,
142
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
P R O G R A M A C I Ó N C O N G U I
this.button});
this.Text = "Prueba con Cajas de Diálogo";
this.ResumeLayout(false);
}
El programa anterior muestra la diferencia entre utilizar una caja de diálogo modal, con
ShowDialog, y una amodal, con Show. También muestra una forma de pasar datos desde la caja
de diálogo y la ventana que la crea.
Dado que es común validar la forma en que fue respondida una caja de diálogo modal, se
provee la propiedad DialogResult, cuyo tipo es el enumerado DialogResult con los siguientes
valores: Abort, Cancel, Ignore, No, None, OK, Retry, Yes. Esta propiedad se establece
automáticamente en algunos casos (cuando se cierra la ventana, se establece a Cancel) o
manualmente desde eventos programados.
Es fácil ver cual es el directorio de trabajo actual del shell (en Windows lo indica el mismo
prompt, en Linux se puede consultar con un comando, por ejemplo pwd) y cambiarlo
(utilizando un comando como cd). De igual forma, utilizando las llamadas a las funciones
adecuadas del API del sistema operativo, cualquier proceso puede modificar su directorio de
trabajo durante su ejecución.
Para programas diferentes a los shells, desde donde también es posible arrancar otros
programas, el directorio de trabajo actual puede no ser claro, por lo que heredarlo puede no ser
lo que el usuario espera. Por ejemplo, al arrancar un programa desde el explorador de Windows
haciendo un doble-click sobre el nombre del archivo ejecutable, el usuario espera que se inicie
dicho programa teniendo como directorio de trabajo inicial el mismo directorio donde se
encuentra el archivo ejecutable, independientemente de cual sea actualmente el directorio de
trabajo del explorador de Windows. Este comportamiento puede modificarse creando accesos
143
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
P R O G R A M A C I Ó N C O N G U I
144
1
Capítulo
11
Programación
Concurrente
El objetivo de este capítulo es dar una base teórica y práctica al lector sobre el menejo de la
concurrencia en un programa así como sus ventajas y desventajas.
Procesos e Hilos
Un proceso es todo aquello que el computador necesita para ejecutar una aplicación.
Técnicamente, un proceso es una colección de:
Espacio de memoria virtual.
Código.
Datos (como las variables globales y la memoria del montón).
Recursos del sistema (como los descriptores de archivos y los manejadores o handles).
Un hilo es la instancia de ejecución dentro de un proceso, es código que está siendo ejecutado
serialmente dentro de un proceso. Técnicamente, un hilo es una colección de:
Estados de la pila.
Información del contexto de ejecución (como el estado de los registros del CPU)
Un procesador ejecuta hilos, no procesos, por lo que una aplicación tiene al menos un proceso,
y un proceso tiene al menos un hilo de ejecución. El primer hilo de un proceso, el que comienza
su ejecución, se le llama hilo primario. A partir del hilo primario pueden crearse hilos secundarios, y a
su vez crearse de éstos. Un hilo puede a su vez crear otros procesos. Cada hilo puede ejecutar
secciones de código distintas, o múltiples hilos pueden ejecutar la misma sección de código. Los
hilos de un proceso tienen pilas independientes, pero comparten los datos y recursos de su
proceso.
A un sistema operativo capaz de “ejecutar” más de un hilo, pertenecientes al mismo o a un
distinto proceso, se le llama multitarea. Es importante no perder de vista que el sistema
operativo mismo es un conjunto de procesos con atribuciones especiales. El sistema operativo
(S.O.) utiliza elementos del hardware del computador, como veremos más adelante, para asistirse
en el control de la multitarea.
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
P R O G R A M A C I Ó N C O N C U R R E N T E
La aparente ejecución simultánea de más de un hilo es sólo un efecto del rápido paso de la
ejecución de un hilo a otro, por turnos, por parte del computador. Un computador con un
único procesador es capaz de ejecutar el código de un único hilo por vez. Dado que el hilo es la
unidad de ejecución de las aplicaciones, es también la unidad de planificación del orden de
ejecución de las mismas. El componente del sistema operativo encargado de determinar cuándo
y qué tan frecuentemente un hilo debe ejecutarse se le llama planificador (scheduler). Cuando
uno de los hilos de un proceso está en ejecución, se dice que su proceso está en ejecución. Sin
embargo, vale insistir, el decir que un proceso se ejecuta significa que uno de sus hilos se está
ejecutando. El computador ejecuta hilos, no procesos.
Para sacar a un hilo de ejecución e ingresar otro, se realiza un cambio de contexto. Este cambio
se realiza mediante una interrupción de hardware, la que ejecuta una rutina instalada por el S.O.
cuando se arrancó el computador (como es de suponerse, el planificador) y que realiza lo
siguiente:
Guarda la información del estado de ejecución del hilo saliente.
Carga la información del estado de ejecución del hilo entrante.
Finaliza la rutina de la interrupción, por lo que “continúa” la ejecución del hilo
configurado, es decir, el hilo entrante.
Cuando el cambio de contexto es entre hilos de un mismo proceso, éste se realiza en forma
rápida, dado que la información del contexto de ejecución relacionada con el proceso, como es
el espacio de direccionamiento, no requiere ser modificada, todos los hilos de un proceso
comparten el mismo espacio de direccionamiento. Cuando el cambio de contexto es entre hilos
de distintos procesos, éste es más lento debido al número de cambios que es necesario realizar.
Éste es el motivo por el cual los S.O. multitarea modernos separan los conceptos de proceso e
hilo, dado que técnicamente un S.O. podría trabajar únicamente con procesos como unidad de
ejecución. El uso de los hilos le permite a los programas que requieren realizar varias tareas en
paralelo o concurrentemente, usar hilos en lugar de crear nuevos procesos, mejorando el
desempeño general del computador.
162
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
P R O G R A M A C I Ó N C O N C U R R E N T E
Esto permite una gran seguridad al trabajar con varios procesos a la vez, pero dificulta la
comunicación entre procesos.
Bloqueado
Dormido
En Espera
Note que un hilo en ejecución puede pasar a un estado de latencia en el que no se le asignará el
CPU hasta que ocurra el evento adecuado que lo saque de dicho estado, pasándolo al estado de
listo. Estos estados de latencia son: Bloqueado, dormido, en espera.
Cuando un hilo ha finalizado (o muerto), la información que el S.O. guarda de él no ha sido aún
eliminada. Esto permite que otro hilo en ejecución pueda verificar dicho estado y pueda tomar
alguna acción, lo que puede servir para sincronizar las acciones de los hilos.
Planificación
Un hilo se ejecuta hasta que muere, le cede la ejecución a otros hilos o es interrumpido por el
planificador. Cuando el planificador tiene la capacidad de interrumpir un hilo, cuando su tiempo
de ejecución vence u otro hilo de mayor prioridad está listo para ser ejecutado, se le llama
preentivo. Cuando debe esperar a que dicho hilo ceda voluntariamente el CPU a otros hilos, se
le llama no preentivo. Éste último esquema ofrece mayor control al programador sobre la
163
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
P R O G R A M A C I Ó N C O N C U R R E N T E
planificación de su programa, por consiguiente es más propenso a las fallas, por lo que los S.O.
multitarea modernos son preentivos. En los S.O. preentivos, una forma de determinar cuándo
un hilo debe salir de ejecución es dándole un tiempo límite, al que se le llama quantum.
Windows 95 y sus predecesores, así como Windows NT y sus predecesores, son sistemas
operativos preentivos por quantums.
Cuando el quantum de un hilo vence, el planificador debe elegir cuál será el nuevo hilo a
ejecutar. Uno de los elementos que se utiliza para esto es la prioridad del hilo. Todo hilo tiene
una prioridad relacionada, la que está relacionada con la prioridad de su proceso. El planificador
maneja una lista con prioridad de “listas de hilos listos”, como se muestra en la Figura 8 - 2. El
planificador suele escoger un hilo dentro de la lista de mayor prioridad.
El trabajo con prioridades permite ejecutar más continuamente los hilos cuyos trabajos se
consideran más prioritarios. Sin embargo, esto trae consigo dos problemas:
Bloqueos muertos (deadlocks), ocurren cuando un hilo espera a que otro hilo, con
menor prioridad, realice algo para poder continuar. Dado que el hilo esperado tiene
menor prioridad que quien espera, nunca será elegido para ejecutarse, por lo que la
condición nunca se cumplirá.
Condiciones de carrera (race conditions), ocurren cuando un hilo acaba antes que otro,
del cual depende para modificar los recursos compartidos, por lo que accede a valores
errados.
Existen técnicas que los S.O. y las aplicaciones utilizan para determinar estas condiciones y
evitarlas o solucionarlas.
Mayor Prioridad
...
...
.
.
.
...
Menor Prioridad
Figura 8 - 2 Lista con prioridad de los hilos "listos" del planificador.
164
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
P R O G R A M A C I Ó N C O N C U R R E N T E
origina que sólo se permita la programación multihilos pero como contraparte se pueda utilizar
las mismas librerías de clases y un comportamiento “bastante” similar desde distintos S.O. C#
hace una implementación interna de la mayor parte de las características de la programación
concurrente, como una forma de solucionar muchos de los problemas más comunes de esta
programación sin modificar o depender del S.O.
Manejo de Hilos
En esta sección revisaremos las técnicas más simples de manejo de hilos vistas desde C/C++,
Java y C#. No se verá el tema de la programación con procesos.
Creación
Una aplicación crea un hilo en Windows haciendo uso de la función CreateThread. El siguiente
código en C++ muestra el uso de esta función:
#include <windows.h>
#include <stdio.h>
Al igual que la función main o WinMain que son los puntos de entrada de nuestros programas,
cuando se crea un hilo es necesario especificar una función de entrada, de forma que el S.O. sepa
que cuando el hilo termina de ejecutar dicha función, finaliza. El ejemplo anterior crea 10 hilos
con la misma función de entrada, FuncionHilo, pasándole como parámetro una cadena de texto,
cuarto parámetro de CreateThread, que es impresa por cada hilo al ejecutar FuncionHilo. Si bien
en este ejemplo pasamos un único dato a la función de entrada, utilizando un puntero a una
estructura o clase es posible pasar toda la información requerida a la función de entrada de un
hilo. Esta función de entrada podrá tener cualquier nombre pero la misma firma (tipos del valor
de retorno y los parámetros) que FuncionHilo. El significado de la palabra reservada __stdcall está
relacionado con un requisito en la declaración de las funciones de entrada de los hilos del API de
Windows. Su significado se verá más adelante.
El ejemplo anterior termina con un getchar al final de main debido a que, si main termina, el
proceso termina. Eso se debe a que, si bien técnicamente un proceso no finaliza hasta que todos
sus hilos finalizan, el hilo primario tiene un tratamiento especial en el sentido de que, si éste
finaliza, se fuerza la finalización de los hilos secundarios y el proceso termina. Podemos probar
esto quitando el getchar de main y corriendo el programa.
165
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
P R O G R A M A C I Ó N C O N C U R R E N T E
class Hilos0 {
public static void main(String args[]) {
for(int i = 0; i < 10; i++) {
Hilo hilo = new Hilo("Hilo" + i);
hilo.start();
}
}
}
En Java se hace uso de la clase Thread. Para crear un hilo se suele heredar de dicha clase y
sobrescribir el método run, colocando dentro de él todo el código que deseamos que el hilo
ejecute. El hilo se crea en estado creado y pasa al estado listo al llamar al método start. El método
run es el equivalente a la función de entrada del hilo en API de Windows, con la ventaja de ser
un método, por lo que todos los datos que se hubiera deseado pasar a su equivalente en API de
Windows, se pasan al crear el objeto de la clase derivada de Thread o bien llamando a sus
métodos y guardándolos como datos miembros del objeto. Esto ofrece un enfoque más orientado
a objetos para el manejo de hilos. Note que se llama a super en el constructor pasándole una
cadena, lo que establece un nombre al objeto, lo que es útil para efectos de depuración. Dicho
nombre se puede modificar y obtener con los métodos setName y getName respectivamente.
A diferencia del ejemplo de API de Windows, no se requiere de ningún artificio al final de main
para evitar que el hilo primario finalice sin darle oportunidad a los secundarios a terminar su
trabajo. Esto se debe a que cuando se retorna de main el intérprete de Java, quien es el que llama
a main, se encarga de esperar a todos los hilos que fueron creados hasta que finalicen y recién allí
finalizar el hilo primario y con él, el proceso.
En conclusión, el intérprete de Java realiza el siguiente algoritmo:
main(...);
if(hay_hilos_pendientes)
joinAll();
return;
class Hilos0 {
public static void Ejecutar() {
Thread hiloActual = Thread.CurrentThread;
Console.WriteLine("Hilo " + hiloActual.Name + " ejecutado");
}
public static void Main() {
for(int i = 0; i < 10; i++) {
Thread hilo = new Thread(new ThreadStart(Ejecutar));
hilo.Name = "Hilo" + i;
hilo.Start();
}
}
}
En C# se utiliza la clase Thread, de la cual “no se puede heredar”, dado que ha sido declarada
como sealed. Lo que se hace con ella es instanciarla pasándole como parámetro un objeto
166
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
P R O G R A M A C I Ó N C O N C U R R E N T E
delegado que será utilizado para llamar a su método cuando el hilo se ejecute. El método
relacionado con el objeto delegado hace el papel de la función de entrada del hilo. Este esquema
ofrece la ventaja de evitar una herencia y poder hacer que cualquier método, estático o no,
pertenecientes a la misma clase o a distintas, sean ejecutados por hilos distintos, todo esto sin
perder el enfoque orientado a objetos.
Al igual que Java, existe código que se ejecuta después de finalizado el método Main y que realiza
una espera hasta que todos los hilos secundarios finalicen, para luego finalizar el primario.
Método Descripción
Start( ) Coloca el hilo en estado “listo”, denominado “Running”. Tanto un hilo “listo”
como uno “en-ejecución” tienen en .NET el estado Running.
Join( ) Permiten que un hilo espere a que otro hilo finalice su ejecución.
Resume( ) Permite que un hilo saque del estado de suspensión a otro hilo.
167
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
P R O G R A M A C I Ó N C O N C U R R E N T E
Método Descripción
Pulse( ) Sacan del estado “WaitSleepJoin” a los hilos inactivos por dicho monitores.
PulseAll( )
Java
Todos los programas que se han mostrado como ejemplos hasta ahora han utilizado más de un
hilo, dado que, en paralelo con el hilo principal que corre nuestro programa, el intérprete de Java
dispara un hilo adicional, el garbage collector.
Sin embargo, a pesar de que uno de los objetivos principales del lenguaje Java es la portabilidad,
la implementación de hilos en Java no llega a ser, hasta ahora, independiente de la plataforma.
Esto se debe a las significativas diferencias entre los diversos sistemas operativos en la forma en
que implementan la multitarea.
La Clase Thread
Es la clase base para la creación de hilos. Cuando se crea un hilo se define una nueva clase que
deriva de Thread. Una vez que un nuevo objeto hilo ha sido creado, se utilizan los métodos
heredados de Thread para administrarlo.
Cuando se instancia un objeto de una clase de hilo su estado es creado (llamado también nacido).
Esto es equivalente a crear en C para Windows un hilo en estado suspendido.
Para que el hilo comience a correr, se debe de llamar a su método start, heredado de Thread. El
método start arranca el hilo, lo que en su momento llama al método run de Thread, cuya
implementación no hace nada dado que se espera que la clase derivada, en base a la que se creó
el objeto hilo, la sobrescriba. Es en run donde se debe de colocar el código que debe de ejecutar
las tareas del hilo. El método run es el equivalente a la función del hilo que se implementa en C
para Windows.
Al igual que en C para Windows, mientras se ejecuta el método run, el hilo pasa del estado listo al
estado en ejecución y nuevamente al estado listo repetidamente, hasta que finaliza este método, lo
que coloca al hilo en estado finalizado (también llamado muerto).
Tabla 8 - 3 Métodos de la clase Thread
Método Descripción
168
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
P R O G R A M A C I Ó N C O N C U R R E N T E
void run( ) Este método debe contener el código que ejecute las tareas
del hilo.
static void sleep( long millis )Pone a dormir a un hilo. El parámetro millis especifica un
static void sleep( long millis, tiempo en milisegundos. El parámetro nanos especifica un
int nanos ) tiempo en nanosegundos.
static void yield( ) Permite que un hilo voluntariamente renuncie a seguir siendo
ejecutado si es que existe algún otro hilo en estado listo.
void join( ) Permiten que un hilo espere a que otro hilo finalice su
void join( long millis ) ejecución. Al igual que sleep, los parámetros permiten
especificar un tiempo, en este caso, un tiempo máximo de
void join( long millis, int nanos espera. Si no se especifica un tiempo o éste es cero, se espera
) indefinidamente.
String toString( ) Permite obtener una descripción textual del hilo: Su nombre,
su prioridad y el grupo al que pertenece.
Los métodos estáticos están pensados para ser llamados desde dentro de la ejecución del hilo,
esto es, para realizar acciones sobre el hilo actual en ejecución. Estos métodos se pueden llamar
desde run o desde alguno de los métodos (de la misma clase o de otras clases) que éste llame.
Tome en cuenta que el método main es llamado por el método run del hilo primario.
El nombre de un hilo permite identificarlo dentro de un grupo de hilos, aunque dicha
característica puede no ser utilizada. Java maneja el concepto de grupo de hilos. El manejo de
los grupos de hilos así como sus semejanzas y diferencias con el concepto de proceso va más allá
de los alcances del curso.
Todo hilo tiene una prioridad. Java maneja prioridades en el rango de Thread.MIN_PRIORITY
(constante igual a 1) a Thread.MAX_PRIORITY (constante igual a 10). Por defecto, el hilo
primario de un programa (el que llama al método main de nuestra clase ejecutable) se crea con la
prioridad Thread.MIN_NORMAL (constante igual a 5). Cada nuevo objeto hilo que se cree
169
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
P R O G R A M A C I Ó N C O N C U R R E N T E
hereda la prioridad del objeto hilo desde donde se instanció. Ahora bien, es importante recordar
que Java se basa en las capacidades multitarea de la plataforma actual de ejecución, por lo que el
efecto de la modificación de las prioridades sobre el orden de ejecución de los hilos es
dependiente de la plataforma.
Un hilo en ejecución puede pasar a estar bloqueado, dormido o en espera. Estos estados pueden ser
interrumpidos por el mismo sistema operativo o por otro hilo (método interrupt). Cuando estos
estados se interrumpen, el hilo pasa al estado listo, de forma que pueda tomar alguna acción,
como consecuencia de la interrupción, cuando el sistema operativo lo pase al estado en ejecución.
El hilo guarda en un dato miembro interno el estado interrumpido, de forma que se pueda
averiguar este hecho. Un hilo puede averiguar si fue interrumpido llamando el método
isInterrupted. Este método no modifica el valor de este flag, a diferencia del método interrupted, que
sí lo hace, colocando el flag a false. Esto significa que dos llamadas consecutivas a interrupted,
luego de que el hilo fue interrumpido, devolverán true y false respectivamente, a menos que entre
llamada y llamada se haya interrumpido nuevamente al hilo. Este flag puede servir como una
forma de sincronizar el trabajo de un hilo con otro hilo.
Otra forma de sincronización de hilos es hacer que uno quede en estado de espera hasta que
otro haya finalizado su trabajo, esto es, pase al estado finalizado. Esto se consigue mediante los
métodos join.
Por último, el acceso a recursos compartidos y sincronización del trabajo entre los hilos se
realiza mediante los objetos monitores. El uso de estos se verá mediante ejemplos.
170
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
P R O G R A M A C I Ó N C O N C U R R E N T E
Parámetro Descripción
struct DatosHilo {
char Nombre[20];
DatosHilo(int i) {
sprintf(Nombre, "Hilo%d", i);
}
};
171
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
P R O G R A M A C I Ó N C O N C U R R E N T E
Al correr el programa la salida será parecida a de la figura 8.3. Note que el hilo primario,
correspondiente al método main, termina su trabajo antes de que cualquier hilo comience a
ejecutarse. Sin embargo, esto no necesariamente es así.
Note que, a diferencia de Java y C# (como se verá más adelante) la “aparente” finalización del
hilo primario sí finaliza el programa. Esto se debe a que en C++ los programas generados no
agregan ningún código especial que permita al hilo primario esperar hasta que todos los
secundarios hayan finalizado. Si se desea hacer esto tendrá que agregar a la función main (o a
funciones llamadas desde ésta) código explicito para esta tarea.
Si bien en la Figura 8 - 3 se muestra que los hilos inician su trabajo con cierto orden, mostrando
su mensaje, esto no necesariamente es así, por lo que no podemos asumir que el orden en que se
arrancan los hilos será el orden en que comiencen a ejecutarse. En general, el orden de ejecución
de los hilos es una decisión del sistema operativo en base a las políticas que implemente su
planificador.
Sincronización
En C++ utilizaremos secciones críticas para la sincronización de hilos. La sección crítica se
define dentro de una función delimitando las instrucciones que se quiere que solamente un hilo
a la vez las ejecute.
Cuando un grupo de instrucciones están delimitadas por una sección crítica y son ejecutadas desde
un hilo, ningún otro hilo puede acceder a éstas. Cuando un hilo entra a ejecutar una sección crítica,
ésta queda bloqueada. Esto implica que si un hilo intenta ejecutar esta sección crítica, quedará en
estado de espera hasta que ésta se desbloquee. Cuando un hilo sale de la sección crítica, ésta se
desbloquea, permitiendo que otros hilos accedan a ella. Si hubiese algún hilo esperando una
llamada pendiente a la sección crítica, el hilo pasa al estado listo, de forma que cuando entre al
estado en ejecución pueda ejecutar el código de la sección crítica.
El siguiente ejemplo muestra el uso de una sección crítica.
#include <windows.h>
#include <stdio.h>
#include <time.h>
class Productor {
int Trabajo;
CRITICAL_SECTION cs;
public:
Productor() {
Trabajo = 0;
InitializeCriticalSection(&cs);
}
~Productor() {
DeleteCriticalSection(&cs);
172
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
P R O G R A M A C I Ó N C O N C U R R E N T E
}
int SiguienteTrabajo() {
EnterCriticalSection(&cs);
int Nuevo = Trabajo++;
Sleep( rand() % 1000 );
LeaveCriticalSection(&cs);
return Nuevo;
}
};
class DatosConsumidor {
char Nombre[20];
Productor* p;
public:
DatosConsumidor(char* Nombre, Productor* p) {
strcpy(this->Nombre, Nombre);
this->p = p;
}
void Ejecutar() {
for( int i = 0; i < 5; i++ ) {
int iTrabajo = p->SiguienteTrabajo();
printf("%s: Obtenido trabajo %d\n", Nombre, iTrabajo);
Sleep(rand() % 1000);
printf("%s: Trabajo %d completado\n", Nombre, iTrabajo);
}
}
};
DWORD dwID;
Productor* p = new Productor();
CreateThread(NULL, 0, FuncionHilo, new DatosConsumidor("Hilo1", p), 0, &dwID);
CreateThread(NULL, 0, FuncionHilo, new DatosConsumidor("Hilo2", p), 0, &dwID);
getchar();
//delete p; // este delete es riesgoso, hay un problema de sincronización
return 0;
}
173
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
P R O G R A M A C I Ó N C O N C U R R E N T E
class DatosHilo {
bool Finalizar;
public:
DatosHilo() {
Finalizar = false;
}
void Finaliza() {
Finalizar = true;
}
void Ejecutar() {
int Trabajo = 0;
while( !Finalizar ) {
Trabajo++;
printf("Hilo: Inicio de trabajo %d\n", Trabajo );
Sleep(rand() % 3000);
printf("Hilo: Finalizo trabajo %d\n", Trabajo );
}
}
};
174
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
P R O G R A M A C I Ó N C O N C U R R E N T E
DWORD dwID;
DatosHilo* Datos = new DatosHilo();
HANDLE hHilo = CreateThread(NULL, 0, FuncionHilo, Datos, 0, &dwID);
A continuación en la figura 8.6 se muestra la salida del programa. Note que el hilo primario, el
que ejecuta al método main, espera a que el nuevo hilo creado finalice para continuar.
class DatosHilo {
private static Random r = new Random();
public void Ejecutar() {
Thread HiloActual = Thread.CurrentThread;
int tiempo = r.Next(5000);
Console.WriteLine("Hilo " + HiloActual.Name + " se va a dormir");
Thread.Sleep(tiempo);
Console.WriteLine("Hilo " + HiloActual.Name + " se desperto" );
}
}
175
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
P R O G R A M A C I Ó N C O N C U R R E N T E
class Hilos1 {
public static void Main() {
DatosHilo datos1, datos2, datos3, datos4;
Thread hilo1, hilo2, hilo3, hilo4;
datos1 = new DatosHilo();
datos2 = new DatosHilo();
datos3 = new DatosHilo();
datos4 = new DatosHilo();
hilo1 = new Thread(new ThreadStart(datos1.Ejecutar));
hilo2 = new Thread(new ThreadStart(datos2.Ejecutar));
hilo3 = new Thread(new ThreadStart(datos3.Ejecutar));
hilo4 = new Thread(new ThreadStart(datos4.Ejecutar));
hilo1.Name = "Hilo1";
hilo2.Name = "Hilo2";
hilo3.Name = "Hilo3";
hilo4.Name = "Hilo4";
Console.WriteLine( "Arrancando los hilos ..." );
hilo1.Start();
hilo2.Start();
hilo3.Start();
hilo4.Start();
Console.WriteLine( "Hilos arrancados. Fin de main.\n" );
}
}
Al correr el programa la salida será parecida a de la figura 8.7. Note que el hilo primario,
correspondiente al método main, termina su trabajo antes de que cualquier hilo comience a
ejecutarse. Sin embargo, esto no necesariamente es así.
Note que la finalización del hilo primario no finaliza el programa. El programa finaliza cuando
todos los hilos creados finalizan.
Si bien en la Figura 8 - 7 se muestra que los hilos inician su trabajo con cierto orden, mostrando
su mensaje, esto no necesariamente es así, por lo que no podemos asumir que el orden en que se
arrancan los hilos será el orden en que comiencen a ejecutarse. En general, el orden de ejecución
de los hilos es una decisión del sistema operativo en base a las políticas que implemente su
planificador.
Como se explicó, un hilo bloqueado, dormido o en espera puede ser interrumpido y sacado de este
estado. Debido a esto, las llamadas a los métodos que producen estos estados disparan
excepciones adecuadas para los casos en que estos estados son interrumpidos. El manejo de
estos cambios de estado y otras acciones con hilos utilizan ampliamente las excepciones para su
control. En el caso del método Interrupt se produce la excepción
System.Threading.ThreadInterruptedException.
El siguiente programa muestra la interrupción de los hilos.
using System;
using System.Threading;
176
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
P R O G R A M A C I Ó N C O N C U R R E N T E
class DatosHilo {
private static Random r = new Random();
private Thread hilo;
public Thread Hilo {
get { return hilo; }
}
public DatosHilo(string Nombre) {
hilo = new Thread(new ThreadStart(Ejecutar));
hilo.Name = Nombre;
Console.WriteLine( "Hilo " + hilo.Name + " creado" );
}
public void Ejecutar() {
int Tiempo = r.Next(5000);
Console.WriteLine("Hilo " + hilo.Name + " se va a dormir");
Thread.Sleep(Tiempo);
Console.WriteLine("Hilo " + hilo.Name + " se desperto");
}
}
class Hilos2 {
public static void Main() {
DatosHilo datos1, datos2, datos3, datos4;
datos1 = new DatosHilo( "Hilo1" );
datos2 = new DatosHilo( "Hilo2" );
datos3 = new DatosHilo( "Hilo3" );
datos4 = new DatosHilo( "Hilo4" );
Console.WriteLine("Arrancando los hilos ...");
datos1.Hilo.Start();
datos2.Hilo.Start();
datos3.Hilo.Start();
datos4.Hilo.Start();
Console.WriteLine("Hilos arrancados. El hilo primario se va a dormir.\n");
Thread.Sleep( 2500 );
Console.WriteLine("Interrunpiendo los hilos ...");
datos1.Hilo.Interrupt();
datos2.Hilo.Interrupt();
datos3.Hilo.Interrupt();
datos4.Hilo.Interrupt();
Console.WriteLine("\nHilos interrumpidos. Fin de main.\n");
Console.ReadLine();
}
}
177
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
P R O G R A M A C I Ó N C O N C U R R E N T E
Sincronización
C# utiliza la clase Monitor para la sincronización de hilos. La clase Monitor mediante sus
métodos estáticos delimita una sección crítica. Esto implica que en cualquier método se puede
definir una zona crítica.
Cuando un grupo de instrucciones están delimitadas por una sección crítica y son ejecutadas desde
un hilo, ningún otro hilo puede acceder a éstas. Cuando un hilo entra a ejecutar una sección crítica,
ésta queda bloqueada. Esto implica que si un hilo intenta ejecutar esta sección crítica, quedará en
estado de espera hasta que ésta se desbloquee. Cuando un hilo sale de la sección crítica, ésta se
desbloquea, permitiendo que otros hilos accedan a ella. Si hubiese algún hilo esperando una
llamada pendiente a la sección crítica, el hilo pasa al estado listo, de forma que cuando entre al
estado en ejecución pueda ejecutar el código de la sección crítica.
El siguiente ejemplo muestra el uso de una sección crítica.
using System;
using System.Threading;
class Productor {
public static Random Rand = new Random();
private int Trabajo = 0;
class DatosConsumidor {
public readonly Thread Hilo;
private Productor P;
178
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
P R O G R A M A C I Ó N C O N C U R R E N T E
crítica delimitando sus instrucciones, sólo un hilo podrá obtener un trabajo a la vez, por lo que
no habrá pérdida de trabajos.
Ahora bien, que pasaría en el caso de tener un productor que produce indiferentemente de que
existan consumidores que consuman dichos trabajos, y a la vez consumidores que consuman
indiferentemente de que exista algo que consumir. Si el productor elimina un trabajo para crear
uno nuevo antes de que el anterior sea consumido, se perderá dicho trabajo. Si el consumidor
consume un trabajo antes de que se haya producido uno nuevo, un trabajo será realizado dos o
más veces. En este caso, ambas labores, la de producir y la de consumir deben de sincronizarse
una con otra, esto es, el productor no debe seguir produciendo mientras que no se haya
consumido el trabajo anterior y el consumidor debe esperar a que se produzca un nuevo trabajo
antes de consumirlo.
En este tipo de situación, la sincronización utilizando la clase Monitor por sí sola no es suficiente.
Se necesita que el hilo productor espere y notifique al consumidor, y éste a su vez espere y
notifique al productor.
Para esto, la clase Monitor provee los método Wait, Pulse y PulseAll. El método Wait coloca al hilo
en estado de espera y desbloquea al monitor. El hilo saldrá de dicho estado cuando otro hilo
acceda y llame a Pulse o PulseAll. Es ese momento el hilo será colocado en estado listo y
competirá con el resto de hilos que intentan acceder a algún sección crítica de algún método (o que
también hayan salido del estado de espera) para bloquearlo y ejecutar su código.
El método Pulse saca al primero de los hilos que esté en la lista de espera, pasándolo del estado
esperando al estado listo. De esta forma el hilo vuelve a entrar a competir por el acceso al objeto
monitor.
El método PulseAll saca a todos los hilos que esté en la lista de espera, pasándolos del estado
esperando al estado listo.
El siguiente ejemplo muestra el uso de los métodos Wait y Pulse.
using System;
using System.Threading;
class Trabajo {
private int NroTrabajo = -1;
private bool PuedoCrear = true; // determina si se puede crear o consumir
179
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
P R O G R A M A C I Ó N C O N C U R R E N T E
PuedoCrear = true;
Monitor.Pulse(this); // notifico que ya se obtuvo el trabajo actual
Console.WriteLine( Thread.CurrentThread.Name + " obtuvo el trabajo " +
NroTrabajo );
Monitor.Exit(this);
return NroTrabajo;
}
}
class Productor{
private Trabajo t;
public Thread Hilo;
public Productor( Trabajo t ) {
Console.WriteLine("Productor");
this.t = t;
Hilo = new Thread(new ThreadStart(run));
Hilo.Name = "Productor";
}
t.CrearNuevo( Contador );
}
class Consumidor {
private Trabajo t;
public Thread Hilo;
180
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
P R O G R A M A C I Ó N C O N C U R R E N T E
p.Hilo.Start();
c.Hilo.Start();
Console.ReadLine();
}
}
También es posible hacer que un hilo quede bloqueado hasta que otro hilo pase a estado
finalizado. Para esta labor se utiliza el método Join. El siguiente ejemplo muestra el uso de dicho
método.
using System;
using System.Threading;
class DatosHilo {
public static Random Rand = new Random();
public readonly Thread Hilo;
private bool Finalizar = false;
181
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
P R O G R A M A C I Ó N C O N C U R R E N T E
Hilo.Name = Nombre;
}
public void Finaliza() {
Finalizar = true;
}
public void Ejecutar() {
int Trabajo = 0;
while( !Finalizar ) {
Trabajo++;
Console.WriteLine(Hilo.Name + ": Inicio de trabajo " + Trabajo );
Thread.Sleep(Rand.Next(3000));
Console.WriteLine(Hilo.Name + ": Finalizo trabajo " + Trabajo );
}
Console.WriteLine(Hilo.Name + ": Finalizo el hilo");
}
}
Console.WriteLine(
"Hilo primario se despierta. Esperando a que finalice el hilo
secundario ...");
datos.Finaliza();
datos.Hilo.Join();
Al ejecutar este programa se obtendrá una salida como la que se muestra en la Figura 8 - 11.
182
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
P R O G R A M A C I Ó N C O N C U R R E N T E
hilo1.start();
hilo2.start();
hilo3.start();
hilo4.start();
Note que el hilo primario, correspondiente al método main, termina su trabajo antes de que
cualquier hilo comience a ejecutarse. Sin embargo, esto no necesariamente es así. Otra ejecución
podría arrojar lo mostrado en la Figura 8 - 13.
183
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
P R O G R A M A C I Ó N C O N C U R R E N T E
Note que la finalización del hilo primario no finaliza el programa. El programa finaliza cuando
todos los hilos creados finalizan.
Si bien en ambos ejemplos los hilos inician su trabajo en orden, mostrando su mensaje going to
sleep, esto no necesariamente es así, por lo que no podemos asumir que el orden en que se
arrancan los hilos será el orden en que comiencen a ejecutarse. En general, el orden de ejecución
de los hilos es una decisión del sistema operativo en base a las políticas que implemente su
planificador.
Note que la llamada al método sleep se coloca dentro de un bloque try, dado que este método
puede disparar la excepción InterruptedException. Como se explicó en la sección anterior, un hilo
bloqueado, dormido o esperando puede ser interrumpido y sacado de este estado. Debido a esto, las
llamadas a los métodos que producen estos estados disparan excepciones adecuadas para los
casos en que estos estados son interrumpidos. El manejo de estos cambios de estado y otras
acciones con hilos utilizan ampliamente las interrupciones para su control. Estas excepciones
son No-Runtime, por lo que el no manejarlas (con bloques try o indicándolas en la lista throws del
método) provocarían un error en tiempo de compilación.
El siguiente programa muestra la interrupción de los hilos.
class MiHilo extends Thread
{
private int sleepTime;
Thread.sleep( sleepTime );
184
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
P R O G R A M A C I Ó N C O N C U R R E N T E
}
}
hilo1.start();
hilo2.start();
hilo3.start();
hilo4.start();
Note que sólo el hilo “Hilo4” logra finalizar su trabajo antes de que sea interrumpido, los demás
hilos son interrumpidos antes. En este caso en particular, dado que no se crea ningún recurso
185
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
P R O G R A M A C I Ó N C O N C U R R E N T E
dentro del bloque try que no pueda ser liberado automáticamente por el intérprete (o alguna otra
acción importante), no es necesario definir un bloque finally que se encargue de ello.
Note que el método main obtiene una referencia al hilo que lo ejecuta utilizando el método
estático currentThread de la clase Thread. Con esta referencia llama al método sleep. Sin embargo,
dado que este método es estático, también se pudo utilizar:
Thread.sleep( 2500 );
El tiempo que se pasa como parámetro al método sleep, y a otros métodos de Thread que
requieran una especificación de tiempo, se dan en milisegundos.
Note que el método main no desea manejar la interrupción que pueda disparar sleep, por lo que
declara dicha interrupción en la lista throws de su declaración.
Sincronización
Java utiliza objetos monitores para la sincronización de hilos. Un objeto monitor es aquel que
contiene métodos declarados con el modificador synchronized, sin importar de qué clase sea el
objeto. Esto implica que cualquier objeto es susceptible de ser utilizado como un monitor.
Cuando un método synchronized de un objeto es llamado desde un hilo, ningún otro hilo puede
acceder a éste u otro método synchronized del mismo objeto. Cuando un hilo entra a ejecutar un
método synchronized de un objeto, el objeto queda bloqueado. Esto implica que si un hilo llama a
un método synchronized de un objeto bloqueado, quedará en estado de espera hasta que éste se
desbloquee. Cuando un hilo sale de la ejecución de un método synchronized de un objeto, éste se
desbloquea, permitiendo que otros hilos accedan a él. Si hubiese algún hilo esperando una
llamada pendiente a un método synchronized de este objeto, el hilo pasa al estado listo, de forma
que cuando entre al estado en ejecución pueda ejecutar el código del método.
En resumen, un hilo que accede a ejecutar un método synchronized de un objeto, bloquea a dicho
objeto para el resto de los hilos. Sólo un método synchronized de un objeto puede ser ejecutado a
la vez por un hilo. Sin embargo, el resto de hilos sí pueden acceder a los métodos de dicho
objeto que no sean synchronized. Esto permite una fácil sincronización en el acceso a los recursos
del objeto monitor. Esta técnica es equivalente al uso de secciones críticas en la programación en C
para Windows.
El siguiente ejemplo muestra el uso de un monitor.
class Productor
{
private int iTrabajo = 0;
return iTrabajo;
}
}
186
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
P R O G R A M A C I Ó N C O N C U R R E N T E
Productor p;
HiloConsumidor( Productor p )
{
this.p = p;
}
try
{
// simulo un tiempo de trabajo
sleep( ( int )( Math.random() * 1000 ) );
187
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
P R O G R A M A C I Ó N C O N C U R R E N T E
un tiempo aleatorio entregarlo. Sin embargo, dado que el método de entrega de un trabajo,
SiguienteTrabajo, es declarado synchronized, sólo un hilo podrá obtener un trabajo a la vez, por
lo que no habrá pérdida de trabajos.
Para ejemplificar mejor esta sincronización, la salida en la Figura 8 - 16 muestra lo que podría
ocurrir si el método SiguienteTrabajo no se sincronizara:
Note como algunos trabajos se pierden y otros se realizan dos veces. Esto se debe a que la
instrucción:
iTrabajo++;
Es ejecutada, en ciertas ocasiones, por más de un hilo antes de que el método SiguienteTrabajo
retorne.
Note el nombre que asigna el constructor por defecto de Thread a los hilos, dado que no se ha
llamado a algún constructor con parámetros de Thread para darle uno.
Ahora bien, que pasaría en el caso de tener un productor que produce indiferentemente de que
existan consumidores que consuman dichos trabajos, y a la vez consumidores que consuman
indiferentemente de que exista algo que consumir. Si el productor elimina un trabajo para crear
uno nuevo antes de que el anterior sea consumido, se perderá dicho trabajo. Si el consumidor
consume un trabajo antes de que se haya producido uno nuevo, un trabajo será realizado dos o
más veces. En este caso, ambas labores, la de producir y la de consumir deben de sincronizarse
una con otra, esto es, el productor no debe de seguir produciendo mientras que no se haya
consumido el trabajo anterior y el consumidor debe de esperar a que se produzca un nuevo
trabajo antes de consumirlo.
En este tipo de situación, la sincronización utilizando métodos synchronized por sí sola no es
suficiente. Se necesita que el hilo productor espere y notifique al consumidor, y éste a su vez
espere y notifique al productor.
Para esto, la clase Object, de la que hereda cualquier objeto que se utilice como monitor, provee
los método wait, notify y notifyAll. Estos métodos sólo pueden ejecutarse dentro de un método
declarado synchronized, o en algún método llamado desde uno declarado synchronized. El no
hacerlo así provocaría un error en tiempo de ejecución. Esto significa que estos métodos sólo
pueden ser ejecutados desde un objeto monitor, por el hilo que haya bloqueado a este objeto.
188
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
P R O G R A M A C I Ó N C O N C U R R E N T E
El método wait coloca al hilo en estado de espera y desbloquea al objeto monitor. El hilo saldrá de
dicho estado cuando otro hilo acceda a algún método synchronized del mismo objeto monitor y
llame a notify o notifyAll. En ese momento el hilo será colocado en estado listo y competirá con el
resto de hilos que intentan acceder a algún método synchronized de dicho objeto monitor (o que
también hayan salido del estado de espera) para bloquearlo y ejecutar su código.
El método notify saca al primero de los hilos que esté en la lista de espera del objeto monitor, del
estado esperando y lo coloca en estado listo. De esta forma el hilo vuelve a entrar a competir por el
acceso al objeto monitor.
El método notifyAll saca a todos los hilos que esté en la lista de espera del objeto monitor, del
estado esperando y los coloca en estado listo.
Tome en cuenta que sólo un hilo puede acceder a ejecutar el código de un método synchronized de
un objeto monitor. Luego, aún cuando exista más de un hilo esperando a ejecutar un método
synchronized y otros tantos que hayan sido sacados del estado de espera para el mismo objeto
monitor, el primero que entre a en ejecución bloqueará al objeto monitor, lo que colocará a todos
los demás hilos en estado esperando. Cuando el hilo ganador termine la ejecución de dicho
método, todos los hilos que están esperando volverán al estado listo, de forma que vuelvan a
competir por el acceso al objeto monitor.
El siguiente ejemplo muestra el uso de los métodos wait y notify.
class Trabajo
{
private int NroTrabajo = -1;
private boolean PuedoCrear = true; // determina si se puede crear o consumir
System.out.println( Thread.currentThread().getName() +
" creo el trabajo " + NroTrabajo );
this.NroTrabajo = NroTrabajo;
PuedoCrear = false;
notify(); // notifico que existe un trabajo listo
}
PuedoCrear = true;
notify(); // notifico que ya se obtuvo el trabajo actual
System.out.println( Thread.currentThread().getName() +
" obtuvo el trabajo " + NroTrabajo );
return NroTrabajo;
}
}
189
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
P R O G R A M A C I Ó N C O N C U R R E N T E
t.CrearNuevo( Contador );
}
p.start();
c.start();
}
}
190
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
P R O G R A M A C I Ó N C O N C U R R E N T E
Note que a pesar de que tanto el productor como el consumidor tratan de crear y obtener
respectivamente un trabajo en distinto orden, éstos son producidos y consumidos en el orden en
que son creados gracias a la sincronización mediante wait y notify.
Note además que en algún momento el productor, por ejemplo, podría utilizar su propia
notificación en su llamada a wait. Sin embargo, dado que se utiliza un flag, PuedoCrear, el hilo del
productor nunca saldrá del bucle de espera hasta que efectivamente el trabajo haya sido retirado
por el consumidor.
En ocasiones es necesario que un hilo verifique que otro haya finalizado para poder continuar
con su trabajo. En este caso se utiliza el método join. Un hilo que llame al método join
utilizando la referencia de otro hilo, pasará al estado esperando hasta que dicho hilo termine, esto
es, pase al estado finalizado. Cuando esto sucede, el hilo que espera pasa al estado listo.
El siguiente ejemplo muestra el uso del método join.
class MiHilo extends Thread
{
private boolean bFinalizar = false;
while( !bFinalizar )
{
iTrabajo++;
191
L E N G U A J E S D E P R O G R A M A C I Ó N 2 : T E M A S D E E S T U D I O
P R O G R A M A C I Ó N C O N C U R R E N T E
}
}
try
{ // simulo un tiempo de trabajo
Thread.sleep( ( int )( Math.random() * 3000 ) );
// reporto el fin
System.out.println( "Metodo main finalizo" );
}
}
Note que el hilo primario, el que ejecuta al método main, espera a que el nuevo hilo creado
finalice para continuar. Al igual que sleep, join también puede ser interrumpido y arrojar una
excepción, por lo que se le ha colocado dentro del bloque try.
Note también que no es necesario definir un constructor en una clase hilo, dado que Thread
posee un constructor por defecto. El único método que siempre debe de implementarse es run.
Aunque todos los ejemplos mostrados son aplicaciones de consola, el uso de hilos se aplica a
todo tipo de programa en Java.
192
Referencias
Libro “Programación Orientada a Objetos con C++”; autor Francisco Javier Cevallos;
editorial Alfaomega; edición 3ra; año 2004; capítulo 6.
Libro “Essential C# 2.0”; autor Mark Michaelis; editorial Addison-Wesley Professional;
edición 1ra, año 2006.
Libro “C++ Como Programar”; autores Harvey M. Deitel y Paul J. Deitel; editorial
Pearson; edición 4ta; año 2003; capítulos 11 y 21.
Libro en-línea gratuito “Introduction to Programming Using Java”; autor David J. Eck;
versión 4.0; fecha julio 2002; sección 12.1.
Documentación de Microsoft Visual Studio 2005.
Documentación de la Api de Java.
http://www.faqs.org/docs/javap/c12/s1.html
Página Web “Introduction to C# Generics Tutorial”.
http://www.deitel.com/articles/csharp_tutorials/20051111/Intro_CSharpGenerics.ht
ml
Página Web “The Collections Framework”.
http://java.sun.com/j2se/1.5.0/docs/guide/collections/index.html
Página Web “Generics”; tutorial de Sun.
http://java.sun.com/j2se/1.5.0/docs/guide/language/generics.html
Página Web “Generics in C#, Java, and C++: A Conversation with Anders Hejlsberg,
Part VII”; artículo; autores Bill Venners y Bruce Eckel; fecha enero 2004.
http://www.artima.com/intv/generics.html
Página Web “C++ Notes: Table of Contents”.
http://www.fredosaurus.com/notes-cpp/index.html
Página Web “A modest STL tutorial”.
http://www.cs.brown.edu/people/jak/proglang/cpp/stltut/tut.html
Página Web “The C++ Standard Library - A Tutorial and Reference”.
http://www.josuttis.com/libbook/idx.html
Página Web “Overview of Generics in the .NET Framework”.
http://windowssdk.msdn.microsoft.com/en-us/library/ms172193.aspx
Página Web “Generic Programming”; autor David R. Musser.
http://www.cs.rpi.edu/~musser/gp/