About this entry
You’re currently reading the article “Extendiendo el Comportamiento de las Clases: Herencia en Java (Inheritance in Java, Spanish only).”
- Published:
- November 25th 06:04 AM
- Updated:
- June 11th 05:36 AM
- Sections:
- Tutorial de Java
Extendiendo el Comportamiento de las Clases: Herencia en Java (Inheritance in Java, Spanish only)
No necesitamos diseñar nuestras clases desde cero. Tenemos la habilidad de utilizar una clase ya implementada y agregarle comportamiento y estado a esta. Así, la nueva clase seria una extensión de la clase original. Ha esto le llamamos herencia, y sobre esto trata este artículo.
Liked it? !
Antes de leer este artículo se recomienda haber leído Iteración y Conjuntos de Objetos.
Índice
- Construyendo un BufferedReader mejorado
- Agregando Comportamiento
- Un BufferedReader Mejorado
- Agregando Estado: Accediendo al Estado de la Superclase
- Re-visitando la clase Nombre: Agregando Estado Adicional
- Mecánica Básica de la Herencia
Construyendo un BufferedReader mejorado
Por ejemplo, la clase BufferedReader nos provee el comportamiento base para modelar la entrada de datos. Pero cuando queremos leer de distintas entradas como el teclado o un archivo necesitamos utilizar o crear otros objetos intermedios (como el System.in y el FileInputStream). Además a pesar de que la lectura de hilera de caracteres se puede hacer de una manera directa a través de readLine, para leer otros tipos de datos necesitamos realizar pasos adicionales. Por ejemplo para leer un dato del tipo int necesitamos leer una hilera usando readLine y después convertirla a numero utilizando Integer.parseInt.
Si quisiéramos modelar una clase mas simple de utilizar en donde no nos preocupáramos por objetos intermedios y pudiéramos leer directamente cualquier tipo de dato, podríamos pensar en modificar la clase BufferedReader. Por varias razones esta solución no seria necesariamente la mas apropiada:
- La clase pudo haber sido probada rigurosamente. Modificar la clase requeriría volver a probar la clase completa.
- Otros que utilizan esta clase podrían no querer el comportamiento adicional. La clase original puede ser todo lo que ellos necesitaban y el modificarla solo aumentaría la complejidad y el costo de la clase.
- Puede que no sea recomendable o posible modificar el código de la clase. Por ejemplo, el modificar una clase predefinida de Java definitivamente no es una buena idea. Cualquier error puede afectar a todos los programas de formas raras. Además, el código fuente de la definición de clase no esta siempre disponible.
- En general, el modificar una clase, especialmente una que no escribió, requiere un entendimiento completo de la implementación de la clase, y tal entendimiento puede o ser imposible o poco practico.
Los lenguajes de programación orientada a objetos como Java, proveen de un mecanismo para la extensión del comportamiento de una clase. Ha este mecanismo se le llama herencia, y es considerado como un elemento crucial requerido para que los lenguajes se llamen orientados a objetos.
Agregando Comportamiento
Con la siguiente declaración:
class BufferedReaderMejorado extends BufferedReader { ... }La palabra clave extends es seguida por el nombre de la clase a extender. Esto define a BufferedReaderMejorado como una clase que automáticamente hereda a todos los métodos y atributos de la clase BufferedReader. Las instancias de BufferedReaderMejorado ya poseen todos los métodos y atributos de la clase BufferedReader. De esta forma, sin escribir una línea de código, hemos definido una nueva clase BufferedReaderMejorado, que contiene un comportamiento completo, el comportamiento heredado del BufferedReader. A la clase BufferedReader se le llama la superclase y a la clase BufferedReaderMejorado se le llama la subclase.
Despues de tener la clase BufferedReaderMejorado definida, se utiliza como cualquier otra clase. Creamos objetos de la clase e invocamos los métodos sobre ellos. En el caso de la subclase, los objetos tienen disponibles todos los métodos definidos en la superclase.
BufferedReaderMejorado brm = new BufferedReaderMejorado(...); String s = brm.readLine();Una subclase además de incluir todo el comportamiento de la superclase, podemos definirle atributos y métodos adicionales. De esta manera le agregaríamos comportamiento nuevo único para la subclase:
class BufferedReaderMejorado extends BufferedReader { ... int readint() ... }Con nuestra referencia brm a un BufferedReaderMejorado podríamos invocar los nuevos métodos de la clase:
int i = brm.readint();Un BufferedReader Mejorado
-
Planteo del Problema
Crear una clase BufferedReader mas conveniente.
-
Buscando el Objeto Primario
El planteo del problema nos provee el objeto primario, un BufferedReader mas inteligente. Le llamaremos ha este BufferedReaderMejorado y le heredaremos del BufferedReader:
class BufferedReaderMejorado extends BufferedReader { ... } -
Determinando el Comportamiento
Nos gustaría que las instancias de la nueva clase poseyeran el siguiente comportamiento:
- Todo el comportamiento del BufferedReader, y en particular el método readLine.
- La habilidad de crear un objeto asociado con un archivo, especificando el nombre del archivo solamente. Si no se especifica un nombre de archivo, que se cree el objeto asociado con System.in.
- Un método que provea una lectura directa de valores int.
-
Definiendo la Interfase
El siguiente podría ser un escenario típico:
BufferedReaderMejorado keyb, file; keyb = new BufferedReaderMejorado(); System.out.println("Ingrese nombre de archivo de articulos:"); System.out.flush(); String fileName = keyb.readLine(); file = new BufferedReaderMejorado(fileName); String nombre = file.readLine(); int precio = file.readint();Definiremos dos constructores:
- Uno que no recibe parámetros (para leer desde el teclado).
- Uno que recibe una hilera como parámetro representando el nombre del archivo desde donde se leería.
Además incluiremos un método readint, para leer enteros directamente.
-
Esqueleto
De la definición de la interfase deducimos el siguiente encabezado de los métodos:
class BufferedReaderMejorado extends BufferedReader { BufferedReaderMejorado(string fileName) { ... } BufferedReaderMejorado() { ... } public int readint() { ... } } -
Definiendo los Atributos
El definir el BufferedReaderMejorado como una extensión del BufferedReader quiere decir que todo los atributos y métodos del BufferedReader son heredados al BufferedReaderMejorado y por ende se convierten en parte del cualquier instancia del BufferedReaderMejorado. Recuerde, una instancia del BufferedReaderMejorado es una instancia del BufferedReader. Por ahora no podemos pensar en ningún atributo que haga falta incluirle al BufferedReaderMejorado.
-
Implementando los Métodos
El trabajo del constructor es proveer un estado inicial valido para el nuevo objeto recién creado. Como el objeto instancia del BufferedReaderMejorado es un BufferedReader, tenemos que el estado asociado a este se inicialice apropiadamente. Normalmente la invocación al constructor del BufferedReader es realizada automáticamente cuando el usuario crea una instancia de este:
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));En nuestra situación, el usuario no esta creando directamente un objeto BufferedReader. En vez de esto, esta está siendo creada automáticamente como parte de la creación del BufferedReaderMejorado:
BufferedReaderMejorado brm = new BufferedReaderMejorado();El constructor del BufferedReaderMejorado se esta invocando ya que estamos creando un objeto BufferedReaderMejorado; sin embargo no hay una invocación automática al constructor del BufferedReader. Esta es responsabilidad del BufferedReaderMejorado. Se logra a través de la palabra reservada super (en vez de hacerlo como lo hemos estado haciendo, a través del nombre de la clase), al cual se le pasa cualquier argumento que necesite el constructor del BufferedReader para ejecutarse correctamente. Esta utilización enfatiza la relación de herencia entre la subclase y la superclase. En el constructor que no recibe ningún parámetro, el argumento que le pasaremos al constructor del BufferedReader será System.in. En el constructor que recibe una hilera, el argumento que le pasaremos al constructor del BufferedReader es un FileInputStream construido sobre un el nombre del archivo representado por la hilera.
Seguido de la invocación del constructor de la superclase, la subclase realiza cualquier inicialización especifica para la subclase. Dado que no tenemos ninguna información de estado en la nuestra subclase, ninguna inicialización subsiguiente es necesaria.
La implementación del método readint consiste en leer una línea a través del método readLine y a continuación usar Integer.parseInt para convertir la línea a un int.
La implementación completa queda así:class BufferedReaderMejorado extends BufferedReader { BufferedReaderMejorado(string fileName) { super(new InputStreamReader(new FileInputStream(fileName))); } BufferedReaderMejorado() { super(new InputStreamReader(System.in)); } public int readint() { String linea = readLine(); return Integer.parseInt(linea); } }Ejercicio de clase
Agréguele un método readdouble al BufferedReaderMejorado para que lea y devuelva un double.
-
Agregando Estado: Accediendo al Estado de la Superclase
La clase BufferedReaderMejorado extiende de la superclase BufferedReader a través de la adición de métodos. Estos métodos realizan su tarea invocando los métodos propios de la superclase y usando variables locales. La nueva clase no requería ningún estado propio ni conocimiento sobre el estado de la superclase.
Sin embargo, la extensión de comportamiento por lo general requiere que la subclase introduzca algunos atributos propios para poder mantener el estado asociado con el nuevo comportamiento. Por ejemplo, a continuación desarrollaremos un NombreExtendido que trabaja con la clase Nombre. El estado extendido agregara un segundo nombre a la información mantenida por la clase Nombre original y esta nueva información será mantenida en la subclase.
class NombreExtendido extends Nombre { // informacion adicional al nombre introducida por NombreExtendido private String segundoNombre; }Además, para que la subclase pueda implementar su comportamiento adicional, algunas veces necesita acceso al estado de la superclase. Mientras los métodos proveen el comportamiento "empacado" apropiadamente para el usuario, los métodos no proveen acceso directo a los atributos, el cual es necesario para la subclase. La nombre extendido necesitara acceso al titulo, primer nombre y apellido si desea crear una hilera compuesta por el titulo, primer nombre, segundo nombre y apellido.
Como declaramos a los atributos de la clase Nombre private la subclase no tiene acceso a ellos. Para resolver este problema, Java posee un tipo de acceso adicional, protected, que es similar a private excepto que además de proveer el acceso a la clase misma también provee el acceso a las subclases. Por lo que en las declaraciones siguientes:
class Nombre { protected String primerNombre, apellido, titulo; ... } class NombreExtendido extends Nombre { ... }los metodos de NombreExtendido tienen acceso a los atributos primerNombre, apellido y titulo, por haber sido declaradas protected.
Re-visitando la clase Nombre: Agregando Estado Adicional
-
Planteo del Problema
Extender el comportamiento de la clase Nombre, para que soporte un segundo nombre. Agregado al comportamiento de la clase Nombre, la nueva clase debe de proveer métodos para devolver la inicial del segundo nombre y el nombre formal consistiendo de el titulo, el primer nombre, el segundo nombre y el apellido.
-
Encontrando el Objeto Primario
Como con el ejemplo anterior, el planteo del problema nos provee del objeto primario, una extensión del clase Nombre que llamaremos NombreExtendido.
-
Determinando el Comportamiento
El comportamiento deseado es también obtenido del planteo del problema como sigue:
- NombreExtendido (constructor)
- middleInitial: devuelve la inicial del segundo nombre
- getFormalName: devuelve el titulo seguido por el primer nombre, el segundo nombre y el apellido.
- Todo el comportamiento de la clase Nombre (obtenemos eso automático a través del proceso de herencia).
-
Definiendo la Interfase
Un código típico que pudiera utilizar esta clase seria:
NombreExtendido ne = new NombreExtendido("Juan","Fernando","Perez"); System.out.println(ne.getMiddleInitial()); System.out.println(ne.getFormalName()); System.out.println(ne.getFirstLast()); ne = new NombreExtendido("Pedro","Suarez"); System.out.println(ne.getMiddleInitial()); System.out.println(ne.getFormalName()); System.out.println(ne.getFirstLast());El constructor original de la clase Nombre requería dos argumentos: primer nombre y el apellido. Estos deben de ser proveídos al constructor de la clase NombreExtendido para que el constructor de la clase Nombre (invocado a través de super) pueda recibir estos valores. Además necesitamos pasarle al constructor el segundo nombre. Podríamos también incluir un constructor que solo reciba un primer nombre y apellido para aquellos nombres que no tienen un segundo nombre. Los métodos middleInitial y formalName no recibirían nada, y devolverían ambos una hilera.
-
Esqueleto
El esqueleto quedaría así:
class NombreExtendido extends Nombre { // atributos public NombreExtendido(String primerNombre, String segundoNombre, String segundoNombre) { } public NombreExtendido(String primerNombre, String segundoNombre) { } public String middleInitial() { } public String getFormalName() { } } -
Definiendo los Atributos
Para mantener el segundo nombre en nuestra clase necesitamos declarar un atributo segundoNombre. Este atributo esta presente solo en la subclase, las instancias de la clase Nombre no poseen dicho atributo. Sin embargo, las instancias de la clase NombreExtendido si heredan el primerNombre, apellido y titulo. Para que los métodos de la clase NombreExtendido tengan acceso a estos atributos, debemos de cambiar su acceso a protected. La dos clases quedarían entonces de la siguiente manera:
class Nombre { protected String primerNombre, apellido, titulo; // metodos ... } class NombreExtendido extends Nombre { private String segundoNombre; // metodos ... } -
Implementando los Métodos
El constructor, además de invocar al constructor de la clase Nombre, también necesita inicializar su propio atributo segundoNombre. El método getMiddleInitial verifica si hay segundo nombre, devolviendo la inicial si lo hay e hilera vacía de lo contrario. El metodo getFormalName simplemente devuelve la concatenación de el titulo, el primer nombre y el apellido. Habiendo declarado el titulo, el primer nombre y el apellido como protected permiten que getFormalName pueda acceder estos atributos.
La implementación quedaría entonces así:
class NombreExtendido extends Nombre { private String segundoNombre; public NombreExtendido(String primerNombre, String segundoNombre, String segundoNombre) { super(primerNombre, apellido); this.segundoNombre=segundoNombre; } public NombreExtendido(String primerNombre, String segundoNombre) { super(primerNombre, apellido); this.segundoNombre=""; } public String middleInitial() { if (segundoNombre.equals("")) return ""; else return segundoNombre.substring(0,1); } public String getFormalName() { return title + " " + primerNombre + " " + segundoNombre + " " + apellido; } }
Ejercicios de clase
- Extienda la clase Nombre para crear una nueva clase, NombreConSufijo, que provee un sufijo, por ejemplo, M.D. o Ph.D. Introduzca un método que imprima el nombre completo con el sufijo.
- Diseñe la clase NombreConSufijo para que extienda a la clase NombreExtendido (en vez de la clase Nombre). Surgen algunos inconvenientes?
-
Mecánica Básica de la Herencia
-
Constructores
Hasta ahora, hemos creado explícitamente cada objeto invocando al operador new, y pasando los argumentos apropiados para el constructor del objeto:
Empleado = new Empleado("Pedro Juarez", 23)En la presencia de la herencia, se complica un poco este asunto. Suponga que creamos un NombreExtendido:
NombreExtendido ne = new NombreExtendido("Miguel", "Angel", "Asturias");A pesar de que un solo objeto esta siendo creado, en cierto sentido este objeto esta compuesto de dos partes anidadas: un Nombre dentro de un NombreExtendido, el segundo consistiendo únicamente de los métodos y atributos definidos dentro de la clase NombreExtendido. A pesar de que ambas partes requiere una inicialización apropiada, únicamente el constructor de NombreExtendido es invocado explícitamente durante la creación del objeto. Por lo que se convierte en la responsabilidad del constructor de NombreExtendido el asegurarse que el constructor de la clase Nombre sea invocado. Hace esto sin la invocación del operador new ya que el objeto Nombre ya ha sido credo en la invocación a new NombreExtendido. Por lo que Java introduce una nueva sintaxis para permitir que una subclase invoque al constructor de la superclase con los argumentos apropiados. La invocación enfatiza la relación sub/super de las clase:
super(argumentos del constructor de la superclase);Normalmente los argumentos que le mandamos a super los obtenemos de los argumentos que recibimos del constructor de la subclase, tal y como mandamos el primerNombre y apellido desde el constructor del NombreExtendido. La invocación al constructor de la superclase debe de ser la primera acción del constructor de la subclase:
public NombreExtendido(String primerNombre, String segundoNombre, String segundoNombre) { super(primerNombre, apellido); this.segundoNombre=segundoNombre; }Para invocar un constructor de la superclase que no recibe ningún parámetro solamente escribimos:
super();Alternativamente, podemos omitir tal invocación sin argumentos completamente, en cuyo caso es realizada automáticamente para nosotros.
-
Relacion es-un
Nosotros creamos la clase NombreExtendido extendiendo la clase Nombre para poder proveer comportamiento adicional. La clase NombreExtendido, sin embargo, siempre incluía todos los metodos de la clase Nombre (además de sus propios) por lo que decimos que cualquier objeto de la clase NombreExtendido es-un objeto Nombre.
La termino es-un es ampliamente usado para referirse a relaciones entre subclases/superclases. Un NombreExtendido puede por lo tanto ser usado libremente como un Nombre. Por ejemplo, la siguiente porción de código crea un NombreExtendido y lo asigna a una referencia a un Nombre. Esta asignación es valida ya que la referencia a un Nombre puede referirse a objetos Nombre y un NombreExtendido es-un objeto Nombre:
Nombre n = new NombreExtendido("Miguel", "Angel", "Asturias");Esto ilustra un punto muy importante y poderoso sobre la herencia: la habilidad para utilizar un objeto de una subclase en cualquier lado que un objeto de una superclase es permitido. Sin embargo, en el contexto de una referencia a un objeto Nombre, instancias del NombreExtendido únicamente se pueden comportar como el objeto Nombre. Por ejemplo:
Nombre n = new NombreExtendido("Wolfgang", "Amadaeus", "Mozart"); System.out.println(n.getInitials()); // Correcto. Objetos Nombre pueden devolver iniciales. System.out.println(n.getFormalName()); // Incorrecto! Estamos comportandonos como un Nombre. // No hay un metodo getFormalName en la clase Nombre. La Palabra Reservada protected
Además de public, que permite que todas las clases tengan acceso al método o atributo, y private, que restringe el acceso a los miembros de la clase, Java provee la palabra reservada protected para permitir acceso a las subclases, pero a ninguna otra clase. Esta palabra reservada permite a una subclase proveer un comportamiento extendido que requiere acceso a los atributos de la superclase.
Hay dos problemas básicos que surgen con el uso de protected. Primero, el diseñador de la clase se asume que tenga conocimiento de antemano de dos posibles eventualidades:
- Puede eventualmente haber una subclase definida extendiendo a la clase.
- La subclase requerirá acceso al estado (atributos) de esta clase.
A pesar de que esta información puede estar disponible bajo ciertas condiciones, no podemos estar seguros que esta información este disponible en general. Hay una escuela de pensamiento que recomienda que todos los atributos sean declarados protected para permitir la eventual definición alguna subclase.
El segundo problema asociado con el uso de protected, es uno de responsabilidad e integridad de estado. Si visualizamos a una clase como el responsable único de su comportamiento, entonces no esta claro que incluso las subclases tengan acceso a su estado interno. Media vez una subclase tiene acceso a los atributos de una superclase, la superclase ya no puede garantizar que su estado siempre estará correcto. No importa que tan verificados estén los métodos de la superclase, las variables pueden ser corrompidas por los métodos de la subclase. Por eso es que la escuela de "todo protected" es una minoría. De hecho hay otra escuela de pensamiento que duda de cualquier uso de protected.
Herencia versus Composición
Definir la clase NombreExtendido heredando a la clase Nombre es completamente distinto a definir una clase que contenga una referencia a un Nombre como atributo:
class Nombre2 { private Nombre n; private String segundoNombre; public Nombre2(String primerNombre, String segundoNombre, String apellido) {...} public String getMiddleInitial() {...} public getFormalName() {...} }Este código muestra el tipo de construcción que hemos estado usando todo el tiempo. Declara referencias a una clase como atributos de una segunda clase. Ha esta es una relacion tiene-un. En la definición de arriba, la clase Nombre2 tiene-un objeto Nombre como parte de su estado.
A la construcción de clases de esta manera se le llama composion, ya que la clase esta compuesta de atributos que son instancias de otra clase. De nuevo la clase Nombre2 esta compuesta por un objeto Nombre y un objeto String.
En este caso, el Nombre2 no hereda los métodos ni los atributos del objeto Nombre. Cuando es creado, solo una referencia al objeto Nombre es creada. Un objeto Nombre debe ser creado como un paso separado, por lo general dentro del constructor.
public Nombre2(String primerNombre, String segundoNombre, String apellido) { n = new Nombre(primerNombre, apellido); this.segundoNombre = segundoNombre; }Además, los únicos métodos disponibles directamente desde el objeto son aquellos definidos en la nueva clase Nombre2. Los métodos de la clase Nombre original no son heredados ni pueden ser invocados desde una instancia de Nombre2:
Nombre2 = new Nombre2("Luis", "Alfonso", "Flores"); System.out.println(n2.getMiddleInitial()); // Correcto, getMiddleInitial esta // definido en la clase Nombre2 System.out.println(n2.getLastFirst()); // Incorrecto. No hay tal metodo.Finalmente, cuando la composición es utilizada, la palabra reservada protected no nos sirve de nada. Este acceso es solo concedido a las subclases, no a las clases que incorporan a objetos como atributos.
Herencia o Composición?
Cada uno, herencia y composición, tienen sus propias aplicaciones; una nos es necesariamente superior ha la otra. Lo idea que hay que recordar es que herencia modela una relación es-un, y composición modela una relación tiene-un. Dada una clase X, y dada la necesidad de una nueva clase relacionada, Y, las siguientes guías pueden ayudar a decidir entre utilizar herencia y composición.
- Si todos los métodos de la clase original X deben ser métodos de la nueva clase Y, entonces utilice herencia.
- Si algunos de los métodos de la clase original X no tiene sentido que como métodos de la nueva clase Y, use composición.
- Si se siente cómodo al decir que "Y es-un X" (como en "un empleado de medio tiempo es un empleado"), entonces use herencia.
- Si se siente cómodo al decir que "Y tiene-un X" (como en "un empleado tiene un nombre"), entonces use composición.
Además, cuando esta decidiendo entre herencia y composición, en Java una clase puede tener únicamente una superclase, esto es, puede heredar de solamente una clase. Mientras que una clase puede contener varios atributos instancias de varias clases. Por lo que la composición no esta restringida a un objeto de una sola clase. Frecuentemente sucede que una clase particular usa ambas técnicas: hereda de una clase y usa composición para otras clases.
Programadores nuevos para herencia frecuentemente olvidan que los atributos de la superclase (la clase Nombre en nuestro ejemplo) son heredados automáticamente. Un error muy común es repetir las declaraciones de estos atributos en la clase heredada:
class NombreExtendido extends Nombre { private String titulo, primerNombre, apellido; // Incorrecto! Estos atributos ya se // heredaron de la clase Nombre private Sting segundoNombre; // Este es el unico atributoque debe ser definido // explicitamente en el NombreExtendido // metodos ... }Jerarquía de Clases
No hay razón por la cual subclases en si a su vez no pueden ser heredada. Podemos por ejemplo definir un NombreConSufijo que extienda el comportamiento de NombreExtendido:
class NombreConSufijo extends NombreExtendido { ... }Además también es posible que dos clases diferentes hereden de la misma clase:
class NombreConSufijo extends Nombre { ... }Las relación resultante entre clases es llamado jerarquía de clases y es ilustrada en una estructura llamada el árbol de jerarquía de clases. Este árbol puede extenderse a múltiples niveles.
De hecho, todas las clases de Java, con excepción a una, son subclases de otra clase. La jerarquía de clases resultante es lo que llamamos la jerarquía de clases de Java. La excepción única, aquella clase que reside hasta arriba en la posición raíz de la jerarquía es la clase Object. Esto quiere decir que todas las clases de Java tiene a la clase Object como su superclase.
Cuando la cláusula extends es omitida de la definición de clase, la clase se asume que hereda directamente a la clase Object. Escribir:
class Myclass { ... }da exactamente lo mismo a escribir:
class Myclass extends Object { ... }Sobrepasando Métodos: polimorfismo
El comportamiento de la clase NombreExtendido era una extensión estricta a la clase Nombre. NombreExtendido agregaba un nuevo comportamiento solamente; no había una modificación sobre el comportamiento de la clase Nombre original.
Con un poco de razonamiento poder encontrar cierta discrepancia en el comportamiento del NombreExtendido, y en particular en el método getInitials heredado del a clase Nombre. Este método devuelve un String correspondiente a la inicial del primer nombre y el apellido, pero con la introducción del NombreExtendido, getInitials debería de devolver las tres iniciales: la del primer nombre, segundo nombre y apellido. Lo que deberíamos de hacer es modificar el comportamiento de getInitials para las instancias de NombreExtendido. Sin embargo no deseamos modificar el comportamiento de las instancias de Nombre, de hecho ni siquiera podemos ya que este no incluye un segundo nombre en la clase Nombre. Escribimos entonces los siguiente:
class NombreExtendido extends Nombre { ... public String getInitials() { return primerNombre.substring(0,1)+ "." + getMiddleInitial() + "." + apellido.substring(0,1); } ... }Esta técnica de redefinir un método que esta en una clase mas abajo en la jerarquía se le llama sobrepasar (overriding). Sobrepasar se refiere al acto de re-implementar un método en una subclase con la misma firma que un método en la superclase. En nuestra situación, la invocación al método getInitials sobre una instancia de NombreExtendido causa que el nuevo método, definido en NombreExtendido sea invocado.
Podemos entender el sobrepaso fácilmente con un modelo simplificado. Durante la ejecución, cuando un mensaje es pasado a un objeto, la firma (nombre del método mas argumentos) del mensaje es comparada con la firma de los mensajes de la clase a la que pertenece dicho objeto. Si se encuentra una igualdad el método correspondiente es ejecutado. Si no la firma del mensaje es comparado con la firma de los mensajes de la superclase. El proceso se repite hasta que una igualdad se encuentre. Una igualdad es garantizada, ya que si el compilador de Java hubiera generado un error en el tiempo de compilación si la firma del mensaje invocado sobre un objeto no corresponde a ninguna firma dentro de la clase a la que pertenece este o alguna superclase.
Apliquemos esto al contexto de la clase Nombre y NombreExtendido. En la siguiente porción de código:
Nombre n = new Nombre("Juan", "Perez"); NombreExtendido ne = new NombreExtendido("Jose", "Fernando", "Flores");En la invocación:
System.out.println(n.getInitials());el interprete de Java buscara la firma de este método en la clase Nombre. Al encontrarla invocara getInitials de la clase Nombre.
En la invocación:
System.out.println(ne.getInitials());el interprete de Java buscara la firma de este método en la clase NombreExtendido. Al encontrarla invocara getInitials de la clase NombreExtendido.
En la siguiente invocación:
System.out.println(ne.getLastFirst());el interprete de Java buscara la firma de este método en la clase NombreExtendido. Al no encontrarla la buscara en la superclase, Nombre. Al encontrarla en la clase Nombre invocara el método getLastFirst de la clase Nombre.
Aun se pone mejor que esto. Suponga la siguiente declaración:
Nombre n = new NombreExtendido("Jose", "Fernando", "Flores");Este código es perfectamente legal ya que como el NombreExtendido es-un Nombre, una referencia a un Nombre puede referirse a un instancia del NombreExtendido. La llamada
System.out.println(n.getInitials());provocaría que el interprete empezara a buscar la firma correspondiente en el objeto sobre el cual se invoco el método, el receptor. n puede ser una referencia a un Nombre, pero el objeto al cual se refiere es una instancia de NombreExtendido. Esto significa que la clase en donde comienza a buscar la correspondencia de firmas es NombreExtendido, por lo que se invocaría el método getInitials de esta clase. Esto nos lleva a una regla importante:
Cuando se invocan métodos sobrepasados, el tipo del objeto actual es los que realmente importa, no el tipo de la variable de referencia. La búsqueda para la correspondencia de firmas comienza por lo tanto en la clase del objeto, no en la clase de la referencia.
Esto quiere decir que la invocación del mismo método sobre la misma referencia puede invocar diferentes métodos dependiendo del objeto al que es referido. Como la búsqueda de la firma correspondiente es realizada en por el interprete de Java, esta es realizada en tiempo de corrida. Esta habilidad de sobrepasar métodos junto con la determinación en tiempo de corrida sobre que método invocar significa que el mismo mensaje puede responder de forma diferentes. A esta capacidad se le conoce como polimorfismo.
Sobre-montar difiere de sobrecargar en que sobre-montar requiere que las firmas sean idénticas, mientras que sobrecargar requiere que las firmas sean diferentes. Desde el punto de vista de la utilización, las dos tienen aplicaciones totalmente distintas. La sobrecarga le permite al programador usar el mismo nombre para dos métodos distintos. El invocador del método especifica implícitamente que método será invocado por la lista de los argumentos que le pasa al mensaje, el cual es comparado contra el prototipo de los distintos métodos candidatos hasta que una identidad es encontrada. La determinación del método a ejecutarse es realizada en tiempo de compilación (por el compilador de Java).
En contraste, el sobre-montar le provee al idad de redefinir el comportamiento de un método en particular en un una subclase. Cuando se utili métodos sobrepasados, el método a invocar es determinado en tiempo de corrida de la forma en que esta descrito arriba.
-
Factorizando el comportamiento común: clase SistemaDeEcuaciones
Hasta ahora hemos trabajado la herencia como una forma de extender el comportamiento de una clase base. Esta habilidad de construir sobre esfuerzos previos le da al programador poder sin precedentes desde el inicio. Por ejemplo, Java provee una interfase grafica poderosa llamada Applet. Esta clase puede ser extendida por un novicio quien solo agrega los elementos específicos de la aplicación que tiene en mano; el sofisticado manejo de ventanas se hereda automáticamente.
Hay otra aplicación común para la herencia, una que surge en durante el diseño de aplicaciones que involucran numerosas clases. Dado que el principiante casi nunca se topa con estas aplicaciones, esta forma de herencia es vista rara menos a menudo. Es sin embargo una parte critica del entendimiento de jerarquía de clases en un sistema orientado a objetos.
Tomemos como ejemplo un problema cuya solución ya se discutió en el tema anterior: resolver un sistema de n ecuaciones lineales con n variables. Para resolver un sistema de ecuaciones necesitamos una forma de proveer los siguientes comportamientos:
- Una forma de poder leer los coeficientes de cada una de las ecuaciones.
- El calculo de los valores de las variables que resuelven el sistema.
- El despliegue de valores de las variables.
Resolvimos este problema a través de dos formas: Gauss-Jordan y Cramer. Si examinamos el esqueleto de ambas soluciones:
class GaussJordan { public GaussJordan(Ecuacion eq[]) { ... } public float[] resolver() { ... } public void println(PrintStream ps) { ... } }y
class Cramer { public Cramer(...) { ... } public float[] resolver() { ... } public void println(PrintStream ps) { ... } }Vemos que ambas soluciones pueden tener métodos en común. Por ejemplo el diseño del método println podría ser:
public void println(PrintStream ps) { int i; float []resp = resolver(); ps.println("La solucion del sistema de ecuaciones es:"); for (i=0; i<resp.length; i++) ps.println("X" + i + " = " + resp[i]); }y aunque los métodos internos sean distintos, comparten gran parte de la interfase.
Para evitar la repetición de código en los métodos iguales y forzar una estandarización en los métodos cuya interfase es la misma pero los cuerpos son distintos, podríamos diseñar una clase genérica SistemaDeEcuaciones. Esta clase tendría implementados aquellos métodos que se pueden implementar para que sean heredados a las subclases y así los incluyan automáticamente y tendría definidos aquellos la interfase estándar de los métodos cuyos cuerpos son distintos, pero sus cuerpos no estarían definidos (por ejemplo, un sistema de ecuaciones sabemos que si se va a resolver aunque los pasos específicos si van a depender si se resuelve a través de Gauss-Jordan o a través de Cramer).
La clase SistemaDeEcuaciones serviría entonces únicamente para ser heredada y no para construir objetos directamente. A este tipo de clases se les llama clases abstractas. Las clases abstractas tendrán ciertos métodos que si están implementados y otros métodos que únicamente su encabezado estaría definido. El cuerpo de estos métodos se implementaría en las clases siendo que heredarían. A estos métodos se les llama métodos abstractos.
La clase SistemaDeEcuaciones queraria asi:
abstract class SistemaDeEcuaciones { /** La implementacion de este metodo dependeria de el procedimiento * escogido para la resolucion del sistema de ecuaciones */ public abstract float[] resolver(); public void println(PrintStream ps) { int i; float []resp = resolver(); ps.println("La solucion del sistema de ecuaciones es:"); for (i=0; i<resp.length; i++) ps.println("X" + i + " = " + resp[i]); } }Las clases GaussJordan y Cramer se implementarían heredando a la clase SistemaDeEcuaciones. De esta forma el método println solo se escribe una vez y las clases GaussJordan y Cramer tendrían que tener implementado el cuerpo del método resolver con la interfase ya definida en SistemaDeEcuaciones. Las clases quedarían entonces así:
class GaussJordan extends SistemaDeEcuaciones { public GaussJordan(Ecuacion eq[]) { ... } public float[] resolver() { ... } } class Cramer extends SistemaDeEcuaciones { public Cramer(...) { ... } public float[] resolver() { ... } }Ejercicio de clase: agréguele el método readi a la clase SistemaDeEcuaciones. Despues implemente el cuerpo de dicho método en la clase GaussJordan y Cramer. Recuerde que el encabezado del método debe ser el mismo!
Especificando Comportamiento Común: interfase
Supóngase que el método println dependería también de la clase especifica y no se podría implementar en la clase SistemaDeEcuaciones. La clase SistemaDeEcuaciones quedaría así:
abstract class SistemaDeEcuaciones { /** La implementacion de este metodo dependeria de el procedimiento * escogido para la resolucion del sistema de ecuaciones */ public abstract float[] resolver(); /** Ahora tambien depende del almacenacimiento escogido para la resolucion del problema */ public abstract void println(PrintStream ps) }La clase SistemaDeEcuaciones termina siendo una clase abstracta con todos sus métodos abstractos. Todavía seria de utilidad para estandarizar todos los métodos de resolución de sistemas de ecuaciones a una interfase común aunque ya no factorizaría ningún código. Para estos casos hay una abreviación. En vez de declarar la clase abstract y después todos los métodos abstract se podría declarar SistemaDeEcuaciones no como una clase sino una interfase:
interfase SistemaDeEcuaciones { /** La implementacion de este metodo dependeria de el procedimiento * escogido para la resolucion del sistema de ecuaciones */ public float[] resolver(); /** Ahora tambien depende del almacenacimiento escogido para la resolucion del problema */ public void println(PrintStream ps) }y una clase pudiera utilizar esta interfase a través de la palabra reservada implements
class GaussJordan implements SistemaDeEcuaciones { public GaussJordan(Ecuacion eq[]) { ... } public float[] resolver() { ... } public void println(PrintStream ps) { ... } }A pesar de que Java no provea herencia múltiple (capacidad de heredar de varias clases simultáneamente) si permite que una clase extienda a una e implemente a otra. Java incluso permite implementar varias clases simultáneamente enumerando todas las clases, separadas cada una por una coma. En el tema de applets veremos ejemplos de esto.
-
Siguiente articulo que se recomienda leer: Esperando lo Inesperado: Excepciones

0 comments
Jump to comment form | comments rss [?]