Java para Aprobar

Anuncio
por
David Muñoz Díaz
Este documento está escrito para ayudar a quien lo necesite. Puedes hacer con él lo que quieras,
pero siempre de forma gratuita y desinteresada. Además, es demasiado malo como para venderlo. Se
reirían de ti :)
Si quieres aportar algo a este documento, no dudes en ponerte en contacto con el Grupo de
Usuarios de Linux de la Universidad Carlos III de Madrid ([email protected]).
Contenido
Aquí tienes la lista de contenidos del texto. Los apartados marcados con * son sólo orientativos y no están
ampliamente explicados aquí, ya que no forman parte del objetivo de este texto. Puedes encontrar una
explicación exhaustiva de estos apartados en cualquier libro de Java, además de las explicaciones que te
dará el profesor de la asignatura. Bueno, en algunas ocasiones, que debería dar.
Prefacio* (que no, que es coña)
Introducción. Cultureta.
pelín de historia
Platón y la OOP
el problema...
... y la solución
concepto de objeto
propiedades
métodos
concepto de clase
Java
¿por qué Java?
caracteríaticas generales de Java *
facilidad (je je jeeee)
lenguaje interpretado
tamaño de los programas
orientado a Internet
JVM
Cómo usar el compilador y la JVM (javac, java)
El movimiento se demuestra andando
definción de una mesa
propiedades
tipos básicos de datos *
métodos
valor de retorno
parámetros
creación y uso de objetos
creación
primero: las referencias
segundo: new
uso
el operador punto
más sobre creación: constructores
¿qué son?
peculiaridades
definición y uso
breve iniciación al polimorfismo *
métodos get/set
¿Por qué?
uso
Variables de clase. Modificador Static
el problema...
... y la solución
uso de static
en propiedades
en métodos
constantes
El método main, arrays y ¡a lanzar programas!
por qué el método main
definición del método main
argumentos
un inciso: arrays
sacabó el inciso
nuestro primer programa
Programando a lo bestia: estructuras de control
Sentencias condicionales
sentencia if
expresión lógica
bloque de sentencias
comparadores de magnitud
AND, OR, NOT
comparadores de cadenas
ifs anidados
if sin elses: CUIDADO!
solución rara
solución más rara
sentencia switch
uso
break
comerte el coco 4
Sentencias repetitivas
definición
sentencia while
regla general para el uso de bucles
variación do - while
sentencia for
¿While o for?
Recursividad
definición
metodología: la cola del cine
partes de un método recursivo
los factoriales
Herencia
para qué sirve
cómo se usa: extends
redefinición de métodos
uso de super()
una nota bastante importante
Casting
preludio al casting: las referencias
la señora referencia
cómo funciona
comparación de referencias
Arreglando bicicletas: parámetros que son referencias
casting
¿Por qué?
cómo se usa
resumen
comerte el coco 6
upcasting, downcasting, Y siguiendo con el inglés, explicit and implicit cast
upcasting
downcasting
casting implícito
casting explícito
La Clase Object
definición
contenido *
¿para qué ese contenido?
la gran utilidad de la clase Object: referencias de tipo Object
Interfaces
definición
uso básico de interfaces
uso avanzado de interfaces: referencias de tipo de interfaces
¿herencia?¿Interfaces?¿comorl?
Epílogo
El contenido de este texto es el estrictamente necesario para poder abordar
cualquier problema de Programación. Este texto es, por tanto, la base necesaria para
poder enfrentarnos a las asignaturas de Programación y Laboratorio de Programación.
En este texto no vamos a ver determinados temas, como Applets, paquetes, pruebas de
programas, etc. Para cualquier consulta sobre dichos temas, te recomiendo los libros de
Java de la editorial Sun o alguno de la infinidad de manuales gratuitos que hay por la
red.
Por tanto, es necesario que te conciencies de que leer este texto no implica
aprobar: implica ser capaz de aprobar. Si te sirve de algo, cuando yo llegué a primero,
sabía bastante menos de lo que hay aquí escrito.
Prefacio
No te engañes: tenemos mucha labor por delante. Pero no hay que pensar que es
ardua y temible. En absoluto. O al menos, espero que no lo sea para ti.
Programar implica siempre dos cosas:
Primero, hay que comerse el coco bastante, pero no para alcanzar complejos
conceptos (bueno, para esto también), sino para todo lo contrario: para conseguir poner
nuestro pensamiento a la altura de un cacho de silicio, que no parece muy listo....
Segundo, la curiosidad no mata al gato, sino que
curiosity
Skilled the cat
(por cierto, n.p.i. de de quién es esta frase. Se la he copiado a un miembro del
GUL. Así que si esta frase es suya, le pido diculpas por no pagar derechos de autor)
O sea, que siempre, repito, siempre es bueno parar de estudiar y encender el
ordenador para hacer alguna prueba de lo que estemos estudiando. Si vemos casting
será muy positivo que mandes a tomar por saco los apuntes y escribas un par de clases
para probar “el acceso a los miembros de una calse vía una referencia de tipo la
superclase”. Parece mentira, pero esta frase tiene sentido. En serio, nunca vas a
aprender a programar con un libro, siempre lo aprenderás por tu cuenta. Así que vete
haciendo a la idea de que vas a pasar muncho rato delante de la pantalla, aunque espero
que no sea demasiado tiempo. O que al menos no sea tedioso.
He intentado explicar todo este rollo usando ejemplos de la vida real. Tal vez
quede “un poco” asquerosamente pedante explicar las clases metiendo a Platón por
medio, pero creo que haciéndolo vamos a entender los conceptos más fácilmente, y
sobre todo, no se van a olvidar. Por supuesto, no creas que todo es Platón, también
hablaremos de tu vecina, de la mesa Margarita, de “páginas gué”, la bicicleta Anacleta,
y de mil cosas más. Bueno, mil... lo que se dice mil....
Por supuesto, todo ello respaldado por el pez Otilio, el pato Onorato y el pájaro
que no sé cómo demonios se llama, que no son más que excusas para apartar un poco la
atención y hacer la lectura un pelín más agradable.
Espero haber logrado, aunque sólo sea en pequeña parte, estos objetivos. Ahora,
como dice la historia de la sopa de piedra, debes poner tú de tu parte. ¡Ánimo!
Introducción. Cultureta.
Normalmente, para empezar se opta por definir qué es programar, cómo se lleva
a cabo, conceptos como “software”, “hardware”,
“memoria”, etc etc etc. Vamos a ignorar todo
este rollo porque realmente no importa
en absoluto. Nosotros, por ahora, no
nos vamos a preocupar de la
Yo soy un programador
capacidad de la memoria o
de la leche.
de la velocidad del procesador. A nosotros nos interesa programar.
Cuando aprendemos
a programar siempre se nos
plantea si
debemos
compaginar la programación con el lenguaje
de programación. Parece una tontería,
pero podemos aprender OOP
sin saber ningún lenguaje. Es cierto: podemos
saber sobre clases, objetos, herencia, casting, etc sin saber una maldita palabra de
ningún lenguaje. Sin embargo, siempre es positivo compaginar nuestro aprendizaje de
programación con el de un lenguaje propicio para ello.
Además de la OOP, y a modo de curiosidad (cultureta), existen otros tipos de
programación, cada uno con un lenguaje de programación típico. Esta parte, si quieres,
te la puedes saltar.
La programación “lineal” tiene como fundador el antiguo BASIC. A lo largo de
los años, el BASIC ha ido cambiando hasta que hoy en día existen compiladores de
BASIC orientado a objetos (Visual Basic, por ejemplo, mantiene una filosofía de OOP).
La programación lineal se daba con los primeros intérpretes de BASIC. Aquí hay un
ejemplo de este lenguaje:
10
20
30
40
INPUT “¿Cómo te llamas?”, NOMBRE$
SALUDO$ = “¡Hola “+NOMBRE$+” ¡”
PRINT SALUDO$
END
¿Ves? El programa empieza en un punto, y acaba en otro. Esto, claro, tiene sus
inconvenientes: no podemos hacer repeticiones, ni nada de eso. Nacieron las “sentencias
condicionales”:
10
20
30
40
50
60
70
80
INPUT “¿Cómo estás?”,COMO$
IF COMO$=”bien” GOTO 50
IF COMO$=”mal” GOTO 70
GOTO 10
PRINT “Vaya, me alegro.”
GOTO 80
PRINT “Alégrate!”
END
El siguiente paso es la programación procedimental: consiste en dividir el código en
fragmentos independientes. Lenguaje típico, el Pascal.
var
cad:String;
procedure debuti;
begin
Writeln('me alegro');
end;
procedure notandebuti;
begin
Writeln('alegrate!');
end;
begin
Writeln('¿Cómo estás?');
readln(cad);
if (cad='bien') then debuti else notandebuti;
end.
Hay muchos más tipos de programación, y también muchos otros lenguajes muy
raros, Smalltalk, ADA, Prolog.... En realidad, hoy en día muchos de estos lenguajes se
usan con fines didácticos o experimentales.
A nosotros nos interesa la “Programación Orientada a Objetos” (OOP, de
Object-Oriented Programming). Se basa en “encapsular” fragmentos de código en
“burbujas” independientes entre sí. Los códigos mostrados antes no tienen sentido en
OOP. Vamos a ver por qué.
Platón y la OOP
La pregunta es las siguiente: Tenemos una mesa en la cocina, una en el salón,
una mesilla de noche y una en un taller. Estas mesas son diferentes entre sí, pero, sin
embargo, hay algo en ellas que hace que todas SEAN mesas. Una es cuadrada, otra
redonda, unas con tres patas y otras con cuatro. ¿Por qué, si son tan diferentes, son todas
mesas?
Este hombre de aquí dijo que había un mundo aparte del
nuestro en el que existían unos “seres” perfectos, inmutables,
universales, etc etc etc, a quienes llamó Ideas. Los seres de nuestro
mundo físico serían “copias” imperfectas de estas Ideas. Imagina por
tanto que todos los objetos que tenemos alrededor son copias de las
Ideas. De esta forma, al ser copias imperfectas, no serían iguales
entre sí, pero seguirían teniendo esa “esencia común”.
Esta teoría sirve para explicar la realidad y dar un poco de
cabida a la ciencia. Si la ciencia trata sobre las cosas globales, es
decir, los hechos universales, no los específicos, entonces tiene que haber algo
universal.
Bueno, pues nuestra intención es semejante: vamos a tratar de explicar, no la
realidad, sino los problemas que nos encontramos, que de una forma u otra, son parte de
la realidad. ¿o no?
En OOP definimos un concepto más que fundamental: el concepto de objeto.
Un objeto es un conjunto de:
a) Datos
b) Métodos
Supongamos dos objetos semejantes, por ejemplo, dos mesas. Datos que pueden
concernir a las mesas, por ejemplo, son su color, número de patas, su forma, etc. Así, si
tenemos dos “objetos mesa”, cada uno con datos diferentes, tenemos dos mesas
diferentes, que es lo que planteábamos al principio: dos objetos de la misma naturaleza
(“mesa”), pero diferentes entre sí. ¡A Platón esto le habría encantado!
Debido a que los datos de un objeto definen en parte este objeto (lo diferencian de
los demás), a veces llamamos a estos datos “Propiedades”.
¿Se te ocurre algún método propio de una mesa? A mí no, las mesas son objetos
puramente pasivos, ellas mismas no hacen nada. Sin embargo, vamos a suponer que el
cambio de color es una acción propia de la mesa (y no del pintor).
De acuerdo, ya tenemos el diseño de nuestra mesa. Ya conocemos el concepto de
objeto. Vamos a seguir con el señor Platón.
¿Cómo un carpintero puede crear una mesa? Según este griego, su alma, antes de
nacer, vislumbró las Ideas, y durante la vida de la persona, el alma recuerda algunas de
estas Ideas (aunque no todas....) Así, un carpintero recuerda haber visto la “Idea de
Mesa”, y por ello sabe hacer mesas. Es decir, el carpintero se FIJA en la “Idea de Mesa”
para crear una mesa.
Si ya conocemos lo que es un Objeto, ¿Qué es una Idea? Pues, sencillamente, la
definición de ese objeto, que, por cierto, nosotros llamamos clases. Platón dice que los
objetos físicos son copias de las Ideas. Nosotros decimos que los objetos con instancias
de las clases.
Para Platón, los objetos son copias de unos
seres universales llamados Ideas. Las Ideas
son, por tanto, la definición de los objetos
físicos, aunque éstos pueden ser diferentes entre
sí. Para la OOP, los Objetos son instancias de
unas definiciones generales que llamamos Clases.
Dos objetos con propiedades diferentes
(dos mesas con diferente color)
siguen siendo instancias de la misma
clase (la “clase mesa”).
Éstos son los
conceptos fundamentales de la
OOP. Es estrictamente necesario comprenderlos a fondo antes de seguir adelante. Párate
un momento a pensar si entiendes a fondo...
OBJETO
CLASE
PROPIEDADES
MÉTODOS
INSTANCIA
¡Cuidado!
Si decimos que dos objetos de la misma clase tienen las mismas propiedades, tal
vez lo que realmente queramos decir es que dos objetos de la misma clase tienen
EL MISMO VALOR PARA TODAS Y CADA UNA.DE SUS PROPIEDADES.
Es evidente que dos objetos de la misma clase van a tener las mismas
propiedades: dos mesas tendrán color, forma, número de patas, etc. Pero tal vez
digamos que dos mesas tienen las mismas propiedades cuando queremos decir
que ambas mesas tienen la misma forma, el mismo color y el mismo número de
patas. Hay que tener cuidado con esto, aunque tampoco es tan importante.
Para comerse el coco.... (1)
Tenemos dos objetos de la misma clase, con exactamente las mismas
propiedades ¿Son el mismo objeto?
Java
Java es un lenguaje de programación. La verdad es que no va a recibir ningún
premio por este simple hecho. Sin embargo, sí va a recibir cierta atención por nuestra
parte, porque, sencillamente, es la herramienta que nosotros vamos a utilizar. Que quede
muy claro, Java es un MEDIO para programar en OOP. Podemos programar en OOP
con otros muchos lenguajes, Delphi, Visual Basic, o C++.
Java nació con una intención muy clara: hacer la vida más fácil al programador.
C++ es el lenguaje más potente que existe ahora mismo. Pero tiene un grave problema:
es lo más complicado que hay. Un ejemplo:
#include <owl.h>
// Define la clase derivada de TApplication
class THelloApp : public TApplication
{
public:
THelloApp(LPSTR AName,
HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPSTR lpCmdLine,
int nCmdShow)
: TApplication(AName,
hInstance,
hPrevInstance,
lpCmdLine,
nCmdShow) {};
virtual void InitMainWindow();
};
// el MainWindow de la clase
void THelloApp::InitMainWindow()
{
MainWindow = new TWindow(NULL, "Hola Mundo");
}
int PASCAL WinMain(HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPSTR lpCmdLine,
int nCmdShow)
{
THelloApp HelloApp ("HelloApp",
hInstance,
hPrevInstance,
lpCmdLine,
nCmdShow);
HelloApp.Run();
return HelloApp.Status;
}
Bueno, pues este fragmento de código abre una ventana con el título “Hola
Mundo!”.
Visto lo visto, Java trata de hacer la vida un poco más fácil. Además, un
programa en Java ocupa bastante poco espacio en disco, mientras que uno en C++ ocupa
todo lo que quieras y un poco más. Así que, gracias a eso, Java es también propicio para
aplicaciones que corran por Internet y esas cosas. Pero todo eso ya lo veremos.
Java NO lo entiende directamente el procesador. Java es un lenguaje
INTERPRETADO, es decir, que se “traduce” para que el procesador pueda entenderlo.
C++, una vez compilado, es entendido perfectamente por el procesador, mientras que
Java no. Java se compila y se generan los “bytecodes”, que es otro lenguaje mucho más
sencillo, pero el procesador sigue sin entenderlo. Para ello, los bytecodes tienen que
INTERPRETARSE por la “Java Virtual Machine” (JVM), que, en realidad, es otro
programa (escrito, por cierto, en C++) que sabe interpretar los bytecodes. Bueno, esto es
un poco extraño, pero en realidad es muy fácil. Al escribir un programa en C++, por
ejemplo, se compila y funciona solito. Pero al escribirlo en Java, se compila y necesita
otro programa que lo haga funcionar. Este otro programa se llama JVM. Ya está. No es
tan mortal, ¿no?
¿Ventajas de Java sobre los lenguajes no interpretados? muchas, por ejemplo,
seguridad: Si un programa en Java intenta hacernos la puñeta, la JVM lo detendrá,
mientras que el procesador no podrá detener nunca un virus escrito en C++. Otra ventaja
es el ya mencionado reducido tamaño de los programas en Java. Otra, mucho más
importante, un programa en Java funciona en Windows, en Linux, en Mac, en Solaris,
etc, porque lo que cambia es la JVM.
Aquí no vamos a extendernos en el funcionamiento de los compiladores de Java
y las JVM. Sin embargo, vamos a recordar un poco el proceso.
1.- Creamos el archivo “xxxx.java”
2.- lo compilamos con “javac xxxx.java”
3.- lo ejecutamos con “java xxxx”
El paso 3 es el que invoca a la JVM. Más explicaciones, ayuda en línea.
Antes de seguir adelante, vamos a pararnos un poco para recapacitar sobre si has
entendido los conceptos de:
LENGUAJE
LENGUAJE INTERPRETADO
Y COMPILADO
COMPILADOR
JVM
BYTECODES
Empiezo a plantearme
esto de programar....
Para comerse el coco......(2)
Teniendo en cuenta que la JVM es
un programa escrito en un lenguaje
compilado, ¿puede ser entendido por la propia JVM?
El movimiento se demuestra andando.
Vamos a escribir un pequeño programa en Java. ¿Su fin? definir las mesas que
tanto tiempo nos han acompañado. Recordemos la CLASE MESA:
Propiedades: color, número de patas, forma
Métodos: cambiar el color
Bueno, vamos a pensar un poco. Como sabrás, los nombres o
IDENTIFICADORES en Java son una cadena de caracteres que almacenan un valor
determinado, o el nombre de un método, o el nombre de una clase, o yo qué sé.
Tenemos que tratar de inventarnos identificadores con nombres sencillos, que no sean
rebuscados, sin caracteres raros. A mí se me ocurren los siguientes:
para el color, pues “color”
para el número de patas, “numPatas”
para la forma, pues “forma”
para el método que cambia el color, pues “cambiarColor”
Definición de propiedades
Vamos a centrarnos en las propiedades, y luego veremos los métodos.
Seguimos pensando. El color será una cadena de caracteres (“marrón”, “violeta”,
joer, una mesa violeta...), que en Java se denomina “String”, al igual que la forma
(“Redonda”, ”cuadrada”). El número de patas será un número entero, que se llama “int”.
La definición de la clase Mesa.java empezaría así:
class Mesa {
String color;
int numPatas;
String forma;
.
.
.
Esto ya tenemos que ser capaces de entenderlo bien. Una mesa tiene un color, un
número de patas y una forma. El número de patas es un número entero, mientras que la
forma y el color son cadenas de caracteres. Bueno, pues así se han definido. Ahora no
ha lugar a pensar cómo se define el color de una mesa determinada, o su forma, o cómo
saber qué color tiene otra mesa cualquiera..... todo eso ya lo veremos. Ahora estamos
DEFINIENDO la clase Mesa, no hacemos nada más.
En cualquier libro encontrarás los tipos básicos de variables y sus rangos de
valores. Tendrás que aprendértelos, aunque sólo para el examen. Luego, puedes
olvidarlos. Básicamente son
int, short, long, double, float, char,
String y boolean.
Este es un buen
momento para
aprenderte los tipos
de datos básicos.
Definición de métodos
¿ya te los has aprendido?
¡Bien! Vamos a por
el método
cambiarColor.
Un método
consta, aparte de su nombre
(o IDENTIFICADOR), claro está, de
a) valor de retorno.
b) parámetros
Un método puede devolver un valor, o puede no hacerlo. Por ejemplo, un
método que calcule la suma de dos números debería devolver otro número, o un método
que abra un fichero del disco debe devolver si ha podido abrirlo o no. Ahora bien, hay
métodos que no tienen por qué devolver nada. Por ejemplo, un método que muestre un
saludo en pantalla no tiene por qué devolver nada.
A la hora de definir el tipo de dato que devuelve un método, se antepone al
nombre de ese tipo de dato (int, float, boolean, String....) al nombre del método. Por
ejemplo:
int calculaSuma()
float raizCuadrada()
boolean abrirArchivo()
// devuelve un entero
// devuelve un real
// devuelve un valor lógico
Los métodos que no devuelven nada se declaran como void:
void saludo() // no devuelve nada
Bien. Vayamos ahora a por los parámetros.
Un método que calcule 5*4 está claro que no va a necesitar parámetros, porque el
método no puede hacer nada más que calcular 5*4. Sin embargo, un método que calcule
la multiplicación de dos enteros cualquiera necesitará conocer esos enteros, es decir, se
los daremos como parámetro. Es como si queremos saber dónde está determinada calle,
y le preguntamos a alguien: tenemos que decirle qué calle buscamos. Imagínate,
“Disculpe, ¿podría decirme dónde está la calle?” No tiene sentido, habría que preguntar
“Disculpe (educación ante todo), ¿podría decirme dónde está la calle Butarque?”.
Bueno, pues como éste se te pueden ocurrir mil ejemplos
Es
MUY IMPORTANTE
que diferencies cuándo
un método necesita
parámetros y cuándo no.
Una cosa sí es cierta: no hay una regla general para saber si un método necesita
parámetros, para saberlo necesitamos la mplementación del código, y el sentido común.
Vamos a ver unos ejemplos:
int suma(int a, int b)
recibe un entero a y un entero b y devuelve otro entero
String concatenar(String a, String b)
recibe dos cadenas de caracteres y devuelve otra cadena de caracteres
float raizCuadrada(int a)
recibe un entero y devuelve un real
Debe quedar MUY MUY MUY claro este rollo de los parámetros y del tipo de
dato devuelto por un método. Vamos a ver los métodos anteriores implementados y
comentados para que se vea todo mucho mejor, y ya de paso aprendemos cómo hace un
método para devolver un valor usando “return”:
int suma(int a, int b){
return a+b;
}
Este método recibe dos enteros, a y b, y devuelve su suma, que será otro entero.
Si hacemos en cualquier parte del programa
int numero = suma(3,5);
estamos diciendo que el entero “numero” va a valer lo que devuelva el método
“suma” pasándole como parámetros 3 y 5. “suma” devolverá 3+5, y entonces “numero”
pasará a valer 3+5.
String concatenar(String a, String b){
return a+“ ”+b;
// las comillas encierran un espacio en blanco
}
Recibe dos cadenas y las concatena poniendo un espacio en blanco entre medias.
Fíjate en la diferencia entre usar “+” con números y usarlo con cadenas. Si en cualquier
punto del programa escribimos
String cad = concatenar(“Hola”,”Mundo”);
estamos diciendo que la cadena (String) “cad” va a valer el resultado que
“concatenar” devuelva pasándole como argumentos las cadenas “Hola” y “Mundo”;
Como “concatenar” coge las dos cadenas que hemos pasado como parámetros (“Hola” y
“Mundo”) y las devuelve juntas con un espacio entre medias (devuelve “Hola Mundo”),
la cadena “cad” pasará a valer “Hola Mundo”
void saludo(){
System.out.println(“Holas!”);
}
este método no tiene parámetros, y no devuelve nada de nada. Así, se le invocará
en cualquier parte del programa:
saludo();
y ya está. Como en el método no hay definidos parámetros, pues no le pasamos
parámetros, y como el método no devuelve nada, pues es ilícito escribir algo como:
int resultado = saludo();
// mal!!!!!
porque “saludo()” no devuelve nada (recuerda: se ha definido como “void” y no
se ha escrito ningún “return”), y como no devuelve nada, Java no sabe qué valor meter
en “resultado”.
Bueno, pues este es el funcionamiento de un método. A modo de resumen,
cuando un método es invocado, puede necesitar o no parámetros. Que los necesite o no
depende de cómo hemos definido dicho método, es decir, si hemos definido variables
entre los paréntesis del método:
... metodo(parametro1, parametro2, ....)
int suma(int a, int b)
o si no las hemos definido:
por ejemplo:
... metodo() por ejemplo:
void saludo()
Un método puede devolver un valor, o bien puede no devolver nada. Depende de cómo
hemos definido el método: si lo definimos como “void”:
void saludo()
entonces no devuelve nada, pero si lo definimos de otra forma:
int suma(....)
entonces devolverá un dato de un tipo determinado (int, float...), y será OBLIGATORIO
escribir un “return” en el cuerpo del método.
int suma(int a, int b){
int resultado = a+b;
}
Este método es casi igual al que hemos visto poco más arriba, peeeeeeeeeero tiene un
grave problema: hemos dicho que devuelve un entero ( INT suma(int a, int b) ), pero no
hay ningún “return” en el cuerpo del método. Habrá un error de compilación!
Una cosa más, que un método devuelva algún valor NO IMPLICA
(curiosamente) que haya alguna variable que reeciba dicho valor. Es decir, las siguientes
líneas son perfectamente legales:
int resultado = suma(5,3);
.
.
.
suma(5,3);
La única pega es que la segunda línea llama al método “suma”, le pasa sus
parámetros pertinentes, pero nadie recibe el resultado. Esto, parece una chorrada, pero
es útil. Supongamos que tenemos un método:
boolean abrirFichero(String nombreFichero)
cuya tarea es abrir un fichero determinado. Este método devuelve un boolean
que indica si se ha abierto el fichero con éxito. Bien, puede que en algún momento
hagamos:
boolean hemosPodido = abrirFichero(“miFichero.txt”);
para saber si hemos podido abrir el fichero. Esto sería lo más normal, pero en algunas
ocasiones no necesitamos saber si se ha abierto exitosamente, porque sabemos que va a
ser así, por ejemplo, al hacer algún programilla tonto en nuestra casa. Si no necesitamos
asegurarnos de si el fichero se ha abierto o no, entonces podemos hacer
abrirFichero(“miFichero.txt”);
e ignoramos el valor que abrirFichero devuelve.
Bueno, ¡pues esto es todo sobre los métodos! Antes de seguir adelante, como
siempre, asegúrate de haber entendido bien los conceptos de:
PROPIEDAD
MÉTODO
TIPO DE DATOS (int, float....)
PARÁMETROS
VALOR DE RETORNO DE UN MÉTODO
SENTENCIA RETURN
VOID
Recuerda siempre que un valor de retorno puede ser ignorado
Ejercicio
Una vez hayas entendido perfectamente todo este rollo, puedes escribir la
definición completa de la clase Mesa. Plantéate qué parámetros le pasaríamos al método
cambiarColor() y qué valor devolvería. ¡Vamos!
Solución
A mí se me ocurre que cambiarColor() tendría como parámetros una única
String, y no devolvería nada. El resto de la clase ya lo hemos definido.
class Mesa {
String color;
int numPatas;
String forma;
void cambiarColor(String nuevoColor){
color = nuevoColor;
}
Empieza lo bueno...
}
Creación y uso de objetos
Ya tenemos definida la clase Mesa. Ahora quiero crear una mesa. ¿Cómo se hace
eso?...
primero: las referencias
Cuando creemos un objeto, la JVM genera una “burbujita” que es nuestro objeto.
Claro, que para que ese objeto haga cosas necesitamos “llamarlo” de alguna forma. Si
quieres que, en medio de una fiesta atestada de gente tu amigo Miguel te diga, por
ejemplo, la hora, como digas “¡Dime la hora!”, no te van a hacer ni caso, porque cada
uno pensará (si te ha oído) que no le estás llamando a él. Tendrás que decir “¡Miguel,
dime la hora!”, para que el señor Miguel te haga caso. En definitiva, que si vas dando
gritos por la calle ni Dios te va a hacer caso, mientras que si a cada frase le antepones el
nombre de la persona a la que imperas, pues ya te puede atender.
En Java esto es exactamente igual, a cada objeto se le debe dar un nombre para
poder “llamarle” de alguna forma cuando le necesites. Bien, pues a estos nombres se les
denomina Referencias. Si yo creo dos mesas y quiero cambiar el color de una sóla,
¿Cómo le digo a la JVM a qué mesa quiero cambiar el color? Pues fácil, si creo dos
mesas, a una la llamo “Margarita” y a la otra “Alberto”, y luego digo que cambie el
color de Margarita.
segundo: el operador “new”
“new“significa “nuevo”. Y este es un ejemplo de otra cosa que por este simple
hecho no va a recibir ningún premio. “new” sirve para
a) crear un nuevo objeto
b) definir la REFERENCIA a ese nuevo objeto.
¿Cómo se usa? Pues muy fácil:
CLASE REFERENCIA = new CLASE();
por ejemplo:
Mesa Margarita = new Mesa();
Mesa Alberto = new Mesa();
por ahora, olvida los
paréntesis de “new CLASE()”
¿Por qué esos paréntesis
en “new Mesa()”? Por ahora,
haz caso al pez.
Pues ya sabemos definir objetos e incluso crearlos! Ahora, vamos a aprender a
utilizarlos.
Supongamos que quiero crear dos mesas, Margarita y Alberto, y quiero cambiar
el color de la mesa Margarita. Tendré que llamar al método cambiarColor() de la mesa
Margarita. ¿Cómo se hace esto? Pues usando algo tan tonto y tan simple como un punto:
Mesa Margarita = new Mesa();
Mesa Alberto = new Mesa();
Margarita.cambiarColor(“Rojo”);
¿Quiero cambiar el color de la mesa Alberto?
Alberto.cambiarColor(“Azul”);
Ya está. Hala. ¿Parecía complicado todo esto? Pues ya ves qué complicación
más grande. Ya sabemos definir objetos, crearlos y usar sus métodos. Para que te
convenzas, aquí tienes un cachito de mi práctica de Junio:
log.write("
log.write("
log.write("
log.write("
tipoReserva
horaInicio
horaFin
fecha
=
=
=
=
"+r.getTipoReserva());
"+r.getHoraInicio());
"+r.getHoraFin());
"+r.getStringFecha();
Sabiendo que “log” y “r” son objetos, ¿Hay algo complicado? ¿Hay algo que no
entiendas? ¡Claro que no!
Constructores
A primera vista, parece que “new” es un CONSTRUCTOR, porque su misión es
construir nuevos objetos. Bueno, pues nooooop. “new” INVOCA a un constructor, NO
ES un constructor. Bueno, no es tan difícil. “new” invoca a un constructor. Vale. ¿Y
qué es un constructor?
Un constructor es un método especial, propio de cada clase. Su función es
INICIALIZAR el contenido, las propiedades, el estado, etc etc del objeto. Podemos
hacer que cuando creemos una mesa cualquiera, comience siendo cuadrada de cuatro
patas y azul. Pero eso lo veremos luego.
¿Por qué el constructor es especial? Por varias razones:
Primero: se invoca UNA VEZ para cada objeto. Los métodos de un objeto se
pueden llamar todas las veces que quieras:
Margarita.cambiarColor(“rojo”); // y acto seguido....
Margarita.cambiarColor(“azul”); // y otra vez...
Margarita.cambiarColor(“verde”);
Sin embargo, el constructor va a ser invocado una sóla vez, cuando lo diga
“new”.
Segundo: nunca devolverá ningún valor
Tercero: sin embargo, no se define como “void”. Recuerda que cuando un
método no devuelve ningún valor, se define como “void”. Bueno, pues el
constructor no.
Cuarto: su nombre es el mismo nombre de la clase a la que pertenece. No
puedes escoger el nombre del constructor.
¿Recuerdas los paréntesis a los que se refería el pez?
Mesa Margarita = new Mesa();
¿Por qué estos paréntesis? Pues está ya claro: “Mesa()” es el nombre de un
método, el CONSTRUCTOR, y por eso lleva paréntesis.
Bueno, pues una vez dicho esto, vamos a cambiar un poco lo que decíamos sobre
el uso de “new”, y vamos a cambiarlo por:
CLASE REFERENCIA = new CONSTRUCTOR();
Antes de seguir, asegúrate de entender bien los conceptos de
REFERENCIA
OPERADOR PUNTO “.”
NEW
CONSTRUCTOR
Definición de constructores
Vamos a definir un constructor para la clase Mesa. Nuestra intención, hacer que
cada mesa nueva sea cuadrada, azul y con cuatro patas. Bueno, pues es muy fácil,
añadimos a la clase el método en negrita:
class Mesa {
String color;
int numPatas;
String forma;
void cambiarColor(String nuevoColor){
color = nuevoColor;
}
RECUERDA:
el constructor tiene el
mismo nombre que su clase,
no devuelve ningún valor,
y se define sin “void”
Mesa() {
color = “azul”;
numPatas = 4;
forma = “cuadrada”;
}
}
Ahora, cuando hagamos
Mesa Margarita = new Mesa();
Margarita será una mesa cuadrada azul de cuatro patas, ¡Una mesa en
condiciones, vaya!
Pero hay un problema: yo no quiero mil mesas iguales. Yo quiero crear una mesa
como a mí me dé la gana, yo no quiero que todas las mesas sean azules y cuadradas con
cuatro patas. ¿Se te ocurre algo para solucionar esto?
¡Podemos definir un constructor con parámetros! Así, cuando creemos una mesa
le daremos su color, su número de patas y su forma iniciales. A ver, sería algo así:
Mesa(String colorInicial, int numPatasInicial, String formaInicial){
color = colorInicial;
numPatas = numPatasInicial;
forma = formaInicial;
}
Esto es genial. Ahora podemos hacer:
Mesa Margarita = new Mesa(“rojo”, 4, “redonda”);
Mesa Catalina = new Mesa(“verde”, 3, “triangular”);
Hala. ¿Qué constructor te gusta más? Podemos prescindir de uno de ellos.
Aunque, gracias a algo que llamaremos “Polimorfismo”, podemos quedarnos con los
dos constructores A LA VEZ. (¡Vaya! dos constructores diferentes!)
El Polimorfismo
permite tener varios
métodos diferentes entre sí
y con el mismo nombre.
Un buen momento para echarle
un vistazo!
Si te digo la verdad, yo pocas veces uso los constructores con parámetros (que
no significa que sean malos, ¿eh? no te creas......). En su lugar, utilizo los.....
Métodos get/set
Cuando no definimos un constructor en una clase, al invocar “new” la JVM
usará el CONSTRUCTOR POR DEFECTO, que inicializa las propiedades del objeto
con ceros, referencias nulas, etc (¡que lo de “referencias nulas” no te asuste! ya lo
veremos). Para inicializar las propiedades usamos un constructor “personalizado”,
“propio”, “característico”, o como lo quieras llamar. Estupendo.
supón que en medio del programa quiero cambiar UNA SOLA propiedad de un
objeto. En nuestro caso, puedo cambiar el color. Pero imagina que tengo una mesa roja:
Mesa Margarita = new Mesa(“rojo”, 4, “redonda”);
Y quiero hacer que sea amarilla. Bueno, pues tengo dos opciones:
primera: crear una nueva mesa, casi igual:
Mesa Margarita = new Mesa(“amarillo”, 4, “redonda”);
(Esto parece guai, pero te aseguro que no siempre va a resultar fácil)
segunda: definir una serie de métodos que cambien UNA SOLA propiedad del
objeto. A este tipo de métodos se les denomina “métodos set”, porque suelen comenzar
con la palabra “set” (“poner”):
Vamos a definir tres nuevos métodos para nuestra clase Mesa:
void setColor(String nuevoColor){
color = nuevoColor;
}
void setForma(String nuevaForma){
forma = nuevaForma;
}
void setNumPatas(int nuevoNumPatas){
numPatas = nuevoNumPatas;
}
Fíjate tú qué cosas, oche. Ahora resulta que “setColor” y “cambiarColor” hacen
exactamente lo mismo. Prescindiremos de uno de ellos. Por ejemplo, de
“cambiarColor”.
Está bien esto, ¿eh?
Bueno, tómate un respiro, y vamos a por los métodos “get”. Su función es
obtener (get) el valor de una propiedad determinada. No es difícil adivinar que todo
método “get” se definirá con algún tipo de dato de retorno (int, String...) y que además
tendrá un “return”. Normalmente los métodos “get” no tendrán parámetros. ¿Recuerdas
todo eso? Si no, ya sabes....
Vamos a definir los métodos “get” de nuestra clase Mesa. Empecemos con el
color. Llamaremos al método “getColor”. Ya que el color es una String, getColor
devolverá una String:
String getColor(){
return color;
}
De forma semejante haremos con getForma:
String getFoma(){
return forma;
}
getNumPatas devolverá un valor entero:
int getNumPatas(){
return numPatas;
}
Bueno, pues resumimos nuestra clase Mesa; tres propiedades, tres métodos “set”
para definir esas propiedades, tres métodos “get” para obtener el valor de las
propiedades, un constructor con parámeros y otro sin parámetros, y pasamos del
método “cambiarColor”. La clase Mesa está escrita en la siguiente página. Aquí no
cabe....
class Mesa {
String color;
int numPatas;
String forma;
void setColor(String nuevoColor){
color = nuevoColor;
}
void setNumPatas(int nuevoNumPatas){
numPatas = nuevoNumPatas;
}
void setForma(String nuevaForma){
forma = nuevaForma;
}
String getColor(){
return color;
}
String getForma(){
return forma;
}
int getNumPatas(){
return numPatas;
}
Mesa(String colorInicial, int numPatasInicial, String formaInicial){
color = colorInicial;
numPatas = numPatasInicial;
forma = formaInicial;
}
Mesa() {
color = “azul”;
numPatas = 4;
forma = “cuadrada”;
}
}
Variables de clase. Modificador static.
Supón que tenemos una empresa que publica páginas web en Internet para otras
compañías. Supón que, como empresa que somos, en cada página queremos anunciar
nuestros servicios, por ejemplo, poniendo nuestro número de teléfono. Así, cada vez que
alguien visitase la página de uno de nuestros clientes sabría cuál es nuestro teléfono para
así poder contratar nuestros servicios. Hasta aquí bien, ¿verdad?
Vale. Supón que tenemos una clase paginaGué con las propiedades dirección,
contenido y numeroTeléfono. Cada vez que creemos una página, le daremos un
contenido, una dirección y nuestro número de teléfono. Por supuesto
definimos en la clase todos los métodos set y get necesarios.
Creo que eres
Ahora supón que cambiamos nuestra sede y, por tanto,
totalmente capaz de
nuestro número de teléfono.
escribir la clase
paginaGué
¡Dios! ¡Hemos creado mil páginas web
y ahora tenemos que cambiar el número de
teléfono de todas! ¡Mil llamadas al método
setNumeroTeléfono!
Pues no, si hacemos que todos
los objetos de la clase paginaGué compartan
la propiedad numeroTeléfono. Así, si cambiamos
el número de teléfono de una sola página, cambiará el de todas las demás páginas. Guai.
Creo que este ejemplo es muy ilustrativo, ¿verdad? Bueno, pues pasemos a la
acción.
En Java, una propiedad que se comparte por todos los objetos de una clase se
llama “variable (propiedad) de clase” o “estática”. ¿Por qué? La primera denominación
hace referencia a que podemos entender que una variable compartida NO pertenece a los
propios objetos, sino sólo a su clase. Es como si las Ideas de Platón definiesen no sólo
las propiedades y el comportamiento de los objetos físicos, sino que además definiesen
el contenido de alguna propiedad. Si todas las mesas fuesen de madera, la propiedad
“material” de una mesa estaría definida en la Idea de mesa, no en cada mesa.
Bueno, pues es sólo una idea. Respecto al otro nombre, “variable estática”, éste
viene dado porque en Java, para definir una variable compartida se le antepone el
modificados “static”:
static int numeroTeléfono;
Hala. Pues ya está. Añadiendo un “static” antes de una propiedad hacemos que
esa propiedad sea compartida por todos los objetos.
Supón ahora que queremos saber cuántas mesas hemos fabricado. Bueno, pues
vamos a añadir una propiedad estática numMesasFabricadas a la clase Mesa.
Inicialmente, numMesasFabricadas valdrá cero, pero cada vez que un constructor sea
invocado, aumentará en una unidad. Después definiremos un método
getNumMesasFabricadas() para saber cuántas mesas llevaremos creadas. Bueno, pues
la clase Mesa quedaría definida asín (de nuevo, en una hoja aparte):
class Mesa {
String color;
int numPatas;
String forma;
static int numMesasFabricadas = 0;
void setColor(String nuevoColor){
color = nuevoColor;
}
void setNumPatas(int nuevoNumPatas){
numPatas = nuevoNumPatas;
}
void setForma(String nuevaForma){
forma = nuevaForma;
}
String getColor(){
return color;
}
String getForma(){
return forma;
}
int getNumPatas(){
return numPatas;
}
Mesa(String colorInicial, int numPatasInicial, String formaInicial){
color = colorInicial;
numPatas = numPatasInicial;
forma = formaInicial;
numMesasFabricadas = numMesasFabricadas + 1;
}
Mesa() {
color = “azul”;
numPatas = 4;
forma = “cuadrada”;
numMesasFabricadas = numMesasFabricadas + 1;
}
int getNumMesasFabricadas() {
return numMesasFabricadas;
}
}
Y ya está. Ahora, si hacemos:
Mesa Margarita = new Mesa(“Azul”, 3, “redonda”);
int num1 = Margarita.getNumMesasFabricadas();
Mesa Catalina = new Mesa(“Verde”, 4, “triangular”);
int num2 = Catalina.getNumMesasFabricadas();
int num3 = Margarita.getNumMesasFabricadas();
¿Cuánto valdrán num1, num2 y num3? Pues, respectivamente, 1, 2 y 2.
Tal vez te hagas la preguna siguiente: si podemos definir propiedades
compartidas, ¿Podemos hacer lo mismo con los métodos? ¿Tiene sentido definir un
método compartido? ¿Cómo se hace? ¿Una existencia divergente implica un universo
divergente?
Bueno, pues sí se puede definir un método estático, y sí que tiene sentido.
Además, se hace igual que con las propiedades, anteponiendo un “static” a la definición
del método. Lo de la existencia lo dejamos para otro momento.
Por ejemplo, podemos saber cuál es el número de mesas fabricadas sin necesidad
de “preguntárselo” a una mesa determinada. ¡Podemos preguntar a la propia clase Mesa!
Cambiamos el método:
static int getNumMesasFabricadas() {
return numMesasFabricadas;
}
y ahora podemos hacer:
Mesa Margarita = new Mesa(“Azul”, 3, “redonda”);
int num1 = Mesa.getNumMesasFabricadas();
Mesa Catalina = new Mesa(“Verde”, 4, “triangular”);
int num2 = Mesa.getNumMesasFabricadas();
int num3 = Mesa.getNumMesasFabricadas();
Fíjate que la llamada a getNumMesasFabricadas() la hacemos sobre Mesa y no
sobre un objeto determinado. Bueno, éste hecho no debe traerte de cabeza nunca de los
jamáses, ¡de verdad! los métodos y las variables estáticas no es que se usen demasiado,
¿eh? Además, los puristas de la OOP (que son personas) no admiten nada estático, ni
propiedades ni métodos, salvo en una excepción: las constantes.
Constantes
¡Sí! También podemos definir constantes en Java. Una constante, como
comprenderás, no sólo no puede cambiar de valor: además, debe ser compartida por
todos los objetos. Vaya una constante tan estúpida aquélla que no es la misma en todos
los objetos de la clase donde esa constante se define. En Pascal, C, C++, BASIC, etc etc
etc las constantes se definen como const (de constant). Bueno, pues en Java, como es un
lenguaje tan especialito el jodío, se definen con final.
Es decir, que si yo quiero definir una constante, por ejemplo, pi, hago:
static final int pi = 3,1415926535897932384626433832795;
Hala. Ahora pi es una variable compartida por todos los objetos de la clase en la
cual hemos definido la propia pi, y además no puede cambiar de valor. Hala.
Antes de seguir adelante, ya sabes...
VARIABLE COMPARTIDA
MODIFICADOR STATIC
MÉTODO COMPARTIDO
LLAMADAS A MÉTODOS MEDIANTE EL NOMBRE DE LA CLASE
( Mesa.getNumMesasFabricadas() )
MODIFICADOR FINAL
Para comerse el coco..... (3)
La clase Math contiene métodos que hacen cálculos numéricos, como
coseno, seno, etc..... Si miras la definición de estos métodos, verás que ninguno
se salva, todos son métodos static. ¿Por qué crees que es así?
El método main, arrays y ¡a lanzar programas!
Seguro que te has dado cuenta de que hemos definido mil clases, métodos y
propiedades, pero que realmente todavía no hemos hecho ningún programa. Bueno.
Vamos a solucionar eso.
Un programa siempre tiene un punto de arranque, es decir, empieza en un
momento determinado. Bueno, pues en Java, este punto de arranque se llama método
main.
Un programa suele constar de varios archivos, uno por cada clase que definimos.
Bueno, pues en una de estas clases debemos definir el método main. Como
comprenderás, será una buena práctica de programación definir una clase
exclusivamente para albergar el main. No es necesario, pero es muy buena práctica, es
decir, que al margen de toda nuestra fabulosa colección de clases definimos otra más
cuyo contenido sea un sólo método main. Bueno, pero vamos con el propio método.
El main se define siempre igual, por norma en Java. Es un método:
estático: ya sabes qué es esto.
público: si lo sabes, te felicito. Básicamente significa que puede accederse a este
método desde cualquier lugar, a diferencia de otros métodos que pueden
ser privados. Bueno, ya lo veremos, ¿vale?
no devuelve datos: se define como void
se llama siempre “main”
recibe como argumentos un array de Strings: también lo veremos.
main queda definido entonces como:
public static void main(String[] IDENT)
Como comprenderás, IDENT es un identificador al que puedes llamar como te
dé la gana. Normalmente recibe el nombre arg o args. ¿Para qué sirve?
Cuando ejecutamos un programa Java, lo que hacemos es escribir
java clase
y esto invoca a la JVM. Pero nosotros somos muy listos, y podemos arrancar el
programa pasándole argumentos:
java clase arg1 arg2 arg3 ... argn
Bueno, pues la JVM coge estos argumentos los mete en una lista. A este tipo de
listas se les denomina array, y funciona de la siguiente manera:
Un inciso: arrays
Un array es una lista ordenada de elementos. Cada elemento tiene asociado un
índice. El primer índice es el cero, y el último depende del número de elementos que
haya guardados en el array.
Un array, como una variable cualquiera, tiene un TIPO (int, float, String...) y un
IDENTIFICADOR. Veamos cómo se declaran:
tipo[] IDENT = new tipo[tamaño];
por ejemplo, hagamos un array de Strings de 5 posiciones:
String[] miArray = new String[5];
y usamos el array como si fuese una variable normal, pero teniendo en cuenta los
índices en los que guardamos valores:
miArray[0] = “Posición primera...”;
miArray[1] = “... segunda...”;
miArray[2] = “...tercera...”;
miArray[3] = “...cuarta...”;
miArray[4] = “y al siguiente día dejé el colegio”;
Para saber el número de posiciones de un array usamos el operador .length
¡Fíjate en el punto inicial de .length!:
int tamaño = miArray.length;
Ahora tamaño vale 5.
sacabó el inciso.
Sigamos con el main.
public static void main(String[] args)
Cuando invocamos a la JVM, ésta determina el número de argumentos que
hemos introducido en
java clase arg1 arg2 arg3 ...
y crea el array que hemos llamado args. Bueno, Podemos, por ejemplo, escribir
un programa al que le pasemos como argumentos un color y la forma, y él nos cree una
mesa con cuatro patas y con ese color y esa forma. Usando System.out.println
podemos mostrar en pantalla las propiedades de la mesa.
Meteremos el main en una nueva clase MesaMain (archivo MesaMain.java):
class MesaMain{
public static void main(String[] args) {
Mesa Margarita = new Mesa( args[0], 4, args[1] );
System.out.println(“Hemos creado una mesa”);
System.out.println(“con ”+Margarita.getNumPatas()+” patas,”);
System.out.println(“de color “+Margarita.getColor());
System.out.println(“y de forma “+Margarita.getForma());
}
}
Ahora, si compilamos Mesa y MesaMain con:
javac Mesa.java
javac MesaMain.java
y hacemos:
java MesaMain rojo, redonda
el programa nos dice:
Hemos creado una mesa
con 4 patas,
de color rojo
y de forma redonda
Hala. Nuestro primer programa. Es una pena, normalmente el primer programa
que se escribe es mostrar en pantalla el saludo “Hola mundo!”, pero alguna vez hay que
saltarse las reglas, ¿no? De todas formas eres perfectamente capaz de programar tal
saludo, ¿no? Haz una clase Saludo que contenga un main que muestre en pantalla el
saludo “¡Hola mundo!”. En total, cinco líneas de código.
¡Cuidado! El programa MesaMain exige que metamos al menos dos
argumentos. Si ponemos sólo uno, habrá un error, y si ponemos siete, los últimos cinco
se ignorarán.
Programando a lo bestia. Estructuras de control.
Hay una serie de estructuras que la mayoría de los lenguajes de programación
poseen. Son las estructuras del control.
Estas estructuras son fundamentalmente las condicionales y las repetitivas.
Empecemos por las primeras.
Sentencias condicionales
sentencia if
if es la estructura condicional por antonomasia en todos los lenguajes. Su
sintaxis es muy fácil:
if (condición) {
bloque1
} else {
bloque2
}
Si cualquier bloque consta sólo de una sentencia, entonces las llaves
correspondientes a ese bloque pueden eliminarse. Es cuestión de comodidad.
Expliquemos. la condición es una expresión lógica, es decir, booleana. Si esta
expresión da como resultado TRUE, se ejecuta el bloque1, mientras que si la expresión
es FALSE se ejecuta bloque2. Así de simple. Claro, que queda un poco al aire eso de la
expresión booleana. Veámosla:
Una expresión booleana es cualquier cosa que pueda dar como resultado TRUE
o FALSE. Por ejemplo, la comparación de dos variables.Este es un buen momento para
ver los comparadores de magnitud:
==
!=
>
<
>=
<=
igual a
diferente de
mayor que
menor que
mayor o igual que
menor o igual que
Una expresión lógica puede ser combinación de expresiones lógicas más
pequeñas combinadas con los Y, O, NO que ya conocemos, ¿verdad? En Java se
escriben así:
AND &&
OR ||
NOT !
Bueno, pues ya no deben asustarte cosas como
((n1 > 5) && (n2 < 3)) || !(n3 >= n4)
Otro tipo de expresión booleana puede ser un valor boolean devuelto por un
método. Veamos un ejemplo:
Para saber si dos números son iguales utilizamos el símbolo ==
if (num1 == num2){
System.out.prinln(“Son iguales”);
} else {
System.out.println(“Son diferentes”);
}
Sin embargo, para saber si dos String son iguales no podemos utilizar el
simbolo de igualdad. Es una peculiaridad de Java que no viene ahora a cuento:
Aparentemente, si tenemos
String string1 = “Hola”;
String string2 = “Hola”;
String string3 = “Adiós”;
la expresión booleana ( string1 == string2 ) debería ser TRUE, pero no es así.
Para comparar Strings utilizamos el método .equals() Éste es un método que
funciona de la siguiente manera: partiendo de las strings anteriores,
string1.equals(string2) da un resultado de TRUE, mientras que
string1.equals(string3) da como resultado FALSE.
En definitiva, .equals() es el método que utilizamos para comparar una String
con otra. Como ves, un método puede devolver un valor booleano, por lo que este valor
puede ser utilizado en una sentencia if:
if (string1.equals( string2 )) {
System.out.println(“Son iguales”);
} else {
System.out.println(“Son diferentes”);
}
por supuesto, dentro de un
bloque de un if podemos anidar
más ifs:
if (tal y cual) {
hacemos esto
if (nosequé){
pues esto otro
} else {
y si no, esto
}
} else {
if (vaya rollo){
bah, yo me piro
} else {
paso de todo
}
}
RECUERDA:
los números se comparan
con == != > < >= <=
pero las Strings se comparan
con .equals()
Una sentencia if puede definirse sin su else correspondiente:
if (string1.equals(string2)){
System.out.println(“Son iguales”);
}
.
.
.
Pero hay que tener mucho cuidado con esto, sobre todo si anidamos ifs.
Supongamos que queremos comparar dos enteros n1 y n2, y saber si son iguales o si n1
> n2. No nos importa saber si n2 > n1. Recuerda que cuando un bloque consta sólo de
una sentencia (que puede ser perfectamente una sentencia if), las llaves pueden
eliminarse.
int n1 = 1;
int n2 = 10;
if (n1 != n2)
if (n1 > n2)
System.out.println(“n1 > n2”);
// no comprobamos si n2 > n1
else
System.out.println(“Son iguales”);
Bueno, pues este fragmento es erróneo. Si n1 y n2 son iguales, el programa no
avisa. De hecho, tomando los valores 1 y 10, el programa dice que son iguales. La razón
es que el else aparentemente pertenece al primer if, pero en realidad pertenece al
segundo if! El fragmento correcto sería el siguiente:
int n1 = 1;
int n2 = 10;
if (n1 != n2)
if (n1 > n2)
System.out.println(“n1 > n2”);
else
// cualquier sentencia que no haga nada, por ejemplo
n1 = n1;
else
System.out.println(“Son iguales”);
¡Bueno, hay que pasar siempre por estas cosas! Mucho cuidado cuando anides
ifs, pon siempre sus elses, aunque no hagan nada. Otra opción, tal vez te guste más, es
la siguiente:
int n1 = 1;
int n2 = 10;
if (n1 != n2)
if (n1 > n2)
System.out.println(“n1 > n2”);
else { }
// bloque de código vacío
else
System.out.println(“Son iguales”);
Aquí hacemos uso de un bloque vacío, lo delimitamos con { }, y sin embargo no
metemos nada entre esas llaves. Bueno, todo son opciones.
Personalmente te recomiendo que cuando empieces a programar siempre
escribas las llaves, aunque encierren una sóla sentencia, o aunque estén vacías:
int n1 = 1;
int n2 = 10;
if (n1 != n2) {
if (n1 > n2) {
System.out.println(“n1 > n2”);
} else { }
} else {
System.out.println(“Son iguales”);
}
Así siempre verás claramente los bloques y la dependencia de ifs y elses.
Como verás en las pruebas de programas, las sentencias condicionales son
siempre una fuente de errores, debido, por ejemplo, a cosas como estas. Estos errores se
eliminan mediante las pruebas de caja blanca, es decir, conociendo el código del
programa. Si tienes que probar un programa y te dan el código, mira siempre la
dependencia de ifs y elses, ¿vale?
sentencia switch
Esta sentencia es muy peculiar. Permite ejecutar determinada parte de un
fragmento de código en función de un número enteo. Me explico.
switch (expresión) {
case caso1:
bloque1
case caso2:
bloque2
.
.
.
}
Bueno, pues esto es fácil. Vamos a ver. expresión es un número entero. Si hay
algún caso que coincida con ese número entero, entonces se ejecutan todos los bloques
que haya a partir de ese caso, no sólo el bloque que corresponde a ese caso. Un
poco raro, ¿no? Bueno, veamos un ejemplo:
switch (n){
case 1:
System.out.println(“uno”);
case 2:
System.out.println(“dos”);
case 3:
System.out.println(“tres”);
}
Date cuenta de que no es necesario poner llaves, sean los bloques como sean,
una sentencia o más.
Si n = 1, por lo que se ejecuta el caso 1 y todos los casos posteriores, es decir,
el 2 y el 3. Por eso, si n = 1 se imprimiría en pantalla
uno
dos
tres
mientras que si n = 2 se imprimiría
dos
tres
y si n = 3, pues se mostraría
tres
Por la razón que hemos visto. Repito. Se ejecuta el bloque que corresponde al
caso y los bloques siguientes. Por eso, si n = 1, se ejecuta el caso 1 y los siguientes, es
decir, los casos 2 y 3.
¿Hay alguna forma de hacer que se ejecute sólo el caso que corresponda, y que
no se ejecuten los casos siguientes? ¡Pues claro! usando break:
switch (n){
case 1:
System.out.println(“uno”);
break;
case 2:
System.out.println(“dos”);
break;
case 3:
System.out.println(“tres”);
break;
}
ahora, si n = 1, se muestra en pantalla
uno
a diferencia de antes.
Otra cosa sobre los switches: existe un caso especial, el llamado default. Se
ejecuta este caso si no hay otro caso que corresponda a la expresión:
switch (n){
case 1:
System.out.println(“uno”);
case 2:
System.out.println(“dos”);
case 3:
System.out.println(“tres”);
case default:
System.out.println(“más de tres”);
}
Aquí, si n = 4, se mostraría en pantalla
más de tres
mientras que si n = 1, se vería
uno
dos
tres
más de tres
Bueno, no es tan mortal.
Para comerse el coco...... (4)
¿se te ocurre algún error en el siguiente código de programa?
switch (n){
case 1:
System.out.println(“uno”);
break;
case 2:
System.out.println(“dos”);
break;
case 3:
System.out.println(“tres”);
break;
case default:
System.out.println(“más de tres”);
}
piensa, piensa............
Sentencias repetitivas (o iterativas)
Adivina por qué se llaman así. Nos permiten repetir determinado fragmento
de código un número definido de veces.
En realidad lo que se hace es determinar una condición, una expresión lógica.
Así, mientras esta expresión sea TRUE el fragmento se repite, hasta que sea FALSE. Es
decir, que no definimos un número de repeticiones, sino una condición para que se
repita. Naturalmente podemos adecuar una condición para que un fragmento se repita
un número determinado de veces. Bueeeno, poco a poco. Vale.
Ya hemos tomado contacto con las expresiones lógicas, ¿verdad? Fantástico.
Apréndetelas bien, porque las vas a usar muncho muncho muncho.
while
permite repetir un bloque mientras una condición sea TRUE. Primero se
comprueba la condición, y si es TRUE entonces se ejecuta el bloque:
while (condición){
bloque
}
Un ejemplo:
int n = 0;
System.out.println(“a contar”);
while (n <=10) {
System.out.println(“Voy por el ”+n);
n = n + 1;
}
System.out.println(“¡Ay!, que me canso”);
Bueno, pues está claro, este fragmento inicializa n a cero, y mientras n <= 10
hace lo que está entre llaves, es decir, imprimir el mensaje y aumentar n en una unidad.
Es decir, cuenta de cero a diez.
Por supuesto puedes meter whiles, ifs y todo lo que quieras dentro de un while.
Esto ya lo sabrás, pero es mi obligación avisarte: Las sentencias repetitivas son
una fuente inagotable de errores. Tienes que tener mucho cuidado a la hora de usarlas,
que va a ser siempre, por cierto. Por favor, ten mucho cuidado en cómo usas las cosas,
cómo planteas las condiciones, el orden de las líneas dentro de un bloque.... cualquier
cosa puede hacer que tu programa estalle. ¿Qué ocurriría si invertimos el orden de las
dos líneas del bloque del while anterior? ¿y si ponemos como condición n < 10?
Por estas razones suele haber una regla, que no siempre se cumple, ni tienes por
qué cumplirla, pero es conveniente:
primero, inicializamos las variables en el primer caso del bucle. Si quiero
contar de 0 a 10 empiezo en 0. Parece obvio, pero no lo es.
segundo, la condición tiene que aceptar el primer valor. Es estúpido que no
pueda entrar en el bucle en el primer caso.
tercero, las variables se actualizan al final del bloque. Es decir, es aconsejable
que las variables cambien de valor justo en el final del bloque, no en la mitad ni al
principio. Fíjate, el “n = n + 1” está al final.
Repito que no es una regla general, que no tiene por qué ser así, pero que
conviene. De hecho, seguir esta regla a veces complica la existencia una barbaridad.
una variante de while: do – while
while, como hemos dicho, primero comprueba la condición y si es TRUE
ejecuta el bloque. do – while (o “duguail” para los amigos) primero ejecuta el bloque y
luego comprueba la condición. De esta forma, while puede no ejecutar nunca un bloque,
ya que primero comprueba la condición, pero do – while, al comprobar la condición al
final, siempre ejecuta el bloque al menos una vez. Típica pregunta de examen.
Bueno, la estructura es semejante a la del while:
do {
bloque
} while (condición)
Y esas “reglas” de antes, yo que tú intentaba aplicarlas aquí también.
for
Esta es la más complicada de todas. Se basa en lo siguiente:
Fíjate que while y do – while tenían como tres partes:
inicialización de las variables
condición
actualización de las variables
¿te suena?
int n = 0;
// INICIALIZACIÓN
System.out.println(“a contar”);
while (n <=10) { // CONDICIÓN
System.out.println(“Voy por el ”+n);
n = n + 1; // ACTUALIZACIÓN
}
System.out.println(“¡Ay!, que me canso”);
Bueno, pues for resume estas tres partes en una sola sentencia:
for (inicialización, condición, actualización){
bloque
}
Por ejemplo:
System.out.println(“a contar”);
for (int n = 0; n <= 10; n = n + 1){
System.out.println(“voy por el ”+n);
}
System.out.println(“¡Ay! que me canso”);
¿Te percatas de cómo for resume las tres partes en una sola sentencia?
Bueno, ¿Crees que hay más sentencias repetitivas? Pues nooooop. En realidad
puedes hacer una diferenciación muy simple: for se usa cuando sabes cuántas veces se
va a repetir un bucle, y while en caso contrario. Por ejemplo, si vas a recorrer un array
usarás un for porque sabes la longitud del array, sabes su tamaño. Si buscas una letra en
una cadena de caracteres usas un for porque sabes cuántos caracteres tiene esa cadena
(usando el método .length() de la clase String, no lo confundas con el .length sin
paréntesis de un array), pero si esperas a que el usuario introduzca una clave, por
ejemplo, usarás un while porque no sabes cuántas veces va a teclear una clave
incorrecta. Y recuerda, do – while no es más que un caso de while.
¿Sabes que con lo dicho hasta ahora puedes programar cualquier cosa? Bueno,
pero no saltes de alegría hasta haberte convencido de que pilotas los conceptos de
SENTENCIAS CONDICIONALES
IF / ELSE
SWITCH / CASE / CASE ELSE
SENTENCIAS REPETITIVAS
WHILE / DO – WHILE / FOR
Para comerse el coco.....(5)
Ya sabes que for engloba una inicialización, una condición y una actualización.
¿En qué momento del bucle se produce cada una de estas partes?
Debo decirte que todo en programación se aprende primero mediante teoría, y
luego mediante horas de práctica. Eso ya lo sabes. Pero en el uso de bucles y
condicionales es especialmente necesario que practiques con ellos. Trata de hacer
programas simples (que sean un un solo main) que, por ejemplo, cuenten de un número
a otro, que cuenten pares, impares, que calculen sumatorios, factoriales..... ¿Se te ocurre
alguna forma de calcular los 100 primeros primos? Puedes implementar la criba de
Erastótenes con dos bucles for anidados y un array de enteros..... En fin, que cualquier
cosa que se te ocurra, a por ella sin temor. Hazlo por mí....
Existe otra forma de abordar los problemas que se resuelven mediante sentencias
repetitivas. Es una visión muy diferente y que, desgraciadamente, cuesta mucho
entender. Así que trataré de esforzarme. Es el temido tema de...
Recursividad
Como acabo de decir, la recursividad es una forma de solucionar problemas.
Siempre se dice que todo algoritmo iterativo puede traducirse a una forma recursiva, y
viceversa. Bueno, pues es cierto: si tienes un algoritmo iterativo puedes cambiarlo y
convertirlo en recursivo. De hecho es una pregunta muy propia de los exámenes de
Laboratorio de Programación. Bueno, vamos a ver entonces qué es esto de la
recursividad.
La recursividad es un concepto muy abstracto. Así que atención. Consiste en que
un fragmento de código se usa a sí mismo. Ese es el corazón de la definición. Por
ejemplo: hay un cuento que narra cómo un príncipe se metió en un gran barrizal, el cual
le impedía caminar, ya que el lodo era demasiado denso. Así que lo que hizo para salir
fue tirar él mismo de sus botas hacia ariba, primero una, luego la otra, y así conseguía la
fuerza suficiente para avanzar por el barrizal, y logró salvarse.
Bueno, es un ejemplo muy simple de cómo algo puede usarse a sí mismo.
Desgraciadamente abordaremos este tema de una forma más difícil.
Hay un ejemplo típico del uso de la recursividad. Es el cálculo de factoriales. Ya
sabes:
n! = n · (n-1) · (n-2) · ... · 3 · 2 · 1
pero, fíjate:
n! =
n · (n-1) · (n-2) · ... · 3 · 2 · 1
(n+1)! = (n+1) · n · (n-1) · (n-2) · ... · 3 · 2 · 1
entonces,
(n+1)! = (n+1) · n!
y, por tanto,
n! = n · (n-1)!
Vale. Cursillo intensivo de Calculo I. Bueno. Observa que la definición de
factorial engloba un factorial. Por eso es recursivo.
Para ver fácilmente cómo funciona la recursividad, vamos a considerar el
siguiente ejemplo. A ver si te gusta.
Supón la cola de la taquilla del cine. Si, antes de entrar a ver la película hay unas
cuantas personas comprando la entrada. Aquí las tenemos:
En un momento dado, el último de ellos, el de la camiseta amarilla, desea saber a
qué hora es la siguiente sesión. Así que lo que hace es preguntárselo al de la camiseta
roja:
Pero, claro, resulta que el de la camiseta roja tampoco lo sabe. Un fastidio. Así
que lo que hace es preguntárselo al de las rastas, mientras mantiene al de la camiseta
amarilla a la espera.
Pero el de las rastas tampoco lo sabe. Así que hace la misma pregunta al de los
zapatos amarillos, mientras mantiene al de la camiseta roja a la espera. Por supuesto, el
de la camiseta amarilla sigue a la espera de que le responda el de rojo.
En fin, puedes adivinar que el de los zapatos amarillos no tiene ni idea, así que lo
que hace es preguntárselo a la taquillera de los pelos rojos. El de las rastas se queda
esperando a que el de los zapatos amarillos le responda. A todo esto, el de rojo está
esperando al de las rastas, y el de la camiseta amrilla está hartándose de esperar.
Pero ¡vaya! resulta que la taquillera de los pelos rojos, a pesar de ser su primer
día de trabajo, está muy puesta en el tema y sabe la respuesta. Claro, que ella responde a
quien le ha preguntado, al de los zapatos amarillos:
Ahora que el de los zapatos amarillos sabe la respuesta, se la dice al de las rastas,
que le estaba esperando. El de rojo sigue esperando, claro, y el de la camiseta amarilla,
ni te cuento.
Ahora el de las rastas ya sabe la respuesta. Pues va y contesta a quien le ha
preguntado. El de la camiseta amarilla, todavía sigue esperando.
Una vez que el de rojo sabe la respuesta, por fin, se la dice al de la camiseta
amarilla, que es el primero que preguntó, justito antes de que le diera un ataque de
histeria.
En fin, acabas de ver intuitivamente cómo funciona un algoritmo recursivo. El
primero llama al segundo y espera a que le responda. El segundo llama al tercero y
espera a que le responda, mientras el primero sigue esperando. Y así sucesivamente... y
así recursivamente....
Todo método recursivo tiene parámetros y valor de retorno. Es parte del juego.
¿Por qué?
Bueno, vayamos poco a poco. Un algoritmo recursivo trata de resolver un
problema, por ejemplo, el método factorial() halla un factorial., por ejemplo, 5!. Si
queremos calcular 5! debemos pasar ese 5 al método:
float resultado = factorial(5);
Lo cual ya es una buena razón para pensar que el método necesita parámetros.
Pero, lo mejor de todo, y lo más importante es que el propio método “factorial()”
llamará a “factorial()” pero con otro parámetro. factorial(5) llama a factorial(4), y
éste a factorial(3), y así sucesivamente. Por tanto, es más que obvio que un método
recursivo necesita parámetros. Ufff...
Pensar que un método recursivo no ha de tener parámetros es como pensar que
en la cola del cine el de la camiseta amarilla pregunta al de rojo “disculpe, ¿podría
decirme?”
Decíamos que un método recursivo siempre devolverá un valor. Claro, ¿Por qué?
Pues muy sencillo: cuando factorial(5) llama a factorial(4) lo hace para averiguar el
factorial de cuatro. Así que factorial(4) debe devolver un valor, exactamente 24.
Pensar que un método recursivo no devuelve ningún valor es como pensar que
en la cola del cine el de rojo responde al de amarillo “¡claro! la película empieza a las”.
Joer, imagina la conversación.
En definitiva, esto es una especie de demostración intuitiva para que te percates
de que un método recursivo necesita, exige, precisa y requiere parámetros, y
siempre devuelve algún valor. Si algún método recursivo no tiene parámetros y/o no
devuelve algún valor, es porque tiene un diseño extraño, poco común.
Mi compañero de la práctica de Programación, primer cuatrimestre, resolvió
varios problemas de la práctica mediante un método recursivo que ni tenía parámetros,
ni devolvía valores. Y funcionaba a las mil maravillas. Pero, repito, es un caso muy
raro. Yo jamás habría hecho tal método recursivo, sino iterativo. O sea, que la máxima
de antes no es tal máxima, pero yo te aconsejo que la sigas siempre que puedas.
Todo método recursivo consta de tres partes. Primero veamos esas partes y luego
te pondré un ejemplo, vale?
a) caso base: el caso en el que no se necesita una llamada recursiva. Por
ejemplo, factorial de 1.
b) caso no base: el caso en el que se hace la llamada recursiva. Por ejemplo, el
factorial de 36.
c) conquista: consiste en, después de hacer la llamada recursiva, obtener el
resultado que piden al método.
El caso base podríamos relacionarlo con la taquillera de los pelos rojos. Ella
puede contestar a la pregunta sin necesidad de preguntar a nadie más, es decir, sin hacer
ninguna llamada recursiva. El caso no base es cualquiera de las personas de la cola:
para resolver la duda han de planteársela a otra persona, no pueden resolverla por sí
mismos. La conquista sería un poco más rara: el paso de la respuesta que a uno le ha
llegado hacia la persona que le ha preguntado. Es decir, el de rojo lleva a cabo la
conquista cuando el de las rastas le responde, y el de rojo toma esa respuesta y se la da
al de amarillo.
No confundas caso no base y conquista: el caso no base es la condición, y la
conquista es la acción. Huy, qué bonito.
Bueno, lo prometido es deuda. Vamos a implementar el método factorial. Lo
implementaremos poco a poco, así que no te hagas un lío con las llaves, ¿vale?
Bueno: primero necesitamos parámetros, un entero, y devolveremos un entero
largo:
long factorial( int numero ) {
Vamos a definir un entero largo, que será la respuesta que el método devolverá:
long respuesta = 0;
Ahora debemos diferenciar entre caso base y caso no base. El caso base será el
factorial de 1, y el caso no base, cualquier otro.
if ( numero == 1 ) {
respuesta = 1;
} else {
// caso base
Ahora viene el caso no base. La respuesta se hallará mediante la llamada
recursiva. Ya que n! = n * (n-1)!, haremos:
respuesta = numero * factorial(numero – 1);
}
Y por último, la conquista. Debemos devolver la respuesta que hemos
hallado a quien nos lo pregunta:
return respuesta;
}
Aquí está el método al completo:
long factorial( int numero ) {
long respuesta = 0;
// lo que devolveremos
if ( numero == 1 ) {
// caso base
respuesta = 1;
// devolveremos 1
} else {
// caso no base: llamada recursiva
respuesta = numero * factorial(numero – 1);
}
return respuesta;
// ¡conquista!
}
Pues nada, esto compila perfectamente, y funciona. Por cierto, este código no
tiene tratamiento de errores, así que no le hagas perrerías, tipo factorial(-3), porque se te
va a quedar más colgao que el teleférico. Claro, que... ¿Por qué no le haces tú las
medidas de seguridad? No es difícil... ¡Ánimo!
Bueno, ahora voy a contarte un pequeño truco que nadie cuenta para entender
bien el funcionamiento de un método recursivo.
Verás, cuando un método se llama a sí mismo, imagina que lo que ocurre es que
en la memoria del ordenador se hace una copia del método. Así que lo que tú deberías
hacer para entender un método recursivo es pensar que existen varias copias del método
en memoria. Si tienes este código:
long factorial( int numero ) {
long respuesta = 0;
if ( numero == 1 ) {
respuesta = 1;
} else {
respuesta = numero * factorial(numero – 1);
}
return respuesta;
}
Trata de pensar que, cuando el programa arranque, por ejemplo, al calcular el
factorial de 3, lo que tendrás es esto:
Uff... no se ve muy bien... he tenido que reducir mucho la letra para que entren
las tres copias. Sorry. Bueno, pues con esta imagen tú puedes imaginar el proceso que se
lleva a cabo al calcular factorial(3). El trascurso del programa está marcado con la línea
roja.
El dibujo está en la página siguiente, porque es un poco grande. En él puedes ver
cómo la primera llamada viene desde las alturas, en la parte de arriba a la izquierda. Esa
llamada “mete” el “3” como parámetro en la primera copia de factorial. Éste, al ver que
no es un caso básico, hace la segunda llamada pasando como parámetro “2” a la
segunda copia del método. La segunda copia actúa de forma semejante, pasando “1” a la
tercera copia. Ésta identifica que es el caso básico y “conquista” la respuesta, en este
caso 1. La respuesta vuelve a la segunda copia, quien lleva a cabo la segunda conquista,
devolviendo a la primera copia “2”. La primera copia, a su vez, lleva a cabo su
conquista devolviendo al ente en las alturas “6”.
Espero que haya sido una explicación muy gráfica. Creo que no soy capaz de
explicarlo de otra manera mejor, así que, si no lo has pillado bien, releelo todo
despacito. Trata de ir muy lentamente, viendo cómo se comporta cada copia de
factorial, identificando cada paso con la situación de la cola del cine, ¿vale? Espero que
hayas perdido el miedo a la recursividad, y estoy convencido de que cada vez que
preguntes algo a alguien, y este alguien se lo pregunte a otro te acordarás de todo esto.
Hala. Se acabó. ¿Crees que te voy a dejar sin la lista de conceptos? Pues
noooooo...
MÉTODO ITERATIVO / RECURSIVO
METODOLOGÍA DE LA RECURSIÓN: la cola del cine
MÉTODO RECURSIVO
PARTES DE UN MÉTODO RECURSIVO
caso base, caso no base, conquista
Como curiosidad....
¿Sabes por qué se le llama “conquista”? Pues es muy sencillo. Los métodos
recursivos se suelen usar para resolver problemas partiéndolos en cachitos más
pequeños. Por ejemplo, buscar a una persona en una cola de gente: primero buscas en la
mitad izquierda, y luego en la derecha. Esto lo verás muy profundamente en Laboratorio
de Programación. Bueno, pues como ya sabrás, hay un refrán árabe que dice: divide y
vencerás. En inglés, “divide and conquer”. De ahí lo de “conquista”.
Bueno, vamos a abordar una de las partes más bonitas de la OOP, aunque trae
sus dolores de cabeza, no te creas. Algunos lenguajes, como C++ basan gran parte de su
ingente potencial en esto que llamamos...
Herencia
Bueno, está claro qué es la herencia. Es lo que unos padres dejan a sus hijos. O
mejor dicho, lo que unos padres transmiten a sus hijos por vía genética. Las personas
somos creadas mediante herencia de genes, a pachas entre papi y mami. En definitiva,
que papá y mamá no nos definen desde el principio, sino que toman sus propios genes
para crearnos a nosotros. Bueno, afortunadamente esto no es tan fácil en genética como
en programación.
Pero el concepto que te tiene que quedar muy claro es que la herencia es un
mecanismo de definición, no un método para suspender a los de primero de Teleco, ni
un galimatías conceptual, sino que es una forma de definir nuevos objetos a partir de
los existentes. La herencia sirve para definir.
En Java una clase sólo puede heredar de otra clase. No se admite que una
clase herede de varias clases, como sí se puede hacer, por ejemplo, en C++. En Java, por
tanto, no hablamos nunca de herencia múltiple. Además, la herencia en Java se
denomina extensión: decimos que una clase extiende a otra.
Por tanto tenemos que tener en cuenta que, en Java, los objetos sólo tienen un
padre, y además, heredan de él el 100%, no como en las personas que heredan el 50%
Bueno, pues sin más preámbulos nos podemos plantear alguna estructura
jerárquica, es decir, algún diagrama de clases en el que dichas clases se relacionen
mediante mecanismos de herencia. Por ejemplo, se me ocurre pensar que una moto es
como una bicicleta con motor. De ahí lo de “motocicleta”. Bieeeeen 
Bueno, pues pasemos a escribir. Por cierto,
el fin de este código que ahora vamos a escribir es
puramente didáctico, no funcional.
Es decir, que vamos a definir una
bicicleta y una moto, pero de una
forma muy estúpida. Verás:
vamos a definir las clases Rueda
y Motor de una forma un tanto
peculiar, aunque simpática
donde quepa:
veremos muchas
clases definidas de forma
muy tonta. A veces incluso
sin definir. Eso es porque
realmente su contenido
no importa nada.
class Rueda {
void saludar() {
System.out.println(“Soy una rueda”);
}
}
class Motor {
void saludar() {
System.out.println(“Soy un motor”);
}
}
Ahora hacemos que una bicicleta sea, simplemente, dos ruedas:
class Bicicleta {
Rueda delantera;
Rueda trasera;
Bicicleta(){
// el constructor
delantera = new Rueda();
trasera = new Rueda();
}
void saludar() {
System.out.println(“Soy una bicicleta”);
}
}
Bueno, pues vamos a definir la moto. Para hacer que una clase “hijo” extienda a
otra “padre”, la primera clase se define así:
class hijo extends padre
Bueno, pues a ello.
class Moto extends Bicicleta {
Motor motor;
Moto() { // el constructor
motor = new Motor();
}
void saludar(){
System.out.println(“Soy una amoto”);
}
}
Estas cuatro clases son lo más fácil del mundo. Si no las entiendes, para y vuelve
atrás, porque son de lo más básico. Pero seguro que las entiendes sin problemas,
¿verdad?
Fíjate en el uso de la herencia: la clase Moto sólo define un método saludar y
una propiedad Motor, pero hereda de la clase Bicicleta, por lo que también posee todos
los contenidos de Bicicleta. Por tanto, Moto hereda de la clase Bicicleta las dos ruedas.
¿Te percatas de cómo se usa la herencia para definir clases? Definimos el contenido de
Moto a partir del contenido de Bicicleta.
Respecto al método saludar, fíjate que Motor redefine el método saludar que
hereda de Bicicleta. La clase padre tenía un método saludar, pero la clase hija también
lo tiene. Se dice que lo redefine (overrides). Sin embargo, debe quedar muy claro que
redefinir un método no implica que el método anterior deje de existir, es decir, que
el método saludar de Bicicleta sigue existiendo a pesar de haberlo redefinido en la
clase moto.
No tendría sentido que una moto saludase como una bicicleta, ¿no? Aunque no
te creas, que gracias al casting, que en breve veremos, podemos hacer que una moto
pueda saludar como moto y como bicicleta.
Bueno, pues eso es todo sobre cómo se usa la herencia. Vemos cómo hemos
definido una moto a partir de la definición de una bicicleta, ¿verdad?. Ahora, en
cualquier parte del código, por ejemplo, en un main podemos hacer sin problemas:
Bicicleta Antonieta = new Bicicleta();
Moto Anacleta = new Moto();
Bueno, pues al crear a Antonieta se crearán las ruedas, y al crear a Anacleta se
creará el motor. Podemos hacer sin problemas:
Antonieta.saludar();
Anacleta.saludar();
y cada una nos saludaría diciendo lo que son. Hala. Voy a compilar este código a
ver si da problemas...... no, parece que no los da.
Vamos a plantear una cuestión: una Bici, al crearse, crea las dos ruedas. Una
moto, al crearse, crea un motor. Pero, ¿Se crean las ruedas al crearse la moto? Fíjate en
el constructor de la clase Moto:
Moto() { // el constructor
motor = new Motor();
}
Pregunta: ¿Crees que al invocar al constructor de Moto se invoca
automáticamente el constructor de su superclase Bicicleta?
Desgraciadamente no. Tal y como estamos, al crear una moto se crea su motor,
pero no sus ruedas. Ayayayayyyyyy.... ¿cómo arreglamos esto?
Opción primera: crear específicamente las ruedas en el constructor de la moto:
Moto() { // el constructor
delantera = new Rueda();
trasera = new Rueda();
motor = new Motor();
}
Esto es perfectamente válido. Pero hay otra solución más mejor:
Opción guai: Hacer uso de la palabra super(). Esta palabreja se utiliza en el
constructor de una clase para llamar al constructor de su superclase. Es decir, que
si hacemos:
Moto() { // el constructor
super();
motor = new Motor();
}
entonces, cada vez que creemos una moto se creará “la bicicleta que reside en su
interior” (oh! una lágrima resbala por mi mejilla....) y luego el motor. Es como si
al crear a una persona primero
En definitiva,
creas lo que tiene de su
cuando una clase extiende
padre y luego creas el
a otra, el constructor de la
resto de cosas.
clase hija debe llamar al
Una visión muy peculiar,
constructor de la clase madre,
¿no crees?
y esto se hace mediante
Por cierto, conviene
super().
que super() sea lo primero que
aparezca en el constructor, antes de
cualquier otra sentencia. ¿Te has preguntado
por qué super() tiene paréntesis? Deberías adivinarlo,
pero por si acaso, te lo digo. super() es el constructor de
la superclase, y recuerda que el constructor no es más que
un método un tanto especialito, pero método al fin y al cabo.
Y si no recuerdas mal, los métodos llevan paréntesis siempre....
nota
Cuando una clase hereda de otra no es necesario que llame al constructor de su superclase, pero
siempre que crees una clase hija, lo más normal es que sí lo haga. Plantéate la lógica de este hecho: supón
un mecánico que sabe que una moto es una bicicleta con motor. Si le piden fabricar una moto, entonces
primero creará una bicicleta y luego le pondrá un motor. Es decir, primero crea la clase padre y luego la
hija. Supón ahora que defines una motopija, que es una moto con aire acondicionado  . Pues al crearla,
primero crearás una moto y luego le pondrás el aire. Esto implica crear primero una bicicleta, luego
ponerle motor para hacer la moto, y luego ponerle el aire para hacer la motopija. Vaya, dos super()....
Weno, pues, como siempre, antes de seguir asegúrate de pilotar:
HERENCIA
CLASE PADRE / CLASE HIJO
USO DE extends
REDEFINICIÓN (OVERRIDING) DE MÉTODOS
(que a continuación veremos con más detalle)
USO DE super()
CONCEPTO DE MOTO CON AIRE ACONDICIONADO 
¡Animo! ¡ya queda poco de OOP!
Preludio al Casting: las referencias
Bueno, no tiene nada que ver con actores. Qué pena, ¿verdad?
Casting significa algo así como “convertir”. El objetivo del casting es hacer que
un objeto que se ha definido mediante herencia se comporte como un objeto de una de
sus superclases. Es decir, que una moto salude como una bicicleta, o que una moto deje
de tener motor, o cosas por el estilo. En realidad el casting es algo un poco más
profundo, pero empezaremos por lo fácil. No te creas, acabaremos con lo difícil....
Bueno, pues recordemos las definiciones de Moto y de Bicicleta:
class Bicicleta {
Rueda delantera;
Rueda trasera;
Bicicleta(){
// el constructor
delantera = new Rueda();
trasera = new Rueda();
}
void saludar() {
System.out.println(“Soy una bicicleta”);
}
}
class Moto extends Bicicleta {
Motor motor;
Moto() { // el constructor
super(); // llamamos al constructor de Bicicleta
motor = new Motor();
}
void saludar(){
System.out.println(“Soy una amoto”);
}
}
Vamos a crear dos objetos:
Bicicleta Antonieta = new Bicicleta();
Moto Anacleta = new Moto();
Si hacemos:
Anacleta.saludar();
veremos en pantalla :
Soy una amoto
Como sabemos, una moto es una bicicleta con motor. Eso significa que una
moto tiene una bicicleta dentro de sí. o lo que es lo mismo, que una moto puede
comportarse como una bicicleta. ¿Cómo? Ahora lo veremos. ¿Para qué sirve? Eso lo
veremos luego.
Supón que tenéis una vecina que conoce muy bien a tu madre, mucho mejor que
a ti. Un buen día te ve y te saluda, y te dice “Vaya, tienes los ojos de tu madre”. ¿Por
qué te dice esto? Aparte de porque realmente puede que tengas los ojos de tu madre, la
vecina te lo dice porque conoce a tu madre. Si no la conociese no podría decirlo, ¿no?
Es decir, que la vecina puede decir lo que has sacado de tu madre si conoce a tu madre.
Bueno, pues una referencia, que se supone que ya sabes a la perfección qué es,
es como la vecina. Es capaz de ver en los objetos los rasgos que conoce. Si una
referencia es del tipo bicicleta, entonces verá los contenidos propios de la bicicleta en el
objeto al que apunte. Por ejemplo:
Bicicleta refABicicleta = new Bicicleta();
Estamos cerando una referencia llamada refABicicleta (“referencia a bicicleta”)
que es del tipo Bicicleta y que apunta a una nueva Bicicleta ( new Bicicleta() ). Esto
es claramente legal, es como si la vecina ve a tu madre, no hay nada que no conozca de
ella. Pero si hacemos un truco...
Bicicleta refAOtraBicicleta = new Moto();
Hacemos que la referencia refAOtraBicicleta apunte a una nueva moto. ¿Cómo
es esto posible? Pues es como la vecina que se encuentra un día contigo. La vecina ve en
ti lo que has heredado de tu madre, porque conoce a tu madre. La referencia
refAOtraBicicleta ve en la nueva moto lo que ha heredado de bicicleta porque
conoce las bicicletas (es del tipo bicicleta).
Entonces, usando la referencia refAOtraBicicleta podemos hacer que una moto
salude como una bicicleta, porque refAOtraBicicleta conoce el método saludar de la
bicicleta: Si hacemos
refAOtraBicicleta.saludar();
veremos
Soy una bicicleta
¡aunque refAOtraBicicleta apunta a una moto!. ¿Por qué? Pues por lo dicho,
porque refAOtraBicicleta no conoce el método que saluda como una moto, sólo
conoce el método que saluda como una bicicleta. Pretender que refAOtraBicicleta
haga saludar a la moto como si fuese una moto sería como pretender que la vecina te
diga que tienes las manos de tu abuelo, al que nunca conoció. Aquí tienes un pequeño
gráfico que tal vez te ayude:
Vamos a olvidar por ahora los constructores, ¿vale? Bueno, pues ¡te presento a la
señora referencia de tipo Bicicleta!:
¡¡¡TACHAAAAAAN!!!
no esperarás que tres flechas piensen como
lo hacen el pez y los otros, ¿no? Vaya unas
ideas que tienes...... :)
Bueno, pues ¿Por qué la referencia de tipo Bicicleta tiene esa forma? Pues fjate
en cómo funciona esta referencia con un objeto de tipo Bicicleta:
¿Te percatas de cómo de “ajusta” la referencia al contenido del objeto? Una
referencia del tipo Bicicleta verá todo el contenido de una bicicleta, así como tu vecina
conoce a tu madre por completo. Ahora fíjate en esto:
Un objeto de la clase Moto es igual que una bicicleta con dos cosas añadidas: un
motor y un método que saluda de forma diferente. Estos dos miembros de la clase están
arriba marcados en rojo. Fíjate cómo una referencia de tipo Bicicleta puede adaptarse a
un objeto de tipo Moto, pero no ve más que el contenido de la bicicleta. En otras
palabras, que la referencia no ve ningún miembro pintado en rojo. Por cierto, perdón por
el desenfoque.
Por supuesto, huelga decir que una referencia de tipo Bicicleta no puede apuntar
a objetos que ni sean bicicletas ni hereden de bicicletas. Es decir, que una referencia de
tipo bicicleta no puede apuntar a la mesa Margarita. Lástima.
Espero que ahora haya quedado bien explicado, o mejor dicho, bien mostrado:
1- Que una referencia de bicicleta se acopla a un objeto moto. Una referencia
de un tipo puede acoplarse a un objeto de otro tipo
2- Que una referencia de bicicleta al acolparse a una moto no ve ni motor ni el
segundo método saludar. Una referencia de un tipo acoplada a un objeto de
otro tipo sólo ve parte del contenido objeto
Bueno, acabas de ver cómo funciona una referencia. Como resumen, una
referencia es un conjunto de flechitas que apuntan hacia determinados contenidos de un
objeto. Los contenidos a los que apuntan vienen determinados por el tipo de la
referencia.
Comparación de referencias
Para comprobar si pilotas el sentido de la palabra “referencia”, hazte la siguiente
pregunta:
Tenemos dos objetos diferentes, con referencias diferentes, pero cuyas
propiedades coinciden. Por ejemplo, dos mesas cuadradas azules de tres patas:
Mesa Margarita = new Mesa(“Azul”, 3, “cuadrada”);
Mesa Catalina = new Mesa(“Azul”, 3, “cuadrada”);
La pregunta es la siguiente: ¿Qué valor lógico (TRUE – FALSE) dará la
siguiente expresión?
Margarita == Catalina
Pues veamos: estamos comprobando si son iguales dos referencias. Cada una de
ellas apunta a una mesa cuadrada, azul y de tres patas. Así que, como son iguales, esto
debería dar TRUE.
Pues no.
Fíjate: las dos mesas son azules, de tres patas y cuadradas. PERO SON DOS
MESAS DIFERENTES, no son la misma mesa. Es como decir que dos gemelos, por ser
idénticos, son la misma persona. Bueno, ten esto muy presente siempre, ¿vale? Puedes
comparar dos númeos con ==, pero no dos objetos.
Antes de seguir asegúrate que has alcanzado la Idea platónica de los dos puntos
anteriores. Si no es así, en realidad lo único que tienes que hacer es pensar muy
profundamente en ello, vuelve a escribir las clases, prueba a hacer pequeños
programas... recuerda lo dicho en el prefacio. Y, por supuesto, si tienes más probelmas,
aquí estoy.
Arreglando bicicletas: parámetros que son referencias
Supón ahora que se nos pincha una rueda de la bicicleta. Pues la llevaremos a un
taller:
class Taller {
void arreglar(Bicicleta bici){
System.out.println(“A arreglar!”);
}
}
Bueno, fíjate: definimos un taller con un método arreglar al que le vamos a
pasar como parámetro una bicicleta. Realmente este método no hace absolutamente
nada con la bicicleta que le damos a arreglar, pero repito que el objetivo de este rollo es
puramente didáctico.
Pues eso, que ahora podemos hacer sin problemas:
Bicicleta Antonieta = new Bicicleta();
Taller elTaller = new Taller();
// después de largas horas de uso...
elTaller.arreglar(Antonieta);
es decir, creamos una bici, un taller, y hacemos que el taller arregle la bici
después de mucho montar. Bueno, pues esto no parece complicado, ¿verdad?
Bueno, plantéate lo siguiente: Si en el taller arrerglan ruedas de bicicleta, y una
moto es una bicicleta con motor, ¿va a poder el taller arreglar las ruedas de una moto?
¡Pues claro que sí! Pero no podemos hacerlo tan a la ligera. Veamos por qué.
El método arreglar requiere como parámetro una Bicicleta. Sin embargo,
queremos areglar una moto. Bueno, pues es aquí donde haremos el uso de referencias de
antes:
Crearemos primero una moto, pero tendrá una referencia de tipo bicicleta:
Bicicleta laMotoRompida = new Moto(); // moto con ref. Bicicleta
Y..... ¡Si! ya podemos arreglar la moto:
Taller elTaller = new Taller();
elTaller.arreglar(laMotoRompida); // arreglamos una moto en realidad
¿Te da cuen? Si arreglar necesita una bicicleta y queremos arreglar una moto,
hacemos que una referencia de tipo bicicleta apunte a una moto.
Bueno, pues fíjate para qué sirve esto de las referencias. ¿Te gusta? Pues verás
ahora.
Un taller que se precie no sólo debe ser capaz de arreglar pinchazos. También
debería poder cambiar el aceite a los motores, ¿no? Bueno, pues vamos a definir otro
método de la clase Taller.
class Taller {
void arreglar(Bicicleta bici){
System.out.println(“A arreglar!”);
}
void cambiarAceite(Moto laMoto){
System.out.println(“El aceite estaba echo una miiiiierda”);
}
}
Bueno, pues ahora tenemos un peazo taller de la leche. Si tenemos una moto
cualquiera podemos arreglarle los pinchazos y cambiarle el aceite. Pero fíjate, el método
cambiarAceite requiere que le pasemos una moto (porque cambiar el aceite a una bici
se las traería, ¿no?), pero arreglar requiere una bicicleta. Madre mía. ¡Necesitamos una
referencia de tipo Bicicleta para arreglar la moto y otra de tipo Moto para cambiarle el
aceite!
Taller elTaller = new Taller();
Bicicleta refMoto = new Moto();
Si queremos cambiar el aceite a la moto, es fácil:
elTaller.cambiarAceite(refMoto);
Pero, para arreglar las ruedas necesitábamos una referencia de tipo bicicleta, y la
referencia que tenemos es de tipo Moto. Tendremos que crear una nueva referencia:
Bicicleta refBicicleta = refMoto;
Esta referencia nueva apunta al mismo objeto al que apuntaba refMoto. Ahora sí
es lícito hacer
elTaller.arreglar(refBicicleta);
Bueno, pues esto es perfectamente legal, funciona. Pero coincidirás conmigo que
es un engorro. Tener dos referencias de tipo distinto para un mismo objeto no es ser
muy ahorrativo. Ni muy práctico, todo sea dicho.
Casting
Como dijimos hace una eternidad, “casting” es algo así como “convertir”. El
objetivo es el siguiente:
Hemos visto casos en los que un objeto necesita varias referencias de distinto
tipo, como con el taller: arreglar las ruedas necesita una bicicleta, pero cambiar el aceite
necesita una moto. Para cambiar el aceite a una moto no hay mucho problema, pero para
arreglarle las ruedas había que hacer una nueva referencia de un tipo diferente.
Bueno, pues existe una manera de hacer esto mismo con una sola referencia.
¿Cómo? Pues muy fácil: convirtiéndola.
Convertir una referencia de un tipo inicial a un tipo final se hace así:
(tipo final)referencia
Por ejemplo, si quiero convertir un número entero en uno de coma flotante
(¡CUIDADO! no te creas que en este caso hay herencia de algún tipo, ¿eh?)
(float)miEntero
O si quiero convertir una moto en bicicleta...
(Bicicleta)refMoto
Es decir, que usando esta conversión, puedo hacer sin problemas:
Taller elTaller = new Taller();
Moto Maroto = new Moto();
y, por fin....
elTaller.cambiarAceite(Maroto);
elTaller.arreglar( (Bicicleta)Maroto
);
// esto es casting!!!!!!!!!
Tenemos una sola referencia, llamada Maroto que es de tipo Moto, pero
podemos usar esta referencia de tipo Moto en métodos que requieran una referencia de
tipo Bicicleta (como el método arreglar) porque podemos convertirla. ¡BIEN!
Relee el párrafo anterior.
Vuelve a leerlo.
¿Está claro?
¿Seguro?
Fabuloso.
¿Realmente te parece el casting lo que te parecía antes?
Hala. A modo de resumen:
1. Una moto puede saludar como una bici. Podemos hacer que un objeto de un
tipo se comporte como un objeto de otro tipo.
2. El método arreglar ve a una moto como si fuese una bici, mientras que
cambiarAceite ve la misma moto como lo que realmente es, una moto.
Podemos hacer que un método vea a un objeto como si fuese de una clase y
que otro método vea el mismo objeto como si fuese de otra clase.
3. Esto lo podemos hacer mediante varias referencias de diferentes tipos que
apunten al mismo objeto....
4. O mediante una sola referencia y el “truco” del casting
El Casting se acaba aquí. Con estos conocimientos puedes enfrentarte a un
fragmento de código que contenga conversiones de referencias, es decir, casting.
Pensado puedes saber qué sentencias serán correctas y cuales no lo serán. Sólo tienes
que pensar qué contenidos del objeto es capaz de ver una referencia.
Antes de seguir (y por supuesto después de realizar tus ejercicios espirituales)
plantéate los conceptos de:
REFERENCIA
TIPO DE UNA REFERENCIA
CÓMO las referencias apuntan a un objeto
CÓMO dos referencias de diferentes tipos ven contenidos diferentes de un
mismo objeto
COMPARACIÓN DE REFERENCIAS: == no funciona como quisiéramos....
CÓMO un método requiere referencias de determinado tipo (como parámetros)
SENTIDO DE usar distintas referencias para un objeto
FINALIDAD DEL CASTING
SINTAXIS DEL CASTING
¡Recuerda!
Vimos cómo se usaba el casting para convertir enteros a coma flotante. A
esto se le llama también CASTING, pero no tiene nada que ver con herencia. El
Casting se usa para convertir
a) tipos básicos (de int a float, por ejemplo)
b) referencias de objetos (llevamos tol rato haciéndolo)
Para comerte el coco.... (6)
Dadas las siguientes definiciones:
class A {
int getn() {
return 1;
}
}
class B extends A {
int getn() {
return 2;
}
}
class ABMain {
public static void main(String[] args){
A a = new A();
B b = new A();
System.out.println(“numero:”+a.getn());
System.out.println(“numero:”+b.getn());
}
}
Si lanzamos el programa, ¿Qué vemos en pantalla? Madre mía, típica cuestión
de examen. Bueno, un puntito para ti.
Upcasting y downcasting. Y siguiendo con el inglés, explicit and implicit
casting.
Este apartado es muy corto y sólo sirve para que aprendamos un par de términos.
Sólo hay un pequeño problema: es un pelín enrevesado. Trataré de explicarlo
fácilmente, pero eso no va a ser suficiente. Así que abre bien los ojos y lee cada párrafo
siete veces. Para hacerlo más corto aún, vamos a seguir con la bicicleta y la moto como
ejemplos, ¿vale?
Bicicleta Anacleta = new Bicicleta();
Moto Maroto = new Moto();
Upcasting: convertir una moto en una bicicleta. Es decir, convertir una
referencia de un tipo hijo a un tipo padre:
(Bicicleta)Maroto
Downcasting: lo contrario:
(Moto)Anacleta
Lo recordarás fácilmente si te imaginas la clase padre encima de la clase hijo.
“UPcasting” es “ir hacia arriba” y “DOWNcasting” sería “ir hacia abajo”.
Voy a adjuntarte este pequeño fragmento de código para entender genial los dos
siguientes conceptos, casting implícito y explícito:
class A {
int getn() {
return 10;
}
}
class B extends A {
int getm() {
return 20;
}
}
class C {
void pillaUnA ( A j ){
System.out.println("n = "+j.getn());
}
void pillaUnB ( B j ) {
System.out.println("m = "+j.getm());
}
}
Ahora supongamos un main cualquiera con las siguientes declaraciones:
public static void main(String[] args) {
A a = new A();
B b = new B();
C c = new C();
.
.
.
Weno. Es perfectamente lícito hacer:
c.pillaUnA(a);
c.pillaUnA(b);
En el primer caso, a es una referencia de tipo A y por eso puede pasarse al
método pillaUnA como parámetro.
En el segundo caso, AL LORO, b es una referencia de tipo B. Si queremos
pasarla al método pillaUnA deberíamos hacer:
c.pillaUnA( (A)b );
es decir, hacer UPCASTING. Sin embargo, este casting no es necesario hacerlo,
porque el compilador lo hace directamente. A esto se le llama casting implícito.
Hagamos ahora:
c.pillaUnB(b);
c.pillaUnB(a);
En el primer caso, no hay ningún problema. En el segundo caso, estamos
pasando una referencia de tipo A a un método que requiere una referencia de tipo B. Es
decir, al contrario de antes. Bueno, pues como pillaUnB() necesita un parámetro de tipo
B, la referencia a tendremos que convertirla a una referencia de tipo B. Es decir, hay que
hacer DOWNCASTING. Bueno, pues este casting no es implícito, no lo hace el
compilador solito, como antes, sino que necesita que nosotros lo hagamos. A este
casting se le denomina casting explícito.
Por regla general, el upcasting es implícito y el downcasting es explícito.
AVISO: dado el código:
A a = new A();
C c = new C();
podemos hacer downcasting así:
c.pillaUnB( (B)a );
per fíjate: la referencia a apunta a un objeto de tipo A que no tiene el método
getM(). Es decir, que sintácticamente el casting es lícito, pero pillaUnB() jamás podrá
acceder al método getM() del parámetro, sencillamente porque en este caso no el
parámetro no tiene dicho método.
Es un poco engorroso, pero básicamente lo que te planteo es que este
downcasting puede hacerse, puede compilar, pero jamás va a poder ejecutarse.
¡Ya hemos acabado! plantéate de nuevo:
UPCASTING
DOWNCASTING
CASTING IMPLÍCITO
CASTING EXPLÍCITO
Ejercicio:
Dado el siguiente main, ¿Qué sentencias son lícitas? ¿cuáles compilan y cuáles
no? ¿cuáles tienen upcasting y downcasting? ¿Cuáles tienen casting implícito y cuáles
explícito?
public static void main(String args[]){
A a = new A();
B b = new B();
C c = new C();
c.pillaUnA(a);
c.pillaUnA(b);
c.pillaUnA ( (A)b );
c.pillaUnA ( (B)a );
c.pillaUnB (a);
c.pillaUnB (b);
c.pillaUnB ( (A)b );
c.pillaUnB ( (B)a );
}
Solución:
Es normal fallar en algunas, así que no te desesperes si no aciertas, ¿eh? Por
cierto, estoy seguro de que esta solución es wena al 99%, pero no al 100%. Así que si no
concuerda con lo que tú dices, y estás seguro de que no te has equivocado, tal vez lo
haya hecho yo... De todas formas, he compilado el código y parece que se ciñe a estos
resultados. Por supuesto, lo mejor cuando tengas dudas es encender el ordenador y
probar.....
c.pillaUnA(a); // normal
c.pillaUnA(b); // upcasting implícito
c.pillaUnA ( (A)b ); // upcasting explícito
c.pillaUnA ( (B)a ); // compila pero no ejecuta
c.pillaUnB (a); // requiere downcasting explícito
c.pillaUnB (b); // normal
c.pillaUnB ( (A)b ); // requiere downcasting explícito
c.pillaUnB ( (B)a ); // compila pero no ejecuta
Bueno, visto esto, vamos a por la maravillosa...
La Clase Object
La Clase Object es una clase que Java tiene definida en sí mismo con su propio
mecanismo. Es decir, que no vas a tener que definirla, así que tranki.
La clase Object se define como superclase de toda clase Java. Es decir, que si
defines una clase cualquiera, será una clase que herede de la clase Object, aunque no lo
definas como tal. La clase Mesa o Bicicleta son, en realidad, extensiones de Object.
Fíjate qué cosas.
Bien. ¿Qué contenidos tiene la clase Object? Pues esta clase tiene unos métodos
muy extraños:
clone(), que sirve para hacer clonar objetos,
equals(), para comparar objetos,
getClass(), para obtener la clase de un objeto,
y algunos más. En definitiva, que la clase Object tiene un contenido bastante
rarito. ¿Cuál es el sentido de este contenido? Pues muy fácil: como cualquier objeto
hereda de Object, Object contiene los métodos que todo tipo de objetos debería tener.
Métodos para que un objeto se clone, se compare con otro objeto y cosas por el estilo.
De esta forma, si tu objeto necesita tales métodos, pues ya los tiene definidos.
Sin embargo, estos métodos sirven para todo tipo de objetos. Por lo que no es
difícil adivinar que serán todos métodos muy generales. Así que lo más probable es que,
si quieres utilizar uno de estos métodos, tendrás que redefinirlo (override).
Pero el auténtico potencial de la clase Objwect no reside en su contenido, sino en
el hecho de que es la superclase de toda clase Java. Y dado que ya pilotas la herencia y
el casting, te darás cuenta de que cualquier objeto podrá verse referenciado por una
referencia de tipo Object..Para ver un ejemplo,vamos a definir las siguientes clases:
class Florero {
void saludar(){
System.out.println(“Soy un florero”);
}
}
class Foto {
void saludar(){
System.out.println(“Soy una fotografía”);
}
}
class Lampara {
void saludar(){
System.out.println(“Soy una lámpara”);
}
}
Bueno, pues el objetivo de nuestro programa es escribir un método que
meteremos en la clase Mesa (¿te acuerdas de ella?) que sirva para colocar cosas encima
de una mesa. Recuerda la clase Mesa:
class Mesa {
String color;
int numPatas;
String forma;
static int numMesasFabricadas = 0;
void setColor(String nuevoColor){
color = nuevoColor;
}
void setNumPatas(int nuevoNumPatas){
numPatas = nuevoNumPatas;
}
void setForma(String nuevaForma){
forma = nuevaForma;
}
String getColor(){
return color;
}
String getForma(){
return forma;
}
int getNumPatas(){
return numPatas;
}
Mesa(String colorInicial,
int numPatasInicial,
String formaInicial){
color = colorInicial;
numPatas = numPatasInicial;
forma = formaInicial;
numMesasFabricadas = numMesasFabricadas + 1;
}
Mesa() {
color = “azul”;
numPatas = 4;
forma = “cuadrada”;
numMesasFabricadas = numMesasFabricadas + 1;
}
int getNumMesasFabricadas() {
return numMesasFabricadas;
}
}
Bueno, pues ahora queremos hacer un método ponerEncima() para colocar
encima de una mesa una lámpara, una foto o un florero, es decir, objetos de las clases
que antes hemos definido. Por supuesto, el contenido del método no importa en
absoluto.
A primera vista se nos ocurre hacer un método para cada tipo de objeto a
colocar, es decir, escribir un ponerEncimaUnFlorero, PonerEncimaUnaLampara,
ponerEncimaUnaFoto, más o menos así:
void ponerEncimaUnFlorero (Florero nuevoFlorero) {
.
.
.
}
void ponerEncimaUnaFoto (Foto nuevaFoto) {
.
.
.
}
void ponerEncimaUnaLampara(Lampara nuevaLampara) {
.
.
.
}
Bueno, normal, ¿no? Un método específico para colocar cada objeto.
Bueno, pues ahora vamos a hacer uso de la clase Object. Como es la superclase
de toda clase Java, podemos hacer un método que coloque cualquiera de los objetos
anteriores:
void ponerEncima ( Object nuevoObjeto ) {
.
.
.
}
Hala. Ya podemos hacer:
Mesa Margraita = new Mesa();
Florero f = new Florero();
Margarita.ponerEncima(f);
Lampara l = new Lampara();
Margarita.ponerEncima(l);
Hala. Revive este pequeño capítulo sobre la clase Object y no sigas adelante si
no has entendido a la perfección:
CLASE OBJECT
SU CONTENIDO
SU USO COMO REFERENCIA GLOBAL (para toda clase Java)
Ejercicio:
Plantéate los siguientes fragmentos de código
Lampara l = new Lampara();
Margarita.ponerEncima( (Lampara) l );
Lampara l = new Lampara();
Margarita.ponerEncima( (Object) l);
Object l = new Lampara();
Margarita.ponerEncima(l);
Object l = new Lampara();
Margarita.ponerEncima( (Object) l);
Échales un ojo de nuevo..........
¿sabes qué es lo mejor?
te lo aseguro:
¡usarás la clase Object
más de lo que tú te crees!
Que todos funcionan.
¿Has visto lo
genial que
es la clase Object?
Verás, ahora debemos parar un
minuto para reconsiderar una cosa. En el prefacio he dicho que iba a explicar todo el
rollo de Java basándome en la vida real, en ejemplos sencillos e ilustrativos, y
desgraciadamente a partir del casting ese hecho ha empezado a decaer, y hemos
alcanzado un punto en el que consideramos métodos ( ponerEncima() ) que ni siquiera
hemos definido, sólo hemos escrito su cabecera. Hay que entender que del casting en
adelante es todo muy abstracto, y por ello poner ejemplos de la vida real, pues ejem.... la
verdad, es complicado.
Vamos, lo que pretendo hacer es disculparme si todo esto te ha parecido
demasiado tedioso, que en realidad lo es. Sólo que sepas que he hecho lo que he creído
mejor, hemos dejado métodos sim implementar porque hacerlo sería complicar las cosas
demasiado y no vendría a cuento; las explicaciones son más escuetas que al principio
porque la intención es mostrar la idea limpiamente: partir de un objetivo, plantear un
problema, aportar soluciones y escoger la mejor.
En definitiva, si esta parte te ha parecido lo peor, siento que haya sido así, y
espero y deseo de corazón que encuentres fácilmente una explicación mejor. Por cierto,
el pez, el pato y el pájaro que no tengo ni idea de cómo se llama, también te piden
disculpas.
Bueno, pues estos tres bichos también te animan a seguir adelante, ya que queda
muy poco. De verdad. Lo prometo. Bueno, abordemos ahora un tema bastante curioso.
Son las...
Interfaces
Vamos a ver.
El objetivo de una interface es definir qué contenidos debe tener una clase
obligatoriamente. Es algo así como un contrato: una clase puede implementar una
interface si se compromete a definir determinados contenidos. Veamos.
Si yo soy un trabajador, tengo obligatoriamente que pagar impuestos. Eso
significa que todo trabajador se compromete a pagar impuestos. Bueno, debería.
Las interfaces, repito, definen qué contenidos debe tener una clase. Por ejemplo,
qué métodos debe tener obligatoriamente, pero no define esos métodos. Es decir, que
una interface sólo es una colección de cabeceras de métodos. La clase que implemente
esa interface debe implementar esos métodos. Lo vemos con un ejemplo: Si definimos:
interface Trabajador {
void pagarImpuestos();
}
Entonces, toda persona que sea trabajador, trabaje en lo que trabaje, debe
implementar esta interface:
class Funcionario implements Trabajador {
String nombre;
String dirección;
int edad;
int sueldo;
.
.
.
void pagarImpuestos(){
System.out.println(“Como funcionario, pago mis impuestos”);
sueldo = sueldo – (0.16*sueldo);
}
}
Hala, ahora cada funcionario pagará el 16% de su sueldo. Pero fíjate, un
presentador de televisión paga más impuestos que un funcionario. Eso significa que su
método pagarImpuestos() será ligeramente diferente. Su sueldo se ve reducido en un
30%, y además, el mensaje que nos avisa de tal pago es un poco más pijo.
class Presentador implements Trabajador {
String nombre;
int cuánInsoportableSoy;
.
.
.
void pagarImpuestos(){
System.out.println(“O sea, yo pago un plussss”);
sueldo = sueldo – (0.30*sueldo);
}
}
¿Te das cuenta? Tenemos dos personas diferentes, que nada tienen que ver la una
con la otra, excepto una cosa: que ambas son trabajadoras. Así, deben pagar impuestos.
Pero fíjate que cada una lleva a cabo ese pago de forma diferente. La Interface
Trabajador no determina cómo va a llevarse a cabo tal pago, solamente exige que
se lleve a cabo.
Bueno, creo que hasta ahora todo va más o menos bien, ¿no? Ahora vamos a por
lo mejor de las interfaces.
¿Lo has pillado? En realidad, el objetivo de una interface es que una clase A
pueda comunicarse con otra clase B independientemente de cómo B esté escrita. Por
ejemplo: supón que ahora definimos la clase Hacienda. Esta clase exigirá a todos los
trabajadores que paguen sus impuestos. Pero hay miles de trabajadores distintos, unos
son funcionarios, otros presentadores, pero otros son otras cosas. Sin embargo,
Hacienda juega con un as en la manga: como hemos exigido que cada trabajador, sea
como sea, implemente a la interface Trabajador, podemos referenciar a cualquier
clase que implemente a Trabajador mediante una referencia de tipo Trabajador:
class Hacienda {
.
.
.
void quePagueElCurrante(Trabajador currante) {
currante.pagarImpuestos();
}
}
Joer, qué cosa. Resulta que al método quePagueElCurrante le pasamos como
parámetro cualquier objeto que implemente a la interface Trabajador, y éste método se
encarga de hacer que pague. ¿Te fijas? Primero: no nos importa quñe tipo de trabajador
sea, sólo nos importa que implemente a Trabajador. Segundo: La referencia a este
objeto es de tipo Trabajador, no es ni Presentador ni Funcionario ni nada.
El mecanismo de la referencia de tipo Trabajador es semejante a la señora
Referencia que vimos en la Herencia: es una flechita que apunta sólo a los contenidos de
Trabajador, el resto de los contenidos ni los ve. Es como si lo único importante de un
trabajador fuese que pague impuestos.
Ahora, en un programa podemos hacer:
Funcionario Fulgencio = new Funcionario();
Presentador JesusVazquez = new Funcionario();
Hacienda malditaHacienda = new Hacienda();
malditaHacienda.quePagueElCurrante( Fulgencio );
malditaHacienda.quePagueElCurrante( JesusVazquez );
Bueno, ahora, si lo has entendido todo, te preguntarás la diferencia entre
Interfaces y Herencia. Bueno, esta es tal vez la mejor pregunta que puedas hacer.
Cuando una clase hereda de otra, hereda todos sus contenidos. En las Interfaces
no se definen contenidos, por lo que no se puede hereda ningún contenido. Una interface
no es una clase, como lo son las superclases en la herencia, por lo que una interface no
se puede instanciar:
Trabajador yop = new Trabajador(); // lo peorcito.
Una clase puede implementar varias interfaces a la vez, pero no puede heredar de
varias clases a la vez. NO EXISTE LA (maravillosa) HERENCIA MÚLTIPLE EN
JAVA, diga lo que diga tu profesor (vale, pero si te lo pregunta en el examen responde
lo que él diga!). Las Interfaces pueden parecer un mecanismo de herencia múltiple, pero
no es así en realidad.
Supongo que tu profesor de dará más razones para diferenciar interfaces de
herencia. Yo sólo te digo las más representativas (para mí).
Hala. Repasa los conceptos de
HERENCIA (ea, fastidiate)
INTERFACE
USO DE IMPLEMENTS
USO DE REFERENCIAS DEL TIPO DE UNA INTERFACE
DIFERENCIAS ENTRE HERENCIA E INTERFACES
¿Te doy una sorpresa?
Ya hemos acabado.
Epílogo
Es una pena, me gusta pensar en un epílogo como en el final de un libro de
aventuras y no de un cursillo rápido de OOP y Java.
Hemos visto cómo es la estructura de la OOP: clases y objetos. Hemos visto
cómo se llevan a cabo en Java. Hemos visto programación estructurada, es decir,
sentencias de control: bucles y sentencias condicionales, la fatal historia de la
recursividad, herencia, casting, interfaces.... y poco más.
Desgraciadamente hay otros puntos que no he tocado, porque realmente son un
tanto innecesarios porque ocuparían un hueco que no es necesario hacer: clases
abstractas, tipos básicos de datos, clases para acceso a disco (los asquerosos flujos o
streams), la estúpida BufferedString.... en fin, unas cuantas cosas que no son realmente
necesarias: tu profesor te las enseñará durante tres horas cada una de ellas, cuando no
son necesarios más de diez minutos.
¿Por qué estas lagunas? Muy sencillo: porque mi intención no es enseñarte Java
a fondo, (eso lo hará el “mastering Java” se la editorial SUN) sino enseñarte a
programar en OOP. ¿Y eso? Fácil: si sabes programar, entenderás cualquier cosa
fácilmente. Cuando hayas leído todo este mamotreto de letras y dibujos, y cuando lo
entiendas a la perfección, no te costrará aprender a usar flujos de entrada y salida en
disco, manejo de excepciones, acceso restringido a miembros de clase, o yo qué sé.
Es decir, que aprendas primero a caminar... para luego echar a correr.
Tal vez algún día escribas tú la segunda parte de “Java para aprobar” explicando
todo lo que me he dejado atrás. Sería muy bonito. Si quieres, te paso los JPG del pez, el
pato y el pajaro ese que no sé cómo se llama.
Bueno, aquí acabamos. Espero que no se te haya hecho muy tedioso (jeeee, que
iluso soy). Pásalo bien, estudia mucho, pregunta lo que quieras y sácale partido a todo
esto, ¿vale?
David Muñoz Díaz
Grupo de Usuarios de Linux de la
Universidad Carlos III de Madrid
[email protected]
The Stone Soup Story
Once upon a time, somewhere in Eastern Europe, there was a great famine.
People jealously hoarded whatever food they could find, hiding it even from
their friends and neighbors. One day a peddler drove his wagon into a village,
sold a few of his wares, and began asking questions as if he planned to stay
for the night.
"There's not a bite to eat in the whole province," he was told. "Better keep
moving on."
"Oh, I have everything I need," he said. "In fact, I was thinking of making
some stone soup to share with all of you." He pulled an iron cauldron from his
wagon, filled it with water, and built a fire under it. Then, with great
ceremony, he drew an ordinary-looking stone from a velvet bag and dropped it
into the water.
By now, hearing the rumor of food, most of the villagers had come to the
square or watched from their windows. As the peddler sniffed the "broth" and
licked his lips in anticipation, hunger began to overcome their skepticism.
"Ahh," the peddler said to himself rather loudly, "I do like a tasty stone
soup. Of course, stone soup with CABBAGE -- that's hard to beat."
Soon a villager approached hesitantly, holding a cabbage he'd retrieved from
its hiding place, and added it to the pot. "Capital!" cried the peddler. "You
know, I once had stone soup with cabbage and a bit of salt beef as well, and
it was fit for a king."
The village butcher managed to find some salt beef...and so it went, through
potatoes, onions, carrots, mushrooms, and so on, until there was indeed a
delicious meal for all. The villagers offered the peddler a great deal of
money for the magic stone, but he refused to sell and traveled on the next
day. And from that time on, long after the famine had ended, they reminisced
about the finest soup they'd ever had.
(tomado de un documento de Monte Davis)
Descargar