Download APUNTES FUNDAMENTOS DE PROGRAMACIÓN

Document related concepts
no text concepts found
Transcript
APUNTES
FUNDAMENTOS DE PROGRAMACIÓN
Primera parte
Grado en Ingeniería en Sistemas de Telecomunicación
Grado en Ingeniería Telemática
Área LSI
Curso 2010-2011
Índice
i
Índice
Introducción
1
Computadores y lenguajes de programación
5
Abstracción de datos
31
Abstracción de control
59
Estructuras estáticas de datos. Arrays.
81
Abstracción Funcional. Métodos.
99
Introducción
Introducción
1
Para empezar: lo que tienen entre manos no es un libro, sino unos apuntes, que transcriben,
completándolo ligeramente, el discurso de las clases teóricas de la asignatura. Escribir un libro es
una tarea abrumadora, sujeta a críticas no siempre constructivas y frecuentemente poco
agradecida. Por consiguiente está reservada a los más valientes y generosos. En los apuntes no se
cuenta nada que no esté contado en los libros, ni se cuenta mejor que en ellos. ¿Por qué entonces
unos apuntes y no un libro? En primer lugar, aunque hay excelentes libros de programación, la
mayor parte de ellos o no se adaptan al temario de la asignatura o no se adaptan a la forma elegida
para impartirla. En segundo lugar, cada libro trata a su manera los diferentes temas, siendo unos
más apropiados que otros para abordar según que parte de la asignatura. En cursos anteriores, bien
al principio del curso o bien cada vez que se impartía un tema, se facilitaban las referencias
bibliográficas más adecuadas para estudiarlo. Por supuesto, se va a seguir haciendo, porque es muy
conveniente y formativo que los alumnos consulten referencias bibliográficas. Sin embargo, la
experiencia demuestra que, salvo excepciones, de la forma en que se facilitaban tales referencias,
los alumnos no las consultaban en absoluto. No es algo sorprendente, habituados como están a un
libro de texto, el manejo de diferentes fuentes para estudiar una asignatura supone una dificultad
añadida, una piedra más en el camino. Los alumnos carecen todavía del entrenamiento necesario
para asimilar informaciones que explican lo mismo, pero desde diferentes puntos de vista, y para
discriminar lo que es esencial para aprobar la asignatura de lo que no lo es. Los alumnos (y el
profesor) prefieren un libro, pero puesto que aún no hemos encontrado ese libro se han facilitado
unos apuntes que explican de forma coherente toda la asignatura y que, ayudados en ocasiones por
referencias bibliográficas muy concretas, sirven para estudiarla de principio a fin. A falta de pan…
Los apuntes son, como ya se ha dicho, una transcripción de las clases de teoría y están
confeccionados a partir de las referencias bibliográficas utilizadas para preparar las clases. A veces
son transcripciones literales o resumidas de los párrafos de un libro. Cuando esto es así se informa
al alumno de la fuente o fuentes principales, para que pueda acudir a ellas si los apuntes no le
satisfacen. En clase se dirá cuando es conveniente acudir a las fuentes porque los apuntes no
alcancen el nivel suficiente. A veces, para introducir un tema hay que adelantar un poco de lo que
hay en otros temas posteriores. En un lenguaje de programación todos los elementos están muy
interrelacionados, los conceptos se van apoyando unos en otros y las distintas facetas del
conocimiento se van construyendo en paralelo adquiriendo todo su sentido cuando se ensamblan
entre sí. Pero como todo no puede explicarse al mismo tiempo, el que explica debe contenerse y el
que escucha debe tener paciencia.
Otro aspecto del que deben estar advertidos es que al principio la Programación se hace bastante
difícil a la mayoría de los alumnos. No es que sea más complicada o más abstracta que las
Matemáticas o la Física, de hecho es bastante más concreta y sencilla, pero al contrario que éstas es
una novedad. Para animarles, me permito incluirles el siguiente párrafo, que, siendo fiel al espíritu
de los apuntes, no es una aportación original, sino que está tomado literalmente de “Informática
Aplicada. Programación en Lenguaje C”. Este libro está disponible en la biblioteca digital de la UPCT.
El párrafo dice así:
Hay una recomendación clásica, que los profesores de programación repetimos
frecuentemente a los alumnos: en el aprendizaje de un lenguaje de programación hay que
programar: oír permite olvidar; estudiar permite entender; sólo programar permite
aprender. No basta con ir a clase. No basta con estudiar este ladrillo. Hay que instalar un
compilador y un editor de programas y ponerse delante de la pantalla. Y programar.
El aprendizaje de la programación es una tarea inicialmente ingrata. Las horas de trabajo y
estudio no parecen cundir. A una hora se sucede otra, en la que el aprendiz no entiende
apenas sobre qué se le está instruyendo. Y otra hora…, y otra…, y otra… Y uno se enfrenta al
primer programa —una auténtica simpleza—y no logra más que una colección de 26 errores.
Introducción
2
¡Veintiséis!: ¡en tan sólo cinco líneas de código! Hay que seguir. Y detectar el punto y coma que
falta en esa línea; y el paréntesis que en realidad debía ser una llave; y la biblioteca que no sé
bien para qué sirve y que para colmo el compilador no me la reconoce, porque la he
deletreado mal; y mil pedradas más que animan a cualquier cosa menos a seguir trabajando
con el dichoso lenguaje C 1. Todos hemos tenido que aprender a programar. Para todos, esos
primeros pasos han sido una personal pesadilla.
Cada año, en el primer con los nuevos alumnos, dibujo en la pizarra del aula una gráfica que
muestra el rendimiento del estudio de un lenguaje de programación. Es una curva a la que le
cuesta levantarse del eje de las abscisas. Muestra, en su primera fase, una desesperante
pendiente casi nula. Pero, en un entorno de valores determinado, distinto para cada
estudiante, esa gráfica sufre un cambio de pendiente. Casi de forma súbita: estudio y no
comprendo… estudio y no comprendo… estudio y… ¡ah!, ¡ahora lo veo!: un pequeño avance. Y
con el paso de las horas, de repente se clarifican un bloque de conceptos, casi de golpe. Y el
estudio cambia de cariz: el aprendiz se pica con la programación. Ha llegado el momento de
avanzar a buen ritmo. Se disfruta aprendiendo, porque se logran cosas nuevas, cada vez más
difíciles. Y se aprende a programar. Se caza el modo en que se deben plantear las cosas al
ordenador. Se coge la filosofía de cómo se construyen los programas. A uno se le ocurren
nuevos retos, y se atreve a intentar buscar una solución. Y se aprende entonces a programar.
Y, por supuesto, se aprueba la asignatura.
Lo que he contado en estas líneas previas es estadísticamente cierto. Porque el que no llega a
franquear el tiempo de estudio de inflexión, se queda en nada. Y el que lo supera, con poco
esfuerzo alcanza un buen nivel. Que programar no es difícil; pero el inicio es la más difícil de
las fases en su aprendizaje.
Fácil o difícil, deben ser conscientes de que aprender a programar requiere tiempo y esfuerzo. Para
realizar programas es necesario comprender los conceptos teóricos de la asignatura. Ciertamente,
la teoría y la práctica se realimentan entre sí: para aplicar un concepto hay que comprenderlo y
para comprenderlo casi siempre es necesario aplicarlo. Pero el alumno debe tener claro que su
proceso de aprendizaje comienza en el estudio de la teoría y termina en la comprensión de la teoría.
Es una base teórica sólida la que permite afrontar problemas diferentes y elegir en cada caso dónde,
cuándo y cómo aplicar las técnicas de programación que se presentan en el curso. Lo demás son
recetarios. Las prácticas y los problemas son necesarios para entender la teoría (“vi y recordé, hice y
comprendí”), pero el alumno debe estudiar la teoría y reflexionar sobre ella antes de aplicarla. Las
1
También vale para Java, pero hay que reconocer que C puede ser mucho más frustrante.
Introducción
3
prácticas y los problemas le permitirán después poner a prueba su conocimiento, revelarán
aspectos importantes no considerados en un primer estudio, pondrán en evidencia
interpretaciones equivocadas y suscitarán dudas que deberá consultar al profesor. Muchas veces,
los problemas con los que más se aprende son los que no han llegado a resolverse, pero han servido
para probar diferentes alternativas y para razonar sobre el uso y significado de los conceptos. El
aprendizaje requiere también reiteración: volver a aplicar una y otra vez los mismos principios, las
mismas técnicas, sobre problemas en apariencia muy diferentes. Prácticas y problemas ofrecen el
marco donde dicha reiteración es posible. Por último, tengan presente que el profesor puede
ayudarles a comprender, pero no puede liberarles de su obligación de estudiar, razonar y
preguntar.
Cartagena, 10 de septiembre de 2010
Tema 1: Computadores y lenguajes de programación.
Tema 1: Computadores
programación.
y lenguajes
5
de
Objetivos
Situar a la asignatura en el contexto de las tecnologías de la información y las comunicaciones.
Proporcionar conocimientos básicos sobre la informática, los sistemas informáticos, y los sistemas
operativos.
Introducir el concepto de programa, los lenguajes de programación y las herramientas para editar,
compilar, depurar y ejecutar programas.
Contenidos
Estructura y funcionamiento de un computador.
Lenguaje máquina y ensamblador.
Lenguajes de programación de alto nivel.
Paradigmas de programación.
Sistemas operativos.
Anexo: codificación de números enteros.
Tema 1: Computadores y lenguajes de programación.
¿Qué es programar?
6
El objetivo de la asignatura Fundamentos de Programación 1 es “transmitir conocimientos básicos
sobre el uso y programación de los ordenadores, sistemas operativos, bases de datos y programas
informáticos con aplicación en ingeniería”. La asignatura se centra casi en su totalidad en la
programación, puesto que el uso de los sistemas informáticos se aborda fundamentalmente desde la
perspectiva del programador. De hecho, se trata de que, al final del curso, el alumno conozca las
técnicas básicas de la programación para desarrollar programas correctos tanto desde el punto de
vista computacional como desde el punto de vista de su mantenimiento y uso. Esto implica conocer
y saber utilizar un lenguaje de programación concreto, siendo Java el lenguaje elegido.
El objetivo de la asignatura invita a comenzar esta introducción respondiendo a la pregunta ¿Qué es
programar? Ahí va una primera respuesta:
“Programar es decirle a un computador lo que tiene que hacer. “
Esta respuesta es tal vez demasiado simple para un proceso que, como iremos viendo, puede ser
muy complejo. No debemos tomarla demasiado en serio, sino como punto de partida para plantear
otras dos preguntas:
¿Qué es y cómo funciona un computador?
¿Cómo se le dice a un computador lo que tiene que hacer?
La primera pregunta se contesta con detalle en la asignatura Fundamentos de Computadores, pero
hace falta responderla brevemente para poder abordar la respuesta de la segunda en esta
asignatura. Por esta razón la asignatura comienza describiendo la estructura interna de un
computador y su funcionamiento.
1
Competencia específica B2 del plan de estudios de los grados que se imparten en la ETSIT.
Tema 1: Computadores y lenguajes de programación.
7
Estructura y funcionamiento de un computador1
Esquema básico de un computador
Empecemos con una definición:
“Un computador es una máquina automática programable que obtiene unos datos de
salida aplicando unas operaciones básicas predefinidas en él sobre unos datos de entrada”
Para poder realizar su función un computador dispone de diversos elementos que se muestran
esquemáticamente en la figura 1.
PROCESADOR
Unidad de Tratamiento
CPU Unidad de Control
rD
RT
RF
FF
reloj
Z
SP
S
rI
r0
ALU
C
V
AR
IR
MEMORIA
M
Lógica de
control
PC
Periférico
Entrada
Periférico
Salida
DR
Bus de direcciones
Bus de datos
Bus de control
Figura 1: Esquema básico de un computador
La figura 1 representa el esquema básico de la arquitectura típica de un computador. El elemento
principal es el procesador (CPU: Central Processing Unit) que a su vez se compone de una unidad de
tratamiento y una unidad de control. Además del procesador, el computador consta de memoria
interna, de diferentes tipos de buses (de datos, de direcciones y de control) y de elementos
periféricos.
La unidad de tratamiento (o camino de datos) está formada por la unidad aritmético-lógica (ALU:
Arithmetic and Logic Unit) y otros elementos auxiliares por donde se transmiten o almacenan
temporalmente los operandos de las operaciones aritméticas y lógicas. Suele contener un banco de
registros de propósito general (RF en figura 1) donde se almacenan los datos y resultados parciales
reduciéndose de esta manera los accesos a la memoria principal, que son siempre más lentos. La
longitud de estos registros suele ser la de una palabra de memoria o un múltiplo de ésta. Del
esquema de la figura 1 se puede deducir que la ALU opera con el contenido de uno de los registros
de RF y el contenido del registro RT y guarda el resultado en uno de los registros de RF. Además de
RT, en la figura 1 se representan otros dos registros con un significado especial 2:
Fuente principal [Prieto 06], capítulo 8.
En la figura 1 no se ha incluido uno de los registros más significativos de una ALU: el acumulador. Recuérdese que el
objetivo de este tema no es explicar de forma exhaustiva la arquitectura de un computador, sino preparar el terreno para
entender algunos conceptos que aparecen en temas posteriores. En otras asignaturas se explican con detalle los elementos
de los computadores.
1
2
Tema 1: Computadores y lenguajes de programación.
−
−
8
AR (Address Register): Registro de direcciones: Almacena la dirección del dato o instrucción
a leer o escribir.
DR (Data Register): Registro de datos: Almacena el dato a escribir en la memoria o en
puerto de salida o la información leída de la memoria o de un puerto de entrada.
La ALU dispone además de unos biestables de condición (FF: fip-flops) que se ponen a 1 ó a 0
dependiendo de la última operación realizada en la ALU. En la figura 1 se han representado algunos
de lo más típicos:
−
−
−
−
C: acarreo: indica si el resultado implica acarreo 1.
S: signo: indica el signo del resultado.
Z: cero: indica si el resultado es cero.
V: desbordamiento: indica si ha habido desbordamiento. El resultado es demasiado grande
para guardarse en una palabra.
La memoria está dividida en posiciones (denominadas también palabras de memoria) de un
determinado número de bits en las que se almacena la información. La entrada y salida de datos
puede hacerse a través de un único bus de datos bidireccional como en la figura 1 o a través de
buses de datos separados para la entrada y para la salida. Si la palabra de memoria tiene n bits los
buses de datos tendrán también n bits de forma que puedan leerse o escribirse en una sola
operación todos los bits de una posición de memoria. Además, la memoria dispone también de un
bus de direcciones de m bits, pudiendo direccionar 2m posiciones de memoria distintas. Como se ve,
el tamaño de la palabra determina el tamaño del bus de datos y el número de posiciones de
memoria el tamaño del bus de direcciones. También puede hacerse la interpretación inversa: la
capacidad máxima de la memoria viene determinada por el tamaño del bus de direcciones.
Capacidad
palabras =
bytes
Aunque los accesos a la memoria principal para leer o escribir una palabra son bastantes rápidos
(del orden de decenas de nanosegundos), son bastante más lentos que las operaciones que la ALU o
la unidad de control pueden realizar sobre el contenido de esa palabra (del orden de los
nanosegundos), por lo que estas unidades se ven frenadas muy considerablemente cuando tienen
que captar o escribir una palabra de memoria. Para paliar esto se utilizan los registros internos de
la ALU a pequeña escala y a mayor escala la memoria caché. La caché (no representada en la figura
1) es una memoria intermedia entre la memoria principal y la CPU unas 10 veces más rápida que la
memoria principal, pero mucho más cara y con unos consumos de energía mucho mayores. Por
estas razones la capacidad de la caché suele ser mucho menor que la de la memoria principal y se
usa para mantener las palabras más comúnmente usadas por la ALU y la unidad de control. Decidir
cuáles son estas palabras es un problema difícil de resolver de forma completamente satisfactoria y
cuyo estudio queda fuera del alcance de esta asignatura 2.
La unidad de control contiene la lógica de control, que está constituida por los circuitos que
decodifican las instrucciones para el computador (instrucciones máquina) y generan las distintas
señales de control para el resto de elementos del computador. La unidad de control dispone de un
reloj, que es un generador de pulsos, con los que se sincronizan todas las microoperaciones
implicadas en la ejecución de las distintas instrucciones máquina. Asimismo, en la unidad de control
vamos a encontrar siempre tres registros muy significativos:
−
−
1
2
IR (Instruction Register): Registro de instrucción. El IR almacena la instrucción que la
unidad de control está en ese momento interpretando o ejecutando.
PC (Program Counter): Contador de programa. El PC contiene la dirección en memoria
donde se encuentra la siguiente instrucción a ejecutar. Cuando termina de ejecutarse una
Acuérdense de cómo sumaban de pequeños. Por ejemplo: ocho más tres once y me llevo una… Esa una es el acarreo.
Se estudia en el contexto de los sistemas operativos.
Tema 1: Computadores y lenguajes de programación.
−
9
instrucción se carga en IR el contenido de la celda indicada en el PC y el PC cambia de valor
para indicar cuál será la nueva siguiente instrucción.
SP (Stack Pointer): Puntero de pila. El SP está relacionado con la gestión de las llamadas a
subrutinas y los retornos al punto de llamada 1.
La presentación de la unidad de control con su lógica de control, su reloj y sus registros especiales
nos permite abordar brevemente la ejecución de las instrucciones. El proceso, para el procesador
descrito en la figura 1, es en términos generales 2 como se explica a continuación:
Para iniciar la ejecución de un programa se almacena en el PC la dirección de memoria donde
comienza dicho programa.
−
Se transfiere el contenido del contador de programa al registro de direcciones. Lo
expresaremos así 3:
AR
−
−
← PC.
La unidad de control da orden de leer la instrucción almacenada en el AR
Pasado el tiempo de acceso a memoria la instrucción estará disponible en el bus datos y se
cargará en el registro de datos:
DR ← M(AR)
−
Posteriormente la instrucción se pasa al registro de instrucción:
IR ← DR
−
Y se incrementa el contador de programa para apuntar a la siguiente instrucción
PC ← PC + 1
Una vez que la instrucción se encuentra en el IR la unidad de control la decodifica y ejecuta. Si, por
ejemplo, la instrucción es: sumar el contenido de dos registros con un dato y dejar el resultado en
otro, la UC genera las señales de control para llevar el primer operando a RT , el otro a la entrada de
la ALU, realizar la suma y llevar el resultado al registro indicado.
Después de ejecutada la instrucción la unidad de control vuelve a repetir el ciclo anterior, es decir
capta una nueva instrucción, la lleva al IR y después la decodifica y ejecuta y así hasta que el
programa termina. Todas las instrucciones comienzan siempre con una fase de captación de la
instrucción y continúan con una fase de ejecución. El tipo de operaciones indicadas anteriormente
(carga de registros: AR ← PC, IR ← DR; lectura de memoria DR ← M(AR); incremento del contador
de programa PC ← PC + 1, y otras por el estilo) reciben el nombre de micro-operaciones o
microinstrucciones. Cada instrucción máquina implica la realización de un conjunto determinado
de micro-operaciones sincronizadas por las señales del reloj interno de la unidad de control.
Ejecución de un programa
Una vez que conocemos los componentes básicos de un computador estamos preparados para
entender su funcionamiento interno. Supongamos que tenemos un programa escrito en lenguaje
El SP es un elemento clave para la ejecución de programas, pero es pronto para explicar su funcionamiento. Cuando se
describan las subrutinas la función de este registro se hará evidente sin necesidad de grandes explicaciones.
2 Los detalles cambian de una máquina a otra y la figura 1 es tan esquemática que podría utilizarse para explicar secuencias
de funcionamiento un poco distintas a la elegida. Lo interesante es descubrir la lógica de la secuencia de funcionamiento y
hasta que punto pueden cambiar los detalles si cambia algún elemento de la arquitectura (p.e.: buses separados para entrada
y salida y para datos e instrucciones, segmentación de la unidad de control, longitud de la instrucción, etc.).
3 Diferentes autores utilizan diferentes convenciones para expresar la transferencia de información entre registros. La que
se emplea aquí es bastante común.
1
Tema 1: Computadores y lenguajes de programación.
10
máquina. Para simplificar supondremos que las instrucciones y datos son secuencias de ceros y
unos del tamaño de la palabra de memoria.
Lo primero que hay que hacer con el programa es cargarlo en memoria 1. Una vez cargado el
programa, se carga en el PC la dirección de su primera instrucción (podemos suponer que las
instrucciones del programa ocupan posiciones de memoria consecutivas). A partir de ese momento
la unidad de control repite sucesivamente las dos siguientes fases:
−
−
−
Fase de captación de instrucción: lleva la instrucción que está en la posición de memoria
almacenada en el PC de la memoria al registro IR e incrementa en 1 el PC.
Fase de ejecución de la instrucción: la unidad de control interpreta el código de la
instrucción y ejecuta las micro-operaciones correspondientes.
Ciertas instrucciones (instrucciones de salto y bifurcación y llamadas a subrutinas) pueden
alterar el orden secuencial del programa e implican saltar a una posición de memoria m. En
dicho caso se cambia el contenido del PC por el valor m, de forma que en la siguiente fase
de captación se ejecuta la instrucción que está en m.
Supongamos un lenguaje máquina con las siguientes instrucciones:
−
−
−
−
Entrada de información desde un dispositivo de entrada, IPv, a un registro, rx.
Código de la instrucción: 0100
Representación: IN rx, IPv
Salida de información desde un registro, rx, a un dispositivo de salida, OPv.
Código de la instrucción: 0101
Representación: OUT OPv, rx
Suma en la ALU de los registros ra y rd y ubica el resultado en rs.
Código de la instrucción: 0110
Representación: ADDS rs, ra, rd
Finalización del programa y espera de otra tarea.
Código de la instrucción: 1111
Representación: HALT
Con este repertorio de instrucciones se desea hacer un programa que sume dos números.
Supondremos que los números se introducen desde el teclado, dispositivo de entrada IP2, y que la
pantalla es el dispositivo de salida OP2. El programa resultante sería el mostrado en la tabla 1:
Tabla 1: Ejemplo de programa en código máquina.
Acciones
Instrucción máquina
Representación en
ensamblador
Introducir a través de teclado (IP2) el
primer dato y llevarlo al registro r0
0100 0000 0000 0010
IN r0, IP02
Idem segundo dato y llevarlo a r1.
0100 0001 0000 0010
IN r1, IP02
Sumar en la ALU los contenidos de los
registros r0 y r1 y llevar el resultado a
r3
0110 0011 0000 0001
ADDS r3, r0, r1
Sacar por la pantalla (OP2) el resultado,
que está en r3.
0101 0011 0000 0010
OUT OP2, r3
Fin del programa. Detención del
computador
1111 0000 0000 0000
HALT
Esta función la realiza el sistema operativo. Más adelante se darán algunas nociones sobre los sistemas operativos. De
momento confórmense con saber que es un programa o conjunto de programas que gestionan el software de un
computador.
1
Tema 1: Computadores y lenguajes de programación.
Instrucciones de control de flujo
11
Para acabar nuestro pequeño tour por la arquitectura de los computadores vamos a abordar un
último asunto. Hay dos tipos de instrucciones que provocan que se cambie el valor del contador de
programa alterando el orden secuencial de la ejecución del programa:
−
−
Bifurcaciones (o saltos condicionales) y saltos incondicionales.
Llamadas a subrutinas y retornos de las mismas.
Bifurcaciones y saltos.
Con estas instrucciones se puede alterar el orden de ejecución de un programa haciendo que el
contador de programa contenga la dirección de cualquier instrucción. El valor de esta dirección se
especifica en la propia instrucción de salto (direccionamiento directo) o en algún registro o
posición de memoria (direccionamiento indirecto).
−
Salto incondicional: Supóngase, por ejemplo, que en las posiciones de memoria 1A30 se tuviese
la instrucción de salto BR 32B7 (branch to 32B7) que significa saltar a la posición 32B7. La
ejecución de este tipo de instrucciones implica simplemente cambiar el contenido del contador
de programa por el valor indicado en la instrucción, en nuestro caso:
PC ← 32B7
−
Salto condicional: En las instrucciones de salto condicional, el salto sólo se produce si se cumple
una condición establecida por el valor de algún biestable o registro. Por ejemplo, la instrucción
BZ 32B7 (branch to 32B7 if Z=1) carga 32B7 en el contador de programa sólo si el valor del
biestable Z es uno).
Instrucciones de control de flujo que hacen referencia a subrutinas.
Una llamada a subrutina (también denominada rutina, subprograma o procedimiento) es una
ruptura de la secuencia normal de ejecución de las instrucciones de un programa de forma que tras
la ejecución de la instrucción de llamada se ejecuta otro programa, denominado subrutina. Una vez
ejecutada la subrutina se retorna, mediante la ejecución de una instrucción de retorno, al programa
desde el que se hizo la llamada. A su vez, una subrutina puede contener llamadas a otras subrutinas
y así sucesivamente.
Desde el punto de vista de la arquitectura del computador, la diferencia entre un salto y una
llamada a subrutina es que cuando una subrutina acaba debe retornarse al programa o subrutina
que llamó, concretamente a la instrucción inmediatamente posterior a la llamada. Esa instrucción
se encuentra en el PC y debe guardarse para ser utilizada en el momento oportuno. El
almacenamiento de las direcciones de retorno se realiza en una zona de memoria especial llamada
pila (stack). La pila funciona de tal manera que el último elemento en entrar es el primero en salir
(Estructura LIFO: Last in, First out). En la figura 2 pueden verse tres llamadas sucesivas a
subrutinas. Obsérvese que la recuperación de las direcciones se realiza en orden inverso a su
almacenamiento, lo que explica la utilización de una estructura LIFO para el mismo. Lo único que
debe hacer la unidad de control para producir un retorno es transferir la dirección almacenada en
la cabecera de la pila (dirección de retorno) al PC.
En algunos computadores la pila se implementa con circuitos específicos (pila hardware) lo cual
incrementa enormemente la velocidad de acceso. En la mayoría, la pila se gestiona en una zona de
la memoria principal y el procesador dispone de un registro específico llamado puntero de pila o SP
(stack pointer) que apunta a la cabecera de la pila, donde está almacenada la dirección de retorno.
Tema 1: Computadores y lenguajes de programación.
07CD
07CE
…
107A
107B
…
12
PC
10A3
PC
7CD9
PC
003C
Pila
107B
…
…
Pila
6FAC
107B
…
…
Pila
AB36
6FAC
107B
…
…
1
…
CALL 10A3
10A3
…
…
6FAB
6FAC
…
6FFF
2
…
CALL 7CD9
RET
6
2FFF
7CD9
…
…
AB35
AB36
…
AC55
3
…
CALL 003C
RET
4
5
PC
107B
PC
6FAC
PC
AB36
Pila
…
…
…
Pila
107B
…
…
Pila
6FAC
107B
…
…
Figura 2: Gestión de llamadas (CALL) y retornos (RET).
003C
…
…
…
05AC
RET
Tema 1: Computadores y lenguajes de programación.
13
Lenguajes máquina y ensamblador 1
Instrucciones máquina.
Comenzábamos esta lección con una definición y dos preguntas. La definición era:
“Programar es decirle a un computador lo que tiene que hacer. “
Y las preguntas:
¿Qué es y cómo funciona un computador?
¿Cómo se le dice a un computador lo que tiene que hacer?
La respuesta que hemos dado en la sección anterior a la primera pregunta nos permite empezar a
contestar la segunda. Lo que se le puede decir a un computador viene definido por sus repertorio de
instrucciones máquina. Estas instrucciones se pueden clasificar en cuatro grupos:
−
−
−
−
Instrucciones de transferencia de información.
Instrucciones de transferencia de control (saltos condicionales, bifurcaciones, llamadas
a subrutina y retornos).
Instrucciones aritmético-lógicas.
Misceláneas.
Las instrucciones se estructuran en campos, uno de los cuales identifica el código de operación, el
resto permite identificar a los operandos de forma directa o indirecta. Dependiendo del
computador, puede ser que en una instrucción se expliciten uno o más operandos, cada uno de los
cuales, como veremos en breve, puede direccionarse de una forma distinta. Los formatos de las
instrucciones máquina varían enormemente de unos computadores a otros. Por ejemplo:
Instrucciones de un procesador RISC.
Formato muy regular:
Sólo tres tipos de instrucciones.
Todas las instrucciones de 32 bits
rd ← rs op rt
shamt: número de desplazamientos.
func: extensión de codop.
Repertorio de instrucciones IA-32
para el Pentium 4.
Gran variedad de tamaños, desde 1 a 12
bytes.
Todas las instrucciones tienen codop,
pero la presencia y longitud del resto de
los campos depende del tipo de
instrucción
31
25
codop
rs
20
31
25
codop
rs
20
rt
rt
15
rd
10
shamt
5
func
15
Dirección o dato inmediato
31
25
codop
Dirección
codop
1 ó 2 bytes
dirección
1 ó 2 bytes
0
0
0
desplazam.
1 ó 2 bytes
valor
1 ó 2 bytes
Las instrucciones máquina suponen ya un primer nivel de abstracción, si bien muy pequeño: un
computador queda definido y descrito por su repertorio de instrucciones máquina. Estas
instrucciones permiten decirle al computador lo que tiene que hacer sin necesidad de conocerlo a
nivel de micro-máquina (microoperaciones).
1
Fuente: Introducción a la Informática, capítulo 8.
Tema 1: Computadores y lenguajes de programación.
14
Lenguaje ensamblador
La figura 3 resume el repertorio de instrucciones del computador didáctico CODE-2 1. Por supuesto
no hay que aprenderlo, la figura sólo trata de dar una idea de las instrucciones habituales en un
repertorio de instrucciones máquina e introducir una nueva forma de programar muy cercana a las
instrucciones máquina y sin embargo mucho más cómoda: el lenguaje ensamblador.
Representación en lenguaje ensamblador
de las instrucciones máquina
Figura 3: Lenguaje ensamblador
El lenguaje ensamblador es la representación simbólica de la codificación binaria del repertorio de
instrucciones máquina de un procesador. El uso de referencias simbólicas es una característica
básica del lenguaje ensamblador, que evita trabajar con la codificación binaria de las instrucciones
sustituyéndola por un nemónico. Un nemónico es un dato simbólico que identifica a un comando
generalmente numérico (binario, octal, hexadecimal) de una forma más sencilla que su numeración
original, facilitando enormemente el uso y la memorización de este comando para el programador.
Por ejemplo 2, un procesador x86 puede ejecutar la siguiente instrucción binaria como se expresa en
código de máquina:
Binario: 10110000 01100001 (Hexadecimal: 0xb061)
La representación equivalente en lenguaje ensamblador es más fácil de recordar:
MOV al, 061h
1
2
Descrito en “Introducción a la Informática”, capítulo 8.
Tomado de http://es.wikipedia.org/wiki/Lenguaje_ensamblador
Tema 1: Computadores y lenguajes de programación.
Esta instrucción significa: asigna el valor hexadecimal 61 (97 decimal) al registro "al".
15
El nemónico "MOV" es un código de operación u "opcode", elegido por los diseñadores de la
colección de instrucciones para abreviar "move" (mover, pero en el sentido de copiar valores de un
sitio a otro). El opcode es seguido por una lista de argumentos o parámetros, completando una
instrucción de ensamblador típica.
La palabra ensamblador tiene dos significados. Por un lado hace referencia al lenguaje ensamblador
y por otro a un programa ensamblador que realiza la interpretación del código escrito en lenguaje
ensamblador y genera el código máquina adecuado. Usualmente hay una correspondencia 1 a 1
entre las instrucciones simples del lenguaje ensamblador y el lenguaje de máquina. Sin embargo, en
algunos casos, un lenguaje ensamblador puede proveer pseudo-instrucciones que se expanden en
un código de máquina más extenso.
El lenguaje ensamblador supone un segundo nivel de abstracción. En este caso lo que se está
abstrayendo es el repertorio de instrucciones máquina. El lenguaje ensamblador permite decirle al
computador lo que tiene que hacer sin necesidad de conocer como se codifican sus instrucciones
máquina. La tabla 2 establece una comparación entre las características del lenguaje máquina y del
lenguaje ensamblador, en la que pueden apreciarse los beneficios aportados por este último. Aun
así, el lenguaje ensamblador está muy cercano al lenguaje máquina y es específico de un procesador
determinado. La complejidad de los programas en ensamblador aunque menor que la de los
programas escritos en código máquina sigue siendo exasperante 1. Véase el programa de la figura 4,
que realiza la enorme tarea de cambiar los puntos y coma por comas en una cadena de caracteres.
Tabla 2: Características lenguaje máquina y lenguaje ensamblador
Lenguaje máquina
Lenguaje ensamblador
Los datos se utilizan por medio de las direcciones
de memoria donde se encuentran.
Utilizan direcciones simbólicas de memoria en
lugar de direcciones binarias. Los datos pueden
recibir un nombre, ya que existen sentencias
declarativas que establecen correspondencias
entre direcciones simbólicas y direcciones binarias.
Las instrucciones son cadenas de ceros y unos.
Las instrucciones realizan operaciones muy
simples. Por ejemplo: algunos computadores no
tienen instrucciones específicas para multiplicar
o dividir.
Absolutamente ligado a un procesador. Los
programas escritos en lenguaje máquina son
intransferibles.
En un programa escrito en lenguaje máquina no
pueden incluirse comentarios que ayuden a
entenderlo.
1
Y desesperante.
Utilizan una notación simbólica o nemotécnica
Extienden las instrucciones máquina con otras de
mayor nivel (agrupaciones de instrucciones
máquina)
Ligado a un procesador. No obstante existe un
estándar notacional IEEE694
Es posible insertar comentarios. El programa
ensamblador
los
detecta
y
elimina
automáticamente, no incluyéndolos en el código
máquina.
Tema 1: Computadores y lenguajes de programación.
Modos de direccionamiento.
16
Con frecuencia en la instrucción no se dan las direcciones de memoria donde están los operandos,
sino información a partir de la cual el procesador puede obtenerlas. El significado de esta
información depende del modo de direccionamiento empleado en la instrucción.
Un modo de direccionamiento describe la forma con que el procesador determina, durante la ejecución
de una instrucción, el lugar donde se encuentra o donde se almacenará un operando o resultado o la
dirección a la que hay que saltar, en caso de una instrucción de transferencia de control.
Figura 4: Programa en ensamblador.
Los modos de direccionamiento más usuales se resumen en la tabla 3. Es interesante tener una idea
general de las diversas formas de direccionamiento para comprender, más adelante, porque ciertas
estructuras de datos de los lenguajes de programación de alto nivel (por ejemplo Java) son más
eficientes o más flexibles que otras. La implementación de cada una de estas estructuras de datos se
apoyará en el modo de direccionamiento que mejor se adapte a sus características y a su vez este
modo determinará sus propiedades en cuanto al espacio necesario para almacenar los datos y la
velocidad de acceso a los mismos.
Tema 1: Computadores y lenguajes de programación.
17
Tabla 3: Modos de direccionamiento
Direccionamiento
Significado
Implícito: El operando o su dirección se encuentran en un
registro que está implícitamente indicado en el código de
operación.
Inmediato: El operando forma parte de la propia
instrucción. Según el estándar
prefijo #.
SUB .0,#H’25
IEEE694 1,
se denota con el
Direccionamiento inmediato
32
Instrucción
codop
operando
ALU
R0 ← R0 – 25h
Directo o absoluto: La instrucción contiene la dirección de
memoria o el registro donde se encuentra el operando.
Según el IEEE694 si el direccionamiento se hace a un
registro, el número de orden va precedido por un punto (.), y
si corresponde a una posición de memoria por una barra
inclinada (/).
SUB .0,#H’25A3
A
Direccionamiento directo
32
Instrucción
codop
37
37
ALU
A
operando
R0 ← R0 – M(25A3h)
Indirecto: En la instrucción se indica el puntero o lugar
(posición de memoria o código de registro donde se
encuentra la dirección del operando. Puede ir acompañado
de una operación de incremento o decremento automático
que puede realizarse antes (pre) o después (pos) de la
captación del dato.
SUB .0,[.7]++
R1
R2
Direccionamiento indirecto
Instrucción
codop
…
A3B2
32
A3B2 operando
ALU
R0 ← R0 – M(R7), R7 ← R7 + n
Indexado: Interviene un registro especial o registro índice,
que almacena un desplazamiento. La instrucción contiene la
dirección de referencia (DIRR). La dirección efectiva (DIRF)
se obtiene sumando a DIRR el valor i contenido del registro
índice (DIRF ← DIRR + i). Este tipo de direccionamiento está
pensado para optimizar los accesos a tablas almacenadas en
memoria. Se denota como el direccionamiento indirecto pero
anteponiendo la dirección de referencia.
A
Direccionamiento indexado
Registro
índice
R3
R4
8
…
32
Instrucción
codop
A3B2
A3C0 operando
R7
+
ALU
A
SUB .0,#H’32[.7]
R0 ← R0 – M(32h + R7)
Direccionamientos relativos
Como en el caso del indexado, la dirección se obtiene
añadiendo un desplazamiento (offset) a una dirección de
referencia. Dependiendo de la ubicación de la dirección de
referencia se distingue direccionamiento relativo a base en el
que dicha dirección está contenida en un registro especial
(registro base) y direccionamiento relativo a contador de
programa en el que la dirección efectiva se obtiene sumando
al contenido del PC un desplazamiento incluido en la propia
instrucción.
Relativo a base: SUB .0,RB!.2
R0 ← R0 – M(RB + R2)
Relativo a programa: BR $9
PC ← PC + 9
Expresados en notación estándar IEEE 694. Si bien cada procesador tiene su propio lenguaje ensamblador existe un
estándar notacional.
1
R7
.7
Tema 1: Computadores y lenguajes de programación.
Lenguajes de programación de alto nivel 1
18
Nivel de abstracción de un lenguaje.
Abstraer:
1.
Separar por medio de una operación intelectual las cualidades de un objeto para considerarlas
aisladamente o para considerar el mismo objeto en su pura esencia o noción.
−
En filosofía, un acto mental en el que se aísla conceptualmente un objeto o una propiedad de un objeto.
2.
−
−
Prescindir, hacer caso omiso.
En psicología, un proceso que implica reducir los componentes fundamentales de información de un
fenómeno para conservar sus rasgos más relevantes.
En programación, la abstracción se aplica al control o a los datos. La abstracción de control es la
abstracción de las acciones e implica el uso de estructuras de selección y repetición de acciones y la
definición de subprogramas, en tanto que la abstracción de datos consiste en dotar de significado a las
secuencias de bits que manejan los computadores.
Como hemos visto, las instrucciones máquina proporcionan un nivel de abstracción: un computador
queda definido y descrito por su repertorio de instrucciones máquina. Estas instrucciones permiten
decirle al computador lo que tiene que hacer sin necesidad de conocerlo a nivel de micro-máquina
(micro-operaciones). Basta con conocer su arquitectura interna, sus registros, sus modos de
direccionamiento, etc. El lenguaje ensamblador supone un segundo nivel de abstracción que permite
decirle al computador lo que tiene que hacer sin necesidad de conocer como se codifican sus
instrucciones máquina. El lenguaje ensamblador está muy cercano al lenguaje máquina y como el
lenguaje máquina es específico de un procesador determinado. La construcción de programas
grandes y complejos exige disponer de lenguajes de programación que se alejen de la arquitectura
de los computadores y se acerquen al tipo de lenguaje que se utiliza para plantear los problemas.
Por ejemplo, si quisiéramos programar la solución a una ecuación nos gustaría usar un lenguaje de
programación en el que simplemente tuviéramos que escribir la ecuación en notación algebraica.
Para que esto sea posible los lenguajes de programación deben proporcionar mayor nivel de
abstracción.
Cuando hablamos del nivel de un lenguaje nos estamos refiriendo a su capacidad para proporcionar
abstracciones que permitan escribir programas sin necesidad de conocer la arquitectura del
computador subyacente. Un lenguaje de programación es de mayor nivel cuanto más se aleja de la
estructura del computador y más se acerca al lenguaje con el que expresamos los problemas. No
hay nada de más bajo nivel a disposición del programador que el repertorio de instrucciones
máquina. Al lenguaje máquina le sigue en nivel de abstracción el ensamblador, que sigue siendo un
lenguaje de muy bajo nivel. Lenguajes de programación como C o FORTRAN se consideran ya de
alto nivel pues proporcionan abstracciones que permiten ignorar muchas de las características del
hardware subyacente. Los lenguajes estructurados como Pascal y los orientados a objetos como
C++ o Java son lenguajes de muy alto nivel, pues proporcionan abstracciones muy flexibles que
facilitan extraordinariamente la realización de programas muy grandes y complejos sin saber
apenas nada de las características de los ordenadores donde se ejecutarán tales programas.
Hay un límite en el nivel de abstracción que puede proporcionar un lenguaje de programación. Este
límite viene dado por la necesidad de traducir los programas escritos en dicho lenguaje a
programas equivalentes en lenguaje máquina. Esta traducción es necesaria porque el único
lenguaje que entiende un computador es su lenguaje máquina. Traducir un programa escrito en
ensamblador es relativamente fácil, las instrucciones en ensamblador se corresponden uno a uno
con las instrucciones máquina y los nombres simbólicos se relacionan con su codificación binaria
1
Fuente principal: [Prieto 06], cáp. 14.
Tema 1: Computadores y lenguajes de programación.
19
en tablas de símbolos. Pero a medida que las abstracciones se alejan de la máquina, la traducción se
hace más difícil, hasta llegar a un punto en el que el esfuerzo no merece la pena. El límite es más
tecnológico que teórico y posiblemente surjan en el futuro lenguajes de programación de propósito
general y uso común de un nivel de abstracción que supere a los lenguajes orientados a objetos que
son, de momento, la tecnología dominante.
Podemos ahora retomar las dos preguntas que nos hacíamos al principio de tema (¿qué es y cómo
funciona un computador? y ¿cómo se le dice a un computador lo que tiene que hacer?) para terminar
este apartado con una pequeña reflexión: mientras utilicemos para programar un lenguaje máquina
o un lenguaje ensamblador la respuesta a la segunda pregunta está íntimamente ligada a la
respuesta a la primera ya que, utilizando dichos lenguajes, sólo podemos programar si conocemos
con todo detalle la estructura interna del computador al que se refieren. Sin embargo, utilizando un
lenguaje de programación de alto nivel, la respuesta a la segunda pregunta es independiente de la
primera. Puesto que los lenguajes de programación de alto nivel abstraen la arquitectura de los
computadores, a través de ellos podemos decirles a tales computadores lo que tienen que hacer sin
conocer su estructura ni su funcionamiento interno. Aunque estrictamente hablando el lenguaje
máquina y el lenguaje ensamblador son lenguajes de programación, a partir de ahora cuando
hablemos de lenguaje de programación nos estaremos refiriendo exclusivamente los lenguajes de
programación de alto nivel.
Características de los lenguajes de programación.
Los lenguajes de programación (como cualquier lenguaje) tienen un léxico, una sintaxis y una
semántica 1. El léxico es el vocabulario o conjunto de símbolos incluidos en el lenguaje. La sintaxis es
el conjunto de reglas que nos indican cómo escribir oraciones correctas en el lenguaje. A partir de
ahora llamaremos a esas oraciones sentencias 2. La semántica define el significado de las sentencias
y por tanto es la semántica la que determina “lo que tiene que hacer el computador”. En los lenguajes
de programación, la relación entre sintaxis y semántica es muy estrecha pues el significado del
programa está totalmente ligado a la estructura sintáctica de sus sentencias. El proceso de
traducción de un código fuente escrito usando un lenguaje de programación a código máquina es, en
esencia, la generación de un código en lenguaje máquina con el mismo significado que el código
fuente.
Las características principales de los lenguajes de programación son las siguientes:
−
−
−
Utilizan notaciones cercanas al ámbito en que se usan. El léxico es simbólico y las operaciones
se describen con sentencias muy cercanas al lenguaje matemático o al lenguaje natural, que se
expresan por medio de texto, que incluye palabras en lengua inglesa 3 y expresiones
matemáticas y pueden incluirse comentarios.
Son independientes de la arquitectura física del computador. Esto, como ya hemos visto,
significa que podemos programar sin conocer los detalles del computador, pero significa algo
más: significa que los programas así escritos son portables, es decir pueden ejecutarse en
computadores distintos. El programa se escribe una vez y se traduce de forma diferente para
cada plataforma de ejecución 4.
Una sentencia en un lenguaje de alto nivel da lugar al ser traducida a varias instrucciones en
lenguaje máquina.
Es decir, tienen una gramática.
Existen lenguajes específicos para definir la sintaxis de los lenguajes de programación (metalenguajes). Los más usuales
son la notación BNF (Backus-Naur Form) y los diagramas sintácticos.
3 La programación es un “invento americano”.
4 De momento consideraremos que la plataforma de ejecución es el computador. Más adelante generalizaremos el concepto
de plataforma de ejecución.
1
2
Tema 1: Computadores y lenguajes de programación.
El proceso de traducción.
20
Un traductor es un programa que recibe como entrada un texto en un lenguaje de
programación y produce como salida un texto equivalente en lenguaje máquina.
El programa inicial se denomina programa fuente y el programa obtenido programa objeto.
El traductor puede ser un compilador o un intérprete. En términos generales la diferencia estriba
en que un compilador toma el programa fuente entero y produce un programa objeto, en tanto que
un intérprete traduce el programa fuente sentencia a sentencia, de forma que el programa se va
ejecutando a medida que se va traduciendo.
La traducción por un programa compilador (compilación) consta de dos etapas fundamentales
(figura 5): análisis del programa fuente y síntesis del programa objeto.
Programa fuente
Análisis de léxico
El programa fuente se descompone en sus símbolos léxicos (tokens):
literales, palabras reservadas, nombres de variables y funciones, etc.
Se obtiene una representación del programa formada por tablas de
símbolos, se identifican comentarios, espacios en blanco, tabulaciones,
etc. y se eliminan.
Se detectan errores léxicos.
Análisis sintáctico
Se comprueba que el programa es sintácticamente correcto, es decir
que todas las sentencias están bien construidas.
Análisis semántico
Se obtiene el significado de las sentencias a partir de la identificación
de las construcciones sintácticas y de la información almacenada en la
tabla de símbolos.
Análisis
Síntesis
Generación de Código
Optimización
Se crea un archivo con un código en lenguaje objeto (normalmente
lenguaje máquina) con el mismo significado que el código fuente.
Es muy habitual que este archivo no sea directamente ejecutable, sino
que se requieran otros pasos previos a la ejecución como ensamblado,
encadenado y carga.
Programa objeto
Figura 5: El proceso de compilación.
Una vez traducido, el programa puede encontrarse ubicado en varios ficheros objeto. Esto requiere
buscar el código correspondiente al programa en dichos ficheros y enlazarlo para obtener el
programa. Un enlazador (linker) es un programa que:
−
Lee los archivos objeto del programa identificando las rutinas que utiliza.
Tema 1: Computadores y lenguajes de programación.
−
−
21
Busca el código objeto de las rutinas en un conjunto dado de ficheros objetos. Este conjunto
incluye los ficheros objeto obtenidos al compilar el programa y otros que se le hayan indicado,
por ejemplo los incluidos en bibliotecas software utilizadas para realizar el programa.
Recopila de los ficheros objetos el código que utiliza el programa y lo enlaza para obtener un
fichero ejecutable.
Paradigmas de programación.
Acabamos de ver que los lenguajes de alto nivel deben proporcionar abstracciones que nos
permitan programar sin conocer la estructura interna del computador. La naturaleza de estas
abstracciones puede variar mucho de unos lenguajes a otros definiéndose diferentes paradigmas de
programación. Un paradigma de programación representa un estilo de programación determinado
caracterizado por un conjunto de conceptos y abstracciones que sirven para representar los
elementos de un programa y la forma de combinarlos. Algunos lenguajes de programación soportan
un único paradigma de programación, otros pueden soportar varios (lenguajes multiparadigma).
El paradigma de programación más cercano a la arquitectura interna de los computadores es la
programación imperativa. En este estilo de programación una computación se describe en términos
de sentencias que el computador ejecuta de forma secuencial y que a medida que se ejecutan
cambian el estado del programa. Este es el estilo de programación que necesariamente se sigue
cuando se programa en lenguaje máquina o ensamblador y que siguen también los lenguajes de alto
nivel más antiguos.
Evolución de la programación imperativa.
La programación imperativa evolucionó hacia la programación procedural. En la programación
procedural el programa se construye a partir de uno o más procedimientos (también llamados
subrutinas o funciones). Algunos autores no hacen distinción entre programación procedural e
imperativa, sin embargo, la encapsulación de las acciones en procedimientos (abstracción funcional)
tiene un enorme efecto en la forma en que pueden diseñarse y construirse los programas. La
programación estructurada es, a su vez, una evolución de la programación procedural en la que se
restringe la forma en que pueden usarse las estructuras de control de flujo y los saltos y retornos de
los procedimientos. Como evolución de la programación procedural y estructurada aparece el
paradigma de programación dominante hoy en día: la programación orientada a objetos. La
programación orientada a objetos es, como la procedural, programación imperativa, pero no es sólo
programación imperativa. En la programación orientada a objetos la visión global del programa no
es la de una secuencia de sentencias ejecutándose de forma secuencial, sino la de un conjunto de
objetos que colaboran intercambiando datos y servicios. La programación del comportamiento
interno de cada objeto sigue las reglas de la programación estructurada 1, pero para entender el
programa en su conjunto debe aplicarse un modelo (paradigma) distinto.
La programación declarativa.
Existe una familia de paradigmas de programación cuya forma de entender los programas es muy
distinta a la programación imperativa y que recibe por tanto un nombre distinto, es la
programación declarativa. En los lenguajes declarativos no se le dice al computador cómo resolver
un problema, sino el problema que tiene que resolver junto con un conjunto de reglas que permiten
resolverlo. No existe o se restringen al máximo los cambios de valor en las variables a través de
asignaciones y se utiliza de forma exhaustiva la recursión (funciones que se llaman a sí mismas 2).
1
2
Aunque podría seguir cualquier otro estilo.
La recursión puede utilizarse también con lenguajes imperativos y de hecho se verá durante el curso,
Tema 1: Computadores y lenguajes de programación.
22
Los programas declarativos no son una secuencia de instrucciones, sino un conjunto de reglas que
deben aplicarse para conseguir un resultado. Ejemplos de lenguajes declarativos son los lenguajes
funcionales y lógicos. En la programación funcional los programas se construyen a base de funciones
matemáticas. La programación lógica ve los programas como un conjunto de formulas lógicas a
partir de la cuales se realizan razonamientos para llegar a un resultado o realizar una acción. El
problema se expresa mediante fórmulas lógicas y los programas consisten en la aplicación de reglas
de inferencia sobre dichas fórmulas hasta que se llega a la solución del problema o se prueba que el
conjunto de fórmulas utilizado es inconsistente.
Los lenguajes declarativos son tremendamente efectivos y de una concisión y elegancia imposibles
de conseguir con los lenguajes imperativos. Además, como alguno habrá sospechado, la
programación declarativa está íntimamente ligada con la inteligencia artificial. Aunque es fácil
percibir lo lejos que se encuentran de la arquitectura de los computadores es posible construir
compiladores eficientes para los mismos No obstante, no todos los problemas se adaptan bien al
paradigma declarativo. En la tabla 4 se resumen los principales paradigmas de programación y los
lenguajes más conocidos asociados a los mismos. Sobre la tabla habría que hacer las siguientes
observaciones:
−
−
−
−
Aparece un paradigma, la programación dirigida por eventos, que no se han comentado en el
texto y que puede considerarse como una variante de la programación procedural.
La tabla no incluye todos los paradigmas de programación que pueden encontrarse en la
literatura técnica.
Ninguno de los paradigmas tiene una definición precisa, ni hay un acuerdo general sobre cuál
de ellos es el más adecuado para desarrollar software. Se admite que el paradigma más
adecuado viene dado por el tipo de problema que se pretende resolver.
Algunos de los lenguajes que aparecen asociados a un paradigma soportan también otros. No se
ha intentado ser exhaustivo, sino poner algún ejemplo representativo.
Es pronto para que puedan entender todas las características de los paradigmas y sus
implicaciones. La mayor parte de las características de los lenguajes imperativos y orientados a
objetos se ven en este curso. Las características de la programación dirigida por eventos se verán
en la asignatura de 3º, Programación para Ingeniería Telemática, ciertas características de la
programación declarativa se verán al estudiar bases de datos al final de esta asignatura y con más
extensión en la asignatura de 3º Programación para Ingeniería Telemática.
Tabla 4: paradigmas de programación.
Paradigma
Características/abstracciones
Lenguajes
Programación imperativa: Los programas se
describen en términos de sentencias que se
ejecutan de forma secuencial cambiando el estado
del programa.
Sentencia de asignación.
C, COBOL,
FORTRAN
Pascal, Modula 2.
Programación dirigida por eventos:
Programación procedural en la que el flujo de
control del programa es determinado por eventos
(movimientos y click de ratón, entradas desde
teclado, llegada de mensajes, vencimiento de
temporizadores, etc.).
No hay instrucción GOTO.
Únicamente tres estructuras de control:
secuencia, selección e iteración.
Cada estructura tiene un punto de
entrada y uno de salida.
Manejadores de eventos.
Retrolladas (callbacks)
Procesamiento asíncrono, hilos
servidores.
Programación procedural estructurada.
Programación imperativa con encapsulación de
acciones en funciones y restricciones en el control
de flujo.
Programación orientada a objetos.
Encapsulación de datos y servicios en objetos. Los
objetos que colaboran intercambiando datos y
servicios
Estructuras de datos globales.
Inversión de control.
Clases, interfaces, objetos, métodos,
tipos abstractos de datos, polimorfismo.
Mensajes.
Los lenguajes
procedurales
pueden soportar la
programación
conducida por
eventos.
Simula, C++, ada95,
Java, C#, Eiffiel.
Tema 1: Computadores y lenguajes de programación.
23
Programación declarativa: expresa la lógica del
programa sin describir con detalle su flujo de
control. Se describe el problema a resolver junto
con el conjunto de reglas que permiten resolverlo.
Evita la modificación de los datos y los cambios de
estado.
Fórmulas
SQL
Programación lógica: El problema se expresa
mediante fórmulas lógicas y los programas
consisten en la aplicación de reglas de inferencia
sobre dichas fórmulas hasta que se llega a la
solución del problema o se prueba que el conjunto
de fórmulas utilizado es inconsistente.
Lambda Calculus.
Composicionalidad.
Recursion
Prueba de teoremas.
Scheme, Ocaml,
Haskell
Programación funcional: Los programas
consisten en la evaluación de funciones
matemáticas.
Reglas de inferencia.
Sin efectos colaterales.
Modelos, restricciones, reglas de
inferencia.
Prolog
Sistemas Operativos
Un sistema operativo 1 es un programa o conjunto de programas que:
−
−
Gestiona y asigna los recursos hardware y software que requieren los programas para poder
ejecutarse.
Proporciona a los usuarios una interfaz de acceso que define una máquina virtual más fácil de
entender y utilizar que la máquina real subyacente.
Los sistemas operativos más populares son aquellos que están dirigidos a los ordenadores
personales como Microsoft Windows, Mac OS X o Linux, pero puesto que el sistema operativo hace
de intermediario entre las aplicaciones y el hardware de la computadora, están presentes en casi
cualquier dispositivo que contenga un procesador (teléfonos móviles, video consolas, servidores
web, etc.). En muchas ocasiones los sistemas operativos para estos dispositivos son versiones
reducidas de los sistemas operativos usados en los ordenadores personales. Algunas de las
funciones típicas de un sistema operativo son:
−
−
−
−
−
−
Facilitar el uso del procesador y la comunicación computador/usuario.
Gestionar y asignar recursos a los distintos programas, principalmente tiempo de procesador,
memoria y periféricos.
Contabilizar los recursos utilizados por programas y usuarios.
Gestionar y mantener los archivos en dispositivos de almacenamiento secundario.
Proteger los datos y los programas, especialmente en sistemas multiusuario y distribuidos.
Proporcionar control de acceso, identificando y acreditando a los usuarios.
El sistema operativo establece una frontera (a veces muy difusa) entre dos tipos de software: los
programas de soporte que ofrece el propio sistema operativo y las aplicaciones que utilizan los
servicios del sistema operativo.
El nivel de máquina operativa 2.
El sistema operativo define un nivel de máquina virtual, que también se puede denominar de
máquina operativa, que permite utilizar el computador sin conocer los detalles internos del
computador. En este sentido, el sistema operativo proporciona, como hacen los lenguajes de
programación de alto nivel, un nivel de abstracción que permite ignorar la arquitectura interna del
Los sistemas operativos se estudian en Fundamentos de Computadores. Aquí sólo se pretende presentar algunas de sus
características más relevantes.
2 Tomado de Introducción a la Informática, capítulo 13.
1
Tema 1: Computadores y lenguajes de programación.
24
computador. El sistema operativo suele estar constituido por una serie de módulos que ejecutan
sus servicios en respuesta a las llamadas al sistema que realizan los clientes del sistema operativo
(figura 6). Estos clientes pueden ser los usuarios del computador, que lanzan las llamadas a través
de la interfaz de usuario del sistema operativo. Cualquier programa que tenga que interactuar con
un operador humano dispone de una interfaz de usuario, que se fundamenta en la utilización de un
lenguaje de comandos. Cuando en un computador introducimos una orden (por ejemplo, formatear
un disco), esta orden es captada por el intérprete de comandos (shell 1) que es un módulo o
programa independiente del sistema operativo. El intérprete se encarga de traducir los comandos
de usuario en llamadas al sistema. En los antiguos sistemas operativos la órdenes se introducían de
forma textual en una consola de texto, hoy en día la forma habitual es introducirlas a través de
ventanas y diálogos. No obstante, los clientes más habituales del sistema operativo no son los
operadores humanos, sino los propios programas de aplicación. A su vez, unos módulos del sistema
operativo son clientes de otros módulos del sistema operativo, hasta llegar a una parte del sistema
operativo conocida como núcleo o kernel que es el módulo del sistema operativo que interactúa
directamente con el hardware del computador.
Una característica muy importante del sistema operativo es que tiene que estar disponible en
cuanto se arranca el computador. Cuando se enciende un computador, se lanza a ejecución un
programa de autodiagnóstico (POST: Power On Self Test) que identifica la memoria disponible, los
discos, el teclado, la tarjeta de vídeo, el ratón y en general todos los dispositivos con los que cuenta
el computador. Posteriormente se lanza a ejecución el cargador inicial (bootstrap loader) que, a su
vez, carga un programa cargador más sofisticado, cuyo objetivo es buscar el sistema operativo y
cargar una parte de él (denominada comúnmente residente) en la memoria principal. Tanto el
programa de auto diagnóstico como el cargador inicial suelen estar grabados en la memoria ROM
del computador. Una vez arrancado, el sistema operativo presenta en pantalla una interfaz de
usuario que queda a la espera de las órdenes del usuario.
Usuarios y programas de aplicaciones
Sentencias de
lenguajes de alto
nivel
Instrucciones
de edición
Órdenes del
sistema
operativo
Interfaz de Usuario
Interfaz de Usuario
Interfaz de Usuario
Compiladores
Editores
Intérpretes del
lenguaje de comandos
Llamadas al sistema
Máquina operativa o
virtual
Instrucciones en
lenguaje
máquina
Núcleo del sistema operativo
Instrucciones en
lenguaje máquina
Máquina Real
Procesador
Figura 6: El sistema operativo dentro del contexto de los niveles conceptuales.
Shell significa literalmente cáscara y recibe ese nombre porque es (o forma parte de) la capa más externa del sistema
operativo.
1
Tema 1: Computadores y lenguajes de programación.
Plataforma de ejecución.
25
El conjunto computador-sistema operativo define una plataforma de ejecución de los programas.
Los sistemas operativos normalmente tienen versiones para distintos tipos de computadores de
forma que un mismo programa puede ejecutarse en computadores diferentes siempre que ambos
tengan instalado el mismo sistema operativo. Esto es así porque los programas acceden a los
recursos del computador a través de llamadas al sistema y la sintaxis y la semántica de las llamadas
no cambian en tanto no cambie el sistema operativo. De esta manera la plataforma de ejecución que
ve el programa es la misma aunque haya cambiado el computador subyacente. Esta idea ha tratado
de generalizarse definiendo un estándar para las llamadas al sistema (estándar POSIX). En teoría,
un programa podría ejecutarse sobre cualquier plataforma que siga dicho estándar aunque tanto el
computador como el sistema operativo sean distintos. A pesar de ello, en la práctica, si cambia el
computador, aunque se mantenga el mismo sistema operativo, es bastante habitual que haya que
recompilar el programa, ya que el código objeto generado por los compiladores puede asumir una
arquitectura hardware determinada. Por otro lado, no todos los sistemas operativos siguen el
estándar POSIX.
Como cambiar el sistema operativo de un computador no es una tarea trivial, una forma de
conseguir ejecutar en una plataforma programas compilados para otra plataforma es emular las
plataformas de ejecución. Por ejemplo, supongamos que tenemos instalado Windows y deseamos
ejecutar una aplicación sobre una versión de Linux. Una posibilidad es emular la plataforma Linux
sobre la plataforma Windows (también podría hacerse al revés). La potencia de los computadores
actuales permite realizar tipo de emulaciones y ejecutar los programas sobre los emuladores de
forma muy eficiente.
Tema 1: Computadores y lenguajes de programación.
26
Anexo: Codificación de números enteros 1
Existen muchas definiciones de “Información”, la que mejor viene para los propósitos de este
apartado es la siguiente 2:
Secuencia de símbolos que pertenecen a un alfabeto y que representan
algún hecho, concepto o idea.
La información en un ordenador va codificada en binario, (0, 1). Esta codificación, en el caso de los
números está basada en el uso del sistema de numeración en base 2.
Fundamentos matemáticos para un sistema de numeración. Bases, dígitos y
cifras.
Todo número viene expresado en una base
signos.
={
/
0=
0;
+1 =
> 1. Una base es un conjunto finito y ordenado de
+ 1, ∀ = 0, 1,…,
– 1} .
La expresión de arriba puede expresarse también de la siguiente manera:
1.
El primer elemento de la base es el cero.
3.
El máximo elemento de la base es igual al cardinal de la base menos uno.
2.
Los sucesivos elementos ordenados de la base son tales que cualquier elemento es igual a
su inmediato anterior más uno.
La base 10, B = 10, por ejemplo, está formada por los siguientes elementos:
{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
En cada base, todo número entero a > 0 puede ser escrito de modo único en la forma
a = ak · Bk + ak-1 · Bk-1 + ak-2 · Bk-2 + … + a1 · B1 + a0 · B0
(1)
donde k > 0 es un entero, y cada uno de los ai son enteros tales que:
0≤
≤
− 1, para i = 1, 2, 3, … , , y
Por ejemplo, en base 10, B = 10
≠0
71053 = 7 · 104 + 1 · 103 + 0 · 102 + 5 · 101 + 3 · 100
A los coeficientes
se les llama dígitos del número . A la expresión (1) se la llama expansión del
número. El número habitualmente se representa como:
=
−1
−2
…
1
0
Fuente: [Alcover 09], disponible en la biblioteca digital de la UPCT.
Una definición especialmente interesante es la de la “Teoría da la Información”, parte de la cual se estudia en el grado, que
caracteriza a la misma en términos probabilísticos. Dicha definición, que es complementaria al proporcionada en el cuadro,
está orientada al estudio de la capacidad de transmisión de los canales, la compresión de datos o la detección y corrección de
errores.
1
2
Tema 1: Computadores y lenguajes de programación.
27
De esta manera, cualquier número viene representado, en una base determinada, por una serie
única de coeficientes . A cada serie de coeficientes, que codifica un número de modo único en una
determinada base, se le llama cifra.
Supongamos ahora que queremos expresar un numéro a > 0 en binario, base 2. Tendríamos:
a = ak · 2k + ak-1 · 2k-1 + ak-2 · 2k-2 + … + a1 · 21 + a0 · 20
Por ejemplo
11011 = 1 · 24 + 1 · 23 + 0 · 22 + 1 · 21 + 1 · 20
Es fácil ver las correspondencias entre los primeros diez dígitos en base 10 y su representación en
base 2:
Base 10
0
1
Base 2
0
1
2
10
4
100
6
110
3
5
7
8
9
11
101
111
1000
1001
Cambios de base.
Paso de base dos a base diez: Para este cambio de base es suficiente con desarrollar la expansión
del número. Por ejemplo:
10011101 2 = 1 · 27 + 0 · 26 + 0 · 25 + 1 · 24 + 1 · 23 + 1 · 22 + 0 · 21 + 1 · 20 = 128 + 16 + 8 + 4 + 1
= 157 10.
Paso de base diez a base dos: Para este cambio se divide el entero por dos (división entera), y se
repite sucesivamente esta división hasta llegar a un cociente menor que la base. Simplemente
vamos dividiendo por la base el número original y vamos repitiendo el procedimiento para los
cocientes que vamos obteniendo. Los restos de estas divisiones, y el último cociente, son los dígitos
buscados. El último cociente es el dígito más significativo, y el primer resto el menos
significativo.Por ejemplo, en el ejemplo recogido en el cuadro de abajo vemos que el valor 157
expresado en base diez es, en base dos, 10011101.
Tema 1: Computadores y lenguajes de programación.
28
Las bases octal y hexadeximal facilitan el manejo de las cifras codificadas en base dos, que
enseguida acumulan gran cantidad de dígitos, todos ellos ceros o unos. Para pasar de base dos a
base dieciséis es suficiente con separar la cifra binaria en bloques de cuatro en cuatro dígitos,
comenzando por el dígito menos significativo. Al último bloque, si no tiene cuatro dígitos, se le
añaden tantos ceros a la izquierda como sean necesarios.
Complementos a la Base.
Vamos a introducir dos conceptos nuevos, muy sencillos, pero muy útiles para representar
números binarios con signo. Los de complemento a la base y complemento a la base menos uno.
Supongamos el número N expresado en una base B determinada. Y supongamos que ese número
tiene k dígitos.
Llamamos complemento a la base de ese número expresado en esa base determinada, a la
cantidad que le falta para llegar a la cifra de (k + 1) dígitos, en el que el dígito más significativo es el
uno y los demás son todos ellos iguales a cero. De una forma más precisa, definiremos el
complemento a la base de un número N codificado con k cifras en base B como:
CB,k(N) = Bk - N
Por ejemplo, en base diez, el complemento a la base del número (279)10 es la cantidad que hace falta
para llegar a (1000)10, que es (721)10.
En esta definición hay que destacar que, si el número N viene expresado de forma que a su
izquierda se tienen algunos dígitos iguales a cero, entonces el complemento a la base es diferente
que si estuviese sin esos dígitos, aunque la cantidad codificada sería la misma. Siguiendo con el
ejemplo anterior, el complemento a la base del número codificado como (0279)10 ya no es (721)10,
sino (9721)10, porque ahora no se trata de calcular lo que falta para llegar a (1000)10, sino para
llegar a (10000)10, puesto que ahora la cantidad numérica está codificado con cuatro dígitos.
El concepto de complemento a la base menos uno es muy semejante: es la cantidad que dista
entre el número N, codificado con k dígitos en la base B, y el número formado por k dígitos, todos
ellos con el valor del mayor elemento de la base B en la que se trabaja. De una forma más precisa,
definiremos el complemento a la base menos uno de un número N codificado con k cifras en base B
como
CB-1,k(N) = Bk – N - 1
Tema 1: Computadores y lenguajes de programación.
29
Por ejemplo el complemento a la base menos uno del número (541)10 expresado en base diez es la
cantidad que hace falta para llegar a (999)10, que es (458)10.
De nuevo aquí también cambia el complemento a la base menos uno de un número si ponemos en
su codificación más o menos ceros a su izquierda. Es inmediato también deducir que la relación
entre los dos complementos es que la diferencia entre ambos es igual a uno. De hecho se puede
definir el Complemento a la Base menos uno de un número N codificado con k dígitos en la base B,
como el Complemento a la Base de un número N codificado con k dígitos en la base B, menos 1.
CB,k(N) = CB-1,k(N) - 1
Una propiedad de los complementos es que, en cierta sentido, facilitan el cálculo de las restas. Se
pueden ver algunos ejemplos en base diez. Se cumple que la resta de dos enteros se puede también
calcular haciendo la suma entre el minuendo y el complemento a la base del sustrayendo,
despreciando el posible acarreo final. Por ejemplo:
Donde si, como se ha dicho, despreciamos el último acarreo, tenemos que se llega al mismo
resultado:127.
También se puede realizar un procedimiento similar si se trabaja con el complemento menos uno.
En ese caso, lo que se hace con el último acarreo, si se tiene, es eliminarlo del resultado intermedio
y sumarlo para llegar al resultado final. Por ejemplo:
Hasta ahora todo lo expuesto puede aparecer como un enredo algo inútil porque, de hecho, para el
cálculo del complemento a la base es necesario realizar ya una resta, y no parece que sea mejor
hacer la resta mediante uno de los complementos que hacerla directamente. Pero las cosas cambian
cuando se trabaja en base dos. Veamos algunos ejemplos de cálculo de los complementos en esa
base:
Si se compara la codificación de cada número N con su correspondiente complemento a la base
menos uno, se descubre que todos los dígitos están cambiados: allí donde en N corresponde el
dígito 1, en C1(N) se tiene un 0; y viceversa. Es decir, que calcular el complemento a la base menos
uno de cualquier número codificado en base dos es tan sencillo como cambiar el valor de cada uno
de los dígitos de la cifra. Y esta operación es muy sencilla de realizar con circuitos electrónicos. La
ventaja clara que aportan los complementos es que no es necesario incorporar un restador en la ALU,
puesto que con el sumador y un inversor se pueden realizar restas.
Tema 1: Computadores y lenguajes de programación.
Números binarios en complemento a 1 y en complemento a 2
30
Cuando se trabaja con números binarios la base es 2 y por tanto el complemento a la base se
denomina complemento a 2 y el complemento a la base menos 1 se denomina complemento a 1.
Como acabamos de ver, el uso de el complemento a 1 simplifica los circuitos electrónicos para
realizar las restas, sin embargo, existe una desventaja a la hora de utilizar el complemento a 1 para
representar números negativos que hace más adecuado el complemento a 2, y es que existen dos
posibles representaciones para el número cero (todo ceros o todo unos).
El cálculo del complemento a 2 es también muy fácil de realizar mediante puertas lógicas: basta con
invertir el valor de cada una de sus cífras, es decir realizar el complemento a 1, y sumarle 1 al
número obtenido (véase cuadro anterior).
Una forma rápida de hallar el complemento a dos es comenzar por la derecha (el dígito menos
significativo 1), copiando el número original (de derecha a izquierda) hasta encontrar el primer 1,
luego de haber copiado el 1, se niegan (complementan) los dígitos restantes (es decir, copia un 0 si
aparece un 1, o un 1 si aparece un 0). Este método es mucho más rápido para las personas, pues no
utiliza el complemento a uno en su conversión.
Por ejemplo, el complemento a dos de «0011 11010» es «1100 00110»-
1 Algunos procesadores codifican el bit más significativo a la izquierda y otros a la derecha (codificación little-endian o bigendian).
Tema 2: Abstracción de datos.
Tema 2: Abstracción de datos
Objetivos
•
•
•
Introducir la abstracción de datos de los lenguajes de alto nivel.
Presentar los tipos de datos primitivos de Java.
Introducir los elementos léxicos fundamentales del lenguaje Java.
Contenido
•
•
•
•
•
•
Tipos de datos.
Tipos de datos primitivos de Java.
Identificadores y palabras reservadas.
Variables y constantes.
Expresiones y asignación.
Tipos de operadores y reglas de asociatividad y precedencia.
31
Tema 2: Abstracción de datos.
Abstracción y lenguajes de programación 1.
32
Como se vio en el tema anterior, todos los lenguajes de programación proporcionan “abstracciones”,
estando la complejidad de los problemas que somos capaces de resolver con dichos lenguajes
directamente relacionada con el tipo y la calidad de tales abstracciones. El tipo de la abstracción
hace referencia a aquello que se está abstrayendo. Los lenguajes ensambladores son una pequeña
abstracción de la máquina subyacente. Los lenguajes de programación imperativos (como Fortran,
Basic y C) son a su vez abstracciones de los lenguajes ensambladores. Los lenguajes imperativos
suponen una enorme mejora sobre los lenguajes ensambladores, pero sus abstracciones todavía
requieren pensar en términos de la estructura de los computadores en lugar de en términos de los
problemas que se quieren resolver. Y esto es un grave problema. Veamos por qué.
Un conjunto de abstracciones define un espacio de modelado para plantear y resolver problemas.
Por ejemplo, el algebra puede verse como un lenguaje con abstracciones en las que es fácil plantear
y resolver un cierto tipo de problemas. Un matemático puede plantear un problema geométrico
utilizando las abstracciones del algebra (matrices, vectores, productos escalares y vectoriales, etc.)
y lo puede resolver utilizando las mismas abstracciones. Pero ¿qué ocurre cuando la solución se
quiere programar en un computador? Ocurre que hemos planteamos el problema en un espacio de
modelado (por ejemplo el algebra), pero tenemos que expresar la solución en un espacio de
modelado distinto (el lenguaje de programación). En términos de la Informática se dice que el
espacio del problema y el espacio de la solución no coinciden. El programador debe entonces
establecer una asociación entre el modelo de la máquina y el modelo del problema que se pretende
resolver. El esfuerzo que se requiere para realizar esta asociación produce programas difíciles de
escribir y aún más difíciles de mantener. Para evitar este esfuerzo algunos lenguajes de
programación adoptan una visión particular del mundo adaptada a un cierto tipo de problemas. Por
ejemplo, LISP reduce todos los problemas a manejo de listas de datos y APL reduce todos los
problemas a algoritmos, los lenguajes declarativos plantean los problemas en términos de fórmulas
lógicas y razonamientos. Cada uno de estos lenguajes es adecuado para el tipo de problema al que
está orientado, pero cuando se intenta usarlos para otros tipos de problemas se vuelven extraños y
difíciles de utilizar.
Los lenguajes de programación orientados a objetos (Java entre ellos) son una evolución de los
lenguajes imperativos que proporcionan herramientas para que el programador pueda definir las
abstracciones del espacio del problema y trabajar con ellas. La representación de estas
abstracciones se realiza de forma lo suficientemente general para no restringir al programador a un
determinado tipo de problemas. Tanto los elementos del espacio del problema como los del espacio
de la solución se modelan mediante objetos. La idea es permitir al programador adaptarse a
cualquier tipo de problema mediante la adición de nuevos tipos de objetos, de forma que cuando se
lea el código que describe la solución se estén leyendo los mismos términos en los que se ha
expresado el problema. Esta es una abstracción flexible y poderosa: la orientación a objetos permite
describir la solución en los mismos términos en que se ha descrito el problema, en lugar de en los
términos de la estructura interna de los computadores donde tal solución se ejecutará. No obstante,
existe todavía una conexión muy fuerte con la estructura interna de la máquina subyacente: el
comportamiento y las acciones de cada objeto se programan haciendo uso de las abstracciones de
los lenguajes imperativos. Los conceptos fundamentales de la programación orientada a objetos se
verán en el tema 6.
De todos los esfuerzos de abstracción que nos alejan del computador y nos acercan al espacio del
problema el primero que vamos a ver es la “abstracción de datos”. Mediante la abstracción de datos
se separan las propiedades abstractas de un determinado tipo de datos de los detalles de su
implementación en una máquina concreta. Sólo las propiedades abstractas son visibles para el
programador y para el resto del código, mientras que la implementación permanece enteramente
privada y puede cambiarse sin que el programa se vea afectado ya que el cambio de tal
implementación no implica ningún cambio en las propiedades abstractas de los datos.
1
Fuentes: Tomado [Eckel 02] y http://en.wikipedia.org/wiki/Abstraction_%28computer_science%29
Tema 2: Abstracción de datos.
Tipos de datos.
33
Tipos de datos.
Los lenguajes ensambladores utilizan los datos tal y como se codifican en las celdas de memoria de
un computador, como una simple colección de bits. Dichos lenguajes no tienen capacidad para
expresar el significado de los datos, toda la responsabilidad recae en el programador y los errores
son muy frecuentes. Hacen falta abstracciones más elaboradas para poder construir programas
grandes o incluso de tamaño moderado.
La mayoría de los lenguajes de programación incluyen un conjunto de tipos de datos simples
(números enteros y reales, caracteres y valores booleanos). Estos tipos de datos vienen definidos
como parte de los lenguajes de programación y se denominan tipos de datos primitivos o build-in
types. Los lenguajes de programación suelen definir asimismo mecanismos para construir nuevos
tipos de datos a partir de los ya existentes. Estos nuevos tipos pueden ser agrupaciones de datos de
un mismo tipo simple (por ejemplo, una tabla de enteros) o agrupaciones de datos de distintos
tipos simples. El mecanismo de construcción de tipos puede generalizarse para construir tipos de
datos complejos a partir de cualquier tipo definido con anterioridad, sea este simple o complejo.
Un tipo de datos es un conjunto de valores con ciertas propiedades y un
conjunto de operaciones que pueden realizarse sobre dichos valores
Una vez definidos los tipos de datos, todos los datos de un programa se clasifican de acuerdo a
dichos tipos. Un dato sólo podrá contener los valores definidos en su tipo y sólo podrá usarse en las
operaciones que permita su tipo. Los tipos de datos abstraen la representación interna de los datos
en el computador, ocultando 1 los detalles de su codificación y manipulación.
Tipado fuerte 2.
El tipado fuerte implica que el lenguaje de programación impone restricciones muy severas sobre
las operaciones que pueden realizarse entre datos de distinto tipo, evitando la compilación o
ejecución del código que viola tales restricciones (por ejemplo, no se permite sumar un número
entero con un carácter). La naturaleza de tales restricciones varía mucho de unos lenguajes a otros
por lo que no es posible dar una definición formal del tipado fuerte. Sin embargo, suele
considerarse que un lenguaje fuertemente tipado debe:
1.
2.
3.
Determinar en tiempo de compilación (análisis estático 3) si las operaciones que se realizan
sobre los datos son consistentes con su tipo. Un lenguaje que no realice esta comprobación no
se considera fuertemente tipado.
Proporcionar seguridad de tipos 4 tanto en tiempo de compilación como en tiempo de ejecución,
es decir rechazar todas aquellas operaciones que violen las restricciones impuestas a los tipos.
Garantizar que cuando se detecte una violación de tipos en tiempo de ejecución se llevará a
cabo una acción bien definida. La idea es que el programador sepa exactamente cómo se va a
comportar el programa si se detecta una violación de tipos.
Volveremos a menudo sobre este punto cuando hablemos del principio de ocultación de la información.
Tomado de http://en.wikipedia.org/wiki/Strongly_typed_programming_language
3 En general el término análisis estático se refiere a aquellas comprobaciones que se hacen antes de ejecutar el programa,
durante su compilación, mientras que el de análisis dinámico se reserva para aquellas comprobaciones que deben realizarse
durante la ejecución del programa. Obviamente cuantos más lejos puedan llevarse los análisis estáticos mejor, hasta el caso
ideal de poder garantizar que no se producirán violaciones de tipos en tiempo de ejecución, evitando así el análisis dinámico.
4 Traducción de type safety.
1
2
Tema 2: Abstracción de datos.
34
Normalmente se exigen estas tres condiciones, sobre todo la primera, para considerar a un lenguaje
fuertemente tipado. Algunos autores van todavía más lejos y exigen además que:
4.
5.
No exista ninguna forma de soslayar el sistema de tipos (por ejemplo, pudiendo trabajar
directamente con los bits almacenados en la memoria).
No se realicen conversiones implícitas de tipos (por ejemplo, muchos lenguajes permiten
sumar un entero y un real convirtiendo automáticamente el número entero en real). Todas las
conversiones de tipos deben ser explícitas (realizadas por el programador). Algunos autores
van todavía más lejos y exigen que no pueda realizarse ninguna conversión de tipos, ya sea
implícita o explícita.
Obsérvese que la mera existencia de tipos de datos en el lenguaje no implica que este sea
fuertemente tipado. Existen lenguajes con tipos de datos como Fortran, Cobol o C que no disponen
de tipado fuerte porque no cumplen las tres primeras condiciones. Estos lenguajes se denominan
débilmente tipados: disponen de un sistema de tipos y de mecanismos para definir tipos complejos,
pero no tienen un sistema que compruebe la consistencia de las operaciones respecto de dichos
tipos.
Lenguajes como Java, C++ y Ada, especialmente este último, son lenguajes fuertemente tipados.
Todos ellos cumplen las tres primeras condiciones, aunque permiten las conversiones de tipos en
determinadas circunstancias.
Beneficios de los tipos de datos 1.
El tipado fuerte es una ayuda inestimable para conseguir programas correctos y eficientes, ya que:
1.
La información de tipos estáticos permite a los compiladores:
−
−
2.
3.
4.
Asignar memoria con eficiencia y generar un código máquina que manipula los datos
también con gran eficiencia.
Reducir la cantidad de código es necesario compilar con lo cual mejora la eficiencia de
traducción.
La verificación de tipos estáticos permite detectar muchos errores de programación antes de
ejecutar los programas mejorando la seguridad y confiabilidad de un programa al reducir la
cantidad de errores que pueden ocurrir durante la ejecución.
Se mejora la legibilidad del código, ya que los tipos explícitos de datos permiten comprender el
papel que desempeñan los datos dentro de un programa.
Permite definir con precisión la forma en que puede utilizarse un código para construir
programas (definición de interfaces).
Tipos y lenguajes de programación.
A la hora de seleccionar (juzgar) un lenguaje de programación es necesario considerar:
−
−
1
El conjunto de tipos primitivos que proporciona.
Si permite combinar los tipos de datos primitivos para crear otros tipos de datos más complejos
(arrays, estructuras,…).
Tomado de [Louden 03].
Tema 2: Abstracción de datos.
−
−
35
Si permite definir nuevos tipos de datos y la manera en que serán tratados por el lenguaje
(¿serán tipos de datos de primera clase? ¿se les aplicará el tipado fuerte?).
Si pueden realizarse conversiones de tipos de forma segura.
Tipos de datos primitivos de Java.
La tabla 1 muestra los tipos de datos primitivos de Java y su tamaño en bytes. Como se puede
observar en la tabla, los tipos de datos primitivos modelan caracteres, valores numéricos (enteros y
reales) y valores lógicos (verdadero y falso). Los tipos de datos primitivos de Java presentan dos
peculiaridades:
1.
2.
Al contrario de lo que ocurre en otros lenguajes, el tamaño de los tipos de datos no cambia de
un computador a otro. Esta invariancia es una de las razones que hacen que los programas de
Java sean portables.
Los tipos de datos primitivos son en Java una “irregularidad”.
Tabla 1: Tipos de datos primitivos en Java.
Tipo de datos
primitivo
Tamaño
Mínimo
Máximo
16-bit
Unicode 0
Unicode 216-1
32-bit
-231
231 -1
boolean
char
byte
short
int
long
float
double
8-bit
16-bit
64-bit
32-bit
64-bit
-128
-215
-263
127
215 -1
263 -1
El tamaño de un tipo de datos numérico determina su rango (mínimo y máximo) y en el caso de los
tipos “reales” también determina su precisión (fracción más pequeña o número más grande que
pueden representarse sin redondeo). Muchos programas asumen un rango y una precisión
determinados. Si disminuye el tamaño de los datos, su rango y precisión también disminuyen y el
programa puede dejar de comportarse correctamente. Lenguajes como C++ o Ada permiten al
usuario definir sus propios tipos numéricos para asegurar la portabilidad de los programas.
Como se explicará más adelante, en Java todo es un objeto. En realidad esto no es del todo cierto ya
que en un programa Java hay algunos datos que no se tratan como objetos: los datos
correspondientes a los tipos de datos primitivos. Una de las propiedades más importantes de un
lenguaje de programación es su regularidad. La regularidad está relacionada con la simplicidad y
con la economía de conceptos y por tanto con la claridad de los programas resultantes. Por ello,
introducir una irregularidad en un lenguaje debe responder a una causa importante. En el caso de
Java esta causa es la eficiencia de los programas. Por razones que se verán más adelante la gestión
de los objetos, su creación y destrucción con la consiguiente reserva y liberación de memoria,
suponen una sobrecarga importante que, en el caso de los tipos de datos primitivos, los diseñadores
de Java prefirieron evitar. Esto supone que en Java hay que considerar dos clases de tipos de datos,
los tipos de datos primitivos y los tipos referencia que se usan con los objetos. De momento sólo nos
ocuparemos de los tipos de datos primitivos. En un apartado posterior hablaremos someramente
de los tipos referencia.
Tema 2: Abstracción de datos.
36
Los valores numéricos enteros se representan usando un bit de signo (0: positivo, 1: negativo) y se
almacenan en complemento a dos. El cálculo del complemento a dos es muy sencillo y muy fácil de
realizar mediante puertas lógicas, donde reside su utilidad:
−
−
Los números positivos se quedarán igual en su representación binaria.
En los números negativos deberemos invertir el valor de cada una de sus cifras, es decir
realizar el complemento a uno, y sumarle 1 al número obtenido.
Cabe recordar que debido a la utilización de un bit para representar el signo, el rango de valores
será diferente al de una representación binaria habitual; el rango de valores decimales para «n» bits
será:
-2n-1 ≤ rango ≤2n-1 - 1
Los valores reales son los más complicados de representar. Un valor real tiene una parte entera y
una parte fraccionaria. La parte fraccionaria provoca pérdida de precisión, ya que el ordenador
tiene precisión finita. Existen valores que no se pueden expresar, como 1/3. La forma de
representar los dos campos da lugar a dos tipos de representaciones: coma fija y coma flotante.
Puesto que Java utiliza la representación en coma flotante nos centraremos exclusivamente en esta
última que se basa en la notación científica de mantisa y exponente:
Número = signo · mantisa · 10exponente
El estándar IEEE754 utilizado por Java distribuye los bits entre signo, mantisa y exponente como se
resume en la tabla 2.
Tabla 2: Estándar IEEE754
TIPO
SIGNO
MANTISA
EXP
float
1 bit
23 bit
8 bit
double
1 bit
52 bit
11 bit
La representación en punto flotante se adapta muy bien a “cualquier” número, pero complica
operaciones aritméticas y algoritmos. La precisión depende de los bits de la mantisa y el rango de
los bits del exponente. Además, hay que tener en cuenta que la precisión de los números
representados en punto flotante no es constante en todo el rango (véase Ejemplo21 en el código
suministrado).
No entraremos en más detalles sobre la representación interna de los tipos numéricos. Por dos
razones. La primera, es que no deberíamos preocuparnos por ella ya que trabajamos a otro nivel de
abstracción, la segunda es que tales representaciones se ven en otras asignaturas. De estas dos
razones, sólo la segunda responde completamente a la verdad. Los tipos numéricos no son
exactamente los números que representan. En Matemáticas el conjunto de los enteros es un
conjunto infinito, mientras que los tipos enteros tienen un rango finito bien definido. La cantidad de
números enteros que pueden representarse es finita, porque el tamaño de la celda de memoria
donde tales números se guardan también lo es. Lo mismo puede decirse para los reales, y algo más.
En los números enteros no hay problemas de precisión, pero en los reales sí. Cualquier intervalo del
conjunto de los números reales es infinito, pero los números de los tipos reales tienen una precisión
finita. El programador en Java, o en cualquier lenguaje que proporcione tipos numéricos, si bien no
tiene que saber cada detalle de la representación interna de los datos numéricos, sí necesita saber
Tema 2: Abstracción de datos.
37
el rango y la precisión de los tipos de datos que maneja y el comportamiento del lenguaje cuando
tales rangos y precisiones son excedidos (véase nuevamente Ejemplo21).
Elementos léxicos del lenguaje Java 1
El léxico de un lenguaje determina su vocabulario, es decir el conjunto de palabras que pertenecen
a dicho lenguaje. En este apartado se presentarán resumidamente los elementos léxicos más
importantes del lenguaje Java, en concreto:
−
−
−
−
−
−
−
Conjunto de caracteres-Unicode Escapes
Comentarios
Identificadores
Palabras reservadas
Separadores
Literales
Operadores
Los literales y operadores se describen muy brevemente ya que se explicarán con mayor extensión
en otros apartados. La mayor parte de los lenguajes de programación definen elementos léxicos
semejantes, si bien con algunas diferencias entre unos y otros.
Conjunto de caracteres.
Los programas Java se escriben en el estándar Unicode. Unicode define tres formas de codificación
bajo el nombre UTF o Formato de Transformación Unicode (Unicode Transformation Format):
−
−
−
UTF-8 — codificación orientada a byte con símbolos de longitud variable.
UTF-16 — codificación de 16 bits de longitud variable optimizada.
UTF-32 — codificación de 32 bits de longitud fija, y la más sencilla de las tres.
De estos tres, Java utiliza la codificación de caracteres en dos bytes (UTF-16). Con 16 bits se pueden
representar 216 = 65536 caracteres, de modo que Unicode codifica los alfabetos de la mayoría de los
lenguajes que se usan en el mundo. Un subconjunto importante de Unicode es el estándar ASCII de
codificación de caracteres en 7 bits que se corresponde con los 128 primeros caracteres de
Unicode.
Se pueden realizar operaciones aritméticas con caracteres:
− ‘A’ se representa como 65 en Unicode
− ‘F’ se representa como 70 en Unicode
− ‘A’ + 5 da como resultado el carácter ‘F’
Pero el resultado no es de tipo char, sino de tipo entero. Para utilizar el resultado como char es
necesario realizar un cambio de tipo de entero a char. Más adelante veremos cómo se hace esto.
1
Puede verse una descripción más completa en
http://java.sun.com/docs/books/jls/second_edition/html/lexical.doc.html#44591 que es la fuente principal de este
apartado.
Tema 2: Abstracción de datos.
38
Comentarios.
Los comentarios son frases que el compilador ignora y que se incluyen para documentar el
programa. El lenguaje Java soporta tres tipos de comentarios (véase tabla 3 y ejemplo).
Tabla 3: Comentarios en Java.
Tipo de comentario
Descripción
/* texto */
El compilador ignora cualquier cosa entre /* y */
/** documentación */
Indica un comentario para generar documentación. Como antes, el
compilador ignora cualquier cosa entre /** y */, pero la
herramienta javadoc de JDK usa estos comentarios para generar
de forma automática documentación.
El compilador ignora cualquier cosa desde // hasta el final de la
línea
// texto
/**
* La clase HolaMundoApp implementa una aplicación que
* simplemente escribe "¡Hola Mundo!" en la salida
estándar.
*/
class HelloWorldApp {
public static void main(String[] args) {
System.out.println("¡Hola Mundo!"); //Escribe la
frase.
}
/* se acabó el programa. */
}
Identificadores en Java.
Cada vez que el programador define un nuevo elemento en el programa debe declararlo
asignándole un nombre. Este nombre es el identificador de ese elemento.
Identificador: secuencia de caracteres que puede ser usada como un
nombre en un programa
Un identificador en Java es una serie de caracteres que consiste en letras, números, subrayados ( _ )
y signos de dólar ($) que no empieza con un número. Las “letras” incluyen los caracteres de todos
los alfabetos de los lenguajes codificados en Unicode.
Se permiten identificadores de cualquier longitud:
Pero_Que$_variablemaslarga_es_la_NUMERO4
y se distingue entre mayúsculas y minúsculas (case-sensitive):
Mivariable
≠ mivariable
Tema 2: Abstracción de datos.
39
Identificadores válidos:
Identificadores no válidos:
VariableNumero1
variableNumero2
$variable_numero_3
Variable1.2
7_variable
.variable
Palabras reservadas en Java.
Existen algunas palabras que aunque son secuencias válidas para definir identificadores no pueden
usarse como tales, ya que son parte del propio lenguaje. Estas palabras se denominan palabras
reservadas y en Java son las de la tabla 4. En la tabla 5 se las clasifica según su función en el
lenguaje.
Tabla 4: Palabras reservadas en Java
abstract
do
if
package
synchronized
boolean
double
implements
private
this
break
else
import
protected
throw
byte
extends
instanceof
public
throws
case
false
int
return
transient
catch
final
interface
short
true
char
finally
long
static
try
class
float
native
strictfp
void
const
for
new
super
volatile
continue
goto
null
switch
while
default
asssert
Tabla 5: Clasificación de las palabras reservadas en Java
Palabras reservadas para
tipos de datos
boolean,
double
Palabras reservadas para
modificadores
abstract, final, native, private, protected,
public, static, transient, synchronized,
volatile, strictfp
Palabras reservadas para
control de flujo
break, case, continue, default, do, while, for,
switch, if, else
Palabras reservadas para
valores predefinidos
true, false, null
byte,
char,
int,
long,
short,
float,
Palabras reservadas para
control de acceso
private, protected, public
Palabras reservadas para
manejo de excepciones
try, catch, finally, throw
Palabras reservadas para
clases y funciones
class, extends, implements, import, instanceof,
new, package, return, interface, this, throws,
void, super
Otras palabras reservadas
const, goto
Tema 2: Abstracción de datos.
40
Literales en Java.
Un literal es la representación de un valor de un tipo primitivo, una cadena de caracteres (tipo
String 1) o el valor null 2. En consecuencia, se definen los siguientes tipos de literales:
−
−
−
−
−
−
Literales enteros.
Literales de punto flotante.
Literales booleanos.
Literales de caracteres.
Literales String
Literal null
Estos literales se describen más adelante en esta lección.
Separadores en Java
Los separadores tienen la función de signos de puntuación. Sirven para agrupar elementos léxicos
de forma que se formen sentencias correctas en Java. Java tiene los siguientes separadores:
(
)
{
}
[
]
;
,
.
El uso y significado de estos separadores se irá viendo durante el curso.
Operadores en Java
Java dispone de un conjunto de 37 operadores, cuyo uso y significado se explicarán más adelante.
=
>
<
!
~
?
:
==
<=
>=
!=
&&
||
++
--
+
-
*
/
&
|
^
%
<<
>>
>>>
+=
-=
*=
/=
&=
|=
^=
%=
<<=
>>=
>>>=
1
String es un tipo referencia que modela cadenas de caracteres. Volveremos sobre el más adelante. De
momento sólo hace falta saber que hay literales asociados a este tipo de datos.
2
null es un valor referencia. Volveremos sobre el más adelante. De momento sólo hace falta saber que hay un
literal asociado a este valor.
Tema 2: Abstracción de datos.
Declaración de variables y constantes
41
Variables
Una variable identifica la localización en memoria en la que se almacena un
valor determinado.
Toda variable tiene un tipo, un nombre y un valor. La variable recibe su nombre y su tipo cuando se
declara y, opcionalmente, recibe un valor mediante una sentencia de asignación. El valor contenido
en la variable debe ser uno de los definidos en su tipo y sólo podrá usarse en las operaciones que
permita su tipo.
Para declarar una variable se escribe su tipo seguido del nombre que vamos a asignarle y
opcionalmente su valor:
Tipo nombreVariable;
int number;
short num = 7;
En el primer ejemplo (int number;) declaramos una variable de tipo int a la que damos el
nombre de number. El compilador reservará una zona de memoria para ella (4 bytes). Puesto que
number no recibe ningún valor, pueden pasar dos cosas:
1.
2.
Que reciba un valor por defecto (si es variable de instancia)
Que su valor esté indefinido (si es variable local de un método).
Todavía no es momento para explicar qué es una variable de instancia y qué es una variable local.
Más adelante, cuando veamos nuestro primer programa completo en Java, volveremos sobre este
asunto. En la tabla 6 se muestran los valores por defecto de las variables de instancia.
En el segundo ejemplo (short num = 7;) declaramos una variable de tipo short a la que damos
el nombre de num y asignamos el valor de 7. El signo = no denota en Java comparación, sino la
asignación de un valor a una variable. El compilador reservará una zona de memoria para num (2
bytes) y guardará en ella el valor 7 codificado en binario en 2 bytes.
La sintaxis de Java permite declarar más de una variable del mismo tipo en la misma línea,
separando con comas los identificadores:
Tipo nombreVar1 [=valor1], nombreVar2 [=valor2] … ;
int nroAprobados, nroSuspensos, nroEstudiantes;
int nAprobados = 2, nSuspensos = 0, nEstudiantes = 2;
Tema 2: Abstracción de datos.
42
Constantes
Una constante identifica una localización en memoria en la que se almacena
un valor determinado que no puede cambiar en tiempo de ejecución.
La forma que tiene Java de definir una constante es anteponiendo la palabra reservada final en la
declaración de una variable. Las constantes deben inicializarse asignándolas un valor cuando se
declaran:
final float PI = 3.141592;
// declaración válida de constante
final float FACTORDECORRECION;
inicialización.
//
declaración
NO
válida.
Falta
Tabla 6: Valores por defecto de las variables de instancia
Tipo
Valor inicial por defecto
Tipos numéricos enteros: byte, short, int,
long
0
Tipos numéricos reales: float, double
boolean
false
char
\u0000
Tipos referencia
null
0.0
Asignación y expresiones en Java.
Sentencia de asignación
Sentencia de asignación: Sentencia mediante la cual se asigna un valor a una variable.
variable = valor
// Asignación en Java
El valor de la variable puede asignarse cuando se declara o diferirse a la ejecución del programa.
Las asignaciones de valores se realizan mediante el operador =. Su significado es el siguiente: toma
el valor que aparece a su derecha (llamado frecuentemente rvalue: right-value) y lo copia en la
variable que aparece a su izquierda (llamada frecuentemente lvalue: left value).
Un rvalue es cualquier constante, variable o expresión que produce un valor. Un lvalue debe ser el
identificador de una variable.
int
int
a =
b =
4 =
a = 1;
b = 2;
b;
1.0;
a;
//
//
//
//
//
Declara variable a y la inicializa a 1.
Declara variable b y la inicializa a 2.
Copia el contenido de b en a.
Error. Intento de asignar un real a un entero.
Error. Lvalue no es un identificador de variable.
// Consideremos la declaración de tres variables
int operando1;
int operando2;
float operando3;
Tema 2: Abstracción de datos.
43
// Asignaciones válidas
operando1 = 7;
operando2 = 9;
operando3 = 4.7;
// Asignaciones no válidas
suma = operando1 + operando2;
operando1 = 7.83;
operando1 = operando3;
entero
// variable suma no declarada
// 7.83 no es de tipo entero
// operando3 no es de tipo
Asignación y expresiones
Expresiones: Secuencia de operandos y operadores que unidos según ciertas
reglas producen un resultado.
// Consideremos la declaración de tres variables
int operando1 = 1;
int operando2 = 2;
float operando3;
// Conversiones implícitas:
// Asignación curiosamente válida: Java convierte el resultado de
la suma en
// float y después se lo asigna a operando3.
operando3 = operando1 + operando2;
// Uso de la misma variable como lvalue y rvalue:
// Primero se ejecuta operando1 + operando2.
// Después se efectúa la asignación.
operando1 = operando1 + operando2;
El uso de una misma variable a ambos lados de una asignación es muy habitual. En el ejemplo
de arriba:
−
−
−
Los valores de operando1 y operando2 son 1 y 2 antes de la asignación.
Se efectúa la suma con esos valores y el resultado se asigna a operando1.
Después de efectuar la asignación el valor almacenado en operando1 es 3 y en
operando2 2.
Tema 2: Abstracción de datos.
44
Conversión de tipos
Las operaciones determinan el tipo a devolver a partir de los operandos:
4 / 3 -> 1
(int, int) -> int
Pero a veces, no es el resultado deseado. En Java existen dos formas de conversión de tipos
−
−
Implícita (definida en el lenguaje) y realizada automáticamente.
Explícita (casting o promoción), forzada por el programador.
onversión explícita (casting)
(nombre de tipo) expresión
Convierten el valor de la expresión a un valor del tipo entre paréntesis.
(char)98
(int)10.0
(double)10
// devuelve ´b´
// devuelve el entero 10
// devuelve el double 10.0
Conversiones implícitas
Sólo se aplica a tipos de datos primitivos y de tipos “pequeños” a “grandes”:
−
−
−
byte a short, int, long, float, o double
short a int, long, float o double
char a int, long, float, o double
−
int
−
long a float o double
−
a long, float o double
float a double
Se puede perder información: LOS DÍGITOS MENOS SIGNIFICATIVOS PUEDEN PERDERSE
−
−
En el paso de long a float o double
En el paso de int a float
La promoción explícita tiene prioridad sobre la implícita.
int A = 5;
int B = 4;
int C = A / B;
float x = (float)(A/B);
x = (float)A / B;
// valor de C igual a 1
// valor de x igual a 1.0;
// valor de x igual a 1.25;
Tema 2: Abstracción de datos.
45
Literales1
Literales booleanos
Sólo hay dos valores booleanos: true y false.
Literales enteros
Un literal entero puede expresarse en decimal (base 10), hexadecimal (base 16) y octal (base 8).
Decimal:
Hexadecimal:
0xC0B0L
Octal:
−
0
2
0xDadaCafe
1996
0x00FF00FF
0x00FF00FF, 0x100000000L,
0372
0372, 0l
0777L
Un literal entero es de tipo int salvo que vaya precedido por el sufijo l o L
tipo int:
tipo long:
−
−
−
−
−
100, 0xC0B0, 0372
L100, l200, 0l
0777L, 0xC0B0L
Por defecto el literal se considera expresado en decimal.
Para indicar que un literal entero está en hexadecimal se pone delante 0x o 0X
Para indicar que un literal entero está en octal se comienza por 0:
Todos los literales enteros expresados en octal tienen al menos dos digitos.
Si en los literales aparecen caracteres no permitidos o exceden el valor máximo permitido para
el tipo se produce un error de compilación.
Literales de punto flotante.
Los literales en punto flotante se escriben como números decimales con un exponente opcional:
digitos.[digitos][(e|E)entero]
23.f
−
−
−
.5
0.0
3.1415
1e-9
1e9
1e+9
1E12
El tipo por defecto es double. Se puede indicar el tipo con un sufijo:
Las letras f y F indican que el literal es float:
Las letras d Y D indican que el literal es doublé:
1e-9f 1E12F
1e-9d 1E12D
Se produce un error de compilación si:
Un literal representa un número tan grande que su representación en el estándar IEEE754 es
infinito.
1
Tomado de http://java.sun.com/docs/books/jls/second_edition/html/lexical.doc.html
Tema 2: Abstracción de datos.
46
Un literal representa un número tan pequeño que su representación en el estándar IEEE754 es
cero.
Existen dos constantes predefinidas Float.NaN y Double.NaN (NaN: Not a Number) que
representan valores que resultan de expresiones aritméticas, pero que no se corresponden con un
número real (p.e: 0/0).
Literales de caracteres.
Los caracteres ASCII pueden escribirse tal cual entre comillas simples. Todos los caracteres (ASCII o
no) pueden escribirse utilizando su codificación en octal o en hexadecimal:
Caracteres ASCII: ‘1’, ‘a’, ‘%’
Caracteres en hexadecimal: \u seguido de 4 dígitos hexadecimales que representan los 4 bytes en
que se codifica el carácter: ‘\u5496’
Existen una serie de caracteres especiales que pueden escribirse utilizando “secuencias de escape”
(tabla 7)
Tabla 7: Secuencias de escape
Secuencia
de escape
Unicode
Descripción
\n
\u000A
Nueva línea
\b
\u0008
\f
\u000C
\’
\u0027
\t
\u0009
\r
\u000D
\\
\u005C
\”
\u0022
Tabulador
Retroceso (Backspace)
Retorno de carro
Nueva página (Form feed)
\ (Backslash)
Comilla simple
Comilla doble
Literales de cadenas de caracteres (literales String).
El tipo String no es un tipo primitivo de Java, sino un tipo referencia. String es una clase que
modela cadenas de caracteres. Cada cadena de caracteres es un objeto de la clase (o tipo) String.
A pesar de ello, las constantes de tipo String pueden escribirse como literales, cosa que no ocurre
con ningún otro tipo referencia en Java.
Un literal String consiste en una secuencia de caracteres, incluyendo secuencias de escape,
escrita entre comillas dobles:
“Soy un literal String”
“Soy \t un literal String\n con un salto de línea y un tabulador”
“\u5469 \u5561 Soy un String con dos caracteres chinos”
“\”Soy un literal String entre comillas \””
""
// the empty string
"\""
// a string containing " alone
Tema 2: Abstracción de datos.
"This is a string"
// a string containing 16 characters
"This is a " +
// actually a string-valued constant expression,
"two-line string"
// formed from two string literals
47
Operadores y expresiones 1
Operadores aritméticos
Los operadores aritméticos de la tabla 8 pueden aplicarse a todos los tipos numéricos (enteros y de
punto flotante). Los datos del tipo char pueden participar en las operaciones aritméticas (Java los
convierte en enteros). Todos los operadores de la tabla son operadores binarios (operan sobre dos
operandos).
Tabla 8: Operadores aritméticos
Operación
Operador
Expresión → Resultado
Suma
+
f + 7
Resta
-
p - c
Multiplicación
*
b * m
División
/
5 / 2 → 2
Módulo (Resto)
%
5.0/2 → 2.5
7 % 3 → 1
7.0 % 3 → 1
(-7) % 3 → -1
(-7.1) % 3 → -1,09999
Java aplica los operadores de las expresiones aritméticas en el mismo orden que en álgebra.
Reglas de precedencia de operadores aritméticos
1.
Las expresiones contenidas entre paréntesis se evalúan primero, aplicando primero los
operadores del par de paréntesis más interno.
3.
A continuación se aplican las operaciones de suma y resta de izquierda a derecha.
2.
1
A continuación se aplican las operaciones de multiplicación, división y residuo de izquierda a
derecha.
Apartado tomado en su mayoría de “Object Oriented Software Development Using Java. Principles,
Patterns and Frameworks”, Xiaoping Jia.
Tema 2: Abstracción de datos.
48
Cuando comentábamos las características de los lenguajes fuertemente tipados decíamos que si se
detectaba una violación de tipos en tiempo de ejecución se debía llevar a cabo una acción bien
definida. Al realizar operaciones aritméticas puede ocurrir que el resultado esté fuera de rango
(desbordamiento) o que se intenten realizar operaciones absurdas (p.e: 0/0). Veamos que hace Java
en estos casos.
Las operaciones aritméticas en Java nunca se desbordan ni por arriba ni por abajo: si un valor
excede el rango de su tipo, la operación devuelve un resultado que es el resultado que debería dar
la operación módulo el rango de su tipo:
Valor máximo de int: 2147483647
2147483647 + 1 → -2147483648
2147483647 + 2 → -2147483647
2147483647 * 2 → -2
Las operaciones aritméticas enteras x/y y x%y producen un error (excepción) llamado
ArithmeticException cuando y es igual a 0 1.
Las operaciones en punto flotante no producen errores (excepciones) en ninguna circunstancia. En
lugar de ello devuelven valores especiales definidos en el estándar IEEE754. El programa no dejará
de funcionar ni siquiera ante divisiones por cero. El estándar IEEE754 define dos números mágicos:
NaN (not a number) e infinito. Las reglas aplicables en la división y multiplicación de números en
punto flotante cuando ninguno de los operandos es NaN se resumen en la tabla 9.
Tabla 9: Casos especiales de multiplicación y división
de números en punto flotante
x
y
x/y
x*y
Finito
±0
±∞
±0
Finito
±∞
±0
±∞
±0
±0
±0
±∞
±∞
±∞
±∞
Finito
NaN
±∞
±0
±∞
NaN
±∞
±0
NaN
Cuando uno de los dos operandos en NaN el resultado es NaN.
1
La detección, captura y tratamiento de excepciones se verán en la lección [].
Tema 2: Abstracción de datos.
Operadores de incremento y decremento
49
Las operaciones en las que se resta o suma 1 a una variable son tan comunes que Java y otros
lenguajes definen operadores especiales para abreviar su escritura. La tabla 10 resume los
operadores de incremento y decremento de Java. Se pueden aplicar a todos los tipos numéricos.
Tabla 10: Operadores de incremento y decremento
Operador
Operación
Ejemplo
Significado
++
Preincremento
++a
Incrementa a en 1 y luego usa el valor de a en la
expresión donde a reside.
--
Predecremento
--a
Decrementa a en 1 y luego usa el valor de a en
la expresión donde a reside.
Posincremento
Posdecremento
a++
a--
Utiliza el valor de a en la expresión donde a
reside y después incrementa a en 1
Utiliza el valor de a en la expresión donde a
reside y después decrementa a en 1
Operadores de igualdad y relacionales
Los operadores relacionales se resumen en la Tabla 11:
Tabla 11: Operadores relacionales.
Operador
Ejemplo
Significado
==
x == y
x es igual a y
!=
x != y
x distinto de y
>
x > y
x mayor que y
<
X < y
x menor que y
>=
x >= y
x mayor o igual que y
<=
x <= y
x menor o igual que y
Todas las expresiones relacionales devuelven un valor booleano (true o false).
Los operadores == y != se pueden utilizar con cualquier tipo. Los operadores de comparación se
utilizan sólo con tipos numéricos y caracteres.
Tema 2: Abstracción de datos.
50
Operadores lógicos
Los operadores lógicos se resumen en la tabla 12. Se aplican siempre sobre operandos booleanos y
devuelven un resultado booleano.
Tabla 12: Operadores lógicos.
Operación
lógica
Operador/
Ejemplo
AND lógico
&&
A && B
OR lógico
||
A || B
NOT lógico
!
!A
Significado
true si ambas condiciones son verdaderas
false si una condición es falsa.
true si una de las condiciones es verdadera.
false si ambas condiciones son falsas.
true si A es falsa
false si A es verdadera.
Los operadores lógicos se evalúan en cortocircuito, es decir:
−
−
En exprA && exprB, exprB sólo se valúa si exprA es true
En exprA || exprB, exprB sólo se evalúa si ExprA es false
Como consecuencia de lo anterior es aconsejable:
−
No utilizar expresiones dependientes (la verdad o falsedad de una de ellas depende de la
verdad o falsedad de la otra). He aquí un ejemplo de lo que NO debe hacerse:
int counter = 0;
int media = 5;
if (counter != 0 && media/counter++ > 5) { ... }
−
No realizar operaciones en las expresiones a evaluar, ya que podrían no ejecutarse:
sexo == varon || ++edad > 65
−
1
En expresiones con && si las condiciones son independientes 1 colocar primero la que tenga
mayores probabilidades de ser falsa.
Deben procurar que siempre sea así.
Tema 2: Abstracción de datos.
−
51
En expresiones con || si las condiciones son independientes colocar primero la que tenga
mayores probabilidades de ser verdadera.
Operadores lógicos de bits (bitwise operators) y de desplazamiento de bits
(shift operators)
Operadores lógicos de bits.
Los operadores lógicos de bits (bitwise) se resumen en la tabla 13. Se aplican sobre operandos
booleanos o enteros.
Cuando se aplican a operandos booleanos devuelven un resultado booleano equivalente a la
operación lógica correspondiente, pero con una diferencia respecto de los operadores lógicos: no se
evalúan en cortocircuito. Por ejemplo, en una expresión exprA & exprB siempre se evalúan
ambos operandos, independientemente de si la primera expresión evaluada es true o false.
Cuando se aplican a operandos enteros operan sobre cada bit individual y devuelven un resultado
entero.
Tabla 13: Operadores lógicos de bits.
Operación
lógica
AND
OR
OR
Exclusivo
NOT
Operador/
Ejemplo
&
A & B
|
A | B
^
A ^ B
∼
∼A
Significado
A y B booleanos: como &&, pero sin evaluación en cortocircuito
A y B enteros. A = 0xff, B= 0x55 → A & B = 0x55
A y B booleanos: como || pero sin evaluación en cortocircuito
A y B enteros. A = 0xAA, B= 0x55 → A | B = 0xFF
A y B booleanos.
true si una condición es verdadera y la otra falsa.
false si ambas condiciones son verdaderas o ambas son falsas.
A y B enteros. A = 0xAA, B= 0x55 → A ^ B = 0xFF
A = 0xFF, B= 0x55 → A ^ B = 0xAA
A booleano: como !
A entero. A = 0x55 → ∼A = 0xAA
Tema 2: Abstracción de datos.
52
Operadores de desplazamiento de bits.
Los operadores lógicos de desplazamiento de bits se resumen en la tabla 14. Se aplican
exclusivamente sobre operandos enteros.
Tabla 14: Operadores de desplazamiento de bits.
Operación
Operador/
Ejemplo
Significado
Desplazamiento de bits a
la izquierda
x << k
Desplaza los bits de x k posiciones a la izquierda
rellenando con ceros la parte derecha:
0xff << 4 → 0xf0
Desplazamiento de bits a
la derecha con signo
x >> k
Desplaza los bits de x k posiciones a la derecha rellenando
la parte derecha con el valor del bit que está en el extremo
izquierda (bit de signo)
Ver ejemplo
Desplazamiento de bits a
derecha sin signo
x >>> k
Desplaza los bits de x k posiciones a la derecha rellenando
con ceros la parte izquierda:
Ver ejemplo
Desplazamientos de bits:
0xff << 4 = 0xff0 = 111111110000
0xff >> 4 = 0xf = 1111
0xff >>> 4 = 0xf = 1111
-0xf5 = 0xffffff0b =
-0xf5 << 4 = 0xfffff0b0 =
-0xf5 >> 4 = 0xfffffff0 =
11111111111111111111111100001011
11111111111111111111000010110000
11111111111111111111111111110000
Relleno a izquierda con bit de signo
-0xf5 >>> 4 = 0xffffff0 =
00001111111111111111111111110000
Relleno a izquierda con ceros
Tema 2: Abstracción de datos.
53
Operadores de asignación
Un operador de asignación puede ser el símbolo = o cualquiera de los que aparecen en la tabla 15.
Todos ellos están formados por el signo = y un operador binario.
Tabla 15: Operadores de asignación.
Operador
Expresión
Expresión equivalente
+=
X += 7
X = X + 7
-=
X -= 7
X = X - 7
*=
X *= 7
X = X * 7
/=
X /= 7
X = X / 7
%=
X %= 7
X = X % 7
<<=
X <<= k
X = X << k
>>=
X >>= k
X = X >> k
>>>=
X >>>= k
X = X >>> k
&=
X &= 0xff
X = X & 0xff
^=
X ^= 7
X = X ^ 7
|=
X |= Y
X = X | Y
Los operadores de asignación y los de incremento y decremento son lo que en la jerga de los
programadores se llama “syntactic sugar”. Realmente no son necesarios, basta con la sentencia de
asignación. En general, no está bien visto que en un programa una misma cosa pueda hacerse de
formas diferentes, pero estos operadores responden a un tipo de asignación tan común que su
inclusión en el lenguaje está plenamente justificada. Facilitan en gran medida la labor del
programador y la hacen más segura. Se recomienda su uso.
El operador condicional ?:
Condición ? Expresion1 : Expresion2
Tres operandos:
•
•
•
Condición:
Expresión booleana (verdadero o falso)
Expresión 2:
Resultado si Condición es falsa
Expresión 1:
Resultado si Condición es verdadera
Tema 2: Abstracción de datos.
Resumen de operadores. Asociatividad y precedencia
54
Finalmente, la tabla 16 resume los operadores, su predecedencia (está ordenada de mayor a
menor) y asociatividad.
Tabla 16: Precedencia y asociatividad de operadores.
Operadores
Asociatividad
Tipo
()
Izda Dcha
Paréntesis
++ -- !
Dcha Izda
Unarios
* / %
Izda Dcha
Multiplicativos
+ -
Izda Dcha
Aditivos
< <= > >=
Izda Dcha
Relacionales
== !=
Izda Dcha
De igualdad
&
Izda Dcha
And bitwise
^
Izda Dcha
Xor bitwise
|
Izda Dcha
Or bitwise
&&
Izda Dcha
And lógico
||
Izda Dcha
Or lógico
?:
Dcha Izda
Condicional
= += -= …
Dcha Izda
Asignación
Tema 2: Abstracción de datos.
Primer programa en Java.
55
Java es un lenguaje completamente orientado a objetos: todos los elementos de un programa Java
son objetos. Los objetos se definen mediante clases que, a su vez, son también objetos, aunque con
características especiales. Sin embargo, la asignatura está organizada de forma que hay que
programar en Java antes de saber siquiera lo que es un objeto. Esto, sin embargo, es un problema
mucho menor de lo que parece:
1.
2.
3.
Todo el código que necesitamos puede definirse en el interior de clases y definir una clase en
Java es muy sencillo.
Java posee un mecanismo para que parte o la totalidad del código definido en una clase pueda
utilizarse sin necesidad de crear objetos.
El tipo de ejemplos que van a verse hasta el tema 5 pueden encapsularse dentro de clases muy
simples.
Aún así algo tenemos que saber de las clases y de los objetos porque en Java son ineludibles.
Fijémonos en el cuadro de abajo que muestra en un ejemplo muy sencillo cómo serán nuestros
primeros programas en Java. A pesar de su sencillez, para poder entender el ejemplo hay que
adelantar, aunque de forma muy resumida, muchos de los conceptos que se verán en temas
sucesivos. Los detalles de edición, compilación y ejecución se explican en el manual de prácticas.
import java.util.*;
public class HelloDate {
static Date hoy;
static int x = 1;
public static void main(String[] args) {
hoy = new Date();
String s = “Hola, la fecha de hoy es:”;
System.out.println(s);
System.out.println(hoy);
}
}
Importación de paquetes: import java.util.*;
El código de un programa casi nunca está ubicado en un único fichero, sino que se organiza en
múltiples ficheros que a su vez se organizan en múltiples directorios. Cuando el código escrito en
un fichero necesita utilizar el código ubicado en otro fichero tiene que importarlo. En Java los
ficheros se importan mediante la sentencia import.
La primera línea del ejemplo indica que importamos todo el código incluido en el paquete
java.util. Aunque no es estrictamente cierto, podemos asimilar de momento los conceptos de
paquete y directorio. Si asumimos esto import java.util.* significa que importamos todo el
código visible definido en los ficheros del directorio java.util. Realmente, de todos los
elementos definidos en java.util sólo utilizamos la clase Date que modela fechas. Podríamos
haber importado exclusivamente este elemento cambiando el * por el nombre del elemento a
importar (import java.util.Date). Hacer esto clarifica las dependencias de nuestro código y
hace la compilación más rápida. El programa en sí es igual de eficiente tanto si importamos
paquetes completos como si sólo importamos los elementos que necesitamos.
Tema 2: Abstracción de datos.
56
Clases: public class HelloDate.
Con excepción de las sentencias import todo el código de un programa Java está encapsulado
dentro de clases. La forma de definir una clase en Java es:
modificadorAcceso class nombreDeLaClase {
}
códigoDelaClase
La palabra public es un modificador de acceso que indica que la clase es visible desde cualquier
otro paquete. class es una palabra reservada que indica que estamos definiendo una clase y
HelloDate es el nombre de nuestra clase.
Tipos referencia en Java: static Date hoy;
Date es un tipo de datos, pero no un tipo de datos primitivo, sino un tipo referencia. Los tipos
referencia proporcionan acceso a los objetos y por tanto son los tipos más comunes de Java y en
general de todos los lenguajes orientados a objetos. Los tipos referencia se van a estudiar con cierta
profundidad en el tema 6, sin embargo, puesto que en Java no se pueden eludir ni siquiera en los
ejemplos más sencillos, algo hay que saber ya de ellos. No se trata de dar ahora muchos detalles,
sólo algunas nociones básicas.
Consideremos las variables hoy y x. hoy identifica a un objeto de tipo Date que modela fechas y x
identifica a una variable de tipo primitivo int. Cuando en el programa utilizamos el identificador x
nos estamos refiriendo directamente a la celda de memoria que representa x y que contiene,
porque así se ha inicializado, el valor 1. Pero cuando utilizamos el identificador hoy NO nos
estamos refiriendo directamente al objeto al que identifica, porque hoy no contiene al objeto en sí,
sino una referencia al objeto. Si echáramos un vistazo a los contenidos de las posiciones de memoria
identificadas por x y hoy veríamos que en x guardamos el valor 1, y que hoy no contiene al objeto
sino una referencia a las celdas en las que realmente se encuentra el objeto.
x
1
objeto
20 sept 2010
hoy
Referencia al objeto
Cuando hacemos la declaración Date hoy estamos creando una referencia que no apunta a ningún
objeto. A partir de su declaración, la referencia ya existe, pero no hemos dicho a quien apunta y por
tanto no apunta a nada. Esto se expresa diciendo que la referencia es nula (null). Para que hoy
apunte a algo tenemos que decirle a que objeto apunta y para tener un objeto al que apuntar
primero hay que crearlo (new Date()). Una vez creado el objeto la sentencia de asignación hace
que hoy apunte a ese objeto (hoy = new date()).
Tema 2: Abstracción de datos.
57
Aunque la explicación que se ha dado de los tipos referencia es muy incompleta nos va a dar los
argumentos suficientes para entender algunas cosas de nuestros primeros programas en Java que
de otro modo serían un misterio.
Miembros estáticos: static
En una clase Java se pueden definir dos cosas: variables y métodos. En principio estas variables y
métodos pertenecen a los objetos de dicha clase, de modo que sólo podemos acceder a ellos a través
del objeto al que pertenecen. Pero ¿y si no queremos o no nos conviene utilizar objetos? ¿Podemos
definir variables y métodos que puedan utilizarse aunque no existan objetos? Sin entrar de
momento en más explicaciones, diremos que la respuesta es afirmativa y que la forma de hacerlo es
marcar a la variable o método como static. En nuestro ejemplo hay dos variables (int y hoy) y
un método (main) todos ellos marcados como static.
El método principal de un programa Java:
public static void main(String[] args)
Se acaba de decir que todo el código de un programa Java está encapsulado dentro de clases y que
en una clase Java se pueden definir dos cosas: variables y métodos. Un método encapsula un
fragmento de código ejecutable. Cada vez que se invoca un método se ejecuta el código encapsulado
en el mismo. El método principal de un programa es su método main. Desde el método main se
llama al resto del código del programa. Cuando en un programa hay más de una clase con un
método main habrá que indicar al compilador cuál es la clase principal del programa, para que
ejecute el método de dicha clase y no otro. Observemos que:
−
−
−
−
−
La clase principal, aquella que ubica el método main que representa al programa debe ser
pública public.
El método principal es público.
El método principal es static (independiente de los objetos)
El método principal no devuelve ningún resultado (ese es el significado de la palabra void)
Al método principal pueden pasársele datos (ese es el significado de String [] args)
aunque de momento no vamos a hacerlo.
Creación de objetos
En el código de main aparecen dos sentencias de creación de objetos:
hoy = new Date();
String s = “Hola, la fecha de hoy es:”;
Los objetos se crean a partir de “métodos constructores” que tienen el mismo nombre que su clase.
Hay una excepción a esta regla: los objetos de la clase String. La clase String modela cadenas de
caracteres y puede crearse un objeto de la clase String simplemente indicando a qué cadenas de
caracteres representa, tal y como se hace en el ejemplo.
Tema 2: Abstracción de datos.
Variables de instancia y locales
58
La variable s presenta una diferencia con hoy: ha sido declarada dentro del método main. Las
variables pueden ser locales, definidas dentro de un método, o de instancia definidas fuera de los
métodos. La diferencia estriba en que las variables locales sólo pueden utilizarse dentro del método
en el que han sido definidas y las de instancia pueden utilizarse en todos 1 los métodos de una clase.
Además, las variables de instancia reciben un valor por defecto (tabla 6) en tanto que las locales si
no se inicializan tienen un valor aleatorio.
Invocación de los métodos de un objeto.
En el código de main aparecen dos invocaciones a un método de un objeto:
System.out.println(s);
System.out.println(hoy);
El objeto en cuestión se identifica mediante la referencia System.out y el método es println.
Estas sentencias pueden (y deben) interpretarse como un mensaje que mandamos al objeto
System.out para que imprima en salida estándar un String y una fecha. El mensaje sería
println. De hecho cada método de un objeto representa un mensaje que dicho objeto puede
recibir. El código que hay dentro del método es la respuesta que da el objeto a ese mensaje.
El paquete java.lang
El alumno puede estar ahora preguntándose de que chistera nos hemos sacado System.out.
Podría estar en java.util puesto que hemos importado todo lo que hay en dicho paquete. Sin
embargo no es así. Hay un paquete cuyas clases se encuentran disponibles en todo fichero Java sin
necesidad de importarlo. Dicho paquete es java.lang. Para saber que hay en java.lang basta
con conectarse a la página de sun 2 y mirar la documentación del paquete 3. De igual manera pueden
verse los contenidos de cualquier paquete. Si mirásemos la documentación de java.util
veríamos que incluye la clase Date. Es fácil bajarse la documentación de todos los paquetes de Java
desde sun y es igualmente fácil consultarla remotamente on-line.
Volviendo al paquete java.lang, puede observase que contiene la clase System y que en dicha
clase hay definido un objeto out que pertenece a una clase PrintStream en la cual se define el
método println.
1
Matizaremos esto cuando veamos con mayor profundidad el significado de static.
2
Se asume que los alumnos dispone de un navegador y lo saben utilizar.
http://download.oracle.com/javase/1.4.2/docs/api/java/lang/package-summary.html
3
Tema 3: Abstracción de control.
Lección 3: Abstracción de control
59
Objetivos
Presentar la abstracción de control.
Describir las reglas de la programación estructurada y las estructuras de selección y repetición.
Introducir parte del vocabulario básico de la algoritmia y de los programas estructurados.
Aprender a utilizar las reglas de la programación estructurada para diseñar, implementar y probar
pequeños programas. Diseño mediante refinamientos sucesivos.
Contenido
Abstracción y lenguajes de programación.
Algoritmos.
Sentencias en Java.
Sentencias de selección en java.
Sentencias de iteración en Java.
Algoritmos y refinamientos sucesivos.
Construcción de programas estructurados.
Tema 3: Abstracción de control.
Abstracción y lenguajes de programación.
60
Abstracción de control 1
En las ciencias de la computación, el flujo de control se refiere al orden en el que se ejecutan las
sentencias de un programa. La regla general es que las instrucciones se ejecuten sucesivamente una
tras otra, pero dependiendo de que se cumpla alguna condición es posible que diversas partes del
programa se ejecuten o no. Las sentencias de control de flujo pueden clasificarse de la siguiente
manera:
−
−
−
−
−
−
Continuar con la siguiente sentencia.
Salto incondicional a una sentencia.
Salto condicional: ejecución de un conjunto de sentencias solo si se cumple una condición.
Bucle: ejecución de un conjunto de sentencias mientras se cumpla una determinada
condición.
Llamada a subrutina y retorno: salto a ejecución de un subprograma y retorno al punto de
llamada.
Parada del programa.
Habría que añadir un mecanismo adicional de alteración del flujo de un programa: las
interrupciones y señales. Las señales e interrupciones son mecanismos de bajo nivel que alteran el
flujo de control de forma similar a las llamadas a subrutina, pero que ocurren como respuesta a un
evento externo en lugar de cómo respuesta a la ejecución de una sentencia de control de flujo 2, por
lo que no se han incluido en la lista.
A nivel de código máquina o ensamblador, las instrucciones de control de flujo trabajan usualmente
alterando el valor del contador de programa. Sin la abstracción de control el programador tendría
que especificar a nivel de registros y posiciones de memoria cada una de las acciones de control
arriba enumeradas y tendría que hacerlo de forma diferente para cada computador, ya que
diferentes procesadores utilizan diferentes conjuntos de instrucciones para expresar saltos,
bifurcaciones y llamadas a subrutinas.
Los lenguajes de programación más antiguos solían apoyarse en una sola instrucción para
modificar la secuencia de ejecución de las instrucciones mediante una transferencia incondicional
de su control (con la instrucción goto, del inglés "go to", que significa "ir a"). Pero estas
transferencias arbitrarias del control de ejecución hacen los programas muy poco legibles y difíciles
de comprender. A finales de los años sesenta, surgió una nueva forma de programar que reduce a la
mínima expresión el uso de la instrucción goto y la sustituye por otras más comprensibles. Esta
forma de programar, llamada programación estructurada, se basa en un famoso teorema,
desarrollado por Edsger Dijkstra, que demuestra que todo programa puede escribirse utilizando
únicamente las tres estructuras básicas de control siguientes:
−
−
−
Secuencia: el bloque secuencial de instrucciones, instrucciones ejecutadas sucesivamente, una
detrás de otra.
Selección: la instrucción condicional con doble alternativa, de la forma "if condición then
instrucción-1 else instrucción-2".
Iteración: el bucle condicional "while condición do instrucción", que ejecuta la instrucción
repetidamente mientras la condición se cumpla.
Los programas que utilizan sólo estas tres instrucciones de control básicas o sus variantes, pero no
la instrucción goto, se llaman estructurados. Ésta es la noción clásica de lo que se entiende por
programación estructurada (llamada también programación sin goto) que hasta la aparición de la
programación orientada a objetos se convirtió en la forma de programar más extendida. Una
Fuentes principales: http://en.wikipedia.org/wiki/Control_flow y
http://www.mcgraw-hill.es/bcv/guide/capitulo/8448148703.pdf
2 La captura de interrupciones y la programación de las rutinas de servicio a las mismas queda fuera del
alcance de la asignatura.
1
Tema 3: Abstracción de control.
61
característica importante en un programa estructurado es que puede ser leído en secuencia, desde
el comienzo hasta el final, sin perder la continuidad de la tarea que cumple el programa. Este hecho
es importante porque es mucho más fácil comprender completamente el trabajo que realiza una
función determinada si todas sus instrucciones son contiguas y están encerradas por un bloque. La
facilidad de lectura, de comienzo a fin, es una consecuencia de utilizar solamente tres estructuras
de control, y de eliminar la instrucción de transferencia de control goto. De hecho, la programación
estructurada es una repuesta al “código spaghetti” en el que no hay una estructura lógica bien
definida. Con la programación estructurada cada bloque de código tiene un punto de entrada y un
punto de salida bien definidos y define un ámbito para declarar variables que sólo son accesibles
dentro de dicho ámbito, lo que incrementa la seguridad y legibilidad de los programas.
Algoritmos1.
La programación estructurada facilita el planteamiento de problemas que pueden ser resueltos
mediante la definición de algoritmos. Estos problemas son muy comunes en muchos dominios de
aplicación por lo que la algoritmia es una de las ramas más importantes de la informática. Puesto
que las estructuras de control nos van a permitir programar algoritmos, este es un buen tema para
introducirlos. Empecemos por una definición:
Algoritmo: conjunto de instrucciones o reglas bien definidas, ordenadas y finitas que permite
realizar una actividad mediante una serie de pasos sucesivos. Dados un estado inicial y una
entrada, siguiendo los pasos sucesivos se llega a un estado final y se obtiene una solución.
Los algoritmos son el objeto de estudio de la algoritmia
Expresión de los algoritmos.
Los algoritmos pueden ser expresados de muchas maneras, incluyendo entre otras a:
−
−
−
−
−
El lenguaje natural.
Los diagramas de flujo
El pseudocódigo.
Notaciones formales.
Los lenguajes de programación.
Las descripciones en lenguaje natural tienden a ser ambiguas y extensas. El pseudocódigo y los
diagramas de flujo evitan muchas ambigüedades del lenguaje natural. La descripción de un
algoritmo usualmente se hace en tres niveles:
−
−
−
Descripción de alto nivel. Se establece el problema, se selecciona un modelo matemático y
se explica el algoritmo de manera verbal, posiblemente con ilustraciones y omitiendo
detalles.
Descripción formal. Se usa pseudocódigo, diagramas de flujo o una notación formal para
describir la secuencia de pasos que encuentran la solución.
Implementación. Se muestra el algoritmo expresado en un lenguaje de programación
específico.
También es posible incluir un teorema que demuestre que el algoritmo es correcto, un análisis de
complejidad o ambos. La complejidad de los algoritmos será comenta brevemente más adelante.
1
Tomado principalmente de http://es.wikipedia.org/wiki/Algoritmo
Tema 3: Abstracción de control.
62
Diagramas de flujo.
Los diagramas de flujo son descripciones que usan símbolos conectados con flechas para indicar la
secuencia de instrucciones. Los símbolos utilizados siguen un estándar ISO.
Los diagramas de flujo se usan para representar algoritmos pequeños, ya que abarcan mucho
espacio y su construcción es laboriosa. Por su facilidad de lectura se utilizan frecuentemente como
introducción a los algoritmos, para la descripción de las estructuras de un lenguaje y para la
descripción de procesos a personas ajenas a la computación. La figura 1 muestra un algoritmo para
calcular la raíz cuadrada de un número x. Se incluye el significado de los símbolos más utilizados.
Óvalo: Inicio y término (Abre y/o cierra el
diagrama).
Rectángulo: Actividad (Representa la ejecución de
una o más actividades o procedimentos).
Rombo: Decisión (Formula una pregunta o cuestión).
Círculo: Conector (Representa el enlace de
actividades con otra dentro de un procedimiento).
Triangulo boca abajo: Archivo definitivo (Guarda
un documento en forma permanente).
Triangulo boca arriba: Archivo temporal
(Proporciona un tiempo para el almacenamiento del
documento).
Figura 1: Diagrama de flujo
Pseudocódigo
El pseudocódigo (falso código) asemeja a un lenguaje de programación pero con algunas
convenciones del lenguaje natural. Tiene varias ventajas con respecto a los diagramas de flujo,
entre las que se destaca el poco espacio que se requiere para representar instrucciones complejas.
El pseudocódigo no está regido por ningún estándar. En este tema veremos algunos ejemplos de
expresión de algoritmos en pseudocódigo.
Notaciones formales
La manera más precisa de expresar un algoritmo es mediante un lenguaje formal. También es la que
requiere más esfuerzo y formación. Las notaciones formales quedan fuera del alcance de la
asignatura, no obstante, merece la pena mencionar algunas como los lenguajes lógicos formales, la
máquina de Turing 1, las máquinas de estados finitas 2 y las funciones recursivas. Estos modelos
eliminan toda ambigüedad y son independientes de cualquier computadora y de cualquier
implementación.
1
La máquina de Turing es uno de los artefactos más ingeniosos y curiosos utilizados en las ciencias de la
computación.
2
Las máquinas de estados finitas, incluidas en la teoría de autómatas, se utilizan para describir el comportamiento de
los circuitos electrónicos secuenciales, de modo que tendrán ocasionar de estudiarlas en otras asignaturas.
Tema 3: Abstracción de control.
Complejidad algorítmica.
63
La teoría de la complejidad computacional estudia los recursos requeridos durante el cómputo de
un algoritmo para resolver un problema. El objetivo de esta sección es fundamentalmente la
introducción de terminología. El estudio de la complejidad algorítmica, salvo para ejemplos muy
concretos, está mucho más allá del alcance de la asignatura.
Los problemas que tienen una solución con orden de complejidad lineal son los problemas que se
resuelven en un tiempo que se relaciona linealmente con su tamaño. Hoy en día las computadoras
pueden resolver problemas mediante algoritmos que tienen como máximo una complejidad o coste
computacional polinómico, es decir, la relación entre el tamaño del problema y su tiempo de
ejecución es polinómica. Los problemas se clasifican según esto en P y NP:
−
−
Problemas de la clase P: problemas de complejidad polinómica, que pueden ser resueltos por
un computador en un tiempo finito.
Problemas de la clase NP: problemas de complejidad superior a la polinómica, en general
complejidad factorial o combinatoria, que no pueden ser resueltos por nuestras computadoras.
Hay muchos problemas aparentemente sencillos que caen dentro de esta categoría 1. Los
problemas que pueden ser resueltos en teoría, pero no en práctica, se llaman intratables.
La complejidad temporal de un problema es el número de pasos que toma resolver una instancia
del problema utilizando el algoritmo más eficiente posible. Intuitivamente, si se toma una instancia
con entrada de longitud n que puede resolverse en n² pasos, se dice que ese problema tiene una
complejidad en tiempo de n² o cuadrática. Por supuesto, el número exacto de pasos depende de la
máquina en la que se implementa, del lenguaje utilizado y de otros factores. Para no tener que
hablar del coste exacto de un cálculo se utiliza la notación O. Cuando un algoritmo tiene un coste en
tiempo O(n²) en una configuración de computador y lenguaje dado, este coste será el mismo en
todos los computadores, de manera que esta notación generaliza la noción de coste
independientemente del equipo utilizado. Por ejemplo:
−
−
−
Acceder a un elemento de un vector mediante un índice es una operación de complejidad
constante O(1). El número de pasos a ejecutar es siempre el mismo independientemente del
tamaño del vector.
Buscar en un diccionario tiene complejidad logarítmica. Se puede iniciar la búsqueda de una
palabra por la mitad del diccionario. Inmediatamente se sabe si se ha encontrado la palabra o,
en el caso contrario, en cuál de las dos mitades hay que repetir el proceso hasta llegar al
resultado. En cada búsqueda el problema se ha reducido a la mitad, lo que se corresponde con
la función logarítmica. Este procedimiento de búsqueda (conocido como búsqueda binaria) en
una estructura ordenada tiene complejidad logarítmica O(log2 n).
El proceso más común para ordenar un conjunto de elementos tiene complejidad cuadrática: el
procedimiento es de orden cuadrático O(n²).
Cuando haya que comparar dos algoritmos que solucionan un mismo problema, un criterio será
comparar su coste en tiempo. Así, un algoritmo lineal O(n) será mejor que uno cuadrático O(n2),
porque implicará comparativamente mucho menos pasos de ejecución para obtener un mismo
resultado, pero peor que uno logarítmico O(log2 n).
Heurísticos
A menudo los problemas que no pueden ser abordados con una solución algorítmica se abordan
mediante heurísticos. Un heurístico es un algoritmo que no garantiza que se llegue a la solución en
todos los casos, pero sí en la mayoría de los casos.
1 Un ejemplo muy típico es el problema del viajante: Sean N ciudades de un territorio. El objetivo es encontrar
una ruta que, comenzando y terminando en una ciudad concreta, pase una sola vez por cada una de las
ciudades y minimice la distancia recorrida por el viajante.
Tema 3: Abstracción de control.
64
Los heurísticos se usan cuando no existe una solución algorítmica óptima bajo las restricciones
dadas (tiempo, espacio, etc.). Los heurísticos se basan en la experiencia, en el buen juicio y en las
probabilidades y no siempre funcionan de forma satisfactoria ni producen un resultado correcto.
En algunos casos el heurístico puede producir un mal resultado, consumir muchos más recursos de
los esperados o ejecutarse muy lentamente. Aún así, merece la pena utilizarlo cuando no existe
ningún algoritmo que pueda resolver el problema en un tiempo razonable, porque la experiencia o
el sentido común nos indican que las probabilidades de fallo son pequeñas. En muchos problemas
existe un algoritmo que no puede encontrar una solución en tiempo finito, pero puede comprobar
con gran eficiencia si una solución es correcta. Así, puede utilizarse un heurístico para hallar la
solución y después comprobarla mediante el algoritmo. Si el heurístico no da una solución correcta
o se ejecuta de forma muy lenta se puede probar con otro heurístico.
Sentencias en Java
Sentencias en Java
La secuencia de ejecución de un programa viene determinada por las sentencias. Una sentencia
expresa una acción o un conjunto de acciones que debe realizar el computador. Podemos distinguir
tres tipos de sentencias:
−
−
−
Expresiones: calculan y asignan valores a variables y pueden incluir llamadas a métodos.
Declaraciones: declaran variables.
Sentencias de control de flujo: determinan qué sentencias de un programa se ejecutan y el
orden en que se ejecutan.
Las sentencias de Java (y todos de los lenguajes estructurados) pueden ser sentencias simples y
compuestas. En el caso de Java:
−
−
Las sentencias simples incluyen expresiones, declaraciones de variables locales y las sentencias
break, continue y return que iremos viendo en este tema y en otros posteriores.
Las sentencias compuestas están compuestas de sentencias simples o de otras sentencias
compuestas. Incluyen bloques de código entre llaves, las sentencias de selección e iteración y
los bloques de captura y tratamiento de excepciones en aquellos lenguajes que las soportan
(Java entre ellos).
Bloques Java.
Un bloque Java consiste de una secuencia de sentencias encerradas entre un par de llaves { }. Las
sentencias de un bloque pueden ser simples o compuestas y se ejecutan secuencialmente.
{
{
Sentencia1
Sentencia2
…
SentenciaN
}
int i = 0;
int j = 2;
int k = i * j;
…
}
Tema 3: Abstracción de control.
Sentencias de control de flujo en Java.
65
En Java se definen las siguientes estructuras de control de flujo:
Sentencias de selección en Java 1.
Java tiene dos tipos de sentencias de selección: por un lado la sentencia if o if/else y por otro
la sentencia switch.
La sentencia if o if/else
La sentencia if condiciona la ejecución de otra sentencia al cumplimiento de una condición:
if (condición)
sentencia
true.
// Sentencia if
// sentencia que se ejecuta si condición evalúa a
Si se cumple la condición se ejecuta la sentencia, si no, se salta hasta la siguiente instrucción.
condición es una expresión lógica (booleana) cuyo resultado es verdadero (true) o falso
(false)
if (calificación >= 5)
System.out.println(“Aprobado”);
La sentencia if puede describirse mediante un diagrama de flujo de la siguiente forma:
1
Fuente principal: Dietel y Deitel
Tema 3: Abstracción de control.
Opcionalmente, la sentencia if puede tener una cláusula else
if (condición)
sentencia1
true.
else
sentencia2
false
66
// Sentencia if
// sentencia que se ejecuta si condición evalúa a
// Sentencia que se ejecuta si condición evalúa a
Si se cumple la condición se ejecuta la sentencia1, si no, se ejecuta la sentencia2.
if (calificación >= 5)
System.out.println(“Aprobado”);
else
System.out.println(“Suspenso”);
La sentencia if/else puede describirse mediante un diagrama de flujo de la siguiente forma:
Si se quiere indicar que se ejecuta más de una sentencia, hay que ponerlas entre llaves:
if (condicion){
sentencia_1;
sentencia_2;
}
else {
sentencia_3;
sentencia_4;
}
Cuando una sentencia o conjunto de sentencias están anidados en otra sentencia es muy
importante cuidar la edición sangrando un poco el bloque anidado. De esta forma se mejora mucho
la legibilidad del código.
En el caso de las sentencias if es bastante habitual que unas se aniden unas sentencias dentro de
otras en cascada. En este caso se prefiere la edición de la derecha porque evita sangrados excesivos
y es más legible:
if (calificacion >= 9)
System.out.println(“Sobresaliente”);
else
if (calificacion >= 7)
System.out.println(“Notable”);
else
if (calificacion >= 5)
System.out.println(“Aprobado”);
else
System.out.println(“Suspendido”);
if (calificacion >= 9)
System.out.println(“Sobresaliente”);
else if (calificacion >= 7)
System.out.println(“Notable”);
else if (calificacion >= 5)
System.out.println(“Aprobado”);
else
System.out.println(“Suspendido”);
Tema 3: Abstracción de control.
67
La sucesión de sentencias if/else da lugar a errores de programación, algunos de los cuales no
son detectados por el compilador pues no responden a errores de sintaxis, y, por tanto, hacen
perder mucho tiempo y desesperan al programador, sobre todo si es principiante. La mayoría de
estos errores pueden evitarse muy fácilmente adoptando un buen estilo de edición de los
programas. Veamos algunos ejemplos típicos:
− Un else siempre se refiere al if anterior:
if (x>5)
if (y>5)
System.out.println(“x e y son mayores que 5”);
else
System.out.println(“x es menor o igual que 5”);
Si x es mayor que 5 e y es menor que 5 imprime que “x es menor o igual que
5”, lo cual obviamente no es lo que queremos. El último else se refiere al último if.
Si queremos otro comportamiento tenemos que forzarlo con llaves:
if (x>5){
if (y>5)
System.out.println(“x e y son mayores que 5”);
}
else
System.out.println(“x es menor o igual que 5”);
De hecho, se considera una buena práctica de programación poner siempre llaves,
aunque el bloque conste de una única sentencia. Así el mejor estilo de edición para las
sentencias del ejemplo sería:
if (x>5){
if (y>5) {
System.out.println(“x e y son mayores que 5”);
}
}
else {
System.out.println(“x es menor o igual que 5”);
}
− Un punto y coma (;) mal colocado puede producir un error de sintaxis conocido como
“dangling else” (else colgante) o lo que es peor un error de lógica:
Error de lógica:
if (nota >= 5);
condición
System.out.println(“Aprobado”);
Error de sintaxis:
// No haría nada después de evaluar
// Siempre se ejecuta
if (calificacion>= 5);
// if acaba aquí
System.out.println(“Aprobado”);
// Siempre se ejecuta
else
System.out.println(“Suspendido”); // ¿A qué if se refiere?
Tema 3: Abstracción de control.
68
Los errores de sintaxis son detectados por el compilador por lo que son menos graves que
los de lógica. Pero no siempre es fácil detectar el else colgante en una estructura con
muchas sentencias if anidadas. Y lo que es peor puede arreglarse el error de sintaxis y dejar
un error de lógica. Nuevamente el uso de llaves hace mucho más difícil que ocurran estos
errores:
if (calificacion>= 5){
System.out.println(“Aprobado”);
}
else {
System.out.println(“Suspendido”);
}
Hay otra buena razón para el uso de llaves: es muy probable que lo que en principio iba a ser
una única sentencia se convierta a medida que el programa crece en un grupo de sentencias.
Poner las llaves desde el principio nos ahorra trabajo posterior y hace más difícil que la
adición de nuevas sentencias produzca uno de los errores que se han descrito.
La sentencia de selección switch
Aunque la sentencia if/else nos permite resolver todos los casos en los que necesitamos elegir
un camino de ejecución u otro 1, en algunas ocasiones nos interesa seleccionar las instrucciones a
ejecutar no en función de una condición booleana, sino en función del valor de una variable. Para
estos casos, Java proporciona la sentencia switch.
switch (expresión) {
case valor_1:
sentencia_1;
break;
case valor_2:
sentencia_21;
sentencia_22;
break;
...
case valor_i, valor j:
sentencia_i;
break;
default:
sentencia_por_defecto;
break;
}
valor_1, valor_2, … valor_i deben ser constantes enteras o constantes carácter. Una
constante de carácter se representa como el carácter en cuestión encerrado entre apóstrofos: ´A’
expresión debe ser una expresión entera constante, es decir, una combinación de constantes de
carácter y constantes enteras que al evaluarse produzca un valor entero constante.
La ejecución de una sentencia switch comienza con la evaluación de la expresión entera. El
resultado de la evaluación se va comparando secuencialmente con los valores de las cláusulas case
(puede haber más de un valor en cada cláusula case separados por comas). En cuanto se
encuentra un valor coincidente se ejecutan las sentencias asociadas al case correspondiente. Si
ninguno de los valores de las cláusulas case coincide con el evaluado se ejecuta el código de la
1
En realidad bastaría con el if, sin else.
Tema 3: Abstracción de control.
69
cláusula default. Esta cláusula es opcional, pero se considera una buena práctica de
programación incluirla siempre.
Obsérvese que después de cada caso (case) no se incluyen llaves { } aunque haya más de una
sentencia.
Debajo se muestra un ejemplo anterior resuelto ahora con la sentencia switch.
if (calificacion >= 9) {
System.out.println(“Sobresaliente”);
}
else if (calificacion >= 7){
System.out.println(“Notable”);
}
else if (calificacion >= 5){
System.out.println(“Aprobado”);
}
else {
System.out.println(“Suspendido”);
}
switch(calificación){
case 9:
System.out.println(“Sobresaliente”);
break;
case 8, 7:
System.out.println(“Notable”);
break;
case 6, 5:
System.out.println(“Aprobado”);
break;
default:
System.out.println(“Suspendido”);
break;
}
Sentencias de iteración en Java1.
Java tiene tres sentencias de iteración: por un lado las sentencias while y do/while y por otro
la sentencia for.
Las sentencias while y do/while
La sentencia while ejecuta una sentencia mientras se cumpla una condición:
while (condición)
// Sentencia while
sentencia
// sentencia que se
evalúa a false.
ejecuta
hasta
que
condición
// Encontrar la primera potencia de 2 mayor que 1000.
int producto = 2;
while (producto <= 1000){
// Uso de llaves recomendado con una
sola
producto = 2 * producto;
// sentencia
}
// Es necesario el uso de llaves cuando la sentencia while
// incluye más de una sentencia.
while (condición){
sentencia_1;
……
sentencia_n;
}
1
Fuente principal: [Deitel 99]
Tema 3: Abstracción de control.
70
La sentencia do/while permite al programador especificar que una acción se repita en tanto se
cumpla una condición.
do {
sentencia;
}
while (condicion);
// Encontrar la primera potencia de 2 mayor que 1000.
int producto = 2;
do {
producto = 2 * producto;
}
while (producto <= 1000)
Con la sentencia do/while la sentencia o sentencias del bucle se ejecutan al menos una vez. La
condición se comprueba después de haber realizado la primera iteración.
Ojo con los bucles infinitos: hay que asegurarse de que la condición deja de cumplirse.
La sentencia for
La sentencia for permite controlar el número de iteraciones mediante un contador.
for (inicialización del contador;
condición;
expresión de incremento del contador)
{
sentencia_1;
……
sentencia_n;
}
La semántica de la sentencia for es la siguiente:
1.
2.
3.
4.
5.
Se inicializa contador (sólo la primera vez).
Se evalúa condición.
− Si true → ejecutar sentencias.
− Si false → a paso 5 (salir del bucle).
Se incrementa contador según expresión de incremento.
Volver al paso 2.
Salir del bucle.
// Imprimir la tabla de multiplicar del 3 (del 0 al 10)
int i = 0;
for (i = 0; i <= 10; i++){
System.out.println(“3 x “ + i + “ = “ + (3 * i));
}
// Es mucho mejor declarar la variable en el propio bucle.
for (int i = 0; i <= 10; i++){
System.out.println(“3 x “ + i + “ = “ + (3 * i));
}
El bucle while equivalente sería
Tema 3: Abstracción de control.
71
Inicialización contador;
while(condicion){
sentencia_1;
…
sentencia_n;
incremento del contador
}
int i = 0;
while(i <= 10){
System.out.println(“3 x “ + i + “ = “ + (3 * i));
i++;
}
La variable de control puede modificarse de acuerdo con cualquier expresión aritmética:
// variable de control de 7 a 77 en incrementos de 7.
for (int i = 7; i <= 77; i += 7){…};
// variable de control de 99 a 0 en incrementos de
for (int i = 99; i >= 0; i = i - 11) {…};
-11
// Incrementos de 5 en 5
for ( int j = 2; j <= 80; j += 5 ){
System.out.println(j);
}
// Ejemplos de lo que NO se debe hacer
// Las expresiones de un for pueden contener expresiones aritméticas, pero
no
// utilice expresiones cuyo significado es difícil de entender.
int x = 2;
int y = 10;
for ( int j = x; j <= 4 * x * y; j += y / x ) {
System.out.println(j);
}
// Este bucle for
tardado en darse
// cuenta?
es
equivalente
al
anterior,
pero
¿cuánto
tiempo
ha
// NO modifique el valor de la variable de control dentro del bucle for.
for (int i = 7; i <= 77; i++){
i *= 7;
// No haga cosas de este estilo.
}
Las tres expresiones de la estructura for son opcionales:
−
Se puede omitir la inicialización del contador si se ha inicializado antes.
int i = 1;
for( ; i < 10; i++) { ... }
−
Si se omite la comprobación de la condición, Java supone que la condición para continuar el
ciclo se cumple, creándose un bucle infinito.
for( ; ; i++) { ... }
for( ; ; ) { ... }
−
// Bucle infinito. Posible desbordamiento de i.
// Bucle infinito.
La propia variable de control se puede utilizar dentro del cuerpo del for.
for( ; i < 10; i++ ) {
if ( i%2 == 0) { ... } // Se hace algo cuando i es par.
}
−
El incremento del contador puede omitirse si se hace dentro del cuerpo del for.
for( ; i < 10; ) { ...
i++ ... }
// Nada aconsejable.
Tema 3: Abstracción de control.
72
Elección de una estructura de repetición:
−
−
−
Si número fijo de veces, elegir for
Si 1 o más veces, elegir do { } while { }
Si 0 o más veces, elegir while ( ) { }
Las sentencias break y continue.
La palabra reservada break en el cuerpo de un ciclo causa la terminación inmediata de dicho ciclo.
int i = 0;
int s = 0;
while (i <= 5) {
s += 1;
if ( s == 7) break; // sale del bucle.
// otras sentencias …
i++;
}
// Se pasa aquí si i > 5 o s == 7
La palabra reservada continue causa el salto a la siguiente iteración del ciclo, pero sin salirse del
ciclo .
for (int i = 0; i < 100; i++) {
if (i % 2 == 1) continue;
// Sólo se pasa aquí si i es par
// otras sentencias ...
}
En las estructuras while y do/while la condición para continuar el ciclo se evalúa
inmediatamente después de ejecutarse continue. En la estructura for se incrementa el contador
y luego se evalúa la condición.
Las siguientes porciones de código son equivalentes:
for (int i = 1; i <= 10; i++ ) {
// sentencia 1
// sentencia n
}
Pero las dos siguientes NO son equivalentes:
// Incremento antes de continue
for (int i = 1; i <= 10; i++ ) {
// sentencia 1
continue;
// sentencia n
}
int i = 1;
while ( i <= 10 ) {
// sentencia 1
// sentencia n
i++;
}
// Incremento después de continue
int i = 1;
while ( i <= 10 ) {
// sentencia 1
continue;
// sentencia n
i++;
}
Tema 3: Abstracción de control.
break y continue etiquetados
73
El enunciado break sólo causa la salida de la estructura while, do/while o for que lo encierra
directamente. Para salirnos de una serie de estructuras anidadas se usa el enunciado break
etiquetado:
La ejecución del programa continúa con el primer enunciado después del enunciado compuesto
etiquetado, el cual consiste en una serie de enunciados encerrados en llaves y precedidos por una
etiqueta.
// Enunciado compuesto rotulado o etiquetado
stop:
for (int fila = 1; fila <= 10; fila++) {
for (int columna = 1; columna <= 5; columna++) {
if (fila == 5) {
break stop; // Se sale del bloque etiquetado.
}
System.out.println(“Fila es menor que 5”);
}
}
// Si se sale por break, continúa por aquí.
System.out.println(“La fila es 5”);
El enunciado continue continúa con la siguiente iteración de la estructura while, do/while o
for que lo encierra directamente. El enunciado continue etiquetado se salta el resto de los
enunciados del cuerpo de la estructura de iteración que lo contiene y cualquier cantidad de
estructuras de iteración que la encierren y continúa con la siguiente iteración de la estructura de
repetición etiquetada.
// Enunciado compuesto rotulado o etiquetado
stop:
for (int fila = 1; fila <= 10; fila++) {
for (int columna = 1; columna <= 5; columna++) {
if (fila == 5)
continue stop;
System.out.println(“Fila es distinta que 5”);
}
// Continúa por aquí.
}
System.out.println(“La fila es 10”);
break y continue suponen una violación de las reglas de la programación
estructurada.
El uso de break está justificado en pocas ocasiones. Si hay más de una salida del ciclo del programa
se hace más difícil de leer y analizar y se va en contra de los principios de la programación
estructurada. Puede evitarse con una escritura más cuidadosa de las condiciones de terminación
del ciclo:
Tema 3: Abstracción de control.
int i;
char s;
while (i <= 5) {
if (s == ´a´) break;
// Sentencias
i++;
}
74
int i;
char s;
while (i <= 5 && s != ´a´){
// Sentencias
i++;
}
El uso de continue puede evitarse con una escritura más cuidadosa del cuerpo del ciclo y de la
expresión de incremento de la variable de control.
for (int i = 0; i < 100; i ++) {
if (i%2 == 1) continue;
// Procesamiento según valor de i
}
for (int i = 0; i < 100; i += 2) {
// Procesamiento según valor de i
}
continue ayuda a reducir los niveles de anidado en el cuerpo de la estructura de iteración, pero es
peligroso si no se hacen explícitas las condiciones para ejecutar el resto del ciclo.
// int a, b (declaradas antes)
// posible bucle ∞ si b es cero al
// entrar
while (a < 5){
if (b == 0) continue;
// otras sentencias;
a++;
}
// int a, b (declaradas antes)
while (a < 5) {
if (b == 0){
a++;
continue;
}
// otras sentencias;
a++;
}
Algoritmos y refinamientos sucesivos 1
Supongamos que tenemos que hacer un programa para resolver el siguiente problema:
Diez alumnos se presentan a un examen. Se desea conocer la nota media de la clase.
Podríamos empezar por expresar la solución en pseudocódigo:
Poner la suma de notas a 0
Poner el contador de notas a 1
Mientras el contador de notas sea menor o igual que 10
Pedir y leer nota
Añadir la nota a suma
Incrementar en 1 el contador de notas
Calcular la media
Mostrar el resultado
Y posteriormente traducir el pseudocódigo a un programa Java (Cuadro 1).
1
Fuente: [Deitel 99]
Tema 3: Abstracción de control.
75
El programa del Cuadro 1 funciona bien si todo va bien y tiene grandes limitaciones. Por ejemplo, no
sirve para calcular la media si se presenta un número de alumnos distinto de 10, ni comprueba que
las notas están en el rango correcto. Vamos a tratar de mejorarlo independizándolo del número de
alumnos que se presenten al examen:
Desarrollar un programa de cálculo de la media de las notas de una clase que procese un
número arbitrario de notas.
CUADRO 1
public class ejemplo31 {
public static void main(String[] args) {
// Declaraciones e inicializaciones.
int total = 0;
// suma de notas
int contador = 0;
// contador de notas
int nota = 0;
// valor de la nota
int media = 0;
// media de todas las notas
// Inicialización variable de control del bucle.
contador = 1;
// Petición de notas y cálculo de suma de notas.
while (contador <= 10){
// Pedir nota y leerla.
System.out.print("Introduzca nota: ");
nota = Teclado.readInt();
// Incrementar el total.
total += nota;
// Incrementar variable de control.
contador++;
}
// Cálculo de la media.
media = total / 10;
// Muestra del valor de la media.
System.out.println("La media de la clase es: " + media);
}
}
Como no sabemos a priori el número de notas que van a ser introducidas debemos idear un
mecanismo que nos permita saberlo. Una posible solución es pedir al usuario cuántas notas va a
introducir y utilizar este número como utilizábamos el 10 en la solución anterior. Sin embargo,
vamos a proceder de otra manera: definiremos un valor especial para la nota que nos indique que el
usuario ya no va a introducir más notas (un centinela).
Como antes, empezamos por expresar la solución en pseudocódigo:
Poner la suma de notas a 0
Poner el contador de notas a 0
Pedir y leer la primera nota (posiblemente el centinela)
Mientras el usuario no haya introducido el centinela
Añadir la nota a la suma
Incrementar en 1 el contador de notas
Pedir y leer una nueva nota
Si el contador de notas es distinto de cero
Calcular la media
Tema 3: Abstracción de control.
Mostrar el resultado
76
Si no
Imprimir “no se han introducido notas”
El Cuadro 2 muestra la implementación de la solución en Java:
CUADRO 2
public class ejemplo32 {
public static final int Centinela = -1;
public static void main(String[] args) {
// Declaraciones e inicializaciones.
int total = 0;
// suma de notas
int contador = 0;
// contador de notas
int nota = 0;
// valor de la nota
double media = 0;
// media de todas las notas
// Petición de primera nota.
System.out.print("Introduzca nota: ");
nota = Teclado.readInt();
// Petición de notas y cálculo de suma de notas.
while (nota != Centinela){
// Incrementar el total.
total += nota;
// Incrementar variable de control.
contador++;
// Pedir nota y leerla.
System.out.print("Introduzca nota: ");
nota = Teclado.readInt();
}
// Cálculo e impresión de la media.
if (contador != 0){
media = (double)total / contador;
System.out.println("La media de la clase es: " + media);
}
else{
System.out.println("no se han introducido notas");
}
}
}
A parte de implementar una lógica diferente, la solución del cuadro 2 difiere de la del Cuadro 1 en
un par de aspectos que deben comentarse:
−
−
Se utiliza una constante Centinela para definir el valor de la nota que se utiliza para salir del
bucle. Utilizar en un programa números, como se utilizaba el 10 en el programa del Cuadro 1,
es una muy mala práctica de programación, que dificulta extraordinariamente la comprensión
y el mantenimiento de los programas. Estos números suelen dispersarse por diferentes lugares
del programa. Si hubiera que cambiar el valor a utilizar habría que inspeccionar el programa
entero. Además, al cabo de cierto tiempo (no mucho) el programador olvida el significado del
número, lo que hace muy difícil determinar en qué expresiones debe cambiarse y en qué
expresiones debe mantenerse. Moraleja: UTILICEN SIEMPRE CONSTANTES CON NOMBRES
SIGNIFICATIVOS EN LUGAR DE NÚMEROS.
La variable media se declara double. Parece lógico pensar que la media puede no ser un
número entero. Sin embargo, total y contador se mantiene como enteros. Eso obliga a
Tema 3: Abstracción de control.
77
hacer un cast en la expresión de cálculo de la media: media = (double)total /
contador;
La expresión total / contador; realiza una división entera puesto que ambos
operandos son int. La forma de forzar una división real es convertir uno de los operandos
en double antes de realizar la división.
Refinamientos Sucesivos.
Consideremos ahora un nuevo problema, aunque no muy diferente a los anteriores:
Un instituto desea saber qué porcentaje de sus alumnos pasan el examen de selectividad, para
lo cual solicita un programa que tome una lista con las notas de 10 estudiantes. Si la nota es
mayor o igual que cinco el estudiante ha pasado selectividad, si no ha suspendido. El
programa debe funcionar de la siguiente manera:
1.
2.
3.
4.
5.
Debe pedir las notas una tras otra, hasta la décima nota.
Debe contar el número de aprobados y de suspensos.
Debe mostrar los resultados absolutos y porcentuales.
Si más del 80% de estudiantes han pasado la prueba debe mostrar el mensaje “Por
encima de la media”.
Si menos del 25% estudiantes han pasado la prueba debe mostrar el mensaje “Por
debajo de umbral de calidad”.
Como antes, empezamos por expresar la solución en pseudocódigo, pero esta vez vamos a hacerlo
por refinamientos sucesivos. La técnica de refinamientos sucesivos puede describirse así 1:
1.
2.
3.
La construcción de un programa consiste en una secuencia de pasos consistentes en
refinamientos sucesivos. En cada paso una tarea dada se descompone en cierto número de
subtareas. Cada refinamiento en la descripción de la tarea puede acompañarse por un
refinamiento de la descripción de los datos de la tarea. Es decir, los refinamientos de la
descripción del programa y de las estructuras de datos se realizan en paralelo.
El grado de modularidad obtenido de esta manera determinará la facilidad o dificultad con
la que un programa podrá adaptarse a futuros cambios o extensiones.
Durante el proceso de refinamiento se utilizará la notación que mejor se adapte a la
naturaleza del problema. A medida que avancen los refinamientos, la notación puede irse
adaptando a las características del lenguaje de implementación.
Cada refinamiento implica un cierto número de decisiones de diseño basadas en un conjunto de
criterios de diseño. Entre estos criterios se encuentran la eficiencia, el ahorro de memoria y la
claridad y regularidad de la estructura resultante 2.
Vamos a aplicar esta técnica al problema de arriba. Nuestra primera descripción del programa
podría 3 ser la siguiente:
Inicializar variables
Introducir las notas contar aprobados y suspensos
Imprimir un informe con los resultados obtenidos.
Adaptación de conclusiones de “Program Development by Stepwise Refinement” , Niklaus Wirth, Communications of the
ACM, Vol. 14, No. 4, April 1971, pp. 221-227. Disponible en http://sunnyday.mit.edu/16.355/wirth-refinement.html#7. Se
recomienda su lectura.
2 Muchos diseños que producen programas igualmente correctos desde un punto de vista funcional pueden ser muy
diferentes cuando se les juzga también a la luz de esos criterios.
3 El condicional podría está usado con toda intención. Un programa puede diseñarse de muchas maneras en función de los
criterios que acaban de mencionarse.
1
Tema 3: Abstracción de control.
78
Es evidente que necesitamos detallar bastante más la solución para poder programarla.
Necesitamos refinar el programa y sus datos.
Inicializar aprobados a 0
Inicializar suspensos a 0
Inicializar contador de notas a 1
Inicializar variables
Mientras el contador de notas sea menor o igual a 10
Leer nota
Si la nota es aprobado
Añadir a aprobados
Si no
Añadir a suspensos
Incrementar contador de notas
Calcular porcentaje de aprobados.
Introducir las notas
contar aprobados y
suspensos
Imprimir número de aprobados
Imprimir número de suspensos
Imprimir porcentaje de aprobados
Imprimir un informe
con los resultados
obtenidos.
Si aprobados > 80%
Imprimir “Por encima de la media”
Si no, si aprobados < 25%
Imprimir “Por debajo de umbral de calidad”
El programa Java resultante se muestra en el cuadro 3.
CUADRO 3
public class ejemplo33 {
public static final int
public static final double
public static final double
NumeroDeNotas = 10;
UmbralExcelencia = 80.0;
UmbralCalidad = 25.0;
public static void main(String[] args) {
// Inicializar variables.
int aprobados = 0, suspensos = 0, contadorNotas = 1;
// Introducir las notas contar aprobados y suspensos
while (contadorNotas <= NumeroDeNotas){
// Pedir nota y leerla.
System.out.print("Introduzca nota: ");
int nota = Teclado.readInt();
if (nota >= 5){
aprobados++;
}
else if (nota < 5) {
suspensos++;
}
contadorNotas++;
}
double porcentajeAprobados =
100.0 * ((double)aprobados / NumeroDeNotas);
// Imprimir un informe con los
System.out.println("Aprobados:
System.out.println("Suspensos:
System.out.println("Porcentaje
resultados obtenidos.
" + aprobados);
" + suspensos);
aprobados: " + porcentajeAprobados);
if (porcentajeAprobados > UmbralExcelencia){
System.out.println("Por encima de la media");
}
else if (porcentajeAprobados < UmbralCalidad){
System.out.println("Por debajo de umbral de calidad");
}
}
Tema 3: Abstracción de control.
Construcción de programas estructurados
79
Un programa estructurado utiliza únicamente las tres estructuras básicas de control siguientes:
−
−
−
Secuencia: el bloque secuencial de instrucciones, instrucciones ejecutadas sucesivamente, una
detrás de otra.
Selección: la instrucción condicional con doble alternativa, de la forma "if condición then
instrucción-1 else instrucción-2".
Iteración: el bucle condicional "while condición do instrucción", que ejecuta la instrucción
repetidamente mientras la condición se cumpla.
Además, cada estructura tiene un punto de entrada y uno de salida perfectamente definidos. La
experiencia ha demostrado que los programas estructurados son más fáciles de escribir y de
entender y más fáciles de probar, depurar y modificar. La corrección de los programas
estructurados es incluso susceptible de ser probada formalmente como se prueba la corrección de
un teorema matemático 1.
Las reglas para construir programas estructurados pueden formularse de forma muy sencilla:
1.
Empezar con el diagrama de flujo más simple posible.
2.
Regla de apilamiento: Cualquier acción, incluyendo entrada y salida, (rectángulos) puede
ser reemplazada por dos acciones en secuencia.
4.
Las reglas 2 y 3 pueden aplicarse en cualquier orden.
3.
Regla de anidamiento: Cualquier acción puede ser reemplazada por una estructura de
control (en Java: secuencia, if, if/else, switch, while, do/while, for).
La aplicación de estas reglas siempre resulta en un diagrama de flujo estructurado con aspecto de
diagrama de bloques anidados. La aplicación sucesiva de la regla 2 da lugar a una secuencia de
acciones, generando una pila de estructuras de control, de ahí su nombre de regla de apilamiento.
La aplicación de la regla 3, regla de anidamiento, resulta en una serie de bloques anidados.
Regla 2
1
Regla 2
Cosa que, aunque casi nunca se realiza, es muy importante en ciertos casos.
Tema 3: Abstracción de control.
80
Regla 3
Regla 3
Regla 3
La elegancia de la programación estructurada reside en su simplicidad, sólo tres estructuras de
control y sólo cuatro reglas para combinarlas (estrictamente hablando sólo dos, apilamiento y
anidamiento). Obsérvese la relación entre estas reglas y el diseño en refinamientos sucesivos. Cada
paso de refinamiento da lugar a la aplicación de la regla de apilamiento o a la de anidamiento.
Tema 4: Arrays.
Tema 4: Estructuras estáticas de
datos. Arrays.
Objetivos
Presentar los arrays.
Conocer cómo se declara, inicializa y se accede a los elementos de un array.
Utilizar técnicas de ordenación y búsqueda de los elementos de un array.
Manejar arrays de múltiples dimensiones.
Contenidos
Arrays
Ordenación de arrays
Búsqueda en arrays
Arrays Bidimensionales
81
Tema 4: Arrays.
Arrays1.
82
Un array es estructura de datos que almacena una cantidad fija de elementos del mismo
tipo, a los cuales se puede acceder por medio de un índice que indica su posición dentro de
la estructura.
Hay dos aspectos de la definición que merece la pena resaltar:
−
−
Los arrays son estructuras de datos homogéneos: todos los datos son del mismo tipo.
Los arrays son estructuras de datos estáticas 2: los arrays contienen siempre el mismo
número de datos. En otras palabras el tamaño del array no puede cambiar durante la
ejecución del programa.
Podemos encontrar estructuras de datos con estas características en otros ámbitos, por ejemplo en
álgebra: vectores (1D), matrices (2D) o tensores (3D).
Declaración de arrays en Java.
La convención para declarar un array en Java es simplemente poner un par de corchetes antes o
después del nombre de la variable:
Tipo_de_los_elementos [] nombre_de_referencia_al_array;
O equivalentemente:
Tipo_de_los_elementos nombre_de_referencia_al_array [];
Por ejemplo:
int [] arrayDeEnteros;
char arrayDeCaracteres [];
Acabamos de decir que un array es una estructura de datos que almacena una cantidad de datos fija
de elementos del mismo tipo. Sin embargo, en las declaraciones de arriba no se ha especificado
ninguna cantidad de datos. No sabemos cuantos enteros se guardan en arrayDeEnteros ni
cuantos caracteres se guardan en arrayDeCaracteres. De hecho, en arrayDeEnteros no se
guarda ningún entero y en arrayDeCaracteres no se guarda ningún carácter. ¿Qué se guarda
entonces?
Los arrays en Java son objetos y a los objetos en Java no se accede nunca de forma directa,
sino a través de referencias.
Los arrays son tipos referencia y a un valor de un tipo referencia nunca se accede de forma directa,
sino a través de referencias. arrayDeEnteros y arrayDeCaracteres no guardan enteros ni
caracteres porque no son arrays de enteros ni arrays de caracteres, sino referencias, es decir
apuntadores, a dichos arrays.
Fuentes: [Deitel 99]
La palabra estático (static) está muy sobrecargada en las ciencias de computación, es decir puede significar muchas cosas
distintas, a veces con muy poca relación, dependiendo del contexto. Cuando vean que algo en informática se define como
estático no hagan suposiciones, estudien su significado en el contexto donde se usa la palabra.
1
2
Tema 4: Arrays.
83
En la declaración no se especifica el tamaño del array porque en la declaración no se crea el array,
sino la referencia que lo apunta. Así, por ejemplo, con la declaración:
int [] serie;
se obtiene una referencia llamada serie que puede apuntar a un array de enteros, pero de
momento, dependiendo de dónde se declare (dentro o fuera de un método 1) o bien no apunta a
nada o bien apunta a algo indefinido. Para que serie apunte a un array debemos crear dicho array.
Creación de arrays en Java.
Crear un array es asignarle un espacio de almacenamiento en memoria.
La creación de objetos en Java, y los arrays son objetos, se realiza mediante el operador new. En el
caso de los arrays el operador new se utiliza de la siguiente manera:
new tipo_elemento [n_elementos]
Por ejemplo:
new int[5];
mismo.
// Crea un array de 5 enteros y devuelve una referencia al
Es decir, se escribe el operador new seguido por el tipo de los elementos del array y el número de
elementos que va a contener entre corchetes. El operador new crea un array de n_elementos del
tipo tipo_elemento y devuelve una referencia al mismo. Crear el array significa reservar un
espacio en memoria suficiente para guardar los elementos del array. Este espacio se reserva de
forma contigua, lo cual facilita enormemente las operaciones de acceso al array. Obviamente, de
poco nos sirve la referencia que devuelve new si no la guardamos en alguna parte. La forma de
guardar dicha referencia es asignársela a una variable referencia previamente declarada:
int [] serie;
serie = new int[5];
En la figura 1 puede apreciarse que serie no es el array, sino un apuntador (referencia 2) a array.
Los arrays se pueden declarar y crear simultáneamente:
int [] serie = new int[5];
Se pueden declarar y crear varios arrays que contengan el mismo tipo de elementos en una sola
línea:
int a[] = new int[20], b[] = new int[100];
Conocemos ya el método main y los métodos se verán en profundidad en el tema siguiente, Abstracción Funcional.
Los términos apuntador, puntero y referencia no son exactamente equivalentes. En el contexto de Java el término que debe
utilizarse es referencia.
1
2
Tema 4: Arrays.
84
Figura 1: Acceso a arrays mediante referencias.
Entender que la diferencia entre objeto y referencia es clave para poder entender y manejar los
arrays de forma correcta. Por ello vamos a ver y comentar un par de ejemplos. Consideremos
primero el ejemplo de la figura 2. En (1) se realizan las siguientes acciones en el siguiente orden:
−
−
−
Declaramos la referencia serie1, que puede referenciar a cualquier array de enteros.
Creamos mediante new un array de 5 enteros.
Asignamos la referencia devuelta por new a serie1
En (2) se realizan las siguientes acciones:
−
−
−
Declaramos la referencia serie2, que puede referenciar a cualquier array de enteros.
Creamos mediante new un array de 3 enteros.
Asignamos la referencia devuelta por new a serie2
En (3) se asigna el valor de serie2 a serie1. La consecuencia de esto es que al array de tres
enteros le apuntan ahora dos referencias serie1 y serie2 y que al array de 5 elementos no le
apunta ninguna referencia. Java es capaz de detectar automáticamente cuando un objeto no es
apuntado por ninguna referencia. Cuando ocurre esto el objeto es borrado 1.
Figura 2: Objetos y referencias.
Supongamos ahora un ejemplo parecido (figura 3). Los tres primeros pasos son iguales a los de la
figura 2. De hecho, sólo hay una diferencia: el array que se crea en (2) es igual al que se crea en (1).
Esto se ha hecho así para poder razonar sobre el significado de la expresión:
Aunque no inmediatamente, sino transcurrido algún tiempo. El mecanismo de Java que revisa si hay objetos que no son
apuntados por ninguna referencia para eliminarlos se denomina recolector de basura (garbage collector). El recolector de
basura simplifica mucho la programación: si no existiera, la liberación de memoria debería gestionarla el programador.
1
Tema 4: Arrays.
85
serie1 == serie2
Si ponemos está expresión entre los pasos (2) y (3) evaluará a false, aunque los arrays a los que
apuntan serie1 y serie2 sean iguales, porque no estamos comparando arrays, sino referencias y
las referencias serie1 y serie2 son distintas porque apuntan a objetos distintos,
independientemente de que ambos objetos sean idénticos. Supongamos ahora que ponemos esa
expresión justo después de (3). En este caso la expresión evalúa a true ya que ambas apuntan al
mismo array.
Finalmente en (4) se hace que serie2 apunte a null, es decir a la nada. El array al que apuntaba
seguirá existiendo porque aun hay una referencia que lo apunta, serie1.
Figura 3: Comparación de referencias.
Inicialización de arrays en Java.
Llegados a este punto sabemos cómo declarar referencias a arrays, como crear arrays y como hacer
que las referencias apunten a los arrays, pero aun no sabemos cómo leer o escribir valores en el
array. De hecho, dada la sentencia:
int [] serie = new int[5];
¿Cuánto valen los enteros del array referenciado por serie? Hay dos posibilidades:
1.
Si serie ha sido declarada a nivel de clase tienen un valor por defecto:
−
−
−
−
2.
Cero, en el caso de tipos primitivos numéricos.
Carácter nulo en el caso de char
false en el caso de boolean
null en el caso de referencias
Si serie ha sido declarada dentro de un método su valor es indefinido.
Los arrays se pueden inicializar en el momento de la declaración, continuando ésta con un signo
igual y una lista separada por comas y encerrada entre llaves de inicializadores. Por ejemplo:
int [] serie = {1,2,3,4,5}; //Se ha creado implícitamente
Tema 4: Arrays.
86
En esta sentencia estamos declarando la referencia serie, creando un array de 5 enteros, asignando
una referencia al array recién creado a serie e inicializando los 5 enteros del array. El tamaño del
array queda determinado por el número de elementos de la lista.
Esta inicialización es muy útil en algunos casos, pero ¿y si necesitamos un array con 5000
elementos? O ¿y si nos interesa que todos los valores del array se inicialicen al mismo valor?
Necesitamos una forma sencilla y eficiente de acceder a cada una de las posiciones del array.
Acceso a los elementos de un array.
Nos podemos referir a cualquier elemento del array mediante el nombre del
array seguido de la posición del elemento entre corchetes.
El número de posición se denomina índice.
Consideremos la sentencia:
int [] serie = new int[10];
La siguiente porción de código que inicializa los elementos del array:
for (int i = 0; i < 10; i++){
serie[i] = i * i;
}
Los elementos del array ocupan posiciones de memoria contiguas (figura 4), que se numeran
empezando por cero. En nuestro ejemplo:
−
−
El primer elemento es serie[0] y el último serie[9].
Para referirnos al i-ésimo elemento escribimos serie[i-1].
Figura 4: Elementos de un array
Tema 4: Arrays.
87
Los subíndices pueden ser cualquier expresión entera que evalúe un valor dentro de los límites del
array. Por ejemplo:
int a = 4;
int b = 3;
serie[a + b] += 2;
// sumar dos a serie[7]
Los elementos de un array se comportan como cualquier otra variable de su tipo y pueden usarse
en expresiones.
int suma = serie[0] + serie[1];
No podemos acceder a posiciones del array inexistentes. Si tenemos:
int [] serie = new int[10];
E intentamos hacer:
int x = serie[17];
La máquina virtual de Java detecta que intentamos acceder a una posición no existente y lanza una
excepción que provoca (si no es tratada) que el programa aborte 1:
IndexOutOfBoundsException
El acceso a posiciones inexistentes es algo peligroso ya que en el mejor de los casos estamos
accediendo a algo indefinido y en el peor podemos destruir información útil. En el caso de Java esto
no puede ocurrir porque cualquier intento de sobrepasar los límites del array es detectado. No
obstante, es una situación que hay que evitar porque da lugar a que se aborte la ejecución del
programa. El límite inferior de un array es siempre 0, el superior se determina al crear el array.
Pero no siempre sabemos a qué array está apuntando una referencia. La cuestión es ¿podemos
preguntarle a un array su tamaño?
Longitud de un array.
Los arrays son objetos y, como veremos en el tema 6, los objetos tienen propiedades (atributos)
que pueden consultarse siempre que sean públicas. Java define para los arrays el atributo length
que guarda el número de elementos del array y que se consulta según la sintaxis:
nombre_array.length
Por ejemplo:
int serie[] = new int[10];
serie.length
// nos devuelve el valor 10
El subíndice no puede sobrepasar la posición del último elemento del array (length –1). Los
cuadros 1 y 2 muestran dos ejemplos completos de uso de arrays en los que se utiliza esta
propiedad.
1
Veremos parcialmente las excepciones en el tema 5 (métodos) y ampliaremos conocimientos en el tema
6 (orientación a objetos).
Tema 4: Arrays.
Cuadro 1: Inicialización y suma de los elementos de un array de enteros.
class Ejemplo41{
final int TamañoArray = 10;
int s[];
int total;
public static void main(String args[]){
s = new int[TamañoArray];
for (int i = 0; i < s.length; i++){
s[i] = 2 + 2 * i; // 2, 4, 6, ..., 20
}
// suma elementos
for ( int i= 0; i < s.length; i ++){
total += s[i];
}
}
} // Fin de clase Suma
Cuadro 2: Se ha realizado una encuesta a 20 personas acerca de la calidad de la comida de un
restaurante en una escala de 1 a 10. Se trata de colocar las 20 respuestas en un array de enteros
y resumir los resultados del sondeo.
class Ejemplo42{
int respuestas[ ] = {1, 2, 6, 4, 8, 5, 9, 7, 8, 10,
1, 6, 3, 8, 6, 10,3, 8, 2, 7};
int frecuencia[ ];
public static void main(String args[]){
frecuencia = new int[11];
// No utilizamos el primer elemento. Cada respuesta es
// el subíndice del array frecuencia.
for ( int i = 0; i < respuestas.length; i++) {
++frecuencia[respuestas[i]];
}
}
} // Fin de la clase Sondeo
88
Tema 4: Arrays.
89
Observaciones.
−
−
−
−
−
−
La palabra array se ha traducido de muchas maneras (arreglos, formaciones, vectores,
matrices,…) pero ninguna de ellas ha cuajado por completo.
Los arrays son objetos y por tanto accedemos a ellos usando referencias pero NO existe una
clase array ni métodos asociados a los arrays. Los arrays son una construcción del lenguaje.
La expresión serie.length es la lectura de un dato asociado al array (su tamaño).
No hay restricciones respecto del tipo de datos que puede contener un array (pueden ser tanto
tipos de datos primitivos como referencias a objetos de una clase).
Los arrays tienen un tamaño fijo por eso se dice que son estructuras estáticas de datos (en
contraposición con las estructuras dinámicas de datos que si pueden variar el tamaño).
Las referencias de arrays se pueden asignar a arrays de diferentes tamaños.
Ordenación de arrays.
Un array a está ordenado si se cumple que:
∀ pareja de subíndices i,j, si i ≤ j
⇒ a[i] ≤ a[j]
Algoritmo de la burbuja.
Uno de los algoritmos de ordenación más usuales es el llamado algoritmo de la burbuja, que dice
así:
Sea el array a.
Repetir (a.length – 1) veces:
1.
2.
Realizar una pasada por el array.
Comparar pares de elementos sucesivos.
→ Si un par está en orden se deja como está.
→ Si no se intercambian los valores.
Se compara en cada pasada a[0] con a[1] , a[1] con a[2] , a[2] con a[3] y así sucesivamente.
−
−
−
−
Un valor grande puede bajar varias posiciones de una sola pasada, pero uno pequeño sólo
puede subir una posición.
En la primera pasada el valor más grande quedará en la última posición del array (a.lenght –1)
En la segunda pasada el segundo valor más grande quedará en la penúltima posición.
Se puede comprobar que hacen falta un número de pasadas igual a la longitud del array menos
1.
Tema 4: Arrays.
90
int a[] = {4, 3, 2, 1}
Inicial
Pasada 1
Pasada 2
Pasada 3
4
3
2
1
2
1
3
3
3
1
2
4
1
4
2
4
La programación en Java del algoritmo se muestra en el cuadro 3.
Cuadro 3: Algoritmo de la burbuja.
public class Ejemplo43 {
final static int TamañoArray = 10;
static int [] a = new int [TamañoArray];
public static void main(String args[] ) {
// Inicializamos el array.
for (int i = 0; i < a.length; i++) {
a[i] = a.length - i;
}
// Traza. Imprimimos el array
System.out.print("Array Inicial\t");
for (int j = 0; j < a.length; j++){
System.out.print(a[j] + " ");
}
System.out.println();
int intercambio = 0;
for (int pasadas = 0; pasadas < a.length-1; pasadas++ ) {
for (int i = 0 ; i < a.length- 1- pasadas; i++) {
if (a[i] > a[i+1]){
intercambio = a[i];
a[i] = a[i+1];
a[i+1] = intercambio;
}
}
// Traza para ver cómo queda el array tras cada pasada.
// Imprimimos el array
System.out.print("Pasada " + (pasadas + 1) + " \t");
for (int j = 0; j < a.length; j++){
System.out.print(a[j] + " ");
}
System.out.println();
}
}
}
Tema 4: Arrays.
91
Un algoritmo de ordenación de arrays muy curioso es el denominado quick-short cuyo estudio se
deja como ejercicio. Existe una buena descripción en http://es.wikipedia.org/wiki/Quicksort.
Búsqueda en arrays.
Se trata de determinar si el array contiene un elemento que coincide con el valor de una clave.
Búsqueda lineal
La búsqueda lineal compara cada elemento del array con la clave de búsqueda. Si el array no está
ordenado es igualmente probable que el elemento buscado esté al principio o al final. El número
medio de comparaciones es igual a la mitad del tamaño del array.
La búsqueda lineal funciona bien en arrays pequeños, pero es muy poco eficiente en arrays grandes
y en arrays ordenados.
Orden de complejidad: O(n)
La programación de la búsqueda lineal es muy sencilla y se muestra en el cuadro 4.
Cuadro 4: Búsqueda lineal.
public class Ejemplo42 {
static int a[] = {2,3,6,1,4,7,9,4,3,5};
static int clave = 7;
public static void main(String[] args) {
for ( int n = 0; n < a.length; n++ ) {
if (a[n] == clave) {
System.out.println("La clave en" + n);
break;
}
}
}
}
Búsqueda binaria.
Si el array está ordenado, un algoritmo muy eficiente es el de búsqueda binaria. El algoritmo dice
así:
Comparar clave con el elemento situado en la mitad del array (elemento medio).
IF (clave == elemento medio) → Hemos terminado.
ELSE IF (clave < elemento medio) → buscar en la primera mitad del array,
ELSE → buscar en la segunda mitad del array.
El proceso se repite sucesivamente hasta que:
La clave sea igual al elemento medio del subarray que queda.
OR
El subarray que queda consta de un solo elemento distinto de la clave
Tema 4: Arrays.
92
El algoritmo de búsqueda binaria elimina de la búsqueda la mitad de los elementos que quedan en
el array después de cada comparación.
1024 elementos (1024 = 210) → 10 comparaciones como máximo.
→ 20 comparaciones como máximo
1.048.576 elementos (220)
El número máximo de comparaciones es el exponente de la primera potencia de 2 mayor que el
número de elementos del array.
Orden de complejidad: O(log2 n)
Es un algoritmo muy eficiente pero sólo funciona con arrays ordenados
int [] a = {2,4,6,8,10,12,14,16,18,20,22}
Clave = 20
Índice
0
1
2
3
4
5
6
7
8
9
10
Valor
2
4
6
8
10
12
14
16
18
20
22
Al aplicar el algoritmo:
Inicial
Pasada 1
Pasada 2
Pasada 3
20
2
4
6
8
10
12
14
14
16
16
18
18
20
20
20
22
22
22
La codificación en Java se muestra en el cuadro 5.
Tema 4: Arrays.
93
Cuadro 5: Búsqueda binaria
public class Ejemplo44 {
static int a[ ] = {2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22};
public static void main (String args []) {
int
int
int
int
bajo = 0;
alto = a.length - 1;
medio;
clave = 20;
while (bajo <= alto) {
medio = (bajo + alto) / 2;
if (clave == a[medio]) { // se encontró
System.out.println ("Lo encontré en " + medio);
break;
}
else if (clave < a[medio]) {
alto = medio - 1;
}
else {
bajo = medio + 1;
}
}
}
}
Arrays Bidimensionales.
Estrictamente hablando en Java sólo pueden definirse arrays de una dimensión. Sin embargo, los
elementos de un array pueden ser a su vez referencias a arrays pudiéndose programar de este
modo tablas de dos o más dimensiones como un array de arrays. Las tablas que requieren dos
índices para identificar un elemento se llaman arrays con doble índice.
−
−
El primer índice se refiere a la fila. Este es el índice del array de arrays e identifica a un
array.
El segundo índice se refiere a la columna. Este es el índice de un array e identifica a un
elemento del array identificado por el primer índice.
Para simplificar hablaremos de arrays de dos o más dimensiones, aunque en Java esto supone un
cierto abuso del lenguaje 1. Por ejemplo, en la figura 5 se muestra la disposición de los elementos del
array:
int [][] a = new int[3][4];
No ocurre lo mismo en otros lenguajes donde los arrays multidimensionales están realmente definidos en el lenguaje. En
Java los definimos aprovechando los mecanismos del lenguaje.
1
Tema 4: Arrays.
94
Figura 5: Array de dos dimensiones.
Obsérvese que:
−
−
−
a es un array de 3 arrays de enteros .
Cada a[i] es un array de 4 enteros.
a[i][j] es un entero.
Los arrays de dos dimensiones (en general de cualquier dimensión) se pueden declarar e inicializar
de manera similar a los arrays de una dimensión. Por ejemplo:
int b [][] = { {1, 2}, {3, 4, 5} }
Los valores se agrupan por fila encerrados entre llaves (figura 6):
−
−
1 y 2 inicializan b[0][0] y b[0][1]
3, 4 y 5 inicializan b[1][0], b[1][1] y b[1][2]
Figura 6: int b [][] = { {1, 2}, {3, 4, 5} }
Obsérvese que en la declaración aparecen dos parejas de corchetes. Esto es porque, como ya se ha
mencionado, Java no soporta arrays multidimensionales, sino arrays de arrays. Hablar de arrays de
dos o más dimensiones no es estrictamente correcto. Lo que estamos declarando en el ejemplo es
un array cuyos elementos son dos arrays de enteros, uno conteniendo dos elementos {1, 2} y el
otro conteniendo tres elementos {3, 4, 5}.
Esto se muestra con más claridad en el ejemplo del cuadro 5 donde se inicializa cada elemento del
array de arrays explícitamente con un array diferente. En el ejemplo también se ve que no es
necesario especificar la longitud de cada array en la declaración del array bidimensional:
int triangulo [][] = new int[3][];
Cuando se recorren arrays “bidimensionales” hay que tener en cuenta que cada “fila” puede tener
una longitud diferente. Se evitan errores preguntando siempre a cada array su longitud:
triangulo[i].length representa la longitud del array i-ésimo de triangulo.
Tema 4: Arrays.
95
Cuadro 5: Arrays bidimensionales con filas diferentes longitudes
public class Ejemplo45 {
public static void main(String [] args){
int triangulo [][]
triangulo[0] = new
triangulo[1] = new
triangulo[2] = new
= new int[3][];
int[1];
int[2];
int[3];
// inicialización
for (int i = 0; i < triangulo.length; i++){
for (int j = 0; j < triangulo[i].length; j++){
triangulo[i][j] = j;
}
}
// imprimimos el array
for (int i = 0; i < triangulo.length; i++){
for (int j = 0; j < triangulo[i].length; j++){
System.out.print(triangulo[i][j] + " ");
}
System.out.println();
}
System.out.println();
int cornisa [][] = {{1, 2, 3}, {1, 2}, {1}};
for (int i = 0; i < cornisa.length; i++){
for (int j = 0; j < cornisa[i].length; j++){
System.out.print(cornisa[i][j] + " ");
}
System.out.println();
}
}
}
Cuadro 6: Suma de los elementos de un array bidimensional
public class Ejemplo46 {
public static void main(String[] args) {
int a [][] = { {1,2}, {4} };
int total = 0;
for (int fila = 0; fila < a.length; fila++){
for (int col = 0; col < a[fila].length; col++) {
total += a[fila][col];
}
}
System.out.println("Suma = " + total);
}
}
Tema 4: Arrays.
96
Cuadro 7: Encontrar la nota máxima y mínima de un array de calificaciones, donde cada fila
representa un estudiante y cada columna representa una calificación en diferentes exámenes.
public class Ejemplo47 {
static int notas[][] = {{77,68,86,73}, {96,87,89,81}, {70,90,86,81}};
public static void main (String args[]){
int notaBaja = 100;
int notaAlta = 0;
for (int i = 0; i < notas.length; i++ ) {
for (int j = 0; j < notas[i].length; j++ ) {
if (notas[i][j] < notaBaja) {
notaBaja = notas[i][j];
}
if (notas[i][j] > notaAlta){
notaAlta = notas[i][j];
}
}
}
System.out.println("La nota baja es: " + notaBaja);
System.out.println("La nota alta es: " + notaAlta);
}
}
Otras estructuras estáticas de datos.
Al principio del tema destacábamos dos características de los arrays:
−
−
Son estructuras de datos estáticas: su tamaño no cambia durante la ejecución del
programa.
Todos los datos son del mismo tipo.
En los programas suelen hacer falta estructuras de datos capaces de agrupar datos de distintos
tipos. Por ejemplo, imagínense el caso de tener que modelar una cuenta bancaria en la que hay
datos que se pueden representar mediante cadenas de caracteres (nombre del titular), otros
mediante números enteros (fondos) y otros mediante tipos de datos definidos por el programador
(fecha de de apertura). Los lenguajes de programación estructurados o los lenguajes orientados a
objeto que son una evolución de los estructurados (por ejemplo Ada05 o C++) proporcionan
estructuras de datos con estas características:
−
−
Son estructuras de datos estáticas: su tamaño no cambia durante la ejecución del
programa.
Sus datos pueden ser de diferentes tipos.
A modo de ejemplo se muestra una estructura (struct) de C y un registro (record) de Ada.
typedef struct Cuenta {
String titular;
int
fondos;
Date
fechaApertura;
}
type Cuenta is
record
titular: String;
fondos: Integer;
fecha_apertura: Date;
end record;
Tema 4: Arrays.
97
Este tipo de estructuras de datos no existen en java porque Java proporciona una estructura de
datos que realiza la misma función y que además es mucho más flexible: la clase, que se explicará en
el tema 6.
Tema 5: Abstracción funcional. Métodos.
Lección 5: Abstracción Funcional
99
Objetivos
Conocer la programación modular a partir del uso de métodos.
Ser capaces de crear nuevos métodos.
Manejar los mecanismos para pasar información entre métodos.
Conocer cómo la visibilidad de identificadores es limitada a regiones específicas. Escribir y
usar métodos recursivos.
Contenido
Abstracción funcional y programación modular.
Definición de métodos en Java.
Paso de parámetros.
Variables locales. Duración y alcance.
Recursión.
Librerías.
Tema 5: Abstracción funcional. Métodos.
Abstracción funcional y programación modular.
100
Funciones y llamadas a función.
Función: Sección de código (conjunto de instrucciones) que puede recibir datos de
entrada y producir datos de salida.
Abstracción funcional: las funciones se invocan para realizar una tarea, pero sin necesidad
de saber cómo la llevan a cabo.
En programación, una función 1 es una porción de código que realiza una tarea específica y que es
relativamente independiente del resto del código. Casi todos los lenguajes de programación
proporcionan algún tipo de mecanismo para la definición de funciones. Las funciones reciben un
nombre, pueden recibir datos de entrada a través de una lista de parámetros y pueden retornar un
resultado. El nombre de la función, su lista de parámetros y el resultado que produce constituyen la
interfaz de la función (figura 1).
Figura 1: Interfaz (cabecera) y cuerpo de una función.
Las funciones pueden ser invocadas desde cualquier punto del programa. Una función puede
invocar otras funciones e incluso invocarse a sí misma. La figura 2 muestra el mecanismo de
llamada o invocación de una función.
Figura 2: Invocación de una función.
También reciben el nombre de subrutina, procedimiento, método, etc. Según la denominación que se emplee puede haber
diferencias de matiz, pero la definición y el discurso del apartado y del tema se ajusta a todos ellos.
1
Tema 5: Abstracción funcional. Métodos.
101
Cada vez que se realiza una invocación de una función, el flujo de control del programa se desplaza
a la instrucción inicial de dicha función, pero antes es necesario almacenar en alguna parte la
información que necesita la función para ejecutarse y para retornar el control al llamante. Para
guardar dicha información, en el momento de la llamada se genera un registro de activación
(también reciben el nombre de stack frames) donde se guardan los parámetros de la función, sus
datos locales, la dirección de retorno y donde se reserva además un espacio para que la función
deposite los resultados que genera (valor de retorno). Este registro de activación se guarda a su vez
en una zona de memoria denominada pila o stack que funciona según un esquema LIFO (Last InFirst Out): último en entrar primero en salir. Cuando la función termina de ejecutarse el control
retorna al punto del programa inmediatamente posterior a la invocación de la función. La
estructura LIFO de la pila permite gestionar las llamadas a función anidadas con mucha facilidad.
Por ejemplo, en la figura 3 se asume que desde el programa principal se llama a la función A y desde
la función a la función B. Cuando B termina su ejecución se elimina su registro de activación y se
retoma la ejecución de A y cuando A termina su ejecución se elimina su registro de activación y se
retoma la ejecución de main.
4. Se retorna a
main y se
elimina el
registro de
activación de A
Código de main()
A()
1. Se crea registro
de activación de
A y se almacena
en Stack
STACK
Reg. Act. de B()
Código de A()
B()
Código de B()
3. Se retorna a A y
se elimina el
registro de
activación de B
2. Se crea registro
de activación de
B y se almacena
en Stack
STACK
STACK
Reg. Act. de A()
Valor de
t
Dir. de retorno
1
Valor de
t
Dir. de retorno
STACK
Reg. Act. de A()
Reg. Act. de A()
Valor de
t
Valor de
t
Dir. de retorno
2
STACK
Dir. de retorno
3
4
Figura 3: Anidamiento de llamadas.
Es muy importante darse cuenta de que lo único que debe conocerse de una función para poder usarla
es su interfaz. Para que esto sea posible, la porción del código encapsulada dentro de la función no
debe manipular más información que la que obtiene a través a de sus parámetros, es decir, las
funciones deben ser auto-contenidas. Si las funciones son auto-contenidas y se organizan en
bibliotecas pueden utilizarse para la construcción de programas que no tienen nada que ver entre
sí. En este sentido, las funciones son el mecanismo más inmediato para llevar a cabo una
programación modular.
Tema 5: Abstracción funcional. Métodos.
Las programación modular.
102
La programación modular es una técnica de diseño de software en la que los programas se
construyen ensamblando porciones de código independientes llamadas módulos. Cada módulo está
orientado a resolver un aspecto concreto del programa y se relaciona con otros módulos a través de
una interfaz que define los elementos provistos y requeridos por el módulo. La separación de
interfaz e implementación es un aspecto esencial que hace posible la programación modular:
−
−
La interfaz oculta la implementación del módulo y este ocultamiento permite cambiar la
implementación de un módulo sin afectar al resto.
Puesto que las interacciones entre los módulos se realizan a través de sus interfaces, una
vez que éstas han sido definidas, es posible asignar la implementación de cada módulo a un
programador diferente, es decir es posible la división del trabajo entre muchos
programadores. Todas las dependencias entre módulos deben poder resolverse a través de
sus interfaces, sin asumir ninguna implementación concreta de los mismos.
Principio de dependencia de interfaces e independencia de implementaciones: haga que su
código dependa de interfaces, pero no de implementaciones.
Es fácil percibir que el diseño modular promueve la reutilización del código, ya que los mismos
módulos pueden utilizarse para construir muchos programas. La consecución de módulos
reutilizables no es, sin embargo, un proceso inmediato. Módulos que se ajustan perfectamente a las
necesidades de un programa pueden ser completamente inconvenientes en otros programas
parecidos. Uno de los factores que más limita la reutilización de un módulo es su dependencia de
otros módulos. Supongamos que queremos usar un módulo A y que éste a su vez necesita usar los
módulos B, C y D. Si esto ocurre estamos obligados a incluir en nuestro programa los módulos B, C y
D 1. Las dependencias entre módulos definen su grado de acoplamiento mutuo. Cuantas más
dependencias hay entre dos o más módulos, mayor es su acoplamiento. Un buen diseño de los
módulos debe estar encaminado a reducir su acoplamiento. Idealmente, un módulo debería ser una
pieza de software independiente.
La definición de módulo software depende del paradigma de programación y del estilo de diseño.
En programación procedural un módulo puede ser una función, pero es más frecuente que sea una
agrupación de funciones altamente cohesionadas, por ejemplo una librería de funciones 2. El
concepto de cohesión aparece cuando un módulo es lo bastante grande como para distinguir en él
diferentes partes y es uno de esos conceptos fáciles de entender, pero difíciles de explicar con
precisión en pocas palabras 3. De forma muy simplificada puede decirse que un módulo está
cohesionado si todos sus elementos están enfocados hacia un mismo objetivo. Si el módulo es una
agrupación de funciones matemáticas no tiene sentido incluir en él funciones que procesan cadenas
de caracteres. Lo correcto sería definir una nueva librería y agrupar en ella a las funciones de
procesamiento de caracteres. Un buen diseño debe maximizar la cohesión interna de los módulos y
minimizar y si es posible eliminar el acoplamiento entre módulos. Existe una relación entre
acoplamiento y cohesión. Cuanto más cohesionados son los módulos, más sencillo es evitar sus
dependencias mutuas 4.
U otros que ofrecieran al menos los mismos elementos en sus interfaces.
Lo cual no quiere decir que la función deje de ser un módulo, simplemente sirve para definir otros elementos más grandes
que también pueden considerarse módulos.
3 Pueden echar un vistazo a http://latecladeescape.com/w0/ingenieria-del-software/acoplamiento-y-cohesion.html
4 Más sencillo, no quiere decir fácil, sino menos difícil.
1
2
Tema 5: Abstracción funcional. Métodos.
Las funciones como mecanismo de abstracción.
103
Llegados aquí, merece la pena recapitular el camino ya andado y ponerlo en el contexto del que
vamos andar en este tema. Mediante la abstracción de datos hemos dotado de significado a las
secuencias de bits que manejan los computadores y esto nos ha permitido ignorar los detalles de su
implementación en una máquina concreta. Sólo las propiedades abstractas de los datos son visibles
para el programador y para el resto del código, mientras que la implementación permanece
enteramente privada y puede cambiarse sin que el programa se vea afectado. La abstracción de
control nos ha provisto de mecanismos para la ejecución condicional o reiterada de una porción de
código sin tener que especificar a nivel de registros y posiciones de memoria cada una de las
acciones de control y sin necesidad de conocer los mecanismos de direccionamiento de la máquina
subyacente. La abstracción funcional nos permite ahora encapsular detrás de una interfaz bien
definida una porción de código que implementa un comportamiento o un algoritmo determinados.
Las funciones se utilizan como herramientas de abstracción (separación) porque su comportamiento
(lo que hacen) puede ser separado (abstraído) de su implementación (cómo lo hacen). Al igual que
ocurría con la abstracción de datos, pero a un nivel de abstracción superior, sólo las propiedades
abstractas de la función definidas a través de su interfaz son visibles para el programador y para el
resto del código, mientras que la implementación permanece enteramente privada y puede
cambiarse sin que el programa se vea afectado. De esta forma, la abstracción funcional nos abre la
puerta de la programación modular, que es la única manera de diseñar, codificar y mantener
grandes programas. La abstracción funcional está estrechamente ligada a las técnicas de diseño
funcional descendente y a la programación estructurada. Los problemas complejos pueden
descomponerse en subproblemas más sencillos 1, cada uno de los cuales puede expresarse como
una llamada a función.
La abstracción funcional permite llevar a cabo el concepto de modularidad:
−
−
−
Una función agrupa a un conjunto de instrucciones altamente cohesionadas
Una función realiza una tarea específica.
Para usar una función no es necesario conocer su implementación.
Las funciones similares se agrupan en bibliotecas (en java en clases, como veremos enseguida):
funciones matemáticas, funciones para el manejo de caracteres, funciones para el maneja de
estructuras de datos complejas, etc. De hecho, el éxito o fracaso de un determinado lenguaje de
programación depende en gran medida de las bibliotecas que lo acompañan. En los lenguajes
procedurales están bibliotecas son bibliotecas de funciones. En los lenguajes orientados a objetos
las bibliotecas son bibliotecas de clases, pero las clases están hechas, entre otras cosas, de
funciones.
Para que la abstracción funcional sea efectiva, es muy importante que la tarea asignada a la función
esté bien definida y que dicha finalidad esté descrita mediante su nombre. En caso de duda es
preferible definir varias funciones pequeñas que una grande. Mejor una función para ordenar un
array y otra para buscar un elemento en un array ordenado, que una única función que ordene el
array y luego busque el elemento. Las funciones sin un objetivo concreto son difíciles de reutilizar.
Las funciones demasiado extensas hacen difícil su comprensión y depuración. Observe como esta
concreción en el objetivo está relacionada con los principios de alta cohesión y bajo acoplamiento:
cuántas más responsabilidades tenga una función menor será su cohesión interna y más llamadas
tendrá que realizar a otras funciones, aumentando de esta forma su acoplamiento. Otro punto
importante es que la función no manipule más datos que aquellos que recibe a través de su interfaz.
Aunque a partir de aquí se va a utilizar el lenguaje Java como vehículo para la explicación de la
abstracción funcional, los conceptos que se van a ver en este tema aparecen en todos los lenguajes
de alto nivel modernos, si bien con considerables diferencias sintácticas entre unos y otros. Se
tratará de separar en la medida de lo posible lo que es general y lo que es específico del lenguaje
Java.
1
En programación se suele aludir a este principio mediante el aforismo “Divide y vencerás”.
Tema 5: Abstracción funcional. Métodos.
Definición de métodos en Java.
104
Métodos y clases.
Para empezar vamos a dejar de hablar de funciones y vamos a empezar a hablar de métodos, porque
ese es el nombre que reciben las funciones en Java y en general en los lenguajes orientados a
objetos. El cambio de nombre está relacionado con el cambio de paradigma de programación (de
procedural a orientado a objetos) y tiene algunas implicaciones semánticas. En este tema vamos a
trabajar con la definición de función dada en el punto anterior. En el siguiente tema se dará una
definición de método que extiende la definición de función y se ampliarán de acuerdo con ella
algunos de los conceptos que se presentan en este tema.
Hablar de métodos en Java significa inevitablemente hablar de clases, ya que en Java no existen
métodos fuera de las clases 1. Sin embargo, no es necesario saber mucho de las clases para poder
abordar este tema. De momento, podemos ver las clases de Java como un ámbito donde agrupar
datos y métodos. Aplicando los principios de alta cohesión y bajo acoplamiento podemos esperar
que si la clase está bien diseñada todos los métodos de una clase tengan un objetivo o finalidad
común (cohesión) y que sus dependencias con los métodos de otras clases sean las imprescindibles
(acoplamiento). Sabemos además que en una clase puede haber un método especial, el método
main, que representa a un programa.
class UnaClase {
// variables
// métodos
}
Las clases también encapsulan datos. Estos datos son globales a la clase y visibles desde el interior
de los métodos. Es decir, un método definido en una clase puede utilizar cualquier variable definida
en dicha clase sin necesidad de que se le pase como parámetro 2. Esto a primera vista parece una
violación de la interfaz del método, ya que el método manipula datos no definidos en su interfaz. Sin
embargo, desde el punto de vista de Java no lo es, porque en Java la unidad de diseño no es el
método, sino la clase. Es decir, los métodos ven y utilizan las variables de su clase, pero los usuarios
del método no ven dichas variables, ni siquiera saben de su existencia, salvo que se hagan
explícitamente visibles (ya veremos cómo). No obstante, hay que ser cuidadoso en la forma de
manipular dentro de los métodos las variables que se definen fuera de los mismos. Finalmente está
el tema de los objetos, que tampoco queremos abordar hasta el siguiente tema. En todos los
ejemplos de este tema las variables y métodos están marcados como static. Cuando un método o
variable de una clase se marca como static es independiente de los objetos de esa clase. Hechas
estas observaciones estamos en disposición de hablar de los métodos en Java sin echar mano, por el
momento, del paradigma de la orientación a objetos. La sintaxis de los métodos en Java es la
siguiente:
[Modificadores] tipoDevuelto identificadorMetodo([argumentos]){
// cuerpo del método
}
Por ejemplo:
public static int maximoDosNumeros(int n1, int n2) // interfaz
{
return n1 > n2? n1:n2;
// cuerpo con la implementación.
}
Hay lenguajes de programación en los que las funciones y las clases pueden existir por separado
Esto no es del todo cierto. Hay una restricción que depende de si variables y métodos son static o no, pero no lo veremos
hasta el siguiente tema.
1
2
Tema 5: Abstracción funcional. Métodos.
105
Los modificadores son las palabras reservadas public, protected, private, final y static,
cuyo significado se verá en el siguiente tema.
En Java no es posible definir un método dentro de otro método.
Tipo del valor devuelto por el método.
El tipo del valor devuelto por un método puede ser un tipo primitivo Java, una referencia a un
objeto o void. void es una palabra reservada de Java que indica que un método no devuelve
ningún valor.
Figura 2: Valores devueltos por los métodos Java.
Terminación de un método.
Existen dos posibilidades para terminar un método:
1. Si el método devuelve void (nada) el método termina cuando llega al final (cierra llaves) o
cuando aparece return:
public static void pintaCirculo(float r){
// código para pintar el círculo
}
// este es el final del método
public static void saluda(String mensaje){
if (!primeraVez){
return; // aquí termina
}
else {
System.out.println(mensaje);
}
}
2. Si el método devuelve algo distinto de void entonces se devuelve el control mediante:
return expresión
donde expresión produce un valor cuyo tipo es el declarado en la cabecera del método.
int potencia(int x, int y){
int contador = 0, resultado = 1;
while (contador != y){
resultado = resultado * x;
contador++;
}
return resultado; //aquí se devuelve el resultado.
}
Cuando se hace return se devuelve el control al invocador del método.
Tema 5: Abstracción funcional. Métodos.
106
Es un error de compilación cuando se devuelve siendo void el tipo de retorno.
public void saluda(String mensaje){
if ((primeraVez))
return false; // error
else
System.out.println(mensaje);
}
Es un error de compilación no hacer return cuando hay que devolver un valor de un tipo.
int potencia(int x, int y){
int contador =0, resultado = 1;
while (contador != y){
resultado = resultado * x;
contador++;
}
// Error: No se retorna valor
}
Lista de parámetros
La lista de parámetros (argumentos) es una lista de las declaraciones de parámetros que se pasan al
método para su ejecución. La lista puede estar vacía pero los paréntesis son obligados:
public void suficiente()
public void pocaCosa
// Correcto
// Error de sintaxis.
Es obligatorio indicar el tipo de cada parámetro:
double potencia (double x, double y)
double potencia (double x, y)
// Correcto
// Error de sintaxis
Parámetros formales y parámetros reales
Se denominan parámetros formales a aquellos que aparecen en la cabecera de la función y
parámetros reales a los que aparecen en la llamada a la función. Los parámetros reales deben
coincidir con los parámetros formales declarados en la interfaz del método.
public static void main(String [] args){
int k = 3;
boolean flag = true;
int x = f(7, false);
int y = f(0, flag);
int z = f(k, flag);
// Llamadas incorrectas: No hay concordancia entre parámetros
formales
f(true, d, flag); // Debería haber dos parámetros
f(false, 7);
// El primer parámetro debería ser int.
}
reales
y
Tema 5: Abstracción funcional. Métodos.
107
A los parámetros también se les llama argumentos. En algunos lenguajes de programación
parámetro y argumento se utilizan como sinónimos. En C y en Java (aunque no todos los autores)
cuando se habla de parámetros nos estamos refiriendo de los parámetros formales y cuando
hablamos de argumentos nos estamos refiriendo a los parámetros reales. En lo sucesivo
seguiremos esta regla.
Promoción de argumentos. Coerción.
Consideremos la siguiente cabecera de un método:
public static double sqrt(double d)
Y el siguiente uso del método:
int numero = 4;
System.out.println(sqrt(numero));
Java promociona implícitamente el argumento de int a double antes de pasarlo a sqrt(),
pasando 4.0 en lugar de 4. A esto se le llama coerción de argumentos y sigue determinadas reglas
de promoción, que especifican cómo los tipos pueden convertirse a otros sin que haya pérdida de
información.
Las reglas de promoción de Java son las siguientes:
Tipo
double
Promociones permitidas
float
double
long
float o double
int
long, float o double
char
int, long, float o double
short
int, long, float o double
byte
short, int, long, float o double
boolean
ninguna (ni true ni false son números)
ninguna (no hay tipo más grande que double)
Estas reglas se aplican en la coerción de argumentos y en las expresiones que contienen dos o más
tipos de datos. Hay que hacer dos observaciones:
−
−
Los valores originales de las variables no se modifican: temporalmente se promocionan.
Las conversiones a tipos más pequeños sólo es posible haciendo conversión explícita
mediante casting. Puede perderse información.
Paso de parámetros por valor y por referencia.
Los métodos obtienen la información que necesitan a través de los argumentos que se les pasa al
invocarlos. Existen dos formas de pasar los argumentos a los métodos: paso por valor y paso por
referencia (figura 3).
Paso de parámetros por valor: se le pasa al método una copia del valor del argumento. El paso
por valor ofrece una gran seguridad. El método no trabaja con el argumento original, sino con una
Tema 5: Abstracción funcional. Métodos.
108
copia, y por ello los cambios realizados dentro del método no afectan a la variable que se utilizó
como argumento.
Paso de parámetros por referencia: se le pasa al método una referencia a los datos. El método
puede modificar los datos que se le pasan como argumento. El paso por referencia ahorra memoria
(no se copian los datos), pero es menos seguro que el paso por valor (el método puede modificar los
datos que se le pasan).
Figura 3: Paso de argumentos por valor y por referencia.
Se dice que en Java, los tipos primitivos siempre se pasan por valor y los objetos siempre por
referencia. Lo mismo puede decirse de la sentencia return, aunque en este caso el sentido en el
que fluye la información es el contrario, del método al código llamante. Si el tipo de retorno es
primitivo la sentencia return devuelve una copia del valor producido en el método, si es un objeto
devuelve una referencia al objeto que retorna el método.
Variables locales en los métodos.
Es posible declarar variables locales dentro de los métodos. La sintaxis que se sigue es la ya
conocida.
[modificador] tipoValorDevuelto nombre([argumentos]){
// declaración de variables locales
// enunciados
}
public static void intercambio(int x, int y){
int auxiliar;
auxiliar = y;
y = x;
x = auxiliar;
}
Con la declaración de variables locales tenemos que en un método puede manipularse información
que puede ser:
−
−
−
Argumentos pasados en la llamada. Para evitar ambigüedades las variables locales no
pueden recibir el mismo nombre que un parámetro formal.
Variables locales.
Variables declaradas en la clase fuera de los métodos. Recordemos que las clases también
encapsulan datos. Estos datos, llamados variables de instancia, son globales a la clase y
visibles desde el interior de los métodos. Es decir, un método definido en una clase puede
utilizar cualquier variable definida en dicha clase sin necesidad de que se le pase como
parámetro. Un método puede usar variables locales y aquellas definidas fuera del método.
Puede haber coincidencia de nombres. ¿Cómo distinguirlas? La respuesta viene dada por la
duración y el alcance de las variables.
Tema 5: Abstracción funcional. Métodos.
109
Los parámetros y variables locales a un método son variables automáticas. Las diferencias entre las
variables definidas en la clase y las variables automáticas se resumen en la tabla 1.
Duración y alcance de las variables
La duración y reglas de alcance se aplican a todos los identificadores de un programa Java y definen
dónde y cómo pueden usarse los identificadores.
La duración o tiempo de vida de un identificador es el tiempo durante el cual la información está
accesible.
El alcance o ámbito de un identificador determina qué parte del programa ve dicho identificador.
Las variables locales sólo son visibles dentro del método en el que son declaradas y a partir del
lugar en el que son declaradas. Si una variable local se declara dentro de un bloque de código, sólo
es visible en dicho bloque y en los bloques anidados dentro de él. Las variables locales sólo existen
desde el momento en que se declaran hasta el momento en que se sale del bloque en el que han sido
declaradas. Puesto que las variables locales se crean y se destruyen dinámicamente durante la
ejecución del programa reciben el nombre de variables automáticas (creación y eliminación
automáticas).
public static void f(){
int x = 10;
while (x > 0){
int y = x*x + 2*x;
System.out.println(y);
x--;
}
}
// x existe desde aquí
// y existe desde aquí
// Aquí deja de existir y
// Aquí deja de existir x
Tabla 1: Variables de instancia y variables automáticas
Variables de instancia (o de ejemplar)
Variables automáticas (argumentos y variables
locales)
Se definen en una clase, no en un método.
Se definen dentro de un método o de un bloque
(porción de código entre llaves) definido dentro de un
método.
Si son static existen desde que se carga la
clase en memoria hasta el final del programa.
Si no son static existen mientras existe el
objeto que las alberga.
Son visibles en toda la clase.
Si no tienen valores iniciales asignados el
compilador los asigna por defecto (véase
tabla [] del tema 2).
Existen desde que se declaran y mientras se está en el
bloque en el que se declaran.
Son visibles sólo dentro del bloque en el que se
declaran y en los bloques anidados en él.
Deben inicializarse explícitamente antes de ser usadas.
Veamos ahora algunos ejemplos relacionados con el paso de parámetros y las variables locales.
Tema 5: Abstracción funcional. Métodos.
110
Cuadro 1: Paso de parámetros por valor. ¿Qué valor tienen x e y después de la llamada?
class Ejemplo51 {
static int x = 5;
static int y = 3;
public static void intercambio(int x, int y){
int auxiliar;
auxiliar = y;
y = x;
x = auxiliar;
}
public static void main(String args){
intercambio(x, y);
System.out.println(“x = “ + x + “, y = “ + y);
}
}
Cuadro 2: Alcance de las variables. ¿Qué se imprime en la consola?
public class Ejemplo52 {
static int x = 0;
private static int f1(int x){
x *= 2;
System.out.println("f1
-> x = " + x);
return x;
}
public static void main (String []
int x = 5;
System.out.println("main -> x =
f1(x);
System.out.println("main -> x =
x = f1(x);
System.out.println("main -> x =
}
}
main
f1
main
f1
main
->
->
->
->
->
x
x
x
x
x
=
=
=
=
=
5
10
5
10
10
args){
" + x );
" + x );
" + x );
Tema 5: Abstracción funcional. Métodos.
111
Cuadro 3: Alcance de las variables. ¿Qué se imprime en la consola?
public class Ejemplo53 {
static int x = 0;
private static int f2(){
System.out.println("f2
-> x =
return x;
}
public static void main (String []
int x = 5;
System.out.println("main -> x =
f2();
System.out.println("main -> x =
x = f2();
System.out.println("main -> x =
}
}
main
f2
main
f2
main
->
->
->
->
->
x
x
x
x
x
=
=
=
=
=
" + x);
args){
" + x );
" + x );
" + x );
5
0
5
0
0
Paso de arrays como parámetros
En todos los ejemplos vistos hasta ahora los argumentos han sido de tipos de datos primitivos, que,
como sabemos, se pasan por valor. Nos falta por ver ejemplos en los que los parámetros se pasen
por referencia. Para ello tenemos que trabajar con objetos y por el momento los únicos objetos que
conocemos con cierta profundidad son los arrays. Consideremos el código del cuadro 4.
Cuadro 4: Paso de arrays como argumentos a métodos.
public class Ejemplo54 {
static int a[] = {0, 1, 2, 3, 4};
public static void imprimirArray(int a []){
if (a == null){
System.out.println("Array nulo");
return;
}
for (int i = 0; i < a.length; i++){
System.out.print(a[i] + " ");
}
System.out.println();
}
public static void modificarArray(int b[]){
for (int j = 0; j< b.length; j++ ) {
b[j] *= 2;
}
}
// Por referencia
public static void modificarElemento(int e){
e *= 2;
}
// Por valor
public static void main(String[] args) {
imprimirArray(a);
modificarArray(a);
imprimirArray(a);
modificarElemento(a[3]);
imprimirArray(a);
}
}
0 1 2 3 4
0 2 4 6 8
0 2 4 6 8
Tema 5: Abstracción funcional. Métodos.
112
El método imprimirArray no realiza ninguna modificación sobre los datos de entrada,
simplemente imprime los elementos del array que se le pasa como argumento.
El método modificarArray modifica los elementos del array. En concreto sustituye cada uno de
los enteros almacenados en el array por el doble de ese entero. Puesto que los arrays se pasan por
referencia es posible modificar los valores contenidos en el array.
El método modificarElemento toma como argumento un tipo de datos primitivo. En este caso,
el argumento se pasa por valor y la variable original no es modificada.
Hasta aquí todo funciona como se ha descrito, los tipos primitivos siempre se pasan por valor y los
objetos siempre por referencia. En consecuencia, los argumentos de tipos de datos primitivos no
pueden modificarse en el interior de los métodos y los objetos si pueden modificarse. Pero hay que
tener cuidado con lo que significa exactamente lo que estamos haciendo. Veamos ahora el ejemplo
del cuadro 5.
Cuadro 5: Paso de arrays como argumentos a métodos.
public class Ejemplo55 {
static int a[] = {1, 2, 3};
public static void modificarArray(int a[]){
int [] b = {4, 5, 5};
a[0] = b[0];
a = b;
// ¿qué se imprime?
System.out.println("Valores de a ANTES de salir del método ");
Ejemplo54.imprimirArray(a);
}
public static void main(String [] args){
System.out.println("Valores de a ANTES de invocar el método ");
Ejemplo54.imprimirArray(a);
modificarArray(a);
// ¿qué se imprime?
System.out.println("Valores de a DESPUÉS de salir del método ");
Ejemplo54.imprimirArray(a);
}
}
Valores de a ANTES de invocar el método
1 2 3
Valores de a ANTES de salir del método
4 5 5
Valores de a DESPUÉS de salir del método
4 2 3
Antes de empezar a comentar la salida que produce el código del cuadro 5 merece la pena que se
den cuenta de que estamos utilizando el código del ejemplo anterior para imprimir el resultado.
Esta es una forma muy desorganizada de reutilizar el código existente, pero lo importante era
resaltar la posibilidad de invocar los métodos de una clase en otra, no la forma en que se ha
organizado el código. Veamos ahora que ocurre con el array a.
En el método modificarArray asignamos a a[0] el valor de b[0] (a[0] = b[0]). La
asignación parece absurda porque posteriormente asignamos la referencia b a a (a = b), es decir
hacemos que a deje de apuntar al array al que apuntaba para apuntar ahora al array al que apunta
b. Cualquier cambio que hubiéramos hecho en el array al que apuntaba la referencia a
anteriormente es inútil porque después de hacer a = b, a dicho array ya no le apunta nadie.
Obsérvese que el array que se imprime antes de abandonar el método contiene {4, 5, 5}.
Tema 5: Abstracción funcional. Métodos.
113
Inmediatamente después de salir del método modificarArray volvemos a imprimir el contenido
del array al que se refiere a y en este caso se imprime {4, 2, 3} es decir el contenido del array al que
apuntaba a, antes de hacer a = b. ¿Qué ha pasado? Lo que ha pasado es que las referencias de los
objetos se pasan por valor. Dentro de un método, podemos cambiar los valores del objeto usando la
referencia pasada como argumento, pero no podemos cambiar la referencia misma. Las referencias
no se cambian dentro de los métodos porque no utilizamos las referencias originales, sino una
copia de las mismas. Los cambios realizados dentro del método se realizan sobre la copia, no sobre
el original. El ejemplo del cuadro 6 ilustra el mismo principio, pero en este caso utilizando
referencias a cadenas de caracteres.
Cuadro 6: Las referencias se pasan por valor.
package ejemplos;
public class Ejemplo56 {
static String s1 = "Aitor";
static String s2 = "Menta";
public static void intercambio2 (String s1, String s2){
String auxiliar;
auxiliar = s2;
s2 = s1;
s1 = auxiliar;
System.out.println("s1 = " + s1 + ", s2 = " + s2);
}
public static void main(String [] args){
intercambio2(s1, s2);
System.out.println("s1 = " + s1 + ", s2 = " + s2);
}
}
s1 = Menta, s2 = Aitor
s1 = Aitor, s2 = Menta
Sobrecarga de métodos: firma y prototipo
Sobrecarga de métodos: Java permite definir métodos con el mismo nombre siempre que difieran
en los argumentos de entrada.
Se denomina firma del método a su nombre y a sus argumentos de entrada. Puesto que los
métodos sobrecargados tienen el mismo nombre, los métodos sobrecargados se diferencian por el
número, tipo y orden de los argumentos en su lista de parámetros.
public class utilidadesMatematicas{
int
square(int x) { return x*x; }
double square(double x) { return x*x; }
}
El prototipo de un método es la firma más el tipo devuelto, lo que hemos venido llamando hasta
ahora la interfaz del método. Los métodos sobrecargados no se distinguen por el prototipo sino por
la firma.
Tema 5: Abstracción funcional. Métodos.
Funciones y procedimientos.
114
Aunque hasta ahora sólo hemos hablado de funciones, en algunos lenguajes de programación (no
en Java) se suele distinguir entre funciones y procedimientos. Las funciones se corresponden con la
noción matemática de función: producen un resultado que es función de los datos de entrada:
−
−
−
y = f(x1, x2,...xn)
Una función bien definida no modifica los parámetros de entrada.
Se utilizan en expresiones.
Los procedimientos agrupan sentencias que realizan una tarea bien definida, pero no tienen por
qué devolver un resultado.
−
−
Se corresponden con el concepto de instrucción.
Los procedimientos actúan sobre el entorno y pueden modificar los parámetros de entrada
(efectos colaterales → side effects).
Los procedimientos pueden verse como una forma de extender las sentencias del lenguaje,
mientras que las funciones pueden verse como una extensión de los operadores. Cuando se invoca
una función se obtiene un valor a través de un valor de retorno. Cuando se invoca un procedimiento
el efecto es una modificación del entorno del llamante, de la cual éste es advertido a través de los
valores de los parámetros de salida.
Recursión.
La recursión es una técnica de programación mediante la cual las funciones se llaman a sí
mismas.
La recursión puede emplearse si:
1.
Existe un caso base que tiene solución no recursiva (el caso del cero).
por ejemplo: 0! = 1, por definición.
Si el método recursivo ha llegado al caso base devuelve un resultado: if(numero == 0)
return 1;
2.
Si no se ha llegado al caso base, el método se llama a sí mismo, pero considerando un problema
más pequeño.
(n!= n * (n-1)!) → return numero * factorial(numero-1);
La parte del problema que no se sabe resolver debe ser más pequeña que la actual:
3.
n - 1 es más pequeño que n.
Los problemas pequeños en los que se va dividiendo el más grande deben converger (como las
series matemáticas) al caso base, si no, su ejecución será infinita.
La recursión se utiliza cuando es difícil encontrar o implementar una solución basada en bucles.
Ciertos problemas tienen una solución más fácil y elegante cuando en su solución se emplea un
método que se llama a sí mismo.
Tema 5: Abstracción funcional. Métodos.
Por ejemplo, el cálculo del número factorial de un número entero n:
115
// Definición recursiva del método factorial.
public long factorial(long numero){
if (numero == 0){
return 1;
}
else {
return numero * factorial(numero-1);
}
}
Otro ejemplo típico es el cálculo de la serie de Fibonacci. La serie de Fibonacci comienza en 0 y 1 y
tiene la propiedad de que cada número de Fibonacci es la suma de los dos números de Fibonacci
previos. Cada llamada recursiva particiona el problema de tamaño n en dos de tamaño menor que
convergen al caso base pues n-1, n-2, n-3, ... convergen a 1 y a 0.
Casos base:
fibonacci(0) = 0
fibonacci(1) = 1
División del problema:
fibonacci(n) = fibonacci(n-1) + fibonacci(n-2)
long fibonacci(long n){
if(n == 0 || n == 1){
return n;
}
else {
return fibonacci(n-1) + fibonacci(n-2);
}
}
Tanto la recursión como la iteración hacen uso de estructuras de control: la recursión usa el
condicional y la iteración usa un bucle. Ambas requieren una condición de terminación y ambas se
aproximan gradualmente a su din: la iteración conforme se acerca al cumplimiento de una
condición y la recursión conforme se divide el problema en otros más pequeños. Ambas pueden
tener (por error) una ejecución potencialmente infinita.
Tema 5: Abstracción funcional. Métodos.
116
La solución recursiva se emplea cuando refleja de manera natural la solución a un problema. Sin
embargo, si existe versión recursiva entonces existe versión iterativa equivalente. La recursión
requiere más memoria que la iteración. Por ejemplo:
−
−
Calcular fibonacci(20) requiere 21.891 llamadas.
Calcular fibonacci(30) requiere 2.692.537 llamadas.
Cada vez que se invoca un método se guardan en la pila las variables locales de la función, copias
locales de los parámetros y la dirección de retorno.
Librerías de métodos. Paquetes.
Librerías software y paquetes Java.
Una librería software es una colección de funciones o de clases que se usan para desarrollar
software. Las librerías contienen código y datos que proporcionan servicios a diferentes programas,
permitiendo la construcción modular de las aplicaciones. Las librerías que acompañan a un
lenguaje de programación constituyen APIs (Application Programming Interfaces). La cantidad y
calidad de las APIs que acompañan a un lenguaje de programación son un elemento clave para
determinar su éxito o su fracaso.
Las librerías hacen referencia a otras librerías mediante enlaces (links) que son usados por un
programa llamado enlazador (linker o binder) para ensamblar los programas. Según como se realice
el ensamblado de los elementos de las librerías, éstas pueden ser estáticas o dinámicas. Los
elementos de las librerías estáticas se enlazan antes de la carga y ejecución el programa, los
elementos de las librerías dinámicas se enlazan durante la carga o incluso durante la ejecución del
programa. Ejemplos de librerías dinámicas son las DLL (dynamic link libraries) de Windows y las
DSO (dynamic shared object) de Unix 1.
En lenguajes orientados a objetos la unidad de abstracción no es la función, sino la clase y por tanto
las librerías contienen fundamentalmente clases. En Java se proporcionan bibliotecas de clases e
interfaces, cada una de las cuales expone o define un conjunto de métodos. Existen lenguajes mixtos
(C++) en los que se proporcionan bibliotecas de funciones y de clases. En Java las librerías se
denominan paquetes. Los paquetes (packages) definen unidades de software que se pueden
distribuir independientemente y combinar con otros paquetes para formar aplicaciones. Los
paquetes pueden contener:
−
−
−
−
Clases,
Interfaces
Otros paquetes.
Archivos con recursos adicionales (p.e: ficheros de imágenes)
Los paquetes permiten crear agrupaciones de clases e interfaces relacionadas, crean espacios de
nombres que evitan conflictos de nombrado (p.e.. dos clases pueden recibir el mismo nombre
siempre que se ubiquen en paquetes distintos) y proporcionan un dominio de protección para el
desarrollo de aplicaciones (desde un paquete sólo se puede acceder a los elementos públicos de
otro paquete).
1
Más información en http://slurp.doc.ic.ac.uk/pubs/observing/linking.html.
Tema 5: Abstracción funcional. Métodos.
117
Importación de clases y paquetes.
Si queremos utilizar una clase definida en un paquete necesitamos, o bien importarla:
import java.applet.Applet;
O bien calificar el nombre de la clase inluyendo el nombre del paquete en que se encuentra:
java.applet.Applet myApp;
La API de Java 1 provee una gran cantidad de clases. Antes de lanzarse a implementar una clase, es
conveniente inspeccionar si existe ya una clase en la API que provea los servicios que necesitamos
(don’t reinvent the wheel). Para importar todos los elementos de un paquete utilizamos el “*”.
import java.applet.*;
La sentencia import no implica que el compilador lea toda la información relativa al paquete. Sólo
le indica dónde puede mirar si no encuentra en su alcance algún elemento declarado. Sólo los
elementos declarados como públicos (public) son visibles mediante la importación de los
paquetes. Aquellos que no lleven etiqueta de acceso sólo son visibles dentro del propio paquete en
el que se declaran 2. Las clases e interfaces que pertenecen al paquete java.lang se disponen por
defecto, por lo que no es necesario importarlas.
Cuando importamos un paquete obtenemos acceso a las clases e interfaces públicas que dicho
paquete contiene directamente, pero no a las que contienen sus subpaquetes. El anidamiento de
paquetes no ofrece más funcionalidad a quienes importan los paquetes. Es únicamente un
agrupamiento lógico.
Creación de un paquete.
Para crear un paquete lo único que hay que hacer es poner en el mismo una clase o interface. Para
ello basta con poner la sentencia package al inicio del fichero fuente que incluya dicha clase o
interfaz.
// Fichero Circulo.java
package Graficos;
sentencia
public class Circulo{
Graficos.
// Esta sentencia debe ser la primera
}
// La clase Circulo pertenece al paquete
Lo que llamamos API de java es en realidad un conjunto de APIs cada una de ellas formada por un conjunto de paquetes con
un propósito determinado (construcción de interfaces gráficas, software de comunicaciones, entrada y salida, estructuras de
datos, etc).
2 En la jerga informática se dice que los elementos definidos en un mismo paquete son amigos y dejan que otros elementos
del mismo paquete accedan a sus servicios.
1
Tema 5: Abstracción funcional. Métodos.
118
Dentro de un fichero fuente puede haber varias clases o interfaces, pero sólo una puede ser pública
y el nombre del fichero fuente debe coincidir con el nombre de la clase o interfaz pública que
contiene.
Aunque la especificación de Java no dice nada al respecto, en la mayor parte de los sistemas la
estructura de los paquetes debe corresponderse con la estructura de directorios en la que se
guardan los ficheros fuente:
En la tabla de abajo se enumeran algunos de los paquetes más representativos de la API de Java y
en la de más abajo algunos de los métodos de la clase Math ubicada en el paquete java.lang
Tema 5: Abstracción funcional. Métodos.
119
En Java, las librerías de paquetes se distribuyen en ficheros JAR (Java ARchive). Las clases,
interfaces y recursos que constituyen un paquetes se comprimen y agrupan en un fichero JAR del
que después puede extraerse usando el comando jar del JDK. Los archivos JAR llevan asociados
meta-datos, que explican y validan el contenido del fichero JAR 1.
1
Más información en http://download.oracle.com/javase/tutorial/deployment/jar/
Tema 5: Abstracción funcional. Métodos.
Bibliografía
121
[Prieto 06]
Introducción a la Informática, Prieto Espinosa Alberto et al, ISBN 84-481-46247, McGraw Hill,2006
[Louden 03]
“Informática Aplicada. Programación en lenguaje C”, Pedro Alcover,
“Lenguajes de Programación. Principios y Práctica”, Kenneth C. Louden,
ISBN 970-686-284-6, Thomson, 2003
[Eckel 02]
“Piensa en Java”, Bruce Eckel, ISBN 84-205-3192-8, Prentice Hall, 2002
[Deitel 08]
Harvey M. Deitel (Universidad Nova Deitel y Asociados) , Paul J. Deitel
(Universidad Nova Deitel y Asociados), 9789702611905, 7ª edición
Prentice Hall
[Alcover 09]