You are on page 1of 19

Universidad Nacional de Trujillo Tecnología de la Programación I

Ingeniería de Sistemas Reutilizando clases

REUTILIZANDO CLASES: COMPOSICIÓN Y HERENCIA

Una de las características más atractivas de Java es la reutilización de código. Pero para ser
revolucionario, es necesario poder hacer muchísimo más que copiar código y cambiarlo.

Este es el enfoque que se utiliza en los lenguajes procedurales como C, pero no ha funcionado muy
bien. Como todo en Java, la solución está relacionada con la clase. Se reutiliza código creando nuevas
clases, pero en vez de crearlas de la nada, se utilizan clases ya existentes que otra persona ya ha construido
y depurado.

Se deben usar las clases sin modificar el código existente. Existen dos formas de lograrlo. La primera
es bastante directa: simplemente se crean objetos de la clase existente dentro de la nueva clase. A esto se le
llama composición, porque la clase nueva está compuesta de objetos de clases existentes. Simplemente se
está reutilizando la funcionalidad del código, no su forma.

El segundo enfoque es más sutil. Crea una nueva clase como un tipo de una clase ya existente.
Literalmente se toma la forma de la clase existente y se le añade código sin modificar a la clase ya
existente. Este acto mágico se denomina herencia, y el compilador hace la mayoría del trabajo. La
herencia es una de las clases angulares de la programación orientada a objetos.

Resulta que mucha de la sintaxis y comportamiento son similares, tanto para la herencia, como para
la composición (lo cual tiene sentido porque ambas son formas de construir nuevos tipos a partir de tipos
existentes).

La composición y la herencia son formas de construir nuevos tipos a partir de tipos existentes.

1. Sintaxis de la Composición.

Para usar la composición simplemente se ubican referencias a objetos dentro de nuevas clases.
Por ejemplo, suponga que se desea tener un objeto que albergue varios objetos de tipo cadena de
caracteres, un par de datos primitivos, y un objeto de otra clase.

En el caso de los objetos no primitivos, se ponen referencias dentro de la nueva clase, pero se
definen los datos primitivos directamente.

public class FuenteAgua {


private String s;
public FuenteAgua() {
System.out.println("FuenteAgua()");
s = new String("Construida");
}
public String toString() {
return s;
}
}

Se ha implementado la clase FuenteAgua, que tiene como atributo una referencia a un objeto
de la clase String, un método constructor y el método toString, conocido también como
método de impresión por defecto, el cual es invocado en situaciones especiales cuando el compilador
desea obtener un objeto como cadena de caracteres.

public class Aspersor {


private String valvula1, valvula2, valvula3, valvula4;
private FuenteAgua fuente;
private int i;
private float f;

Ing. Zoraida Yanet Vidal Melgarejo, Mg. -1-


Universidad Nacional de Trujillo Tecnología de la Programación I
Ingeniería de Sistemas Reutilizando clases

public void escribir() {


System.out.println("valvula1 = " + valvula1);
System.out.println("valvula2 = " + valvula2);
System.out.println("valvula3 = " + valvula3);
System.out.println("valvula4 = " t valvula4);
System.out.println("i = " + i);
System.out.println("f = " + f);
System.out.println("fuente = " + fuente);
}
public static void main(String[] args) {
Aspersor x = new Aspersor();
x.escribir();
}
}

A primera vista, uno podría asumir – siendo Java tan seguro y cuidadoso como es – que el
compilador podría construir automáticamente objetos para cada una de las referencias en el código
del método escribir de la clase Aspersor; por ejemplo, invocando al constructor por defecto
para FuenteAgua para inicializar fuente.

De hecho, al ejecutar la clase Aspersor, la salida de la sentencia de impresión es:

valvula1 = null
valvula2 = null
valvula3 = null
valvula4 = null
i = 0
f = 0.0
fuente = null

Los datos primitivos son campos de una clase que se inicializan automáticamente a cero. Pero
las referencias a objetos se inicializan a null, y si se intenta invocar a métodos de cualquiera de ellos,
se sigue obteniendo una excepción. De hecho, es bastante bueno (y útil) poder seguir imprimiéndolos
sin lanzar excepciones.

Tiene sentido que el compilador no cree un objeto por defecto para cada referencia, porque eso
conllevaría una sobrecarga innecesaria en la mayoría de los casos. Si se quieren referencias
inicializadas, se puede hacer:
1. En el punto en que se definen los objetos. Esto significa que siempre serán inicializados
antes de invocar al constructor.
2. En el constructor de esa clase.
3. Justo antes de que, de hecho, se necesite el objeto. A esto se le llama inicialización perezosa.
Puede reducir la sobrecarga en situaciones en las que no es necesario crear siempre el
objeto.

A continuación, se muestran los tres enfoques a través de la implementación de las clases Hoja
y Libro:

public class Hoja {


private String s;
public Hoja() {
System.out.println("Hoja()");
s = new String("Construida");
}
public String toString() {
return s;
}
}

Ing. Zoraida Yanet Vidal Melgarejo, Mg. -2-


Universidad Nacional de Trujillo Tecnología de la Programación I
Ingeniería de Sistemas Reutilizando clases

public class Libro {


private String
// Inicializando en el momento de la definición:
s1 = new String("Escrita"),
s2 = "En blanco",
s3, s4;
private Hoja pagina;
private int numPaginas;
public Libro() {
System.out.println("Dentro del Libro()");
s3 = new String("Rayada");
numPaginas = 47;
pagina = new Hoja();
}
public void escribir() {
// Inicialización tardía:
if(s4==null)
s4 = new String("Cuadriculada");
System.out.println("s1 = " + s1);
System.out.println("s2 = " + s2);
System.out.println("s3 = " + s3);
System.out.println("s4 = " + s4);
System.out.println("Numero de paginas = " + i);
System.out.println("pagina = " + pagina);
}
public static void main(String[] args) {
Libro b = new Libro();
b.escribir();
}
}

En el constructor Libro se ejecuta una sentencia antes de que tenga lugar ninguna
inicialización. Cuando no se inicializa en el momento de la definición, sigue sin haber garantías de que
se lleve a cabo ningún tipo de inicialización antes de que se envíe un mensaje a una referencia a un
objeto – excepto la inevitable excepción en tiempo de ejecución. Cuando se invoca al método
escribir(), éste rellena s4 para que todos los campos estén inicializados correctamente cuando se
usen. He aquí la salida del programa:

Dentro del Libro()


Hoja()
s1 = Escrita
s2 = En blanco
s3 = Rayada
s4 = Cuadriculada
Numero de paginas = 47
pagina = Construida

2. Sintaxis de la Herencia.

La herencia es una parte integral de Java (y de todos los lenguajes de POO en general). Resulta
que siempre se está haciendo herencia cuando se crea una clase, pero a menos que se herede
explícitamente de otra clase, se hereda implícitamente de la clase raíz estándar de Java Object. He
aquí un ejemplo:

public class ProductoLimpieza {


private String s = new String("Producto de Limpieza");
public void agregar(String a) {
s += a;
}

Ing. Zoraida Yanet Vidal Melgarejo, Mg. -3-


Universidad Nacional de Trujillo Tecnología de la Programación I
Ingeniería de Sistemas Reutilizando clases

public void diluir() {


agregar("\n diluir()");
}
public void aplicar() {
agregar("\n aplicar()");
}
public void frotar() {
agregar("\n fregar()");
}
public void escribir() {
System.out.println(s);
}
public static void main(String[] args) {
ProductoLimpieza x = new ProductoLimpieza();
x.diluir();
x.aplicar();
x.frotar();
x.escribir();
}
}

La sintaxis para la composición es obvia, pero para llevar a cabo herencia se realiza de distinta
forma. Cuando se hereda, se dice: "Esta clase nueva es como esa clase vieja". Se dice esto en el
código dando el nombre de la clase, como siempre, pero antes de abrir el paréntesis del cuerpo de la
clase, se pone la palabra clave extends seguida del nombre de la clase base. Cuando se hace esto,
automáticamente se tienen todos los datos miembro y métodos de la clase base. Note que en el
método agregar() de la clase ProductoLimpieza, se concatenan cadenas de caracteres a s
utilizando el operador +=, que es uno de los operadores (junto con '+') que los diseñadores de Java
"sobrecargaron" para que funcionara con cadenas de caracteres. He aquí la salida del programa al
ejecutar la clase ProductoLimpieza:

Producto de Limpieza
diluir()
aplicar()
fregar()

La clase Detergente se implementa como una extensión de la clase ProductoLimpieza.

public class Detergente extends ProductoLimpieza {


public void frotar() {
agregar("\n Detergente.frotar()");
super.frotar();
}
public void aclarar() {
agregar("\n aclarar()");
}
public static void main(String[] args) {
Detergente x = new Detergente();
x.diluir();
x.aplicar();
x.frotar();
x.aclarar();
x.escribir();
System.out.println("\n Probando la clase base: ");
ProductoLimpieza.main(args);
}
}

He aquí la salida del programa al ejecutar la clase Detergente:

Ing. Zoraida Yanet Vidal Melgarejo, Mg. -4-


Universidad Nacional de Trujillo Tecnología de la Programación I
Ingeniería de Sistemas Reutilizando clases

Producto de Limpieza
diluir()
aplicar()
Detergente.frotar()
fregar()
aclarar()

Probando la clase base:


Producto de Limpieza
diluir()
aplicar()
fregar()

Tanto ProductoLimpieza como Detergente contienen un método main(). Se puede


crear un método main() por cada clase que uno cree, y se recomienda codificar de esta forma, de
manera que todo el código de prueba esté dentro de la clase. Incluso si se tienen muchas clases en un
programa, sólo se invocará al método main() de la clase invocada en la línea de comandos. Dado
que main() es público, no importa si la clase a la que pertenece es o no pública. Por tanto, en este
caso, cuando se escriba javaDetergente, se invocará a Detergente.main(). Pero también se
puede hacer que ProductoLimpieza invoque a ProductoLimpieza.main(), incluso aunque
ProductoLimpieza no fuera una clase pública.

Esta técnica de poner un método main() en cada clase permite llevar a cabo pruebas para cada
clase de manera sencilla. Y no es necesario eliminar el método main() cuando se han acabado las
pruebas; se puede dejar ahí por si hubiera que usarlas para otras pruebas más adelante. Aquí, se puede
ver que Detergente.main() llama a ProductoLimpieza.main() explícitamente, pasándole
los mismos argumentos de la línea de comandos (sin embargo, se podría pasar cualquier array de
cadenas de caracteres).

Es importante que todos los métodos de ProductoLimpieza sean públicos. Si se deja sin
poner cualquier modificador de miembro, el miembro será por defecto "amistoso", lo cual permite
acceder sólo a los miembros del paquete. Por consiguiente, dentro de este paquete, cualquiera podría
usar esos métodos si no hubiera modificador de acceso. Detergente no tendría problemas, por
ejemplo.

Sin embargo, si se fuera a heredar desde ProductoLimpieza una clase de cualquier otro
paquete, ésta sólo podría acceder a las clases públicas. Por tanto, al planificar la herencia, como regla
general, deben hacerse todos los campos privados y todos los miembros públicos (los miembros
protegidos también permiten accesos por parte de clases derivadas).

Note que ProductoLimpieza tiene un conjunto de métodos en su interfaz: agregar(),


diluir(), aplicar(), frotar() y escribir(). Dado que Detergente se hereda de
ProductoLimpieza (mediante la palabra clave extends) automáticamente se hace con estos
métodos en su interfaz, incluso aunque no se encuentren explícitamente definidos en Detergente.
Se puede pensar que la herencia, por tanto, es una reutilización del interfaz. (la implementación
también se hereda, pero esto no es lo importante.)

Como se ve en frotar(), es posible tomar un método que se haya definido en la clase base y
modificarlo. En este caso, se podría desear llamar al método desde la clase base dentro de la nueva
versión. Pero dentro de frotar() no se puede simplemente invocar a frotar(), dado que eso
produciría una llamada recursiva, que no es lo que se desea. Para solucionar este problema Java tiene
la palabra clave super que hace referencia a la "superclase" de la cual ha heredado la clase actual.
Por consiguiente, la expresión super.frotar() llama a la versión que tiene la clase base del
método frotar().

Ing. Zoraida Yanet Vidal Melgarejo, Mg. -5-


Universidad Nacional de Trujillo Tecnología de la Programación I
Ingeniería de Sistemas Reutilizando clases

Al heredar, uno no se limita a usar los métodos de la clase base. También se pueden añadir
nuevos métodos a la clase derivada, exactamente de la misma manera que se introduce un método en
una clase: simplemente se definen. El método aclarar() es un ejemplo de esta afirmación.

En Detergente.main() se puede ver que, para un objeto Detergente, se puede invocar a


todos los métodos disponibles, también en ProductoLimpieza y en Detergente (por ejemplo,
aclarar()).

3.1 Inicializando la clase base.

Dado que ahora hay dos clases involucradas – la clase base y la clase derivada – en vez de
simplemente una, puede ser un poco confuso intentar imaginar el objeto resultante producido
por una clase derivada. Desde fuera, parece que la nueva clase tiene la misma interfaz que la
clase base, y quizás algunos métodos y campos adicionales.

Pero la herencia no es una simple copia de la interfaz de la clase base. Cuando se crea un
objeto de la clase derivada, éste contiene dentro de él un subobjeto de la clase base. Este
subobjeto es el mismo que si se hubiera creado un objeto de la clase base en sí. Es simplemente
que, desde fuera, el subobjeto de la clase base está envuelto dentro del objeto de la clase
derivada.

Es esencial que el subobjeto de la clase base se inicialice correctamente y sólo hay una
forma de garantizarlo: llevar a cabo la inicialización en el constructor, invocando al constructor
de la clase base, que tiene todo el conocimiento y privilegios apropiados para llevar a cabo la
inicialización de la clase base. Java inserta automáticamente llamadas al constructor de la clase
base en el constructor de la clase derivada.

El ejemplo siguiente muestra este funcionamiento con tres niveles de herencia:

public class Arte {


public Arte() {
System.out.println("Constructor de arte");
}
}

public class Dibujo extends Arte {


public Dibujo() {
System.out.println("Constructor de dibujo");
}
}

public class Animacion extends Dibujo {


public Animacion() {
System.out.println("Constructor de animacion");
}
public static void main (String[] args) {
Animacion x = new Animacion();
}
}

La salida de este programa muestra las llamadas automáticas:

Constructor de arte
Constructor de dibujo
Constructor de animacion

Se puede ver que la construcción se da desde la base "hacia fuera", de forma que se
inicializa la clase base antes de que los constructores de la clase derivada puedan acceder a ella.

Ing. Zoraida Yanet Vidal Melgarejo, Mg. -6-


Universidad Nacional de Trujillo Tecnología de la Programación I
Ingeniería de Sistemas Reutilizando clases

Incluso si no se crea un constructor para Animacion(), el compilador creará un constructor


por defecto que invoque al constructor de la clase base.

3.2 Constructores con parámetros.

El ejemplo anterior tiene constructores por defecto; es decir, no tienen ningún parámetro.
Para el compilador es fácil invocarlos porque no hay ningún problema que resolver respecto al
paso de parámetros. Si una clase no tiene parámetros por defecto, o si se desea invocar a un
constructor de una clase base que tiene parámetros, hay que escribir explícitamente la llamada al
constructor de la clase base usando la palabra clave super y la lista de parámetros apropiada:

public class Juego {


public Juego(int i) {
System.out.println("Constructor de juego");
}
}

public class JuegoMesa extends Juego {


public JuegoMesa(int i) {
super(i);
System.out.println("Constructor de JuegoMesa");
}
}

public class Ajedrez extends JuegoMesa {


public Ajedrez() {
super(11);
System.out.println("Constructor de Ajedrez");
}
public static void main (String[] args) {
Ajedrez x = new Ajedrez();
}
}

Si no se invoca al constructor de la clase base de JuegoMesa(), el compilador se quejará


al no poder encontrar un constructor de la forma Juego(). Además, la llamada al constructor
de la clase base debe ser lo primero que se haga en el constructor de la clase derivada (el
compilador así lo recordará cuando no se haga correctamente).

3. Combinando la composición y la herencia.

Es muy frecuente usar la composición y la herencia juntas. El ejemplo siguiente muestra la


creación de una clase más compleja, utilizando tanto la herencia como la composición, junto con la
inicialización necesaria del constructor:

public class Plato {


public Plato(int i){
System.out.println("Constructor de plato");
}
}

public class PlatoCena extends Plato {


public PlatoCena(int i) {
super(i);
System.out.println("Constructor de PlatoCena");
}
}

Ing. Zoraida Yanet Vidal Melgarejo, Mg. -7-


Universidad Nacional de Trujillo Tecnología de la Programación I
Ingeniería de Sistemas Reutilizando clases

public class Utensilio {


public Utensilio(int i) {
System.out.println("Constructor de utensilio");
}
}

public class Cuchara extends Utensilio {


public Cuchara(int i) {
super(i);
System.out.println("Constructor de cuchara");
}
}

public class Tenedor extends Utensilio {


public Tenedor(int i) {
super(i);
System.out.println("Constructor de tenedor");
}
}

public class Cuchillo extends Utensilio {


public Cuchilllo(int i) {
super(i);
System.out.println("Constructor de cuchillo");
}
}

public class Costumbre {


public Costumbre(int i) {
System.out.println("Constructor de costumbre");
}
}

public class PonerMesa extends Costumbre {


private Cuchara cc;
private Tenedor tnd;
private Cuchillo cch;
private Platocena pc;
public PonerMesa(int i) {
super(i+1);
cc = new Cuchara(i+2);
tnd = new Tenedor(i+3);
cch = new Cuchillo(i+4);
pc = new PlatoCena(i+5);
System.out.println("Constructor de PonerMesa");
}
public static void main (String [ ] args) {
PonerMesa x = new PonerMesa(9);
}
}

4. Elección entre composición y herencia.

La composición suele usarse cuando se quieren mantener las características de una clase ya
existente dentro de la nueva, pero no su interfaz. Es decir, se empotra un objeto de forma que se
puede usar para implementar su funcionalidad en la nueva clase, pero el usuario de la nueva la clase
ve la interfaz que se ha definido para la nueva clase en vez de la interfaz del objeto empotrado. Para
lograr este efecto, se empotran objetos privados de clases existentes dentro de la nueva clase. En
ocasiones, tiene sentido permitir al usuario de la clase acceder directamente a la composición de la
nueva clase; es decir, hacer a los objetos miembros públicos. Los objetos miembro usan por sí

Ing. Zoraida Yanet Vidal Melgarejo, Mg. -8-


Universidad Nacional de Trujillo Tecnología de la Programación I
Ingeniería de Sistemas Reutilizando clases

mismos la ocultación de información, de forma que esto es seguro. Cuando el usuario sabe que se
está ensamblando un conjunto de partes, construye una interfaz más fácil de entender. Un objeto
coche es un buen ejemplo:

public class Motor {


public void arrancar() { }
public void acelerar() { }
public void parar() { }
}

public class Rueda {


public void inflar(int psi) { }
}

public class Ventana {


public void subir() { }
public void bajar() { }
}

public class Puerta {


public Ventana ventana = new Ventana();
public void abrir() { }
public void cerrar() { }
}

public class Coche {


public Motor motor = new Motor();
public Rueda[] rueda = new Rueda[4];
public Puerta izquierda = new Puerta(),
derecha = new Puerta();
public Coche() {
for(int i=0; i<4; i++)
rueda[i] = new Rueda();
}
public static void main(String[] args) {
Coche coche = new Coche();
coche.izquierda.ventana.subir();
coche.rueda[0].inflar(72);
}
}

Dado que la composición de un coche es parte del análisis del problema ('y no simplemente
parte del diseño subyacente), hacer sus miembros públicos ayuda al entendimiento por parte del
programador cliente de cómo usar la clase, y requiere menos complejidad de código para el creador
de la clase. Sin embargo, hay que ser consciente de que éste es un caso especial y en general los
campos se harán privados.

Cuando se hereda, se toma una clase existente y se hace una versión especial de la misma. En
general, esto significa que se está tomando una clase de propósito general y especializándola para una
necesidad especial. Simplemente pensando un poco se verá que no tendría sentido componer un
coche utilizando un objeto vehículo -un coche no contiene un vehículo, es un vehículo. La relación
es-un se expresa con herencia, y la relación tiene-un se expresa con composición.

5. Protegido (protected).

Ahora que se ha presentado el concepto de herencia, tiene sentido finalmente la palabra clave
protected. En un mundo ideal, los miembros privados siempre serían irrevocablemente privados,
pero en los proyectos reales hay ocasiones en las que se desea hacer que algo quede oculto del mundo
en general, y sin embargo, permitir acceso a miembros de clases derivadas. La palabra clave

Ing. Zoraida Yanet Vidal Melgarejo, Mg. -9-


Universidad Nacional de Trujillo Tecnología de la Programación I
Ingeniería de Sistemas Reutilizando clases

protected es un nodo de pragmatismo. Dice: "Esto es privado en lo que se refiere al usuario de la


clase, pero está disponible para cualquiera que herede de esta clase o a cualquier otro de este paquete.
Es decir, protegido es automáticamente "amistoso" en Java.

La mejor conducta a seguir es dejar los miembros de datos privados – uno siempre debería
preservar su derecho a cambiar la implementación subyacente. Posteriormente se puede permitir
acceso controlado a los descendientes de la clase a través de los métodos protegidos:

public class Villano {


private int i;
public Villano(int ii) {
i = ii;
}
protected int leer() {
return i;
}
protected void poner(int ii) {
i = ii;
}
public int valor(int m) {
return m*i;
}
}

public class Malvado extends Villano {


private int j;
public Malvado(int jj) {
super(jj);
j = jj;
}
public void cambiar(int x) {
poner(x);
}
}

Se puede ver que cambiar() tiene acceso a poner() porque es protegido.

6. Desarrollo Incremental.

Una de las ventajas de la herencia es que soporta el desarrollo incremental permitiendo


introducir nuevo código sin introducir errores en el código ya existente. Esto también aísla nuevos
fallos dentro del nuevo código. Pero al heredar de una clase funcional ya existente y al añadirle
nuevos atributos y métodos (y redefiniendo métodos ya existentes), se deja el código existente – que
alguien más podría estar utilizando – intacto y libre de errores. Si se da un fallo, se sabe que éste se
encuentra en el nuevo código, que es mucho más corto y sencillo de leer que si hubiera que modificar
el cuerpo del código existente.

Es bastante sorprendente la independencia de las clases. Ni siquiera se necesita el código fuente


de los métodos para reutilizar el código. Como máximo, simplemente habría que importar el paquete
(esto es cierto, tanto en el caso de la herencia, como en el de la composición.).

Es importante darse cuenta de que el desarrollo de un programa es un proceso incremental, al


igual que el aprendizaje humano. Se puede hacer tanto análisis como se quiera, pero se siguen sin
conocer todas las respuestas cuando comienza un proyecto. Se tendrá mucho más éxito – y una
realimentación mucho más inmediata – si empieza a "crecer" el proyecto como una criatura
evolucionaria, orgánica, en vez de construirlo de un tirón como si fuera un rascacielos de cristal.

Ing. Zoraida Yanet Vidal Melgarejo, Mg. - 10 -


Universidad Nacional de Trujillo Tecnología de la Programación I
Ingeniería de Sistemas Reutilizando clases

Aunque la herencia puede ser una técnica útil de cara a la experimentación, en algún momento,
una vez que las cosas se estabilizan es necesario echar un nuevo vistazo a la jerarquía de clases
definida intentando encajarla en una estructura con sentido. Recuérdese que bajo todo ello, la
herencia simplemente pretende expresar una relación que dice: "Esta nueva clase es un tipo de esa
otra clase". Al programa no deberían importarle los bits, sino el crear y manipular objetos de varios
tipos para expresar un modelo en términos que provengan del espacio del problema.

7. Conversión hacia arriba.

El aspecto más importante de la herencia no es que proporcione métodos para la nueva clase.
Es la relación expresada entre la nueva clase y la clase base. Esta relación puede resumirse diciendo:
"La nueva clase es un tipo de la clase existente".

Esta descripción no es simplemente una forma elegante de explicar la herencia – está soportada
directamente por el lenguaje. Como ejemplo, considérese una clase base denominada Instrumento
que representa los instrumentos musicales, y una clase derivada denominada Viento. Dado que la
herencia significa que todos los métodos de la clase base también están disponibles para la clase
derivada, cualquier mensaje que se pueda enviar a la clase base podrá ser también enviado a la clase
derivada. Si la clase Instrumento tiene un método tocar(), también lo tendrán los instrumentos
Viento. Esto significa que se puede decir con precisión que un objeto Viento es también un tipo
de Instrumento. El ejemplo siguiente muestra cómo soporta este concepto el compilador:

public class Instrumento {


public void tocar() { }
static void afinar(Instrumento i) {
// ...
i.tocar();
}
}

Los objetos de viento son instrumentos porque tienen la misma interfaz:

public class Viento extends Instrumento {


public static void main(String[] args) {
Viento flauta = new Viento();
// Conversión hacia arriba
Instrumento.afinar(f1auta);
}
}

Prestemos atención al método afinar(), que acepta una referencia a Instrumento. Sin
embargo, en Viento.main(), se llama al método afinar() proporcionándole una referencia a
Viento.

Dado que Java tiene sus particularidades en la comprobación de tipos, parece extraño que un
método que acepte un tipo llegue a aceptar otro tipo, hasta que uno se da cuenta de que un objeto
Viento es también un objeto Instrumento, y no hay método al que pueda invocar afinar()
para un Instrumento que no esté en Viento.

Dentro de afinar(), el código funciona para Instrumento y cualquier cosa que se derive de
Instrumento, y al acto de convertir una referencia a Viento en una referencia a Instrumento se
le denomina hacer conversión hacia arriba.

7.1. ¿Por qué “conversión hacia arriba”?.

La razón para el término es histórica, y se basa en la manera en que se han venido


dibujando tradicionalmente los diagramas de herencia: con la raíz en la parte superior de la

Ing. Zoraida Yanet Vidal Melgarejo, Mg. - 11 -


Universidad Nacional de Trujillo Tecnología de la Programación I
Ingeniería de Sistemas Reutilizando clases

página, y creciendo hacia abajo (por supuesto, se puede dibujar un diagrama de cualquier manera
que uno considere útil.)

El diagrama de herencia para Viento.java es, por consiguiente:

Instrumento

Viento

La conversión de clase derivada a base se mueve hacia arriba dentro del diagrama de
herencia, por lo que se denomina conversión hacia arriba. Esta operación siempre es segura
porque se va de un tipo más específico a uno más general. Es decir, la clase derivada es un
superconjunto de la clase base. Podría contener más métodos que la clase base, pero debe
contener al menos los métodos de ésta última. Lo único que puede pasar a la interfaz de clases
durante la conversión hacia arriba es que pierda métodos en vez de ganarlos. Ésta es la razón
por la que el compilador permite la conversión hacia arriba sin ningún tipo de conversión
especial u otras notaciones especiales.

7.2. De nuevo composición frente a herencia.

En la programación orientada a objetos, la forma más probable de crear código es


simplemente empaquetando juntos datos y métodos en una clase, y usando los objetos de esa
clase. También se utilizarán clases existentes para construir nuevas clases con composición.
Menos frecuentemente, se usará la herencia. Por tanto, aunque la herencia captura gran parte del
énfasis durante el aprendizaje de POO, esto no implica que se deba hacer en todas partes en las
que se pueda. Por el contrario, se debe usar de una manera limitada, sólo cuando está claro que
es útil. Una de las formas más claras de determinar si se debería usar composición o herencia es
preguntar si alguna vez habrá que hacer una conversión hacia arriba desde la nueva clase a la
clase base. Si se debe hacer una conversión hacia arriba, entonces la herencia es necesaria, pero
si no se necesita, se debería mirar más con detalle si es o no necesaria.

Si uno recuerda preguntar: ''¿Necesito una conversión hacia arriba?" obtendrá una buena
herramienta para decidir entre la composición y la herencia.

8. La palabra clave final.

La palabra clave final de Java tiene significados ligeramente diferentes dependiendo del
contexto, pero en general dice: "Esto no puede cambiarse". Se podría querer evitar cambios por dos
razones: diseño o eficiencia. Dado que estas dos razones son bastante diferentes, es posible utilizar
erróneamente la palabra clave final.

8.1. Para datos.

Muchos lenguajes de programación tienen una forma de indicar al compilador que cierta
parte de código es "constante". Una constante es útil por varias razones:
a. Puede ser una constante en tiempo de compilación que nunca cambiará.
b. Puede ser un valor inicializado en tiempo de ejecución que no se desea que se llegue a
cambiar.

En el caso de una constante de tiempo de compilación, el compilador puede "manejar" el


valor constante en cualquier cálculo en el que se use; es decir, se puede llevar a cabo el cálculo
Ing. Zoraida Yanet Vidal Melgarejo, Mg. - 12 -
Universidad Nacional de Trujillo Tecnología de la Programación I
Ingeniería de Sistemas Reutilizando clases

en tiempo de compilación, eliminando parte de la sobrecarga de tiempo de ejecución. En Java,


estos tipos de constantes tienen que ser datos primitivos y se expresan usando la palabra clave
final. A este tipo de constantes se les debe dar un valor en tiempo de definición.

Un campo que es estático y final sólo tiene un espacio de almacenamiento que no se puede
modificar. Al usar final con referencias a objetos en vez de con datos primitivos, su significado
se vuelve algo confuso. Con un dato primitivo, final convierte el valor en constante, pero con
una referencia a un objeto, final hace de la referencia una constante. Una vez que la referencia se
inicializa a un objeto, ésta nunca se puede cambiar para que apunte a otro objeto. Sin embargo,
se puede modificar el objeto en sí; Java no proporciona ninguna manera de convertir un objeto
arbitrario en una constante (sin embargo, se puede escribir la clase, de forma que sus objetos
tengan el efecto de ser constantes). Esta restricción incluye a los arrays, que también son
objetos.

He aquí un ejemplo que muestra el funcionamiento de los campos final:

class Valor {
int i = 1;
}

public class DatosConstantes {


final int i1 = 9;
static final int VAL_DOS = 99;
public static final int VAL_TRES = 39; // Constante pública
// No pueden ser constantes en tiempo de compilación:
final int i4 = (int)(Math.random()*20);
static final int i5 = (int)(Math.random()*20);
Valor v1 = new Valor();
final Valor v2 = new Valor();
static final Valor v3 = new Valor();
final int[] a = {1, 2, 3, 4, 5, 6}; // Arrays:

public void escribir(String id) {


System.out.println(id + ":" + "i4 = " + i4 + ",i5 = " + i5);
}

public static void main(String[] args) {


DatosConstantes fdl = new DatosConstantes();
//! fdl.i1++; // Error: no se puede cambiar el valor
fdl.v2.i++;
fdl.v1 = new Valor();
for(int i=0; i<fdl.a.length; i++)
fdl.a[i]++;
//! fdl.v2 = new Valor(); // Error: No se puede
//! fdl.v3 = new Valor(); // cambiar ahora la
//! fd1.a = new int[3]; // referencia
fdl.escribir ("fdl");
System.out.println("Creando un nuevo DatosConstantes");
DatosConstantes fd2 = new DatosConstantes();
fdl.escribir ("fdl");
fd2.escribir ("fd2");
}
}

Dado que i1 y VAL_DOS son datos primitivos final con valores de tiempo de
compilación, ambos pueden usarse como constantes de tiempo de compilación y su uso no
difiere mucho. VAL_TRES es la manera más usual en que se verán definidas estas constantes:
pública de forma que puedan ser utilizadas fuera del paquete, estática para hacer énfasis en que
sólo hay una, y final para indicar que es una constante. Fíjese que los datos primitivo static

Ing. Zoraida Yanet Vidal Melgarejo, Mg. - 13 -


Universidad Nacional de Trujillo Tecnología de la Programación I
Ingeniería de Sistemas Reutilizando clases

final con valores iniciales constantes (es decir, las constantes de tiempo de compilación) se
escriben con mayúsculas por acuerdo además de con palabras separadas por guiones bajos. La
diferencia se muestra en la salida de una ejecución:

fdl: i4 = 15, i5 = 9
Creando un nuevo DatosConstante
fdl: i4 = 15, i5 = 9
fd2: i4 = 10, i5 = 9

Fíjese que los valores de i4 para fdl y fd2 son únicos, pero el valor de i5 no ha
cambiado al crear el segundo objeto DatosConstante. Esto es porque es estático y se
inicializa una vez en el momento de la carga y no cada vez que se crea un nuevo objeto.

Las variables de v1 a v4 demuestran el significado de una referencia final. Como se


puede ver en main(), justo porque v2 sea final, no significa que no se pueda cambiar su valor.
Sin embargo, no se puede reubicar v2 a un nuevo objeto, precisamente porque es final. Eso es
lo que final significa para una referencia. También se puede ver que es cierto el mismo
significado para un array, que no es más que otro tipo de referencia (no hay forma de convertir
en final las referencias a array en sí). Hacer las referencias final parece menos útil que hacer
final a las primitivas.

8.1.1. Constantes blancas.


Java permite la creación de constantes blancas, que son campos declarados como
final pero a los que no se da un valor de inicialización. En cualquier caso, se debe
inicializar una constante blanca antes de utilizarla, y esto lo asegura el propio compilador.
Sin embargo, las constantes blancas proporcionan mucha mayor flexibilidad en el uso de
la palabra clave final puesto que, por ejemplo, un campo final incluido en una clase
puede ahora ser diferente para cada objeto, y sin embargo, sigue reteniendo su cualidad
de inmutable. He aquí un ejemplo:

class Elemento { }

class ConstanteBlanca {
final int i=0; // Constante inicializada
final int j; // Constante blanca
final Elemento p; // Referencia a constante blanca
// Constantes blancas DEBEN inicializarse en el constructor:
ConstanteBlanca() (
j=1; // Inicializar la la constante blanca
p = new Elemento();
}
ConstanteBlanca(int x) (
j=x; // Inicializar la constante blanca
p = new Elemento();
}
public static void main(String[] args) {
ConstanteBlanca bf = new ConstanteBlanca();
}
}

Es obligatorio hacer asignaciones a constantes, bien con una expresión en el


momento de definir el campo o en el constructor. De esta forma, se garantiza que el
campo constante se inicialice siempre antes de ser usado.

8.1.2. Parámetros de valor constante.


Java permite hacer parámetros constantes declarándolos con la palabra final en la
lista de parámetros. Esto significa que dentro del método no se puede cambiar aquello a
lo que apunta la referencia al parámetro:

Ing. Zoraida Yanet Vidal Melgarejo, Mg. - 14 -


Universidad Nacional de Trujillo Tecnología de la Programación I
Ingeniería de Sistemas Reutilizando clases

class Artilugio {
public void girar() {
}
}

public class ParametrosConstante {


void con(final Artilugio g) {
//! g = new Artilugio(); // Ilegal - g es constante
}
void sin(Arti1ugio g) {
g = new Artilugio(); // OK - g no es constante
g.girar();
}
// void f(final int i) {
// i++; // No puede cambiar
// }
// Sólo se puede leer de un tipo de dato primitivo:
int g(fina1 int i) {
return i + 1;
}
public static void main(String[] args) {
ParametrosConstante bf = new ParametrosConstante();
bf.sin(null);
bf.con(null);
}
}

Fíjese que se puede seguir asignando una referencia null a un parámetro constante
sin que el compilador se dé cuenta, al igual que se puede hacer con un parámetro no
constante. Los métodos f() y g() muestran lo que ocurre cuando los parámetros
primitivos son constante: se puede leer el parámetro pero no se puede cambiar.

8.2. Métodos constantes.

Hay dos razones que justifican los métodos constantes. La primera es poner un "bloqueo"
en el método para evitar que cualquier clase heredada varíe su significado. Esto se hace por
razones de diseño cuando uno se quiere asegurar de que se mantenga el comportamiento del
método durante la herencia, evitando que sea sobreescrito. La segunda razón para los métodos
constante es la eficiencia. Si se puede hacer un método constante se está permitiendo al
compilador convertir cualquier llamada a ese método en llamadas rápidas. Cuando el compilador
ve una llamada a un método constante puede (a su discreción) saltar el modo habitual de insertar
código para llevar a cabo el mecanismo de invocación al método (meter los argumentos en la
pila, saltar al código del método y ejecutarlo, volver al punto del salto y eliminar los parámetros
de la pila, y manipular el valor de retorno) o, en vez de ello, reemplazar la llamada al método con
una copia del código que, de hecho, se encuentra en el cuerpo del método. Esto elimina la
sobrecarga de la llamada al método. Por supuesto, si el método es grande, el código comienza a
aumentar de tamaño, y probablemente no se aprecien ganancias de rendimiento en la
sustitución, puesto que cualquier mejora se verá disminuida por la cantidad de tiempo invertido
dentro del método. Está implícito el que el compilador de Java sea capaz de detectar estas
situaciones, y elegir sabiamente. Sin embargo, es mejor no confiar en que el compilador sea
capaz de hacer esto siempre bien, y hacer un método constante sólo si es lo suficientemente
pequeño o se desea evitar su modificación explícitamente.

8.2.1. Constante y privado.


Cualquier método privado de una clase es implícitamente constante. Dado que no se
puede acceder a un método privado, no se puede modificar (incluso aunque el
compilador no dé un mensaje de error si se intenta modificar, no se habrá modificado el

Ing. Zoraida Yanet Vidal Melgarejo, Mg. - 15 -


Universidad Nacional de Trujillo Tecnología de la Programación I
Ingeniería de Sistemas Reutilizando clases

método, sino que se habrá creado uno nuevo). Se puede añadir el modificador final a un
método privado pero esto no da al método ningún significado extra. Este aspecto puede
causar confusión, porque si se desea modificar un método privado (que es implícitamente
constante) parece funcionar:

class ConConstantes {
private final void f() { // Idéntico a únicamente "privado"
Systern.out.println("ConConstantes.f()");
}
private void g() { // También automáticamente "constante"
System.out.println("ConConstantes.g()");
}
}

class ModificacionPrivado extends Conconstante {


private final void f() {
System.out.println("ModificacionPrivado.f()");
}
private void g() {
System.out.println("ModificacionPrivado.g()");
}
}

class ModificacionPrivado2 extends ModificacionPrivado {


public final void f() {
System.out.println("ModificacionPrivado2.f()");
}
public void g() {
System.out.println("ModificacionPrivado2.g()");
}
}

public class AparienciaModificacionConstante {


public static void main (String[] args) {
ModificacionPrivado2 op2 = new ModificacionPrivado2();
op2.f();
op2.g();
// Se puede hacer conversión hacia arriba:
ModificacionPrivado op = op2;
// Pero no se puede invocar a los métodos:
//! op.f();
//! op.g();
// Lo mismo que aquí:
ConCostantes wf = op2;
//! wf.f();
//! wf.g();
}
}

La "modificación" sólo puede darse si algo es parte de la interfaz de la clase base. Es


decir, uno debe ser capaz de hacer conversión hacia arriba de un objeto a su tipo base e
invocar al mismo método. Si un método es privado, no es parte de la interfaz de la clasc
base. Es simplemente algún código oculto dentro de la clase, y simplemente tiene ese
nombre, pero si se crea un método público, protegido o "amistoso" en la clase derivada,
no hay ninguna conexión con el método que pudiese llegar a tener ese nombre en la clase
base.

Dado que un método privado es inalcanzable y a efectos invisible, no influye en


nada más que en la organización del código de la clase para la que se definió.

Ing. Zoraida Yanet Vidal Melgarejo, Mg. - 16 -


Universidad Nacional de Trujillo Tecnología de la Programación I
Ingeniería de Sistemas Reutilizando clases

8.3. Clases constantes.

Cuando se dice que una clase entera es constante (precediendo su definición de la palabra
clave final) se establece que no se desea heredar de esta clase o permitir a nadie más que lo haga.
En otras palabras, por alguna razón el diseño de la clase es tal que nunca hay una necesidad de
hacer cambios, o por razones de seguridad no se desea la generación de subclases. De manera
alternativa, se pueden estar tratando aspectos de eficiencia, y hay que asegurarse de que cualquier
actividad involucrada con objetos de esta clase sea lo más eficiente posible.

class CerebroPequenio { }

final class Dinosaurio {


int i=7;
int j=1;
CerebroPequenio x = new CerebroPequenio();
void f() {
}
}

//! class SerEvolucionado extends Dinosaurio { }


// error: No pueda heredar de la clase constante 'Dinosaurio'
public class Jurasico {
public static void main (String[] args) {
Dinosaurio n = new Dinosaurio();
n.f();
n.i = 40;
n.j++;
}
}

Fíjese que los atributos pueden ser constantes o no, como se desee. Las mismas reglas se
aplican a los atributos independientemente de si la clase se ha definido como constante.
Definiendo la clase como constante simplemente se evita la herencia – nada más. Sin embargo,
dado que evita la herencia, todos los métodos de una clase constante son implícitamente
constante, puesto que no hay manera de modificarlos. Por tanto, el compilador tiene las mismas
opciones de eficiencia como tiene si se declara un método explícitamente constante.

Se puede añadir el especificador constante a un método en una clase constante, pero esto
no añade ningún significado.

9. Carga de clases e inicialización.

En lenguajes más tradicionales, los programas se cargan de una vez como parte del proceso de
arranque. Éste va seguido de la inicialización y posteriormente comienza el programa. El proceso de
inicialización en estos lenguajes debe controlarse cuidadosamente de forma que el orden de
inicialización de los datos estáticos no cause problemas. C++, por ejemplo, tiene problemas si uno de
los datos estáticos espera que otro dato estático sea válido antes de haber inicializado el segundo.

Java no tiene este problema porque sigue un enfoque diferente en la carga. Dado que todo en
Java es un objeto, muchas actividades se simplifican, y ésta es una de ellas: el código compilado de
cada clase existe en su propio archivo separado. El archivo no se carga hasta que se necesita el
código. En general, se puede decir que "El código de las clases se carga en el momento de su primer
uso". Esto no ocurre generalmente hasta que se construye el primer objeto de esa clase, pero también
se da una carga cuando se accede a un dato o método estático.

El momento del primer uso es también donde se da la inicialización estática. Todos los objetos
estáticos y el bloque de código estático se inicializarán en orden textual (es decir, el orden en que se

Ing. Zoraida Yanet Vidal Melgarejo, Mg. - 17 -


Universidad Nacional de Trujillo Tecnología de la Programación I
Ingeniería de Sistemas Reutilizando clases

han escrito en la definición de la clase) en el momento de la carga. Los datos estáticos, por supuesto,
se inicializan únicamente una vez.

9.1. Inicialización con herencia.

Ayuda a echar un vistazo a todo el proceso de inicialización, incluyendo la herencia, para


conseguir una idea global de lo que ocurre. Considérese el siguiente código:

class Insecto {
int i=9;
int j;
Insecto() {
visualizar("i = " + i + ", j = " + j);
j = 39;
}
static int xl = visualizar("static Insecto.x1 inicializado");
static int visualizar(String s) {
System.out.println(s);
return 47;
}
}

public class Escarabajo extends Insecto {


int k = visualizar ("Escarabajo.k inicializado") ;
Escarabajo() {
visualizar("k = " + k);
visualizar("j = " + j);
}
static int x2=visualizar("static Escarabajo.x2 inicializado");
public static void main (String[] args) {
visualizar ("Constructor de Escarabajos");
Escarabajo b = new Escarabajo();
}
}

La salida de este programa es:

static Insecto.x1 inicializado


static Escarabajo.x2 inicializado
Constructor de Escarabajos i = 9, j = 0
Escarabaj0.k inicializado
k = 47
j = 39

Lo primero que ocurre al ejecutar Escarabajo bajo Java es que se intenta acceder a
Escarabajo.main() (un método estático), de forma que el cargador sale a buscar el código
compilado de la clase Escarabajo (que resulta estar en un fichero denominado
Escarabajo.class). En el proceso de su carga, el cargador se da cuenta de que tiene una
clase base (que es lo que indica la palabra clave extends), y por consiguiente, la carga. Esto
ocurrirá tanto si se hace como si no un objeto de esa clase.

Si la clase base tiene una clase base, las segunda clase base se cargaría también, y así
sucesivamente. Posteriormente, se lleva a cabo la inicialización estática de la clase base raíz (en
este caso Insecto), y posteriormente la siguiente clase derivada, y así sucesivamente. Esto es
importante porque la inicialización estática de la clase derivada podría depender de que se
inicialice adecuadamente el miembro de la clase base.

En este momento, las clases necesarias ya han sido cargadas de forma que se puede crear el
objeto. Primero, se ponen a sus valores por defecto todos los datos primitivos de este objeto, y

Ing. Zoraida Yanet Vidal Melgarejo, Mg. - 18 -


Universidad Nacional de Trujillo Tecnología de la Programación I
Ingeniería de Sistemas Reutilizando clases

las referencias a objetos se ponen a null – esto ocurre en un solo paso poniendo la memoria
del objeto a ceros binarios. Después se invoca al constructor de la clase base. En este caso, la
llamada es automática, pero también se puede especificar la llamada al constructor de la clase
base (como la primera operación en el constructor de Escarabajo()) utilizando super. La
construcción de la clase base sigue el mismo proceso en el mismo orden, como el constructor de
la clase derivada. Una vez que acaba el constructor de la clase base se inicializan las variables de
instancia en orden textual. Finalmente se ejecuta el resto del cuerpo del constructor.

Ing. Zoraida Yanet Vidal Melgarejo, Mg. - 19 -

You might also like