7 - Polimorfismo.mdi

Anuncio
7: Polimorfismo
El polimorfismo es la tercera característica esencial de los lenguajes de
programación orientados a objetos, después de la abstracción de datos y la
herencia.
Esto aporta otra dimensión para separar interfaz de implementación, para separar
el qué del cómo. El polimorfismo nos permite mejorar la organización y
comprensión del código así como la creación de programas extensibles que pueden
"crecer" no sólo durante la creación del proyecto, sino también cuando se diseñen
nuevas características.
La encapsulación crea un nuevo tipo de datos combinando características y
comportamientos. La implementación oculta la interfaz de la implementación
haciendo los detalles privados. Esta clase de organizaci ón mecánica tiene sentido
para alguien que proceda de un lenguaje procedimental. Pero el polimorfismo trata
del desacoplamiento de en términos de tipos . En él ultimo capitulo se vio cómo la
herencia permite el tratamiento de un objeto como su propio tipo o su tipo base.
Esta habilidad es crítica ya que permite que muchos tipos (derivados del mismo
tipo base) sean creados como si fueran uno solo, y una sola pieza de código
funcionará con todos estos tipos diferentes de la misma forma. La llamada a un
método polimórfico permite a un tipo expresar su diferencia con otro similar,
ambos derivados de la misma clase. Esta distinción es expresada a través de
diferencias en el comportamiento de los métodos a los que podemos invocar a
través de la clase.
En este capitulo aprenderemos a cerca del polimorfismo (también llamado
enlazado dinámico, enlazado diferido o enlazado en tiempo de ejecución )
comenzando por lo básico, con simples ejemplos en los que lo fundamental es el
comportamiento polimórfico del programa.
Conversión ascendente
En el capitulo 6 mostramos como puede usarse un objeto como su propio tipo o
como un objeto de su tipo base. Tomar un manejador de objeto y tratarlo como el
manejador de su tipo base se denomina upcasting (conversión ascendente), dado
que el árbol de herencia se dibujado con la clase base en la cima.
En el siguiente ejemplo mostramos un problema importante que surge: (ver la
página XX si tiene problemas ejecutando este programa)
//: Music.java
// Inheritance & upcasting
package c07;
class Note {
private int value;
private Note(int val) { value = val; }
public static final Note
middleC = new Note(0),
cSharp = new Note(1),
cFlat = new Note(2);
} // Etc.
class Instrument {
public void play(Note n) {
System.out.println("Instrument.play()");
}
}
// Wind objects are instruments
// because they have the same interface:
class Wind extends Instrument {
// Redefine interface method:
public void play(Note n) {
System.out.println("Wind.play()");
}
}
public class Music {
public static void tune(Instrument i) {
// ...
i.play(Note.middleC);
}
public static void main(String[] args) {
Wind flute = new Wind();
tune(flute); // Upcasting
}
} ///:~
El método Music.tune() acepta un manejador instrumental , pero también
cualquier tipo derivado de un instrumento. En la función main() podemos ver
como se pasa el manejador Wind al método tune() sin necesidad de cast. Esto es
aceptable; en Wind debe existir el interface de Instrument , ya que Wind lo
hereda de él. La conversión hacia arriba desde Wind hacia Instrument puede
"estrechar" el interfaz, pero no puede hacer nada menos el interface completo de
Instrument .
¿Por qué upcast?
Este programa puede parecer extraño. ¿Por qué debe alguien olvidar
intencionadamente el tipo de un objeto? Esto es lo que sucede cuando se realiza el
upcast, y parece que seria mucho más simple si tune() simplemente el manejador
Wind como su argumento. Esto trae un punto esencial: suponga que necesita
escribir un nuevo tune() para cada tipo de Instrument en su sistema. Suponga
que seguimos ese razonamiento y añadimos instrumentos de cuerdas y metálicos:
//: Music2.java
// Overloading instead of upcasting
class Note2 {
private int value;
private Note2(int val) { value = val; }
public static final Note2
middleC = new Note2(0),
cSharp = new Note2(1),
cFlat = new Note2(2);
} // Etc.
class Instrument2 {
public void play(Note2 n) {
System.out.println("Instrument2.play()");
}
}
class Wind2 extends Instrument2 {
public void play(Note2 n) {
System.out.println("Wind2.play()");
}
}
class Stringed2 extends Instrument2 {
public void play(Note2 n) {
System.out.println("Stringed2.play()");
}
}
class Brass2 extends Instrument2 {
public void play(Note2 n) {
System.out.println("Brass2.play()");
}
}
public class Music2 {
public static void tune(Wind2 i) {
i.play(Note2.middleC);
}
public static void tune(Stringed2 i) {
i.play(Note2.middleC);
}
public static void tune(Brass2 i) {
i.play(Note2.middleC);
}
public static void main(String[] args) {
Wind2 flute = new Wind2();
Stringed2 violin = new Stringed2();
Brass2 frenchHorn = new Brass2();
tune(flute); // No upcasting
tune(violin);
tune(frenchHorn);
}
} ///:~
Esto funciona, aunque hay mejores formas: Debemos escribir el tipo especifico del
método para cada clase del nuevo Intrument2 que se añada.
Esto significa que programar más en el primer lugar, pero también significa que si
se necesita añadir un nuevo método como tune() o un nuevo tipo de instrumento,
tienes mucho trabajo que hacer. Añadir el echo que el compilador no dará
mensajes de error si olvida sobrecargar uno de tus métodos y todo el proceso de
trabajo con los tipos no se puedan manejar.
¿No será muy agradable si solo escribe un método simple que toma como clase
base como su argumento, y ninguno de las derivadas clases especificas? ¿Esto es,
no será agradable si se olvida que hay clases derivadas, y escribe el código para
hablar solo con las clases base?
Esto es exactamente lo que el polimorfismo te permite hacer. De todos modos, la
mayoría de los programadores (que proceden de programación procedural) tienen
un pequeño problema con la forma que trabaja el polimorfismo.
El enriedo
La dificultad con Music.java pueden verse ejecutando el programa. La salida es
Wind.play() Este es una salida "limpia", pero no parece que funcione de esa forma.
Mire el método tune():
public static void tune(Instrument i) {
// ...
i.play(Note.middleC);
}
Este recibe un manejador Instument. ¿De esta forma como puede saber el
compilador que este manejador Instrument apunta a viento en este caso y no a
metálicos o de cuerda? El compilador no puede saberlo. Para tomar un profundo
conocimiento de la fuente, es útil examinar el tema de binding .
Enlazado en las llamadas a métodos
Conectar una llamada de un método con el cuerpo de un método se denomina
enlazado. Cuando se binding se permite antes de ejecutar el programa (por el
compilador y linkador, si hay alguno), es llamado primero binding . Puede que no
haya oído el termino antes por que nunca ha sido una opción con lenguajes
procedurales. Los compiladores de "C" solo tienen un tipo de llamada a métodos, y
esta es antes de binding .
La parte confusa de arriba del programa resuelve a cerca del early binding por que
el compilador no puede saber el método correcto para llamar cuando este solo
tiene un manejador Instument.
La solución se llama late binding , que significa que el binding ocurre en tiempo de
ejecución basado en el tipo de objeto. Late binding también es llamado binding
dinámico o tiempo de ejecución binding . Cuando un lenguaje implementa late
binding , debe haber algún mecanismo para determinar el tipo de un objeto en
tiempo de ejecuci ón y llamar al apropiado método. Esto es, el compilador todavía
no saber el tipo del objeto, pero el mecanismo de llamada al método busca y llama
el cuerpo del método correcto. El mecanismo late-binding varia de lenguaje a
lenguaje, pero puede imaginar que algunos tipos de información deben ser
instalados en los objetos.
Todos los métodos binding en Java usa late binding menos un m étodo que ha sido
declarado final. Esto significa que normalmente no necesita tomar ninguna
decisión a cerca del cuando ocurrirá el late binding . (Sucederá automáticamente)
¿Por que se declara un método final? Como habrá notado en el ultimo capitulo,
este previene de el sobreescribir métodos. Quizás mas importante, es
efectivamente "turns off" dynamic binding , o mejor este dice al compilador que
dynamic binding no es necesario. Esto permite al compilador generar un código
mas eficiente para llamadas a métodos finales.
Produciendo el comportamiento correcto
Una vez que se sabe que todos los binding en Java suceden polimorficamente vía
late binding , puedes escribir tu código para hablar a clases base y saber que
todos las casos de clases derivadas funcionaran correctamente usando el mismo
código. O para ponerlo de otra forma, tu mandas un mensaje a un objeto y dejas
que el objeto escoge la correcta cosa a hacer.
El clásico ejemplo en OOP es el ejemplo de las formas. Esto es normalmente usado
por que es fácil visualizarlo, pero desafortunadamente puede confundir a los
programadores noveles pensando que OOP es programación solamente para
gráficos, que por supuesto no es el caso.
El ejemplo de las formas tiene una clase base llamada Shape y varios tipos
derivados: Circle, Square, Triangle, etc. La razón del ejemplo funciona por lo fácil
que es decir "Circle es un tipo de Shape" y ser entendido. El diagrama de herencia
nos enseña las relaciones:
El upcast puede ocurrir en una sentencia tan simple como:
Shape s = new Circle();
Aquí, un objeto Circle es creado y el manejador resultante es inmediatamente
asignado a Shape que puede parecer ser un error (asignando un tipo a otro) y
esta bien por que un es un "Shape" por la herencia. Por eso el compilador añade
con las sentencias no da ningún mensaje de error.
Cuando se llama a un método de clase base (que ha sido sobrescrito en las clases
derivadas):
s.draw();
De nuevo, se puede esperar que draw() de Shape es llamado por que esto es un
manejador Shape, por eso, ¿como puede el compilador saber hacer algo mas? Y
todavía el propio Circle.draw() es llamado por el late binding (polimorfismo). El
ejemplo siguiente lo hace de una forma diferente:
//: Shapes.java
// Polymorphism in Java
class Shape {
void draw() {}
void erase() {}
}
class Circle extends Shape {
void draw() {
System.out.println("Circle.draw()");
}
void erase() {
System.out.println("Circle.erase()");
}
}
class Square extends Shape {
void draw() {
System.out.println("Square.draw()");
}
void erase() {
System.out.println("Square.erase()");
}
}
class Triangle extends Shape {
void draw() {
System.out.println("Triangle.draw()");
}
void erase() {
System.out.println("Triangle.erase()");
}
}
public class Shapes {
public static Shape randShape() {
switch((int)(Math.random() * 3)) {
default: // To quiet the compiler
case 0: return new Circle();
case 1: return new Square();
case 2: return new Triangle();
}
}
public static void main(String[] args) {
Shape[] s = new Shape[9];
// Fill up the array with shapes:
for(int i = 0; i < s.length; i++)
s[i] = randShape();
// Make polymorphic method calls:
for(int i = 0; i < s.length; i++)
s[i].draw();
}
} ///:~
La clase base Shape establece el interface común a todos los afectados por la
herencia de Shape, esto es, todos los "shapes" pueden ser dibujados y borrados.
Las clases derivados sobrescritas estas definiciones para provee un único
comportamiento para cada especifico tipo de Shape.
La clase principal Shape continúe un método estático randShape() que produce un
manejador a un objeto shape elegido aleatoriamente cada vez que se le llama.
Notar que el upcasting sucede en las sentencias de retorno, que se toma un
manejador a Circle, Squeare, o Triangle y lo envía fuera del método como un tipo
devuelto, Shape. Por eso siempre que se llame a este método nunca se obtendrá
un cambio para ver que tipo especifico es, desde que se retorna un manejador
plain Shape. Main() contiene un array de manejadores Shape llenos de llamadas a
randShape(). En este punto sabemos que tenemos Shapes, pero no sabemos nada
mas especifico. De cualquier forma, cuando se avanza hacia este array y se llama
Draw() para cada uno, el correcto tipo especifico comportamiento mágicamente
ocurre, como se puede ver de la salida del siguiente ejemplo:
Circle.draw()
Triangle.draw()
Circle.draw()
Circle.draw()
Circle.draw()
Square.draw()
Triangle.draw()
Square.draw()
Square.draw()
Por supuesto, desde que todas las figuras son elegidas aleatoriamente cada vez, la
ejecución tendrá diferentes resultados. El punto de elegir las figuras
aleatoriamente es la unidad "home" el entendimiento que el compilador puede no
tener conocimientos especiales que le permitan hacer las llamadas correctas en
tiempo de compilación. Todas las llamadas a draw() se hacen a través de dynamic
binding .
Extensibilidad
Ahora volvamos al ejemplo de los instrumentos musicales. Por el polimorfismo
usted puede añadir tantos tipos nuevos como desee al sistema sin cambiar el
método tune(). En un programa POO bien diseñado, la mayoría o todos nuestros
métodos seguirán el modelo de tune() y se comunican solo con el interface de
clases base. Semejante a un programa extensible, ya que se puede añadir nuevas
funcionalidades por la heredación de nuevos tipos de datos procedentes de las
clases base comunes. Los métodos que manipulan el interface de las clases base
no necesitaran ser cambiados en absoluto para acomodarlo a las nuevas clases.
Considerar que sucede si tomamos el ejemplo de los instrumentos y añadir mas
métodos en las clases base y un numero de nuevas clases. Aquí esta el diagrama:
Todas estas nuevas clases funcionan correctamente con el método antiguo, sin
cambiar tune(). Incluso si "tune()" esta en un fichero separado y nuevos métodos
son añadidos al interface de Instrumentos tune() funciona correctamente sin
recompilar. Aquí esta la implementación del diagrama siguiente:
//: Music3.java
// An extensible program
import java.util.*;
class Instrument3 {
public void play() {
System.out.println("Instrument3.play()");
}
public String what() {
return "Instrument3";
}
public void adjust() {}
}
class Wind3 extends Instrument3 {
public void play() {
System.out.println("Wind3.play()");
}
public String what() { return "Wind3"; }
public void adjust() {}
}
class Percussion3 extends Instrument3 {
public void play() {
System.out.println("Percussion3.play()");
}
public String what() { return "Percussion3"; }
public void adjust() {}
}
class Stringed3 extends Instrument3 {
public void play() {
System.out.println("Stringed3.play()");
}
public String what() { return "Stringed3"; }
public void adjust() {}
}
class Brass3 extends Wind3 {
public void play() {
System.out.println("Brass3.play()");
}
public void adjust() {
System.out.println("Brass3.adjust()");
}
}
class Woodwind3 extends Wind3 {
public void play() {
System.out.println("Woodwind3.play()");
}
public String what() { return "Woodwind3"; }
}
public class Music3 {
// Doesn't care about type, so new types
// added to the system still work right:
static void tune(Instrument3 i) {
// ...
i.play();
}
static void tuneAll(Instrument3[] e) {
for(int i = 0; i < e.length; i++)
tune(e[i]);
}
public static void main(String[] args) {
Instrument3[] orchestra = new Instrument3[5];
int i = 0;
// Upcasting during addition to the array:
orchestra[i++] = new Wind3();
orchestra[i++] = new Percussion3();
orchestra[i++] = new Stringed3();
orchestra[i++] = new Brass3();
orchestra[i++] = new Woodwind3();
tuneAll(orchestra);
}
} ///:~
Los nuevos métodos son what() que devuelve un manejador String con una
descripción de la clase y adjust() que da unas formas de ajustar cada instrumento.
En Main(), cuando se pone algo en el array Intrument3 automáticamente upcast el
inrumento3.
Puedes ver que el método tune() es dichosamente ignorante de todos los cambios
de código que ocurren por el, y todavía funciona correctamente. Esto es
exactamente lo que el polimorfismo es supuestamente provisto. Los cambios del
código no causaran daños a partes del programa que debería no ser afectado. De
otra forma, el polimorfismo es una de las mas importantes técnicas que permiten
al programador "separar las cosas que cambian de las cosas que continúan igual".
Sobreescritura vs. Sobrecarga
Echemosle un vistazo diferente al primer ejemplo de este capítulo. En el siguiente
programa el interfaz del método play() se cambia en el proceso de sobreescritura
del mismo, lo que significa que no hemos sobreescrito el método, sino que lo
hemos sobrecargado . Como el compilador permite la sobrecarga de métodos no
da ningún error, pero el comportamiento probablemente no es lo que
pretendiamos.
Aquí está el ejemplo:
//: WindError.java
// Accidentally changing the interface
class NoteX {
public static final int
MIDDLE_C = 0, C_SHARP = 1, C_FLAT = 2;
}
class InstrumentX {
public void play(int NoteX) {
System.out.println("InstrumentX.play()");
}
}
class WindX extends InstrumentX {
// OOPS! Changes the method interface:
public void play(NoteX n) {
System.out.println("WindX.play(NoteX n)");
}
}
public class WindError {
public static void tune(InstrumentX i) {
// ...
i.play(NoteX.MIDDLE_C);
}
public static void main(String[] args) {
WindX flute = new WindX();
tune(flute); // Not the desired behavior!
}
} ///:~
Aquí esta otro aspecto confuso que nos muestra el programa. En InstrumentX el
método play() recibe un int con el identificativo NoteX . Aunque NoteX es un
nombre de clase, también se puede usar como identificativo sin ningún problema.
Pero en WindX , play( ) recibe una referencia a NoteX que tiene el identificador
n . (No obstante también se podría haber puesto play(NoteX NoteX) sin que
produzca ningún error.) Parece como si el programador intentara sobreeescribir
play( ) , pero equivocandose un poco a la hora de escribirlo. El compilador, no
obstante, asume que lo que se pretendía era sobrecargar y no sobreescribir el
método. Notar que si seguimos la convención de nombres estándar de Java, el
identificador del argumento hubiera sido noteX ('n' minuscula), que lo distinguiría
del nombre de la clase.
En tune, el InstrumentX "i" es enviado el mensaje play(), con uno de los miembros
de NoteX (MIDDLE_C) como un argumento. Dado que NoteX contiene definiciones
enteras, esto significa que la versi ón entera del método ahora sobrecargado play()
es llamado, y como no ha sido sobreescrito la versión clase-base es usada.
La salida es:
InstrumentX.play()
Ciertamente esto no parece ser una llamada a un método polimórfico. Una vez se
entiende que esta sucediendo, usted puede arreglar el problema fácilmente, pero
imagine lo dificíl que puede ser encontrar el error si este está enterrado en un
programa de tamaño significante.
Clases y m étodos Abstractos
En todos los ejemplos de instrumentos, los métodos en la base de clase
instrumento hay siempre métodos de pruebas. Si esos métodos suenan llamadas
alguna vez, estarás haciendo algo erróneo. Esto es porque el intento de un
Instrumento crear un interface común para todos las clases derivadas de él.
La única razón para establecer este interface común es que pueda ser expresado
diferente para cada diferente subtipo. Esto establece una forma básica, por eso
puedes decir que es común a todas las clases derivadas. Otra forma de ver esto es
llamar a Intrument una clase base abstracta (o simplemente clase abstracta).
usted crea una clase abstracta cuando se quiere manipular un juego de clases a
través de un interface común. Todos los métodos derivados de clases que
emparejados la firma de la declaraci ón de clase base será usada por el mecanismo
de dynamic binding . (Sin embargo como hemos visto en la ultima sección, si el
nombre del método es el mismo que el de la clase base, pero los argumentos son
diferentes tendrás sobreescritura , que probablemente no es lo que quiere.)
Si tiene una clase abstracta como intrument, los objetos de esta clase no siempre
tendrá significado. Esto es, Intrument pretende expresar solo el interface y no una
implementación particular, por eso creando un objeto intrument no tiene sentido y
probablemente querrá prevenir el uso de hacer esto. Esto puede ser llevado a
cabo haciendo todos los métodos en el mensaje de error de Instrument, pero este
aplazar la información hasta el tiempo de ejecución y requiere un test confiable y
exhaustivo en la parte de usuario. Esto es siempre mejor coger problemas en
tiempo de compilación.
Java provee un mecanismo para hacer estos métodos llamados abstractos. Este es
un método que esta incompleto; tiene solo la declaraci ón y no tiene cuerpo del
método. Aquí esta la sintaxis para la declaración de un método abstracto:
abstract void X();
Una clase que contiene métodos abstractos se llama clase abstracta. Si una clase
contiene uno o mas métodos abstractos, la clase debe ser calificada como
abstracta. (De otro modo el compìlador dará un mensaje de error.)
Si una clase abstracta esta incompleta, ¿que supondrá el compilador que debe
hacer cuando alguien intente hacer un objeto de esa clase? Seguramente no podrá
crear un objeto de una clase abstracta, por eso dará un error el compilador. De
este modo el compilador se asegura de la pureza de la clase abstracta y no
necesitaras preocuparse por usarla mal.
Se hereda de una clase abstracta y quieres hacer objetos de un tipo nuevo,
deberás proporcionar las definiciones de los métodos para todos los métodos
abstractos en la clase base. Si no lo hace (puede no hacerlo) entonces las clases
derivadas serán también abstractos y el compilador le obligará a identificar la
clase como abstracta con la palabra "abstract".
Es posible declarar una clase como abstracto sin incluir ningún método abstracto.
Esto es útil cuando se tiene una clase en la que no se hace buen uso para tener
ningún método abstracto y todavía quieres prevenir cualquier ejemplo de esa
clase.
La clase instrument puede ser cambiado fácilmente a una clase abstracto. Solo
algunos de los métodos serán abstractos, desde que haciendo una clase abstracta
no se fuerza a hacer todos los métodos abstractos. Aquí esta como aparece:
Aquí esta el ejemplo orquesta modificado para usar clases y métodos abstractos:
//: Music4.java
// Abstract classes and methods
import java.util.*;
abstract class Instrument4 {
int i; // storage allocated for each
public abstract void play();
public String what() {
return "Instrument4";
}
public abstract void adjust();
}
class Wind4 extends Instrument4 {
public void play() {
System.out.println("Wind4.play()");
}
public String what() { return "Wind4"; }
public void adjust() {}
}
class Percussion4 extends Instrument4 {
public void play() {
System.out.println("Percussion4.play()");
}
public String what() { return "Percussion4"; }
public void adjust() {}
}
class Stringed4 extends Instrument4 {
public void play() {
System.out.println("Stringed4.play()");
}
public String what() { return "Stringed4"; }
public void adjust() {}
}
class Brass4 extends Wind4 {
public void play() {
System.out.println("Brass4.play()");
}
public void adjust() {
System.out.println("Brass4.adjust()");
}
}
class Woodwind4 extends Wind4 {
public void play() {
System.out.println("Woodwind4.play()");
}
public String what() { return "Woodwind4"; }
}
public class Music4 {
// Doesn't care about type, so new types
// added to the system still work right:
static void tune(Instrument4 i) {
// ...
i.play();
}
static void tuneAll(Instrument4[] e) {
for(int i = 0; i < e.length; i++)
tune(e[i]);
}
public static void main(String[] args) {
Instrument4[] orchestra = new Instrument4[5];
int i = 0;
// Upcasting during addition to the array:
orchestra[i++] = new Wind4();
orchestra[i++] = new Percussion4();
orchestra[i++] = new Stringed4();
orchestra[i++] = new Brass4();
orchestra[i++] = new Woodwind4();
tuneAll(orchestra);
}
} ///:~
Puede ver que no hay realmente cambio excepto en la clase base. Esto es útil para
crear clases y métodos abstractos por que hacen la abstracci ón de una clase
concreta y dicen tanto al usuario como al compilador como debe ser usado.
Interfaces
La palabra "clave" interface toma un concepto abstracto un paso más lejano.
Podría pensarse que es como una clase abstracta "pura". Esto permite al creador
establecer la forma para una clase: nombre de los métodos, lista de argumentos y
tipos devueltos, pero no los cuerpos de los métodos. Un interface puede también
contener datos de tipos primitivos, pero estos son implícitamente estáticos y final.
Un interface provee solo una forma, pero no implementación.
Un interface dice: "esto es que todas las clases que implementan este particular
interface se parecerán". Por eso ningún código que use un interface particular sabe
que métodos deben ser llamados por ese interface, y esto es todo. Por lo que el
interface es usado para establecer un "protocolo" entre clases. (Algunos lenguajes
de programación orientados o objetos tienen una palabra reservada llamada
"protocol" para hacer lo mismo.
Para crear un interface, usar la palabra interface en vez de class. Como una clase,
puedes añadir la palabra public antes de interface (pero solo si el interface esta
definido en un fichero del mismo nombre) o dejarlo para darle el estado
"amistoso".
Para hacer una clase que conforma un interface particular (o grupo de interfaces)
usar la palabra implements. Con esto se esta diciendo: "El interface es lo que
parece y aquí esta como funciona". otro como este, lleva un parecido fuerte para
heredar. El diagrama para el ejemplo de instrumentos es este:
Una vez implementado un interface, esa implementaci ón comienza una clase
ordinaria que puede ser extendida a una forma regular.
Usted puede elegir declarar explícitamente las declaraciones del método en un
interface como publico. Pero serán públicos incluso si no se le indica. Por eso
cuando se implementa un interface, los métodos del interface deben ser definidos
como públicos. De otro modo serán por defecto "amistosos" y podría ser
restringido el acceso a un método durante la herencia, que no es permitida por el
compilador de Java.
Puedes ver en esta versión modificada del ejemplo de instrumentos. Apuntar que
todo método en el interface es estrictamente una declaración, que es lo único que
el compilador permite. Además, ninguno de los métodos en Intrument5 son
declarados como públicos, pero ellos son públicos automáticamente:
//: Music5.java
// Interfaces
import java.util.*;
interface Instrument5 {
// Compile-time constant:
int i = 5; // static & final
// Cannot have method definitions:
void play(); // Automatically public
String what();
void adjust();
}
class Wind5 implements Instrument5 {
public void play() {
System.out.println("Wind5.play()");
}
public String what() { return "Wind5"; }
public void adjust() {}
}
class Percussion5 implements Instrument5 {
public void play() {
System.out.println("Percussion5.play()");
}
public String what() { return "Percussion5"; }
public void adjust() {}
}
class Stringed5 implements Instrument5 {
public void play() {
System.out.println("Stringed5.play()");
}
public String what() { return "Stringed5"; }
public void adjust() {}
}
class Brass5 extends Wind5 {
public void play() {
System.out.println("Brass5.play()");
}
public void adjust() {
System.out.println("Brass5.adjust()");
}
}
class Woodwind5 extends Wind5 {
public void play() {
System.out.println("Woodwind5.play()");
}
public String what() { return "Woodwind5"; }
}
public class Music5 {
// Doesn't care about type, so new types
// added to the system still work right:
static void tune(Instrument5 i) {
// ...
i.play();
}
static void tuneAll(Instrument5[] e) {
for(int i = 0; i < e.length; i++)
tune(e[i]);
}
public static void main(String[] args) {
Instrument5[] orchestra = new Instrument5[5];
int i = 0;
// Upcasting during addition to the array:
orchestra[i++] = new Wind5();
orchestra[i++] = new Percussion5();
orchestra[i++] = new Stringed5();
orchestra[i++] = new Brass5();
orchestra[i++] = new Woodwind5();
tuneAll(orchestra);
}
} ///:~
El resto del código funciona lo mismo. No importa si esta upcasting a una clase
"regular" llamada Intrument5, una clase abstracta llamada Instrument5, o a un
interface llamado Intrument5. La acción es la misma. De echo puede ver en el
método tune() que no hay ninguna evidencia a cerca tanto si instrument5 es una
clase "regular", una clase abstracta o un interface. Esto es el intento: cada entrada
da al programador diferentes controles sobre la forma de que son creados y
usados los objetos.
Herencia múltiple en Java
El interface no es simplemente una forma pura mas de una clase abstracta. Tiene
un propósito mayor que esto. Por que un interface no tiene implementación,
entonces, no hay storage asociado con el interface -no hay nada para impedir
interfaces siendo combinados. Esto es evaluable por que hay veces cuando se
necesita decir: "Una x es una a y a b y a c " En C++ esta acción se hace
combinando múltiples interfaces de clases y se llama herencia múltiple, y este
conlleva algunos raras consecuencias por que cada clase puede tener una
implementación. En Java, puedes permitir el mismo acto, pero solo una de las
clases puede tener una implementaci ón, por eso los problemas vistos en C++ no
ocurren con Java cuando se combinan múltiples interfaces:
En una clase derivada no estas obligado a tener una clase base que sea o un
abstracto o "concreto" (que no tiene métodos abstractos). Si usted hereda desde
un no-interface, puedes heredar de uno solo. El resto de los elementos básicos
deben ser interfaces. Se colocan todos los nombres de interfaces después de la
palabra implements y separados por comas. Puedes tener tantos interfaces como
quieras y cada uno trae un tipo independiente que puedes upcast también. El
ejemplo siguiente muestra una clase concreta combinada con varios interfaces
para producir una clase nueva:
//: Adventure.java
// Multiple interfaces
import java.util.*;
interface CanFight {
void fight();
}
interface CanSwim {
void swim();
}
interface CanFly {
void fly();
}
class ActionCharacter {
public void fight() {}
}
class Hero extends ActionCharacter
implements CanFight, CanSwim, CanFly {
public void swim() {}
public void fly() {}
}
public class Adventure {
static void t(CanFight x) { x.fight(); }
static void u(CanSwim x) { x.swim(); }
static void v(CanFly x) { x.fly(); }
static void w(ActionCharacter x) { x.fight(); }
public static void main(String[] args) {
Hero i = new Hero();
t(i); // Treat it as a CanFight
u(i); // Treat it as a CanSwim
v(i); // Treat it as a CanFly
w(i); // Treat it as an ActionCharacter
}
} ///:~
Se puede ver que Hero combina la clase concreta ActionCharacter con los
interfaces CanFight, CanSwin, y CanFly. Cuando se combina una clase concreta
con interfaces esta forma, la clase concreta debe ir primero, después los
interfaces. (El compilador dará error si no es así.)
Notar que la firma para fight() es el mismo en el interface CanFight y la clase
ActionCharacter, y que fight() no es provista con una definici ón en Hero. La regla
para un interface es que tu puedes heredar de él (como se verá pronto), pero
entonces tienes otro interface. Si quiere crear un objeto de un nuevo tipo, este
debe ser una clase con todas las definiciones provistas. Incluso si Hero no provee
explícitamente una definición para fight(), la definición viene con Action Character
por eso es automáticamente provista y es posible crear objetos de Hero.
En la clase Adventure, puedes ver que hay cuatro métodos que toman como
argumento los diferentes interfaces y la clase concreta. Cuando un objeto Hero es
creado, este puede ser pasado a cualquiera de esos métodos, que significa que se
esta upcast a cada interface in turn . Por que la forma de los interfaces son
diseñados en Java, estos funcionan sin tropiezos y sin ningún particular esfuerzo
por parte del programador.
Conserva en mente que the core reason para interfaces se muestra en el ejemplo
de abajo: para poder upcast a mas de un tipo base. Como sea, la segunda razón
para usar interfaces es el mismo que usando una clase base abstracta: para
prevenir al cliente programador de hacer un objeto de esta clase y establecer que
solo es un interface. Esto nos trae una pregunta: ¿Usarías un interface o una clase
abstracta? Un interface te da los beneficios de una clase abstracta y los beneficios
de un interface, por eso es posible crear tu clase base sin definiciones de métodos
o variables podrías preferir siempre interfaces que clases abstractas. De echo, si
sabes algo será una clase base, tu primera elección podría ser hacer un interface,
y solo si te ves forzado a tener definiciones de métodos o variables cambiarías a
una clase abstracta.
Extendiendo un interface con herencia
Usted puede añadir fácilmente nuevas declaraciones de métodos a un interface
usando la herencia y puedes también combinar varios interfaces en un nuevo
interface con la herencia. En ambos casos obtendrás un nuevo interface, como se
ve en este ejemplo:
//: HorrorShow.java
// Extending an interface with inheritance
interface Monster {
void menace();
}
interface DangerousMonster extends Monster {
void destroy();
}
interface Lethal {
void kill();
}
class DragonZilla implements DangerousMonster {
public void menace() {}
public void destroy() {}
}
interface Vampire
extends DangerousMonster, Lethal {
void drinkBlood();
}
class HorrorShow {
static void u(Monster b) { b.menace(); }
static void v(DangerousMonster d) {
d.menace();
d.destroy();
}
public static void main(String[] args) {
DragonZilla if2 = new DragonZilla();
u(if2);
v(if2);
}
} ///:~
Dangerous Monster es una extensión de Monster que produce un nuevo interface.
Esto es implementado en DragonZilla.
La sintaxis usada en Vampire funciona solamente cuando heredas interfaces.
Normalmente, puedes usar extendidos con solo una clase simple, pero desde un
interface puede ser echo desde otros múltiples interfaces, extendidos pueden
referirse a interfaces múltiples base cuando se construye un nuevo interface.
Como puedes ver los nombres del interface son simplemente separados con
comas.
Agrupando constantes
Por que ningún campo que pongas en un interface será automáticamente estático
y final, el interface es una utilidad conveniente para crear grupos de valores
constantes, como una enumeraci ón en C o C++, por ejemplo:
//: Months.java
// Using interfaces to create groups of constants
package c07;
public interface Months {
int
JANUARY = 1, FEBRUARY = 2, MARCH = 3,
APRIL = 4, MAY = 5, JUNE = 6, JULY = 7,
AUGUST = 8, SEPTEMBER = 9, OCTOBER = 10,
NOVEMBER = 11, DECEMBER = 12;
} ///:~
Notar que el estilo de Java para usar las letras mayúsculas (con underscores para
separar múltiples palabras con un identificador simple) para las primitivas finales
estáticas que tienen inicializadores constantes que es constantes en tiempo de
compilación.
Los campos en un interface son públicos automáticamente, por eso no es
necesario especificarlo.
Ahora puede usar las constantes importándolas del paquete c07.* o c07.Months o
de cualquier otro paquete, y referenciando los valores con expresiones como
Months.JANUARY. Pr supuesto, que tendrás un entero por eso no hay el tipo extra
seguro que C++'s enum tenia, pero esta (usada comúnmente) técnica que es
ciertamente un desarrollo del hard-coding numbers en tus programas. (Esto suele
ser referido para usar "números mágicos" y estos producen un código muy difícil
de mantener) Si quiere un tipo seguro extra, puede construir una clase como esta:
(Esta aproximación fue inspirada por un e-mail de Rich Hoffarth.)
//: Month2.java
// A more robust enumeration system
package c07;
public final class Month2 {
private String name;
private Month2(String nm) { name = nm; }
public String toString() { return name; }
public final static Month2
JAN = new Month2("January"),
FEB = new Month2("February"),
MAR = new Month2("March"),
APR = new Month2("April"),
MAY = new Month2("May"),
JUN = new Month2("June"),
JUL = new Month2("July"),
AUG = new Month2("August"),
SEP = new Month2("September"),
OCT = new Month2("October"),
NOV = new Month2("November"),
DEC = new Month2("December");
public final static Month2[] month = {
JAN, JAN, FEB, MAR, APR, MAY, JUN,
JUL, AUG, SEP, OCT, NOV, DEC
};
public static void main(String[] args) {
Month2 m = Month2.JAN;
System.out.println(m);
m = Month2.month[12];
System.out.println(m);
System.out.println(m == Month2.DEC);
System.out.println(m.equals(Month2.DEC));
}
} ///:~
La clase se llama Month2 desde que hay un mes en la librería standard de Java.
Este es una clase final con un constructor privado por eso ninguno puede heredar
de el o hacer ningún ejemplo de él. El único ejemplo es el final estático creado en
la clase él mismo: JAN, FEB, MAR, etc. Estos objetos son también usados en el
array Month, que le deja elegir meses por el número en vez del por el nombre.
(Notar el extra JAN en el array para proveer un offset por uno, por eso que
Diciembre es el mes 12) En main() puedes ver el tipo safety: m que es un objeto
Month2 por lo que puede ser asignado solo a Month2. El anterior ejemplo
Months.java provisto solo valores enteros, que no era demasiado seguro.
Este ejemplo también te permite usar == o "equals()" intercambiables, como se
muestra al final de main().
Inicializando campos en interfaces
Los campos definidos en los interfaces son automáticamente estáticos y finales.
Estos no pueden ser "blank finals", pero pueden ser inicializados con expresiones
no constantes. Por Ejemplo:
//: RandVals.java
// Initializing interface fields with
// non-constant initializers
import java.util.*;
public interface RandVals {
int rint = (int)(Math.random() * 10);
long rlong = (long)(Math.random() * 10);
float rfloat = (float)(Math.random() * 10);
double rdouble = Math.random() * 10;
} ///:~
Desde que los campos son estáticos, son inicializados cuando la clase es cargada
por primera vez, incluso el primer acceso a cualquier campo. Un simple test:
//: TestRandVals.java
public class TestRandVals {
public static void main(String[] args) {
System.out.println(RandVals.rint);
System.out.println(RandVals.rlong);
System.out.println(RandVals.rfloat);
System.out.println(RandVals.rdouble);
}
} ///:~
Los campos, por supuesto, no son parte del interface pero en vez de esto son
almacenados en el área de almacenamiento estático para ese interface.
Clases internas
En Java 1.1 es posible poner una definición de clase sin otra definici ón de clase.
Este es llamado una clase internar. La clase interna es un rasgo hasta por que
permite agrupar clases que lógicamente pertenecen juntos y controlar la visibilidad
de uno sin otro. Como sea, es importante entender que las clases internas son
distintas de la composición.
Con frecuencia, mientras se esta aprendiendo a cerca de ellas, la necesidad por las
clases internas no es obvia inmediatamente. Al final de esta sección, después de
todas las sintaxis y semánticas de las clases internas sean descritas, encontrarás
un ejemplo que dará una idea clara de los beneficios de estas clases.
Al crear una clase interna podrías esperar: poniendo la definición de la clase
dentro de la clase vecina (Ver página 94 si tiene problemas ejecutando este
programa)
//: Parcel1.java
// Creating inner classes
package c07.parcel1;
public class Parcel1 {
class Contents {
private int i = 11;
public int value() { return i; }
}
class Destination {
private String label;
Destination(String whereTo) {
label = whereTo;
}
String readLabel() { return label; }
}
// Using inner classes looks just like
// using any other class, within Parcel1:
public void ship(String dest) {
Contents c = new Contents();
Destination d = new Destination(dest);
}
public static void main(String[] args) {
Parcel1 p = new Parcel1();
p.ship("Tanzania");
}
} ///:~
Las clases internas, cuando se usan dentro ship(), parece el uso de muchas clases.
Aquí la única diferencia práctica es que los nombre están anidadas sin Parcel1.
Verás que esta no es la única diferencia.
Mas típicamente, una clase de salida tendrá un método que devuelve un
manejador a una clase interna, como esta:
//: Parcel2.java
// Returning a handle to an inner class
package c07.parcel2;
public class Parcel2 {
class Contents {
private int i = 11;
public int value() { return i; }
}
class Destination {
private String label;
Destination(String whereTo) {
label = whereTo;
}
String readLabel() { return label; }
}
public Destination to(String s) {
return new Destination(s);
}
public Contents cont() {
return new Contents();
}
public void ship(String dest) {
Contents c = cont();
Destination d = to(dest);
}
public static void main(String[] args) {
Parcel2 p = new Parcel2();
p.ship("Tanzania");
Parcel2 q = new Parcel2();
// Defining handles to inner classes:
Parcel2.Contents c = q.cont();
Parcel2.Destination d = q.to("Borneo");
}
} ///:~
Si quieres hacer un objeto de la clase interna en cualquier sitio excepto de uno con
métodos no estáticos de una clase de salida, debes especificar el tipo de este
objeto como OuterClassName.InnerClassName, como se ve en main().
Clases internas y upcasting
Pues, las clases internas no parecen tan dramaticas. Despues de todo, si esta
escondido Java ya es un perfecto mecanismo de esconder- solo permite a las
clases ser "amistosas" (visibles solo con un paquete) mejor que crearlas como una
clase interna.
De cualquier forma, las clases internas realmente vienen de ellas mismas, cuando
empiezas a upcasting a una clase base, y en particular a un interface. (El efecto
que produce un manejador de interface de un objeto que lo implementa
esencialmente lo mismo que upcasting a una clase base.) Esto es por que la clase
interna puede entonces estar completamente invisibles y no disponibles para
cualquiera, que es conveniente para esconder la implementación. Todo lo que
consigues es un manejador de la clase base o del interface, y es posible que
incluso no encuentres el tipo exacto, como se ense ña a continuación:
//: Parcel3.java
// Returning a handle to an inner class
package c07.parcel3;
abstract class Contents {
abstract public int value();
}
interface Destination {
String readLabel();
}
public class Parcel3 {
private class PContents extends Contents {
private int i = 11;
public int value() { return i; }
}
protected class PDestination
implements Destination {
private String label;
private PDestination(String whereTo) {
label = whereTo;
}
public String readLabel() { return label; }
}
public Destination dest(String s) {
return new PDestination(s);
}
public Contents cont() {
return new PContents();
}
}
class Test {
public static void main(String[] args) {
Parcel3 p = new Parcel3();
Contents c = p.cont();
Destination d = p.dest("Tanzania");
// Illegal -- can't access private class:
//! Parcel3.PContents c = p.new PContents();
}
} ///:~
Ahora Contents y Destinatination representan interfaces disponibles para el
programador. (El interface, recuerda, automáticamente hace todos los miembros
públicos.) Por conveniencia, estos se ponen en un solo fichero, pero
ordinariamente Contents y Destination seria cada uno público en sus propios
ficheros.
En Parcel3, algo nuevo ha sido añadido: la clase interna Pcontents es privada por
eso ninguno excepto Parcel3 puede acceder a él. Pdestination esta protegido, por
eso uno, pero Parcel3, clases en el paquete Parcel3 (desde que se protege dados
los accesos a los paquetes; es, protegido es también "amistoso"), y los herederos
de Parcel3 pueden acceder a Pdestination. Esto significa, que el programador ha
restringido el conocimiento y el acceso de estos miembros. De echo, no puedes
downcast a una clase interna (o a una clase interna protegida a menos que seas
un heredero.), por que no puedes acceder al nombre, como puedes ver en la clase
Test. Así, las clases internas privadas proveen una forma para el diseño de la clase
para prevenir completamente ninguna dependencia type-coding y esconder los
detalles de la implementaci ón. En adicción, la extensión de un interface es menos
útil para la perspectiva del programador desde que este no accede a ningún
método adicional que no esta en la parte publica del interface de la clase. Esto
también provee una oportunidad para el compilador de Java para generar código
mas eficiente.
Las clases normales (no internas) no pueden ser privadas o protegidas - solo
públicas o "amistosas".
Notar que contents no necesitan ser una clase abstracta. Podrías usar una clase
ordinaria, pero los mas típicas empiezan como un dise ño en un interface.
Clases internas en métodos y ámbitos
Lo que hemos visto en encompasses el uso típico de clases internas. En General,
el código que escribirías y leer envolviendo las clases internas serán "simples"
clases internas que son simples y fácil de entender. Sin embargo, el diseño de las
clases internas esta completo y hay un número de otro, mas oscuro, formas que
puedes usar si eliges: las clases internas pueden ser creadas con un método o
incluso con un scope arbitrario. Hay dos razones para hacer esto:
1. Como se ha visto anteriormente, estas implementando un interface de un tipo por lo que
puedes crear y devolver un manejador.
2. Estas resolviendo un complicado problema y quieres crear una clase para ayudar en tu
solución, pero no quieres hacerlo disponible.
En los siguientes ejemplos, el código previo será modificado para usar:
1. Una clase definida con un método.
2. Una clase definida con un scope dentro de un método.
3. Una clase anónima implementando un interface.
4. Una clase anónima extendiendo una clase que tiene un constructor no por defecto.
5. Una clase anónima que permita la inicialización de campos.
6. Una clase anónima que permita construcción usando inicialización de ejemplos. (una
clase interna anónima no puede tener constructores).
Esto te dará con el paquete innerscopes. Primero, los interfaces comunes del
código previo que será definido en sus ficheros propios, por eso pueden ser usados
en todos los ejemplos:
//: Destination.java
package c07.innerscopes;
interface Destination {
String readLabel();
} ///:~
El punto que ha sido construido que Contents podría ser una clase abstracta, por
eso aquí esta en una forma mas natural, como un interface:
//: Contents.java
package c07.innerscopes;
interface Contents {
int value();
} ///:~
Aunque es una clase ordinaria con una implementación, Envoltura es algo que se
viene usando como un interface común para que estas clases derivadas:
//: Wrapping.java
package c07.innerscopes;
public class Wrapping {
private int i;
public Wrapping(int x) { i = x; }
public int value() { return i; }
} ///:~
Notaras abajo que Wrapping tiene un constructor que requiere un argumento
para hacer cosas un poco mas interesante.
El primer ejemplo enseña la creación de una clase entera con un método scope
(en vez de el scope de otra clase).
//: Parcel4.java
// Nesting a class within a method
package c07.innerscopes;
public class Parcel4 {
public Destination dest(String s) {
class PDestination
implements Destination {
private String label;
private PDestination(String whereTo) {
label = whereTo;
}
public String readLabel() { return label; }
}
return new PDestination(s);
}
public static void main(String[] args) {
Parcel4 p = new Parcel4();
Destination d = p.dest("Tanzania");
}
} ///:~
La clase Pdestination es parte de dest() raro que sea parte de Parcel4. (Notar
también que podrías usar el identificador de clase Pdestination para una clase
interna dentro de cada clase en el mismo subdirectorio sin un nombre de clase).
Entonces, Pdestination no puede ser accedido fuera de dest(). Notar que el
upcasting que sucede, en la orden return -nada sale de dest() excepto un
manejador de la clase base de destino. Por Supuesto, el echo de que el nombre de
la clase Pdestination esta dentro de dest(), no significa que Pdestination no sea un
objeto valido una vez que dest() acabe.
El siguiente ejemplo enseña como puede anidar una clase interna con un scope
aleatorio:
//: Parcel5.java
// Nesting a class within a scope
package c07.innerscopes;
public class Parcel5 {
private void internalTracking(boolean b) {
if(b) {
class TrackingSlip {
private String id;
TrackingSlip(String s) {
id = s;
}
String getSlip() { return id; }
}
TrackingSlip ts = new TrackingSlip("slip");
String s = ts.getSlip();
}
// Can't use it here! Out of scope:
//! TrackingSlip ts = new TrackingSlip("x");
}
public void track() { internalTracking(true); }
public static void main(String[] args) {
Parcel5 p = new Parcel5();
p.track();
}
} ///:~
La clase TrackingSlip esta anidada dentro de scope de una orden if. Esto no
significa que la clase sea condicionalmente creada es compilado con todo. En
cualquier caso, no esta disponible fuera de scope en el que esta definido. Otro,
parece una clase ordinaria.
El siguiente ejemplo parece un poco extraño:
//: Parcel6.java
// A method that returns an anonymous inner class
package c07.innerscopes;
public class Parcel6 {
public Contents cont() {
return new Contents() {
private int i = 11;
public int value() { return i; }
}; // Semicolon required in this case
}
public static void main(String[] args) {
Parcel6 p = new Parcel6();
Contents c = p.cont();
}
} ///:~
El método cont() combina la creación de un valor devuelto con la definición de la
clase que representa ese valor devuelto! . Además la clase es anónima, no tiene
nombre. Para asegurarnos un pequeño mal, parece que estas creando un objeto
Contents:
return new Contents()
Pero entonces, antes de conseguir el semicolon , dirás, "pero, espera, yo pienso yo
huiré en una definición de clase:
return new Contents() {
private int i = 11;
public int value() { return i; }
};
Que significa esta extraña sintaxis "crear un objeto de una clase anónima que es
heredada de Contents." El manejador devuelto por la nueva expresi ón es upcast
automáticamente a un manejador Contents. La sintaxis de la clase anónima
interna es una abreviatura para:
class MyContents extends Contents {
private int i = 11;
public int value() { return i; }
}return new MyContents();
En la clase anónima interna, contents es creado usando un constructor por
defecto. El código siguiente enseña que hacer si tu clase base necesita un
constructor con un argumento:
//: Parcel7.java
// An anonymous inner class that calls the
// base-class constructor
package c07.innerscopes;
public class Parcel7 {
public Wrapping wrap(int x) {
// Base constructor call:
return new Wrapping(x) {
public int value() {
return super.value() * 47;
}
}; // Semicolon required
}
public static void main(String[] args) {
Parcel7 p = new Parcel7();
Wrapping w = p.wrap(10);
}
} ///:~
Esto es simplemente pasando el argumento apropiado al constructor de la clase
base, mirando aquí como se pasa x en el nuevo Wrapping(x). Una clase anónima
no puede tener un constructor donde normalmente llamaríamos a super().
Tanto en los ejemplos anteriores, los semicolon no marcan el final de la clase doby
(como se hace en C++). En vez de esto, marca el final de la expresi ón que resulta
de contiene la clase anónima. Así, es idéntico al uso de semicolon en cualquier
otro sitio.
¿Qué sucede si necesitas realizar algún tipo de inicialización para un objeto de una
clase anónima interna? Desde que es anónima, no hay nombre que darle al
constructor, por eso no puedes tener un constructor. Podrías, como sea, realizar la
inicializaci ón en el punto de definición de los campos:
//: Parcel8.java
// An anonymous inner class that performs
// initialization. A briefer version
// of Parcel5.java.
package c07.innerscopes;
public class Parcel8 {
// Argument must be final to use inside
// anonymous inner class:
public Destination dest(final String dest) {
return new Destination() {
private String label = dest;
public String readLabel() { return label; }
};
}
public static void main(String[] args) {
Parcel8 p = new Parcel8();
Destination d = p.dest("Tanzania");
}
} ///:~
Si estas definiendo una clase anónima interna y quieres usar un objeto que esta
definido fuera de la clase anónima interna, el compilador requiere que el objeto de
salida sea final. Esto es por que el objeto de salida sea final. Esto es por que el
argumento para dest() es final. Si lo olvida, obtendrá un mensaje de error en
tiempo de compilación.
Como estas asignando un campo, el ejemplo de abajo es excelente. Pero, ¿qué
sucede si necesitas realizar algún constructor como actividad? Con Java 1.1 la
inicializaci ón de ejemplos, puedes en efecto, crear un constructor para una clase
anónima interna.
//: Parcel9.java
// Using "instance initialization" to perform
// construction on an anonymous inner class
package c07.innerscopes;
public class Parcel9 {
public Destination
dest(final String dest, final float price) {
return new Destination() {
private int cost;
// Instance initialization for each object:
{
cost = Math.round(price);
if(cost > 100)
System.out.println("Over budget!");
}
private String label = dest;
public String readLabel() { return label; }
};
}
public static void main(String[] args) {
Parcel9 p = new Parcel9();
Destination d = p.dest("Tanzania", 101.395F);
}
} ///:~
Dentro del inicializador de ejemplos puedes ver el código que no podrás ejecutar
como parte de un inicializador de un campo (esto es, la orden if). Por eso en
efecto, un inicializador de ejemplos es el constructor para una clase anónima
interna. Por supuesto, esta limitada, no puedes overload inicializadores de
ejemplos por eso solo puedes tener uno de los constructores.
El enlace a la clase exterior
Más adelante, aparece que las clases internas son justo un nombre escondido y un
esquema de organización del codigo, que es útil, pero no totalmente completo.
Aunque, hay otro problema. Cuando se crea una clase interna, los objetos de esa
clase interna tienen un enlace al objeto "padre" que los creó, y por eso ellos
pueden acceder a los miembros del objeto "padre" sin ninguna calificaci ón
especial. Además, las clases internas tienen derechos de acceso a todos los
elementos en la clase "padre".¹ El siguiente ejemplo demuestra esto:
¹Esto es muy diferente del diseño de clases anidadas en C++, que es simplemente un
mecanismo de esconder el nombre. No hay enlace a un objeto "padre" y sin permisos
implicitos en C++.
//: Sequence.java
// Holds a sequence of Objects
interface Selector {
boolean end();
Object current();
void next();
}
public class Sequence {
private Object[] o;
private int next = 0;
public Sequence(int size) {
o = new Object[size];
}
public void add(Object x) {
if(next < o.length) {
o[next] = x;
next++;
}
}
private class SSelector implements Selector {
int i = 0;
public boolean end() {
return i == o.length;
}
public Object current() {
return o[i];
}
public void next() {
if(i < o.length) i++;
}
}
public Selector getSelector() {
return new SSelector();
}
public static void main(String[] args) {
Sequence s = new Sequence(10);
for(int i = 0; i < 10; i++)
s.add(Integer.toString(i));
Selector sl = s.getSelector();
while(!sl.end()) {
System.out.println((String)sl.current());
sl.next();
}
}
} ///:~
La secuencia es simplemente un array de tamaño fijo de objetos con una clase
envuelta en ella. Llama a add() para añadir un nuevo objeto al final de la
secuencia (Lista) (si hay sitio). Para buscar cada objeto en una lista, hay un
interface llamado Selector, que permite ver si estas en el final end(), mirar el
objeto actual current(), y pasar al siguiente next(). Ya que el Selector es un
interface, muchas otras clases pueden implementar el interface de su propia
forma, y muchos métodos pueden tomar un interface como argumento para crear
un código genérico.
Aquí, el Sselector es una clase privada que provee la función de Selector. En main
() puedes ver la creación de una lista, seguida por añadir un numero de objetos
string. Entonces, un selector se produce con una llamada a get Selector() y esta
es usada para moverse a través de la lista y seleccionar cada elemento.
Primero, la creación de Sselector parece otra clase interna. Pero examínelo
cuidadosamente. Note que cada uno de los métodos end() y next() se refieren a
"o", que es un manejador que no es parte de Sselector, pero si es un campo
privado en la clase "padre". Sin embargo, la clase interna puede acceder a
métodos y a campos desde una clase "padre" como si fuera suya. Esto quita que
sea muy conveniente, como puedes ver abajo en el ejemplo.
Por eso una clase, interna tuvo acceso a los miembros de las clases "padres".
¿Cómo puede suceder esto? La clase interna debe conservar una referencia a un
objeto particular de la clase "padre" que fue responsable para crearlo. Entonces
cuando te refieres a un miembro de la clase "padre", que (escondido) referencia es
usada para seleccionar ese miembro. Afortunadamente el compilador tiene
cuidado con todos estos detalles por ti, pero puedes también entender ahora que
un objeto en una clase interna puede ser creado solo asociado con un objeto de
una clase interna. El proceso de construcción requiere la inicialización del
manejador del objeto de la clase "padre" y el compilador reclamará si no puede
acceder al manejador. Muchas veces esto ocurre sin intervención por parte del
programador.
Clases internas estáticas
Para comprender el significado de "estático" aplicando a las clases internas, debes
recordar que el objeto de las clases internas conserva implícitamente un
manejador del objeto de la clase "padre" que lo creó. Esto no es verdad, aunque,
cuando se dice una clase interna es estática. Una clase interna estática significa:
1. No necesitas un objeto de una clase de salida para crear un objeto de una clase interna
estática.
2. No puedes acceder a un objeto de una clase de salida desde un objeto de una clase de
salida desde un objeto de una clase interna estática.
Hay algunas restricciones: los miembros estáticos, pueden estar en un nivel de
una clase por eso las clases internas no pueden tener datos estáticos o clases
internas estáticas.
Si no necesita crear un objeto de una clase de salida para crear un objeto de la
clase interna, puedes hacerlo todos estático. Para esto para trabajar, debes
también hacer las clases internas estáticas:
//: Parcel10.java
// Static inner classes
package c07.parcel10;
abstract class Contents {
abstract public int value();
}
interface Destination {
String readLabel();
}
public class Parcel10 {
private static class PContents
extends Contents {
private int i = 11;
public int value() { return i; }
}
protected static class PDestination
implements Destination {
private String label;
private PDestination(String whereTo) {
label = whereTo;
}
public String readLabel() { return label; }
}
public static Destination dest(String s) {
return new PDestination(s);
}
public static Contents cont() {
return new PContents();
}
public static void main(String[] args) {
Contents c = cont();
Destination d = dest("Tanzania");
}
} ///:~
En main no es necesario objetos de Parcel10; en vez de eso use la sintaxis normal
para seleccionar un miembro estático para llamar a los métodos que devuelve
manejadores a Contents y Destination.
Normalmente no puedes poner ningún código dentro de un interface, pero una
clase interna estática puede ser parte de un interface. Desde que las clases son
estáticas no violan las reglas para los interfaces - las clases internas estáticas es
puesta solo en el espacio del nombre del interface:
//: IInterface.java
// Static inner classes inside interfaces
class IInterface {
static class Inner {
int i, j, k;
public Inner() {}
void f() {}
}
} ///:~
Anteriormente en el libro I sugería poner un main() en todas las clases to act as a
test bed para esa clase. Un inconveniente de este es la cantidad de código extra
debes arrastrar. Si este es un problema, puedes usar una clase interna estática
para contener el código de test:
//: TestBed.java
// Putting test code in a static inner class
class TestBed {
TestBed() {}
void f() { System.out.println("f()"); }
public static class Tester {
public static void main(String[] args) {
TestBed t = new TestBed();
t.f();
}
}
} ///:~
Esto genera una clase aparte llamada TestBed$Tester (para ejecutar el programa
poner java TestBed$Tester) Puedes usar esta clase para testear, pero no necesitas
incluir en tu shipping product .
Refiriéndose al objeto de la clase m ás exterior
Si necesita producir el manejador de salida de la clase objeto, ponga: nombre a la
clase de salida seguido por un punto. Por ejemplo, en la clase Sequence.Sselector
ninguno de sus métodos puede producir el manejador almacenado a la clase de
salida Sequence escribiendo Sequence.this. El manejador resultante es
automáticamente el tipo correcto. (Esto se conoce y es chequeado en tiempo de
compilación, por eso no hay run-time overhead ). A veces quieres decir a algún
otro objeto que cree un objeto de uno de estas clases internas. Para hacer esto
debes proveer un manejador a la otra clase de salida en la nueva expresión, como
esta:
//: Parcel11.java
// Creating inner classes
package c07.parcel11;
public class Parcel11 {
class Contents {
private int i = 11;
public int value() { return i; }
}
class Destination {
private String label;
Destination(String whereTo) {
label = whereTo;
}
String readLabel() { return label; }
}
public static void main(String[] args) {
Parcel11 p = new Parcel11();
// Must use instance of outer class
// to create an instances of the inner class:
Parcel11.Contents c = p.new Contents();
Parcel11.Destination d =
p.new Destination("Tanzania");
}
} ///:~
Para crear un objeto de la clase interna directamente, no sigue la misma forma y
referidos al nombre de la clase d salida Parcel11 como puedes esperar, pero en
vez de esto debes usar un objeto de la clase de salida para hacer un objeto de la
clase interna:
Parcel11.Contents c = p.new Contents();
Así, no es posible crear un objeto de una clase interna a menos que ya tenga un
objeto de la clase de salida. Esto es por que el objeto de la clase de salida es
conectado al objeto de la clase de salida de la que fue creado. Como sea, si creas
una clase interna estática entonces no necesitas un manejador de la clase del
objeto de salida.
Heredando de una clase interna
Por que el constructor de la clase interna debe "engancharse" al manejador del
objeto de la clase "padre", las cosas se complican cuando heredas de una clase
interna. El problema es que el manejador "secreto" del objeto de la clase "padre"
debe ser inicializado, y todavía en la clase derivada no hay mas que un objeto por
defecto a quien engancharse. La respuesta es usar la sintaxis provista para hacer
la asociación explícita:
//: InheritInner.java
// Inheriting an inner class
class WithInner {
class Inner {}
}
public class InheritInner
extends WithInner.Inner {
//! InheritInner() {} // Won't compile
InheritInner(WithInner wi) {
wi.super();
}
public static void main(String[] args) {
WithInner wi = new WithInner();
InheritInner ii = new InheritInner(wi);
}
} ///:~
Puedes ver que InheritInner es extendido solo en las clases interna, y no las de
salida. Pero cuando va a crear un constructor, el de defecto no es bueno no
puedes pasarle un manejador a un objeto "padre". Además, debes usar la sintaxis:
enclosingClassHandle.super();
dentro del constructor. Este provee el manejador necesario y el programa
compilará entonces.
¿Pueden las clases internas ser sobreescritas?
¿Qué sucede cuando creas una clase interna, entonces heredas de una clase
"padre" y redefinir las clases interna? Es posible sobreescribir una clase interna?
Esto parece que seria un concepto poderoso, pero sobreescribir una clase interna
como si fuera otro método de una clase de salida no hace realmente nada:
//: BigEgg.java
// An inner class cannot be overriden
// like a method
class Egg {
protected class Yolk {
public Yolk() {
System.out.println("Egg.Yolk()");
}
}
private Yolk y;
public Egg() {
System.out.println("New Egg()");
y = new Yolk();
}
}
public class BigEgg extends Egg {
public class Yolk {
public Yolk() {
System.out.println("BigEgg.Yolk()");
}
}
public static void main(String[] args) {
new BigEgg();
}
} ///:~
El constructor por defecto es sintetizado automáticamente por el compilador, y
este llama al constructor por defecto de la clase base. Puede pensar que desde un
BigEgg a sido creado, la versi ón sobreecrita de Yolk será usada, pero este no es el
caso. La salida es:
New Egg()
Egg.Yolk()
Este ejemplo simplemente enseña que no hay ninguna clase mágica interna extra
va cuando heredas de una clase mas exterior. Como sea, es posible todavía
heredar explícitamente de la clase interna:
//: BigEgg2.java
// Proper inheritance of an inner class
class Egg2 {
protected class Yolk {
public Yolk() {
System.out.println("Egg2.Yolk()");
}
public void f() {
System.out.println("Egg2.Yolk.f()");
}
}
private Yolk y = new Yolk();
public Egg2() {
System.out.println("New Egg2()");
}
public void insertYolk(Yolk yy) { y = yy; }
public void g() { y.f(); }
}
public class BigEgg2 extends Egg2 {
public class Yolk extends Egg2.Yolk {
public Yolk() {
System.out.println("BigEgg2.Yolk()");
}
public void f() {
System.out.println("BigEgg2.Yolk.f()");
}
}
public BigEgg2() { insertYolk(new Yolk()); }
public static void main(String[] args) {
Egg2 e2 = new BigEgg2();
e2.g();
}
} ///:~
Ahora BiggEgg2.Yok extiende explícitamente Egg2.Yolk y sobreescribe estos
métodos. El método insertYolk() permite a BigEgg2 para upcast uno de sus
propios objetos Yok dentro del manejador "y" en Egg2, por eso cuando g() llama a
y.f() la versión sobreescrita de f() es usada. La salida es:
Egg2.Yolk()
New Egg2()
Egg2.Yolk()
BigEgg2.Yolk()
BigEgg2.Yolk.f()
La segunda llamada a Egg2.Yok() es la llamada al constructor de la clase base
llamada del constructor BigEgg2.Yolk. Puedes ver que la versión overridden de f()
es usada cuando g() es llamada.
Identificadores de clases interna
Desde que todas las clases producen un fichero .class que contiene toda la
información a cerca de cómo crear objetos de ese tipo (esta información produce
meta-clases llamadas Clases objeto), puedes esperar que las clases interna debe
también producir archivos .class para contener la información para su clases
objeto. Los nombres de esos ficheros (clases tienen una formula estricta: el
nombre de la clase "padre", seguida por un $, mas el nombre de la clase interna.
Por ejemplo, los ficheros .class creados por InheritInner.java incluye:
InheritInner.class
WithInner$Inner.class
WithInner.class
Si las clases interna son anónimas el compilador simplemente empieza a generar
números como identificadores de clases interna. Si la clase interna es anidada con
clases interna, sus nombres serán simplemente añadidos después de un $ y el
identificador/es de la clase de salida.
Aunque este esquema de generar nombres internos es simple y derecho, es
también robusto y manejable en muchas situaciones. 3 Desde que este es el
esquema de la forma de nombrar estándar en Java, los ficheros generados son
automáticamente independientes de la plataforma. (Notar que el compilador de
Java cambia tus clases interna en todos los tipos para hacerlos funcionar.)
¿Por qué las clases internas?. Esqueleto de control
En este punto has visto muchas sintaxis y descripciones semánticas la manera en
que las clases interna funcionan, pero esto no contestan a la pregunta de por que
existen. ¿Por qué trae tantos problemas para añadirlo como un rasgo fundamental
del lenguaje en Java 1.1? La respuesta es algo que referiré como un esqueleto de
control (framework)
Una aplicación framework es una clase o un conjunto de clases que son diseñadas
para resolver un tipo particular de problema. Para aplicar una aplicaci ón
framework, tu heredas de una o mas clases y override algunos de los configuras la
solución general provista por esta aplicación framework para resolver tu problema
especifico. El control framework es un tipo particular de aplicaci ón predominada
por framework por la necesidad.
3 En la otra mano, $ es un meta-carácter en el shell Unix y por eso tendrás a
veces problemas cuando listes los ficheros .class. Esto es un poco extraño viniendo
de Sun, una compañía basada en Unix. Supongo que ellos no consideraron este
principio, pero en vez de pensar que naturalmente se centrará en los ficheros de
código fuente.
Para responder a eventos un sistema que preliminarmente responde a eventos se
llama un sistema conducido por eventos. Uno de los mas importantes problemas
en la programación de aplicaciones es el interface gráfico del usuario (GUI), que es
el que es mas dirigido por eventos. Como verás en el capitulo 13, Java 1.1 AWT es
un control framework que elegantemente resuelve el problema del GUI usando
clases internas.
Para ver como las clases interna permiten la creación simple y el uso de control
framework, considerar un control framework aquel cuyo trabajo es ejecutar
eventos siempre que esos eventos esten "listos".> Aunque "listos" puede significar
nada, en este caso el defecto ser á basado en el reloj. Que siguido de un control
framework que no contiene información especifica acerca de que esta controlando.
Primero, aquí esta el interface que describe ningún evento de control. Este es una
clase abstracta en vez de un interface, ya que el comportamiento por defecto es
un control basado on time , por eso algunas de las implementaciones pueden ser
incluidas aquí:
//: Event.java
// The common methods for any control event
package c07.controller;
abstract public class Event {
private long evtTime;
public Event(long eventTime) {
evtTime = eventTime;
}
public boolean ready() {
return System.currentTimeMillis() >= evtTime;
}
abstract public void action();
abstract public String description();
} ///:~
El constructor simplemente captura el tiempo cuando quieras que el Evento se
ejecute, mientras ready() te dice cuando es tiempo de ejecutarlo. Por supuesto,
ready() puede ser overridden en una clase derivada basando el evento en algo
mas que el tiempo. Action() es el método que es llamado cuando el evento esta
ready(), y description() da información textual a cerca del evento.
El siguiente fichero contiene el actual control framework que maneja y fija
eventos. La primera clase es realmente un "ayudador" de clases cuyo trabajo es
contener objetos eventos. Podrías reemplazarlo con cualquier apropiada colección,
y en el capitulo 8 descubrirás otras colecciones que hará el engaño sin requerir
que escribas código extra:
//: Controller.java
// Along with Event, the generic
// framework for all control systems:
package c07.controller;
// This is just a way to hold Event objects.
class EventSet {
private Event[] events = new Event[100];
private int index = 0;
private int next = 0;
public void add(Event e) {
if(index >= events.length)
return; // (In real life, throw exception)
events[index++] = e;
}
public Event getNext() {
boolean looped = false;
int start = next;
do {
next = (next + 1) % events.length;
// See if it has looped to the beginning:
if(start == next) looped = true;
// If it loops past start, the list
// is empty:
if((next == (start + 1) % events.length) && looped)
return null;
} while(events[next] == null);
return events[next];
}
public void removeCurrent() {
events[next] = null;
}
}
public class Controller {
private EventSet es = new EventSet();
public void addEvent(Event c) { es.add(c); }
public void run() {
Event e;
while((e = es.getNext()) != null) {
}
if(e.ready()) {
e.action();
System.out.println(e.description());
es.removeCurrent();
}
}
} ///:~
EventSet contiene 100 eventos arbitrariamente. (Si una colección "real" del
capitulo 8 es usado aquí no necesitas preocuparte a cerca de que es del tama ño
máximo, desde que se redimensione él solo). El índice usado para conservar el
sitio del siguiente espacio disponible, y el siguiente es usado cuando estas
buscando el siguiente Evento en la lista, para ver tanto si has whether you-ve
looped around . Esto es importante durante una llamada a getNext(), por que los
objetos Evento son eliminados de la lista (usando remove Current()) una vez que
se esta ejecutando, por eso getNext() encontrará huecos en la lista que se
mueven por ella.
Notar que removeCurrent() no establece solamente algunas "banderas" indicando
que el objeto no estará en uso durante bastante tiempo. En vez de esto, se le
manda al manejador que lo anule. Esto es importante ya que si la basura del
collector sees un manejador que es todavía en uso entonces no puede clean up el
objeto. Si piensas que tus manejadores pueden "engancharlo", una buena idea
para ponerlos para dar permiso al colector de basura to clean them up .
El controlador está donde el actual trabajo está. Este usa un Set de Eventos para
contener estos objetos Event, y addEvent() te permite añadir nuevos eventos a
esta lista. Pero el método importante es run(). Este método dobla a través de
EventSethunting para un objeto Evento que está listo ready() para ejecutarse.
Para cada uno que esté listo ready(), se llama al método action(), escribe la
descripción description() y entonces elimina el evento de la lista.
Notar que en este dise ño no sabemos nada exactamente sobre que hace un
evento. Y este es el inconveniente del diseño, como "separar las cosas que
cambian de las que continúan igual". O, para usar mi termino, el "vector de
cambio" son las diferentes acciones de los varios tipos de objetos. Evento, y
expresar diferentes acciones creando diferentes subclases de Eventos.
Aquí es donde las clases internas entran en juego. Ellos permiten dos cosas:
1. Expresar la implementación de una aplicación control-framework en una clase simple,
por el encapsulamiento todo lo que es único de esa implementación. Las clases internas
son usadas para expresar los diferentes tipos de action() necesarios para resolver el
problema. Además, el siguiente ejemplo usa clases internas privadas por lo que la
implementación es completamente escondida y puede ser cambiada con impunidad.
2. Las clases internas conservan esta implementación from becoming awkard , desde que
puedes fácilmente acceder a cualquiera de los miembros de la clase "padre". Sin que esto
habilite que el código pueda become unpleasant enough that you´d end up buscando una
alternativa.
Considerar una implementación particular del control-framework diseñada para
controlar las funciones "greenhouse".² Cada acción es completamente diferente:
cambiar luces, agua, y encender o apagar termostatos, llamar al timbre, y
"restart" el sistema. Pero el control-framework es diseñado para aislar fácilmente
este diferente código. Para cada tipo de acción heredas un nuevo evento de la
clase interna, y escribe el código de control dentro de action().
Como estipoco con una aplicación framework, la clase GreenhouseControls es
heredada de un Controlador:
²Por alguna razón esto ha tenido siempre un agradable problema para resolverlo; esto
viene de C++ Inside&Out, pero java permite una forma mucho mas elegante.
//: GreenhouseControls.java
// This produces a specific application of the
// control system, all in a single class. Inner
// classes allow you to encapsulate different
// functionality for each type of event.
package c07.controller;
public class GreenhouseControls
extends Controller {
private boolean light = false;
private boolean water = false;
private String thermostat = "Day";
private class LightOn extends Event {
public LightOn(long eventTime) {
super(eventTime);
}
public void action() {
// Put hardware control code here to
// physically turn on the light.
light = true;
}
public String description() {
return "Light is on";
}
}
private class LightOff extends Event {
public LightOff(long eventTime) {
super(eventTime);
}
public void action() {
// Put hardware control code here to
// physically turn off the light.
light = false;
}
public String description() {
return "Light is off";
}
}
private class WaterOn extends Event {
public WaterOn(long eventTime) {
super(eventTime);
}
public void action() {
// Put hardware control code here
water = true;
}
public String description() {
return "Greenhouse water is on";
}
}
private class WaterOff extends Event {
public WaterOff(long eventTime) {
super(eventTime);
}
public void action() {
// Put hardware control code here
water = false;
}
public String description() {
return "Greenhouse water is off";
}
}
private class ThermostatNight extends Event {
public ThermostatNight(long eventTime) {
super(eventTime);
}
public void action() {
// Put hardware control code here
thermostat = "Night";
}
public String description() {
return "Thermostat on night setting";
}
}
private class ThermostatDay extends Event {
public ThermostatDay(long eventTime) {
super(eventTime);
}
public void action() {
// Put hardware control code here
thermostat = "Day";
}
public String description() {
return "Thermostat on day setting";
}
}
// An example of an action() that inserts a
// new one of itself into the event list:
private int rings;
private class Bell extends Event {
public Bell(long eventTime) {
super(eventTime);
}
public void action() {
// Ring bell every 2 seconds, rings times:
System.out.println("Bing!");
if(--rings > 0)
addEvent(new Bell(
System.currentTimeMillis() + 2000));
}
public String description() {
return "Ring bell";
}
}
private class Restart extends Event {
public Restart(long eventTime) {
super(eventTime);
}
public void action() {
long tm = System.currentTimeMillis();
// Instead of hard-wiring, you could parse
// configuration information from a text
// file here:
rings = 5;
addEvent(new ThermostatNight(tm));
addEvent(new LightOn(tm + 1000));
addEvent(new LightOff(tm + 2000));
addEvent(new WaterOn(tm + 3000));
addEvent(new WaterOff(tm + 8000));
addEvent(new Bell(tm + 9000));
addEvent(new ThermostatDay(tm + 10000));
// Can even add a Restart object!
addEvent(new Restart(tm + 20000));
}
public String description() {
return "Restarting system";
}
}
public static void main(String[] args) {
GreenhouseControls gc =
new GreenhouseControls();
long tm = System.currentTimeMillis();
gc.addEvent(gc.new Restart(tm));
gc.run();
}
} ///:~
Notar que luz, agua, termostato, y timbres pertenecen todas a la clase de salida
GreenhouseControls, y todavía las clases interna no tienen problemas para
acceder a esos campos. También, muchas de los métodos action() también
envuelven algún tipo de control hardware, que envolverán llamadas a código noJava.
La mayoría de las clases eventos parecen similares, pero Bell y Restart son
especiales. Bell suena, y si esto no se hubiera ejecutado todavía suficientes veces
esto añadiría un nuevo objeto Bell a la lista de Eventos, por eso sonará de nuevo
mas tarde. Notar como interna clases parece herencia múltiple: Bell tiene todos
los métodos de Event y esto también aparece para tener todos los métodos de la
clase de salida GreenhouseControls.
Restart es el responsable de inicializar el sistema, por eso añade los eventos
apropiados. Por supuesto, una forma mas flexible de ejemplo esto es avoid hardcoding los eventos en vez de leerlos del fichero. (Un ejercicio en el capítulo 10 te
pedirá modificar este ejemplo para hacer justamente esto). Desde que Restart()
es otro objeto evento puedes tambien añadir un objeto Restart con Restart.action
() por eso el sistema regularmente restarts el mismo. Y todo lo que necesitas
hacer en main() es crear un objeto GreenhouseControls y añadir un objeto Restart
para que funcione.
Este ejemplo te llevará a apreciar el valor de las clases interna, especialmente
cuando se usan con un control framework. Aunque, en la segunda parte del
capitulo 13 veremos como son usados las clases internas elegantemente para
describir las acciones de un interface gráfico de un usuario. Al tiempo que
terminas esta sección debería ser completamente convincente.
Constructores y polimorfismo
Como es usual, los contructores son diferentes de los otros tipos de métodos. Esto
es también verdad cuando el polimorfismo es envuelto. Incluso cuando los
constructores no son polimorficos (aunque puedes tener un tipo de "contructor
virtual", como veremos en el capitulo 11), es importante comprender la forma de
que trabajan los constructores en jerarquías complejas y con polimorfismo. Esto te
ayudará a avoid unpleasant entanglements .
Orden de las llamadas a constructores
El orden de las llamadas a constructores fue discutida en el capitulo 4, pero esto
fue antes de que la herencia y el polimorfismo fuera introducido.
Un constructor para la clase base es siempre llamado en el constructor para una
clase derivada, chaining upward so that un constructor para todas las clases base
es llamado. Este se asegura que el constructor tiene un trabajo especial: ver que
el objeto sea construido correctamente. Una clase derivada tiene acceso solo a
estos propios miembros, y no a los de las clases base (esos miembros son
típicamente primados). Solo el constructor de la clase-base tiene el conocimiento
adecuado y acceso a inicializar sus propios elementos.
Entonces, es esencial que todos los constructores sean llamados, de otro modo el
objeto no será construido correctamente. Esto es por que el compilador forza una
llamada a un constructor para cada porción de una clase derivada. Esto llamará
"silenciosamente" al constructor por defecto si no especificas un constructor de
clase -base en el cuerpo del constructor de la clase-derivada. Si no hay constructor
por defecto, el compilador protestará. (En el caso de que una clase no tuviera
constructores, el compilador automáticamente sintetizará un constructor por
defecto.)
Echemos un vistazo a un ejemplo que enseña los efectos de composición,
herencia, y polimorfismo para la construcción:
//: Sandwich.java
// Order of constructor calls
class Meal {
Meal() { System.out.println("Meal()"); }
}
class Bread {
Bread() { System.out.println("Bread()"); }
}
class Cheese {
Cheese() { System.out.println("Cheese()"); }
}
class Lettuce {
Lettuce() { System.out.println("Lettuce()"); }
}
class Lunch extends Meal {
Lunch() { System.out.println("Lunch()");}
}
class PortableLunch extends Lunch {
PortableLunch() {
System.out.println("PortableLunch()");
}
}
class Sandwich extends PortableLunch {
Bread b = new Bread();
Cheese c = new Cheese();
Lettuce l = new Lettuce();
Sandwich() {
System.out.println("Sandwich()");
}
public static void main(String[] args) {
new Sandwich();
}
} ///:~
Este ejemplo crea una clase compleja de salida de otras clases, y cada clase tiene
un constructor que se anuncia a si mismo. La clase importante es Sandwich, que
refleja 3 niveles de herencia (4, si contamos la herencia implícita del objeto) y 3
objetos miembros. Cuando un objeto Sandwich es creado en main(), la salida es:
Meal()
Lunch()
PortableLunch()
Bread()
Cheese()
Lettuce()
Sandwich()
Esto significa que el orden de llamada al constructor para un objeto complejo
como sigue:
1. El constructor de la clase-base se llama. Este paso se repite recursivamente hasta que el
raiz de la jerarquía se construye primero, seguido por la siguiente clase-derivada, etc.
Hasta que la clase mas derivada sea alcanzada.
2. Los inicializadores son llamados en el orden de declaración.
3. El cuerpo del constructor de la clase-derivada es llamada.
El orden de las llamadas al constructor es importante. Cuando heredas, sabes todo
a cerca de la clase base y puede acceder a cualquier miembros públicos y
protegidos de la clase base. Esto significa que debes be able to assume que todos
los miembros de una clase base son validos cuando estas en una clase derivada.
En un método normal, la construcción ya ha tenido lugar, por eso todos los
miembros de todas partes del objeto que ha sido construido. Dentro del
constructor, Aunque, debes asumir que todos los miembros que usa han sido
construidos. La única forma de garantizar esto es para los constructores de clasebase para ser llamados primeros. Entonces cuando los miembros puedes acceder
en la clase base han sido inicializados. "Sabiendo que todos los miembros son
validos" dentro del constructor es también razón de que, siempre que sea posible,
debes inicializar todos los objetos miembro (que es, objetos puestos en la clase
usando la composición) a su punto de definición en la clase. (e.g.: b, c, y l en el
ejemplo de abajo). Si sigues esta practica, ayudarás ensure que todos los
miembros de la clase base y los miembros objetos del objeto actual ha sido
inicializado. Desafortunadamente, esto no se maneja en muchos casos, como
verás en la siguiente sección.
Herencia y finalize( )
Cuando usas la composici ón para crear una clase nueva, no te preocupes nunca
por finalizar los objetos miembro de esa clase. Cada miembro es un objeto
independiente y así es basura collected and finalized regardless of whether sucede
a un miembro de tu clase. Con la herencia, aunque debes override finalize() in the
clase derivada si no tienes ningún clanup especial que deba suceder como parte de
la colección de basura. Cuando override finalize() en una clase heredada, es
importante recordar llamar la versión clase-base de finalize(), de otro modo la
finalización de la clase-base no ocurrirá. El siguiente ejemplo provee esto:
//: Frog.java
// Testing finalize with inheritance
class DoBaseFinalization {
public static boolean flag = false;
}
class Characteristic {
String s;
Characteristic(String c) {
s = c;
System.out.println(
"Creating Characteristic " + s);
}
protected void finalize() {
System.out.println(
"finalizing Characteristic " + s);
}
}
class LivingCreature {
Characteristic p =
new Characteristic("is alive");
LivingCreature() {
System.out.println("LivingCreature()");
}
protected void finalize() {
System.out.println(
"LivingCreature finalize");
// Call base-class version LAST!
if(DoBaseFinalization.flag)
try {
super.finalize();
} catch(Throwable t) {}
}
}
class Animal extends LivingCreature {
Characteristic p =
new Characteristic("has heart");
Animal() {
System.out.println("Animal()");
}
protected void finalize() {
System.out.println("Animal finalize");
if(DoBaseFinalization.flag)
try {
super.finalize();
} catch(Throwable t) {}
}
}
class Amphibian extends Animal {
Characteristic p =
new Characteristic("can live in water");
Amphibian() {
System.out.println("Amphibian()");
}
protected void finalize() {
System.out.println("Amphibian finalize");
if(DoBaseFinalization.flag)
try {
super.finalize();
} catch(Throwable t) {}
}
}
public class Frog extends Amphibian {
Frog() {
System.out.println("Frog()");
}
protected void finalize() {
System.out.println("Frog finalize");
if(DoBaseFinalization.flag)
try {
super.finalize();
} catch(Throwable t) {}
}
public static void main(String[] args) {
if(args.length != 0 &&
args[0].equals("finalize"))
DoBaseFinalization.flag = true;
else
System.out.println("not finalizing bases");
new Frog(); // Instantly becomes garbage
System.out.println("bye!");
// Must do this to guarantee that all
// finalizers will be called:
System.runFinalizersOnExit(true);
}
} ///:~
La clase DoBaseFinalization simplemente contiene un indicador que indica a cada
clase en la jerarquía al llamar a super.finalize(). Este indicador esta basado en un
argumento de línea de comando, por eso puedes ver el comportamiento con y sin
finalización de la clase-base.
Cada clase en la jerarquía también contiene un objeto miembro de la clase
Characteristic. Veremos que los regardless of whether finalizadores de la clase
base son llamados, los objetos miembros de Characteristic son siempre
finalizados.
Cada método finalize() overridden debe tener acceso al menos a los miembros
protegidos desde que el método finalize() en la clase Object esta protegida y el
compilador no te permitirá reducir el acceso durante la herencia.
("Amistosamente" es menos accesible que protegido.)
En Frog.main(), el indicador DoBaseFinalization esta configurada y un objeto
simple Frog es creado. Recuerde que la colecci ón de basura y en particular la
finalización puede no ocurrir para ningún objeto particular por eso para forzar
esto, System.runFinalizersOnExit(true) añade el overhead extra para garantizar
que la finalizar tiene lugar. Sin la finalización de la clase-base, la salida es:
not finalizing bases
Creating Characteristic is alive
LivingCreature()
Creating Characteristic has heart
Animal()
Creating Characteristic can live in water
Amphibian()
Frog()
bye!
Frog finalize
finalizing Characteristic is alive
finalizing Characteristic has heart
finalizing Characteristic can live in water
Puedes ver que, realmente, no son llamados los finalizadores para la clase base de
Frog. Pero si añade el argumento "finalize" en la línea de comando, consigues:
Creating Characteristic is alive
LivingCreature()
Creating Characteristic has heart
Animal()
Creating Characteristic can live in water
Amphibian()
Frog()
bye!
Frog finalize
Amphibian finalize
Animal finalize
LivingCreature finalize
finalizing Characteristic is alive
finalizing Characteristic has heart
finalizing Characteristic can live in water
Aunque el orden de los objetos miembro están finalizados es el mismo orden en
que fueron creados, técnicamente el orden de finalización de objetos esta
inespecificado. Con las clases base, Aunque, tienes control sobre el orden de
finalización. El mejor orden para usar es el que se muestra aquí, que es inverso al
orden de inicialización. Siguiendo la forma usada en C++ para destructores,
deberías permitir que las clases derivadas en la primera finalización, entonces
finaliza la clase base. Esto es por que la finalización de la clase derivada podría
llamar a algunos métodos en la clase base que requiere que los componentes de la
clase base están todavía "vivas", por eso debes no destruirlos prematuramente.
Funcionamiento de los métodos polimorficos dentro de
los constructores
La jerarquía de las llamadas a constructores nos trae un dilema interesante. ¿Qué
sucede si estas dentro de un constructor y llamas a un método dynamically-bound
del objeto que s esta construyendo? Dentro de un método ordinario puedes
imaginar que sucederá o la llamada al dynamically-bound es resuelta en tiempo de
ejecución por que el objeto no puede saber si pertenece a la clase el método está
dentro o alguna clase derivada de ella. Puedes pensar esto es que podría suceder
dentro de los constructores.
Este no es exactamente el caso. Si llamas a un método dynamically-bound dentro
de un constructor, la definición overridden para ese método es usada aunque, el
efecto puede ser inesperado, y puede conllevar algunos errores difíciles de
encontrar.
Conceptualmente, el trabajo del constructor es traer el objeto a la existenica (que
es duro una ordinaria hazaña). Dentro de cualquier constructor, el objeto puede
ser solo parcialmente formado y solo puedes saber que los objetos de la clasebase han sido inicializados, pero no puedes saber que clases son heredados. Una
llamada a un método dynamically-bound, Aunque, "adelante", o "detrás" dentro de
la jerarquía de herencia. Este llama un método en una clase derivada. Si haces
esto dentro de un constructor, llamas a un método que puedas manipular
miembros que no hayan sido inicializados todavía y una receta para el desastre.
Puedes ver el problema en el ejemplo siguiente:
//: PolyConstructors.java
// Constructors and polymorphism
// don't produce what you might expect.
abstract class Glyph {
abstract void draw();
Glyph() {
System.out.println("Glyph() before draw()");
draw();
System.out.println("Glyph() after draw()");
}
}
class RoundGlyph extends Glyph {
int radius = 1;
RoundGlyph(int r) {
radius = r;
System.out.println(
"RoundGlyph.RoundGlyph(), radius = "
+ radius);
}
void draw() {
System.out.println(
"RoundGlyph.draw(), radius = " + radius);
}
}
public class PolyConstructors {
public static void main(String[] args) {
new RoundGlyph(5);
}
} ///:~
En Glyph, el método draw() es abstracto, por eso esta diseñado para ser
sobreescrito. Realmente, estas forzado para que lo sobreescriba en Round Glyph.
Pero el constructor Glyph llama a este método, y la llamada termina, en
RoundGlyph.draw() que parecería que hace el intento. Pero mire la salida:
Glyph() before draw()
RoundGlyph.draw(), radius = 0
Glyph() after draw()
RoundGlyph.RoundGlyph(), radius = 5
Cuando el constructor de Glyph llama a draw(), el valor de radius no está todavía
inicializado a 1. Es cero, esto resultará, probablemente o un punto o nada será
pintado en la pantalla, y estará staring intentando to figure out porque el
programa no funcionará.
El orden de inicialización descrita en la sección previa isn´t quite complete , y que
es la llave para resolver el misterio. El actual proceso de inicialización es:
1. El almacenamiento señalado para los objetos es inicializado a 0 binario antes de que
nada suceda.
2. Los constructores clase-base son llamados como se describió anteriormente. En este
punto, el método overridden draw() es llamado (si, antes de que sea llamado el
constructor de RoundGlyph) que describe un radio de valor de cero, debido al paso 1.
3. Los inicializadores de miembros son llamados en el orden de declaración.
4. El cuerpo de los constructores de la clase-derivada es llamada.
Hay una razón para este, es que todo es al menos inicializado a cero (o, cero
significa para este particular tipo de dato) y no figura como basura. Esto incluye
los manejadores de objeto que son embedbed dentro de una clase vía
composición. Por eso si olvidas inicializar que el manejador conseguirá una
excepción en tiempo de ejecución. Todo lo demás tiene 0, que es usualmente el
valor a telltale cuando locking at output .
Por otro lado, deberías extrañarte de este programa. Has hecho unas cosas
perfectamente lógicas y su funcionamiento es misteriosamente erroneo, sin
protesta del compilador (C++ produce funcionamiento mas racional en esta
situación.) Errores como esto, podrían ser fácilmente aburridos y que llevaría
tiempo descubrir.
Como resultado, una buena guía en línea para constructores es, hacer lo menos
posible para poner el objeto en el estado bueno, y si puede evitarse, no hay
ningún método." El único método seguro es llamar dentro un constructor es este
que es final en la clase base. (Eso también es aplicable a método privados que son
automáticamente finales). Estos no pueden ser overridden y así no puede producir
este tipo de sorpresa.
Diseñando con herencia
Una vez hemos aprendido a cerca del polimorfismo, puede parecer que todo debe
ser heredado por que el polimorfismo es una inteligente herramienta. Esto puede
ser una carga para tus diseños; de echo si eliges la herencia primero cuando estas
usando una clase existente para hacer nuevas clases con clases puede ser
complicado.
Un mejor ejemplo es elegir primero composición, cuando esto no es obvio cual
debemos usar la composición no fuerza al diseño en una jerarquía de herencia.
Pero la composición es también mas flexible desde que es posible elegir
dinámicamente un tipo (y su funcionamiento) cuando usas composición, como una
herencia requiere un tipo exacto conocido en tiempo de compilación. El siguiente
ejemplo ilustra esto:
//: Transmogrify.java
// Dynamically changing the behavior of
// an object via composition.
interface Actor {
void act();
}
class HappyActor implements Actor {
public void act() {
System.out.println("HappyActor");
}
}
class SadActor implements Actor {
public void act() {
System.out.println("SadActor");
}
}
class Stage {
Actor a = new HappyActor();
void change() { a = new SadActor(); }
void go() { a.act(); }
}
public class Transmogrify {
public static void main(String[] args) {
Stage s = new Stage();
s.go(); // Prints "HappyActor"
s.change();
s.go(); // Prints "SadActor"
}
} ///:~
Un objeto Stage contiene un manejador para un Actor, que es inicializado a un
objeto HappyActor. Esto significa que go() produce un funcionamiento particular.
Pero desde que un manejador puede ser re-bound a un objeto diferente en tiempo
de compilación, el manejador para el objeto SadActor puede ser sustituido en "a" y
entonces el funcionamiento producido por go() cambia. Incluso ganas en
flexibilidad dinámica en tiempo de ejecución. En contraste, no puedes decidir en
heredar diferentemente en tiempo de ejecución; que debe ser completamente
determinado en tiempo de compilación.
Una guía en línea general es usar la herencia para expresar diferencias en su
funcionamiento, y variables miembro para expresar variaciones en el estado." En
el ejemplo de abajo, son usados tanto: 2 clases diferentes son heredados para
expresar la diferencia en el método act(), y Stage usa composición para permitir
que el estado sea cambiando. En este caso, este cambio es el estado sucede para
producir un cambio en su funcionamiento.
Herencia pura vs. extension
Cuando estudiamos la herencia, podría parecer que la forma mas clara de crear
una jerarquía de herencia es tomar el ejemplo puro. Esto es, solo con método que
han sido establecidos en la clase base o interface que serán overridden en la clase
derivada, como se ve en este diagrama:
Este puede ser llamado una relación "ia-a" pura por la interface de una clase
establecida que es. La herencia garantiza que ninguna clase derivada tendrá el
interface de la clase base y nada menos. Si sigues el diagrama de abajo, las clases
derivadas tendrán también no mas que el interface de la clase base.
Esto puede a traves de una sustitución pura, por que los objetos de la clase
derivada pueden ser perfectamente sustituidos por la clase base, y nunca
necesitarás saber ninguna información extra a cerca de las subclases cuando estás
usándolas:
Esto es, la clase base puede recibir cualquier mensaje que puedas mandar a la
clase derivada ya que las dos tienen exactamente el mismo interface. Todo lo que
necesitas hacer es upcast de la clase derivada y nunca volver a mirara que tipo
exacto de objeto es con el que estas tratando. Todo es manejado a través del
polimorfismo.
Cuando ves esta forma, parecerá una relación "is-a" pura es la única forma
sensible para hacer cosas, y ningún otro diseño indica pensamiento "barroso" y es
por definición "broken". Esto es también una trampa. Tan pronto como empieces a
pensar de esta forma, volverás y descubrirás que extendido el interface (con,
desafortunadamente, la palabra clave extiende parece promover) es la perfecta
solución a un problema particular. Esto podría ser llamado una relacción "is-like-a"
por que la clase derivada es como la clase base y esto tiene fundamentalmente el
mismo interface y pero esto tiene otras caracteristicas que requieren métodos
adicionales para implementar:
Mientras esto sea también un ejemplo útil y sensible (depende de la situaci ón)
esto tiene una desventaja la parte extendida del interface en la clase derivada no
esta disponible desde la clase base, por lo que una vez upcast no puedes llamar
los nuevos métodos:
Si no estas upcasting en este caso, no te preocupará pero con frecuencia te
encontrarás en una situación en la que necesitas redescubrir el tipo exacto del
objeto, por eso puedes acceder a los métodos extendidos de este tipo. La secci ón
siguiente enseña como se hace esto.
Downcasting y identificación de tipos en tiempo de
ejecución
Desde que pierdes la información del tipo especifico por un upcast (moviéndose en
la jerarquía de herencia), este se asegura que para recuperar la información del
tipo y esto es, moverse abajo en la jerarquía de herencia y usa un downcast .
Como sea, saber que un upcast es siempre seguro; la clase base no puede tener
un interface mas grande que el de la derivada, entonces todos los mensajes que
se mandan a través del interface de la clase base es garantizado que será
aceptado. Pero con un downcast , no puedes realmente saber que "shape" (por
ejemplo) es actualmente un circulo. Esto puede en vez de ser un triángulo o un
cuadrado u otro tipo.
Para resolver este problema debe existir alguna forma de garantizar que un
downcast es correcto, por eso no cast accidentalmente a un tipo erróneo y luego
mandar un mensaje que el objeto no puede aceptar. Esto no seria seguro.
En algunos lenguajes (como C++) deber permitir una operaci ón especial para
conseguir un downcast de tipo seguro, pero en Java todos los cast son
chequeados. Incluso cuando parecen proporcionados por un parentesco ordinario
en un cast , en tiempo de ejecución este cast es chequeado para asegurarse de
que es de echo el tipo que pensamos que es. Si no lo es, tendrás una
ClassCastException. Este acto de chequear los tipos en tiempo de ejecución es
llamando identificación de tipo en tiempo de ejecución (ITTE). El próximo ejemplo
demuestra el funcionamiento de (ITTE):
//: RTTI.java
// Downcasting & Run-Time Type
// Identification (RTTI)
import java.util.*;
class Useful {
public void f() {}
public void g() {}
}
class MoreUseful extends Useful {
public void f() {}
public void g() {}
public void u() {}
public void v() {}
public void w() {}
}
public class RTTI {
public static void main(String[] args) {
Useful[] x = {
new Useful(),
new MoreUseful()
};
x[0].f();
x[1].g();
// Compile-time: method not found in Useful:
//! x[1].u();
((MoreUseful)x[1]).u(); // Downcast/RTTI
((MoreUseful)x[0]).u(); // Exception thrown
}
} ///:~
Como en el diagrama, MoreUseful extiende el interface de Useful. Pero desde que
es heredado, puede ser también upcast a un Useful. Puedes ver este echo en la
inicializaci ón del array x en main(). Desde que ambos objetos en el array son de la
clase Useful, puedes mandar los f() y g() a ambos, y si intentas llamar al método
u() (que existe solo en MoreUseful) obtendrás un error en tiempo de compilación.
Si quieres acceder al interface extendido del objeto MoreUseful, puedes intentar
downcast . Si es el tipo correcto, será satisfactorio. De otro modo obtendrás un
ClassCastException. No necesitas escribir ningún código especial para esta
excepción, desde que indica un error al programador que puede suceder en
cualquier sitio de un programa.
Hay mas de ITTE que un simple cast . Por ejemplo, hay una forma de ver que tipo
es con el que estas tratando antes de intentar downcast it . Todo el capitulo 11 es
dedicado a estudiar los diferentes aspectos de Java de identificación de tipos en
tiempo de ejecución.
Resumen
Polimorfismo significa "diferentes formas". En la programación orientada a objetos,
tienes la misma cara (el mismo interface en la clase base) y diferentes formas de
usar esa cara: las diferentes versiones de los métodos dynamically-bound .
Hemos visto en este capitulo que es imposible comprender o incluso crear un
ejemplo de polimorfismo sin usar datos de abstracción y herencia. El polimorfismo
es una característica que no puede ser visto aislado (como una orden switch, por
ejemplo), pero en vez de funcionar solo como se planeo, como parte de un "grancuadro" de clases de relaciones. La gente suele confundirse por otros,
caracteristicas de Java no orientadas a objetos, como método overloading que son
algunas veces presentados como objeto orientado. No se engañe: si no es late
binding , no es polimorfismo.
Para usar polimorfimso, y así las técnicas orientadas a objetos, efectivamente en
tus programas debes expandir tu visión de programar para incluir no solamente
miembros y mensajes de clase individuales pero tambien las diferentes clases
comunes y sus relaciones con cada otro. Aunque requiere un esfuerzo significante,
es un preciado esfuerzo, por que los resultados son mas rápidos que el desarrollo
de programas mejores organizados y codificados, programas extensibles, y código
mas fácil de mantener.
Ejercicios
1. Añade un nuevo método a la clase base Shapes.java que imprima un mensaje, pero no
lo sobreescribas en la clase derivada. Explica que ocurre. Ahora sobreescríbelo en una de
las clases derivadas pero no en las otra y observa lo que ocurre. Finalmente,
sobreescr íbelo en todas las clases derivadas.
2. Añade un nuevo tipo de Shape a Shapes.java y verifica en el main() que el
polimorfismo funciona para tu nuevo tipo como lo hacía para los viejos.
3. Cambia Music3.java para que what() sea predominante sobre el método toString()
de Object . Intenta imprimir el objeto Instrument usando System.out.println() (sin
ningún casting).
4. Añade una nuevo tipo de Instrument a Music3.java y verifica que el polimorfismo
funciona para tu nuevo tipo.
5. Modifica Music3.java para que cree objetos tipo Instrument aleatoriamente de la
forma que Shapes.java lo hace.
6. Crear una jerarquía de herencia de Rodent: Mouse, Gerbil, Hamster, etc. En la clase base
se proveen métodos que son comunes a todos los Rodents y override esto en las clase
derivadas para permitir diferentes funcionamientos dependiente de un tipo especifico de
Rodent. Crear un array de Rodent, llenarlo con diferentes tipo específicos de Rodents, y
llame a los métodos de la clase base para ver que sucede.
7. Cambiar el ejercicio 6 parar que Rodent sea una clase abstracta. Transforma los
métodos de Rodent en abstract en la medida que sea posible.
8. Crea una clase como abstract sin incluir ningún método abstracto y verifica que no
puedes crear ninguna llamada a esa clase.
9. Añade la clase Pickle a Sandwich.java .
10. Modifica el ejercicio 6 para que muestre el orden de inicialización de las clases bases y
derivadas. Ahora, añade un objeto miembro a las clases base y derivadas, y observa el
orden en el cual la inicialización ocurre en la construcción de los objetos.
11. Crea una jerarquía de herencia de tercer nivel. Cada clase en la jerarquía debe tener un
método finalize() y debe llamar debidamente a la versión de finalize() de la clase base.
Demostrar que tu jerarquía funciona correctamente.
12. Crear una clase base con dos métodos. En el primero, llama al segundo. De ella hereda
una clase y sobreescribe el segundo método. Crea un objeto de la clase derivada, haz un
"upcast" hacia la clae base y llama al primer método. Explica lo que ocurre.
13. Crea una clase con un método abstract imprime() que sea sobreescrito en una clase
derivada. La versión sobreescrita del método imprime el valor de un entero definido en la
clase derivada. Como valor inicial, dale un valor distinto de cero. En el constructor de la
clase base, llama a este método. En main() , crea un objeto de la clase derivada y llama a
su imprime() . Explica los resultados.
14. Siguiendo el ejemplo en Transmogrify.java , crea una clase Starship que contenga
una referencia a AlertStatus que pueda indicar tres estados diferentes. Incluye métodos
para cambiar los estados.
15. Crea una clase abstracta sin métodos. Deriva de ella una clase y añade un método. Crea
un método estático que haga una referencia a la clase base, haz un "downcasts" hacia la
clase derivada y llama al método. En el main() , demuestra como trabaja. Ahora pon la
declaración de abstracta para el método en la clase base, lo que elimina la necesidad de
hacer un "downcasts".
Descargar