Está en la página 1de 22

Herencia

¿QUÉ ES LA HERENCIA EN PROGRAMACIÓN ORIENTADA A OBJETOS?

Muchas veces distintos objetos comparten campos y métodos que hacen aproximadamente lo mismo (por
ejemplo almacenar y devolver un nombre del ámbito humano con el que se designa al objeto, como el título de un
álbum de música, el título de un libro, el título de una película, etc.).

Por ejemplo en un proyecto que utilice objetos Taxi y objetos Autobus podríamos encontrarnos algo así:

Para una aplicación de gestión de una empresa de transporte que tenga entre sus vehículos taxis y autobuses
podríamos tener otra clase denominada FlotaCirculante donde tendríamos posibilidad de almacenar ambos tipos
de objeto (por ejemplo taxis en un ArrayList y autobuses en otro ArrayList) como reflejo de los vehículos que se
encuentran en circulación en una fecha dada. Esas listas conllevarían una gestión para añadir o eliminar
vehículos de la flota circulante, modificar datos, etc. resultando que cada una de las listas necesitaría un
tratamiento o mantenimiento.

Si nos fijamos en el planteamiento del problema, encontramos lo siguiente:

a) La definición de clases nos permite identificar campos y métodos que son comunes a Taxis y Autobuses. Si
implementamos ambas clases tal y como lo venimos haciendo, incurriremos en duplicidad de código. Por ejemplo
si el campo matricula es en ambas clases un tipo String, el código para gestionar este campo será idéntico en
ambas clases.

b) La definición de clases nos permite identificar campos y métodos que difieren entre una clase y otra. Por
ejemplo en la clase Taxi se gestiona información sobre un campo denominado numeroDeLicencia que no existe
en la clase Autobus.
c) Conceptualmente podemos imaginar una abstracción que engloba a Taxis y Autobuses: ambos podríamos
englobarlos bajo la denominación de “Vehículos”. Un Taxi sería un tipo de Vehiculo y un Autobus otro tipo de
Vehiculo.

d) Si la empresa añade otros vehículos como minibuses, tranvías, etc. manteniendo la definición de clases tal y
como la veníamos viendo, seguiríamos engrosando la duplicidad de código. Por ejemplo, un minibús también
tendría matrícula, potencia… y los métodos asociados.

La duplicidad de código nos implicará problemas de mantenimiento. Por ejemplo inicialmente tenemos una
potencia en caballos y posteriormente queremos definirla en kilowatios. O tenemos simplemente que modificar el
código de un método que aparece en distintas clases. El tener el código duplicado nos obliga a tener que hacer
dos o más modificaciones en sitios distintos. Pueden ser dos modificaciones, tres, cuatro o n modificaciones
dependiendo del número de clases que se vieran afectadas, y esto a la larga genera errores al no ser el
mantenimiento razonable.

En la clase FlotaCirculante también tendremos seguramente duplicidades: por un lado un ArrayList de taxis y por
otro un ArrayList de autobuses, por un lado una operación de adición de taxis y otra operación de adición de
autobuses, por un lado una operación para mostrar los elementos de la lista de taxis y otra para los elementos de
la lista de autobuses…

¿No sería más razonable, si una propiedad o método va a ser siempre común para varios tipos de objetos, que
estuviera localizada en un sitio único del que ambos tipos de objeto “bebieran”? En los lenguajes con orientación
a objetos la solución a esta problemática se llama herencia. La herencia es precisamente uno de los puntos clave
de este tipo de lenguajes.

La herencia nos permite definir una clase como extensión de otra: de esta manera decimos “la clase 1.1
tiene todas las características de la clase 1 y además sus características particulares”. Todo lo que es común a
ambas clases queda comprendido en la clase “superior”, mientras lo que es específico, queda restringido a las
clases “inferiores”. En nuestro ejemplo definiríamos una clase denominada Vehiculo, de forma que la clase Taxi
tuviera todas las propiedades de la clase Vehiculo, más algunas propiedades y métodos específicos. Lo mismo
ocurriría con la clase Autobus y otras que pudieran “heredar” de Vehiculo. Podríamos seguir creando clases con
herencia en un número indefinido: tantas como queramos. Si piensas en el API de Java, hay cientos de clases
que heredan de clases jerárquicamente superiores como la clase Object. En un proyecto propio, podremos tener
varias clases que hereden de una clase común.
Otro ejemplo: para la gestión de un centro educativo, podemos definir la clase Persona que comprende los
campos y métodos comunes a todas las personas. Luego podremos definir la clase Estudiante, que es extensión
de Persona, y comprende los campos y métodos de la clase superior, más algunos específicos. También podrían
heredar de Persona la clase Profesor, la clase Director, la clase JefeDeEstudios, la clase PersonalAdministrativo,
etc.

La primera aproximación a la herencia en Java la plantearemos para plasmar todo lo que hemos discutido en
párrafos anteriores: en vez de definir cada clase por separado con operaciones o campos duplicados en cierta
medida, definiremos una clase “padre” que contendrá todas las cosas que tengan en común otras clases. Luego
definiremos las otras clases “hijo” como extensión de la clase padre, especificando para ellas únicamente aquello
que tienen específico y distinto de la clase padre. Una característica esencial de la herencia es que permite
evitar la duplicidad de código: aquello que es común a varias clases se escribe solo una vez (en la clase
padre). La duplicidad de información, ya sea código o datos, es algo que por todos los medios hay que tratar de
evitar en los sistemas informáticos por ser fuente de errores y de problemas de mantenimiento.

Si nos remitimos al esquema básico de herencia donde hemos representado las clases Vehiculo, Taxi y Autobus,
la clase Vehiculo diremos que actúa como clase padre de las clases Taxi y Autobus. Vehiculo recogería todo
aquello que tienen o hacen en común taxis y autobuses. Aunque aquello que tienen en común se agrupa, tanto
Taxi como Autobus tienen sus campos o métodos específicos.
Que una clase derive de otra en Java se indica mediante la palabra clave “extends”. Por eso muchas veces se
usa la expresión “esta clase es extensión de aquella otra”. En los diagramas de clase la herencia se representa
con una flecha de punta vacía. El aspecto en BlueJ sería algo así:

Este diagrama refleja que la clase FlotaCirculante usa a la clase Vehiculo, mientras que las clases Taxi y Autobus
heredan de la clase Vehiculo (son extensión de la clase Vehiculo). Podemos decir que la clase hijo extiende (hace
más extensa) a la clase padre. Una clase de la que derivan otras se denomina clase padre, clase base o
superclase. Las clases que heredan se denominan clases derivadas, clases hijos o subclases. A partir de ahora
los términos superclase y subclase son los que usaremos con más frecuencia. Una subclase podrá tener un
acceso “más o menos directo” a los campos y métodos de la superclase en función de lo que defina el
programador, como veremos más adelante. Para referirnos a la herencia también se usa la terminología “es un”.
En concreto decimos que un objeto de la subclase es un objeto de la superclase, o más bien en casos concretos,
que un Taxi es un Vehiculo, o que un Estudiante es una Persona. Sin embargo, al revés esto no es cierto, es
decir, un Vehiculo no es un Taxi, ni una Persona es un Estudiante. Dos subclases que heredan de una clase no
deben tener información duplicada. Esto se consideraría un mal diseño. La información común debe estar en una
superclase.
Protected

Al principio los modificadores de acceso pueden resultar un tanto inútiles puesto que, si puedes acceder
directamente a un atributo, ¿para que vas a modificarlos mediante un método?, pues bien, como norma general
los atributos de nuestras clases serán private o protected si utilizaremos herencia. Para modificar atributos o
consultar su valor utilizaremos métodos, siendo esta la base de la encapsulación que consiste en hacer visible
los atributos o métodos que sean realmente necesarios.

Para controlar el acceso a nuestros atributos y métodos se utilizan los modificadores de acceso que no son más
que palabras reservadas del lenguaje que se encargarán de controlar desde dónde serán accesibles los
miembros de una clase, estos modificadores son:

 private (Acceso solo dentro de la clase)


 protected (Acceso desde la clase y sus hijos "herencia")
 Vacío (Sin escribir nada, denominado acceso de paquete)
 public (Acceso publico desde cualquier lugar)
Estos modificadores de acceso se colocan justo delante del valor de retorno de un método, o del tipo de un
atributo, así que de momento y hasta que no expliquemos el concepto de herencia, utilizaremos los más
sencillos, private y public. A continuación podemos ver un ejemplo de su uso en una clase:
?
public class Vehiculo {
1 //Atributos con acceso private
2 private String modelo;
3 private int velocidad;
4 private boolean arrancado;
5 //El constructor siempre debe de ser public
6 public Vehiculo(String modelo, int velocidad, boolean arrancado) {
7 this.modelo = modelo;
8 this.velocidad = velocidad;
9 this.arrancado = arrancado;
10 }
11 //Atributos getter con acceso public
12 public String getModelo() {
13 return modelo;
14 }
15
16 public int getVelocidad() {
17 return velocidad;
18 }
19
20 public boolean isArrancado() {
21 return arrancado;
}}
Como vemos, hemos definido los atributos private, así nadie podrá modificarlos sin nuestro consentimiento, el
constructor es public para permitir instancias de la clase desde fuera del archivo Vehiculo.java, y los métodos get
serán también public, para permitir a cualquiera consultar el valor de nuestros atributos.

Con los modificadores de acceso, evitamos que nuestros programas tengan un uso no deseado, como
modificar atributos introduciendo valores inválidos o llamando a métodos que pueden tener efectos no deseados.

El cuanto al modificador protected, este hace nuestro miembro (atributo o método) accesible desde la propia
clase, y también desde las clases que hereden de esta o subclases.

Buenas prácticas
En generar se aconseja declarar todos los atributos como private, y cuando necesitemos consultar su valor o
modificarlo, utilicemos los métodos get y set. Además en caso de que el tipo de valor devuelto por el método
sea un boolean, se utilizará is en vez de get. A continuación podéis ver un ejemplo donde lo veréis todo más
claro:
?
1 //Atributos private
2 private String nombre;
3 private boolean casado;
4
5 //Metodos get y set para estos atributos
6 public void setNombre(String nombre) {
7 this.nombre = nombre;
8 }
9 public void setCasado(boolean casado) {
10 this.casado = casado;
11 }
12 public String getNombre() {
13 return nombre;
14 }
15 public boolean isCasado() {
16 return casado;
17 }
Como vemos los metodos setCasado y setNombre se encargan de modificar los atributos y los métodos
getNombre y isCasado nos permiten obtener sus valores.

Recuerda que cuando utilizamos "this.nombre" nos referimos al atributo nombre de la clase y con "nombre" nos
referimos al parámetro con el mismo nombre.
Super

EJEMPLO DE HERENCIA EN JAVA. EXTENDS Y SUPER.

Para declarar la herencia en Java usamos la palabra clave extends. Ejemplo: public class MiClase2 extends
Miclase1. Para familiarizarte con la herencia te proponemos que escribas y estudies un pequeño programa donde
se hace uso de ella. Escribe el código de las clases que mostramos a continuación.

//Código de la clase Persona ejemplo aprenderaprogramar.com


public class Persona {
private String nombre;
private String apellidos;
private int edad;
//Constructor
public Persona (String nombre, String apellidos, int edad) {
this.nombre = nombre;
this.apellidos = apellidos;
this.edad = edad; }
//Métodos
public String getNombre () { return nombre; }
public String getApellidos () { return apellidos; }
public int getEdad () { return edad; }
} //Cierre de la clase

//Código de la clase profesor, subclase de la clase Persona ejemplo aprenderaprogramar.com


public class Profesor extends Persona {
//Campos específicos de la subclase.
private String IdProfesor;
//Constructor de la subclase: incluimos como parámetros al menos los del constructor de la superclase
public Profesor (String nombre, String apellidos, int edad) {
super(nombre, apellidos, edad);
IdProfesor = "Unknown"; } //Cierre del constructor
//Métodos específicos de la subclase
public void setIdProfesor (String IdProfesor) { this.IdProfesor = IdProfesor; }
public String getIdProfesor () { return IdProfesor; }
public void mostrarNombreApellidosYCarnet() {
// nombre = "Paco"; Si tratáramos de acceder directamente a un campo privado de la superclase, salta un
error
// Sí podemos acceder a variables de instancia a través de los métodos de acceso públicos de la
superclase
System.out.println ("Profesor de nombre: " + getNombre() + " " + getApellidos() +
" con Id de profesor: " + getIdProfesor() ); }
} //Cierre de la clase

//Código de test aprenderaprogramar.com


public class TestHerencia1 {
public static void main (String [ ] Args) {
Profesor profesor1 = new Profesor ("Juan", "Hernández García", 33);
profesor1.setIdProfesor("Prof 22-387-11");
profesor1.mostrarNombreApellidosYCarnet();}
} //Cierre de la clase

El diagrama de clases y el resultado del test son del tipo que mostramos a continuación:
Profesor de nombre: Juan Hernández García con Id de profesor: Prof 22-387-
11

Los aspectos a destacar del código son:

a) La clase persona es una clase “normal” definida tal y como lo venimos haciendo habitualmente mientras que la
clase Profesor es una subclase de Persona con ciertas peculiaridades.

b) Los objetos de la subclase van a tener campos nombre, apellidos y edad (heredados de Persona) y un campo
específico IdProfesor. El constructor de una subclase ha de llevar obligatoriamente como parámetros al menos
los mismos parámetros que el constructor de la superclase.

c) El constructor de la subclase invoca al constructor de la superclase. Para ello se incluye,


obligatoriamente, la palabra clave super como primera línea del constructor de la subclase. La palabra super irá
seguida de paréntesis dentro de los cuales pondremos los parámetros que requiera el constructor de la
superclase al que queramos invocar. En este caso solo teníamos un constructor de superclase que requería tres
parámetros. Si p.ej. hubiéramos tenido otro constructor que no requiriera ningún parámetro podríamos haber
usado uno u otro, es decir, super(nombre, apellidos, edad) ó super(), o bien ambos teniendo dos constructores
para la superclase y dos constructores para la subclase. Ejemplo:

En la superclase: public Persona() {


nombre = "";
apellidos = "";
edad = 0; }

public Persona (String nombre, String apellidos, int edad) {


this.nombre = nombre;
this.apellidos = apellidos;
this.edad = edad; }
En la subclase: public Profesor () {
super();
IdProfesor = "Unknown";}

public Profesor (String nombre, String apellidos, int edad) {


super(nombre, apellidos, edad);
IdProfesor = "Unknown"; }

Modifica el código de las clases Persona y Profesor para que queden con dos constructores tal y como hemos
mostrado aquí. Crea objetos de ambos tipos en BlueJ y prueba sus métodos.

¿Qué ocurre si olvidamos poner super como primera línea de la subclase? Hay dos posibilidades: si la
superclase tiene un constructor sin parámetros, el compilador incluirá en segundo plano super de forma
automática y no saltará un error. De cualquier manera se considera contrario al buen estilo de programación, ya
que no queda claro si se trata de un olvido. Por ello incluiremos siempre la palabra clave super. La otra
posibilidad es que no haya un constructor sin parámetros, en cuyo caso saltará un error.

A modo de resumen: la inicialización de un objeto de una subclase comprende dos pasos. La invocación al
constructor de la superclase (primera línea del constructor: super…) y el resto de instrucciones propias del
constructor de la subclase.
Final

PALABRAS CLAVE STATIC Y FINAL. CONSTANTES EN JAVA.

En los programas que generemos usualmente intervendrán constantes: valores matemáticos como el número Pi,
o valores propios de programa que nunca cambian. Si nunca cambian, lo adecuado será declararlos como
constantes en lugar de cómo variables. Supongamos que queremos usar una constante como el número Pi y que
usamos esta declaración:

// Ejemplo aprenderaprogramar.com
public class Calculadora {
private double PI = 3.1416;
public void mostrarConstantePi () { System.out.println (PI); }
… constructor, métodos, … código de la clase … }

Si creas un objeto de tipo Calculadora, comprobarás que puedes invocar el método mostrarConstantePi para que
te muestre el valor por pantalla. No obstante, una declaración de este tipo presenta varios problemas. En primer
lugar, lo declarado no funciona realmente como constante, sino como variable con un valor inicial. Prueba a
establecer this.PI = 22; dentro del método y verás que es posible, porque lo declarado es una variable, no una
constante. En segundo lugar, cada vez que creamos un objeto de tipo calculadora estamos usando un espacio de
memoria para almacenar el valor 3.1416. Así, si tuviéramos diez objetos calculadora, tendríamos diez espacios
de memoria ocupados con la misma información, lo cual resulta ineficiente. Para resolver estos problemas,
podemos declarar constantes en Java usando esta sintaxis:

CaracterPublico/Privado static final TipoDeLaConstante = valorDeLaConstante;


En esta declaración intervienen dos palabras clave cuyo significado es importante:

a) static: los atributos miembros de una clase pueden ser atributos de clase o atributos de instancia; se
dice que son atributos de clase si se usa la palabra clave static: en ese caso la variable es única para
todas las instancias (objetos) de la clase (ocupa un único lugar en memoria). A veces a las variables de
clase se les llama variables estáticas. Si no se usa static, el sistema crea un lugar nuevo para esa
variable con cada instancia (la variable es diferente para cada objeto). En el caso de una constante no
tiene sentido crear un nuevo lugar de memoria por cada objeto de una clase que se cree. Por ello es
adecuado el uso de la palabra clave static. Cuando usamos “static final” se dice que creamos una
constante de clase, un atributo común a todos los objetos de esa clase.

b) final: en este contexto indica que una variable es de tipo constante: no admitirá cambios después de
su declaración y asignación de valor. final determina que un atributo no puede ser sobreescrito o
redefinido. O sea: no funcionará como una variable “tradicional”, sino como una constante. Toda
constante declarada con final ha de ser inicializada en el mismo momento de declararla. final también se
usa como palabra clave en otro contexto: una clase final (final) es aquella que no puede tener clases que
la hereden. Lo veremos más adelante cuando hablemos sobre herencia.

Cuando se declaran constantes es muy frecuente que los programadores usen letras mayúsculas (como práctica
habitual que permite una mayor claridad en el código), aunque no es obligatorio.

Una declaración en cabecera de una clase como private final double PI = 3.1416; podríamos interpretarla como
una constante de objeto. Cada objeto tendrá su espacio de memoria con un contenido invariable. Una declaración
en cabecera de una clase como private static final double Pi = 3.1416; la interpretamos como una constante de
clase. Existe un único espacio de memoria, compartido por todos los objetos de la clase, que contiene un valor
invariable. Veamos ejemplos de uso:

// Ejemplo aprenderaprogramar.com
// Sintaxis: CaracterPublico/Privado static final TipoDeLaConstante = valorDeLaConstante;
private static final float PI = 3.1416f; //Recordar f indica que se trata de un float
private static double PI = 3.1416;
public static final String passwd = "jkl342lagg";
public static final int PRECIO_DEFAULT = 100;
Cuando usamos la palabra clave static la declaración de la constante ha de realizarse obligatoriamente en
cabecera de la clase,junto a los campos (debajo de la signatura de clase). Es decir, un atributo de clase hemos
de declararlo en cabecera de clase. Si tratamos de incorporarlo en un método obtendremos un error. Por tanto
dentro del método main (que es un método de clase al llevar incorporado static en su declaración) no podemos
declarar constantes de clase. Por otro lado, final sí puede ser usado dentro de métodos y también dentro de un
método main. Por ejemplo una declaración como final String psswd = “mt34rsm8” es válida dentro de un método.
En resumen: en cabecera de clase usaremos static final para definir aquellas variables comunes a todos los
objetos de una clase.

Modifica el código de la clase Calculadora que vimos anteriormente para declarar PI como una constante de
clase. Luego, intenta modificar el valor de PI usando una invocación como this.PI =22;. Comprobarás que se
produce un error al compilar ya que los valores de las constantes no son modificables.
Abstract

CLASES Y MÉTODOS ABSTRACTOS EN JAVA.

Supongamos un esquema de herencia que consta de la clase Profesor de la que heredan ProfesorInterino y
ProfesorTitular. Es posible que todo profesor haya de ser o bien ProfesorInterino o bien ProfesorTitular, es decir,
que no vayan a existir instancias de la clase Profesor. Entonces, ¿qué sentido tendría tener una clase Profesor?

El sentido está en que una superclase permite unificar campos y métodos de las subclases, evitando la repetición
de código y unificando procesos. Ahora bien, una clase de la que no se tiene intención de crear objetos, sino que
únicamente sirve para unificar datos u operaciones de subclases, puede declararse de forma especial en Java:
como clase abstracta. La declaración de que una clase es abstracta se hace con la sintaxis public abstract class
NombreDeLaClase { … }. Por ejemplo public abstract class Profesor. Cuando utilizamos esta sintaxis, no resulta
posible instanciar la clase, es decir, no resulta posible crear objetos de ese tipo. Sin embargo, sigue funcionando
como superclase de forma similar a como lo haría una superclase “normal”. La diferencia principal radica en que
no se pueden crear objetos de esta clase.

Declarar una clase abstracta es distinto a tener una clase de la que no se crean objetos. En una clase abstracta,
no existe la posibilidad. En una clase normal, existe la posibilidad de crearlos aunque no lo hagamos. El hecho de
que no creemos instancias de una clase no es suficiente para que Java considere que una clase es abstracta.
Para lograr esto hemos de declarar explícitamente la clase como abstracta mediante la sintaxis que hemos
indicado. Si una clase no se declara usando abstract se cataloga como “clase concreta”. En inglés abstract
significa “resumen”, por eso en algunos textos en castellano a las clases abstractas se les llama resúmenes. Una
clase abstracta para Java es una clase de la que nunca se van a crear instancias: simplemente va a servir como
superclase a otras clases. No se puede usar la palabra clave new aplicada a clases abstractas. En el menú
contextual de la clase en BlueJ simplemente no aparece, y si intentamos crear objetos en el código nos saltará un
error.
A su vez, las clases abstractas suelen contener métodos abstractos: la situación es la misma. Para que un
método se considere abstracto ha de incluir en su signatura la palabra clave abstract. Además un método
abstracto tiene estas peculiaridades:

a) No tiene cuerpo (llaves): sólo consta de signatura con paréntesis.

b) Su signatura termina con un punto y coma.

c) Sólo puede existir dentro de una clase abstracta. De esta forma se evita que haya métodos que no se
puedan ejecutar dentro de clases concretas. Visto de otra manera, si una clase incluye un método abstracto,
forzosamente la clase será una clase abstracta.

d) Los métodos abstractos forzosamente habrán de estar sobreescritos en las subclases. Si una subclase no
implementa un método abstracto de la superclase tiene un método no ejecutable, lo que la fuerza a ser una
subclase abstracta. Para que la subclase sea concreta habrá de implementar métodos sobreescritos para todos
los métodos abstractos de sus superclases.

Un método abstracto para Java es un método que nunca va a ser ejecutado porque no tiene cuerpo.
Simplemente, un método abstracto referencia a otros métodos de las subclases. ¿Qué utilidad tiene un método
abstracto? Podemos ver un método abstracto como una palanca que fuerza dos cosas: la primera, que no se
puedan crear objetos de una clase. La segunda, que todas las subclases sobreescriban el método declarado
como abstracto.

Sintaxis tipo: abstract public/private/protected TipodeRetorno/void ( parámetros … );

Por ejemplo: abstract public void generarNomina (int diasCotizados, boolean plusAntiguedad);

Que un método sea abstracto tiene otra implicación adicional: que podamos invocar el método abstracto sobre
una variable de la superclase que apunta a un objeto de una subclase de modo que el método que se ejecute sea
el correspondiente al tipo dinámico de la variable. En cierta manera, podríamos verlo como un método
sobreescrito para que Java comprenda que debe buscar dinámicamente el método adecuado según la subclase a
la que apunte la variable.

¿Es necesario que una clase que tiene uno o más métodos abstractos se defina como abstracta? Sí, si
declaramos un método abstracto el compilador nos obliga a declarar la clase como abstracta porque si no lo
hiciéramos así tendríamos un método de una clase concreta no ejecutable, y eso no es admitido por Java.

¿Una clase se puede declarar como abstracta y no contener métodos abstractos? Sí, una clase puede ser
declarada como abstracta y no contener métodos abstractos. En algunos casos la clase abstracta simplemente
sirve para efectuar operaciones comunes a subclases sin necesidad de métodos abstractos. En otros casos sí se
usarán los métodos abstractos para referenciar operaciones en la clase abstracta al contenido de la
sobreescritura en las subclases.

¿Una clase que hereda de una clase abstracta puede ser no abstracta? Sí, de hecho esta es una de las
razones de ser de las clases abstractas. Una clase abstracta no puede ser instanciada, pero pueden crearse
subclases concretas sobre la base de una clase abstracta, y crear instancias de estas subclases. Para ello hay
que heredar de la clase abstracta y anular los métodos abstractos, es decir, implementarlos.

Vamos a ver un ejemplo basado en el siguiente esquema:

En este diagrama de clases vemos cómo hemos definido una clase abstracta denominada Profesor. BlueJ la
identifica señalando <<abstract>> en la parte superior del icono de la clase. Sin embargo, hereda de la clase
Persona que no es abstracta, lo cual significa que puede haber instancias de Persona pero no de Profesor.

El test que hemos diseñado se basa en lo siguiente: ProfesorTitular y ProfesorInterino son subclases de la clase
abstracta Profesor. ListinProfesores sirve para crear un ArrayList de profesores que pueden ser tanto interinos
como titulares y realizar operaciones con esos conjuntos. El listín se basa en el tipo estático Profesor, pero su
contenido dinámico siempre será a base de instancias de ProfesorTitular o de ProfesorInterino ya que Profesor es
una clase abstracta, no instanciable. En la clase de test creamos profesores interinos y profesores titulares y los
vamos añadiendo a un listín. Posteriormente, invocamos el método imprimirListin, que se basa en los métodos
toString de las subclases y de sus superclases mediante invocaciones sucesivas a super.

Por otro lado, en la clase ListinProfesores hemos definido el método importeTotalNominaProfesorado() que se
basa en un bucle que calcula la nómina de todos los profesores que haya en el listín (sean interinos o titulares)
mediante el uso de un método abstracto: importeNomina(). Este método está definido como abstract public float
importeNomina (); dentro de la clase abstracta profesor, e implementado en las clases ProfesorInterino y
ProfesorTitular. El aspecto central de este ejemplo es comprobar cómo una clase abstracta como Profesor nos
permite realizar operaciones conjuntas sobre varias clases, ahorrando código y ganando en claridadpara
nuestros programas. Escribe este código:

public class Persona { //Código de la clase Persona ejemplo aprenderaprogramar.com


private String nombre; private String apellidos; private int edad;
public Persona() { nombre = ""; apellidos = ""; edad = 0; }
public Persona (String nombre, String apellidos, int edad) { this.nombre = nombre; this.apellidos = apellidos; this.edad
= edad; }
public String getNombre() { return nombre; }
public String getApellidos() { return apellidos; }
public int getEdad() { return edad; }
public String toString() { Integer datoEdad = edad;
return "-Nombre: ".concat(nombre).concat(" -Apellidos: ").concat(apellidos).concat(" -Edad:
").concat(datoEdad.toString() ); }
} //Cierre de la clase

En la clase Persona transformamos edad en un Integer para poder aplicarle el método toString(). De otra manera
no podemos hacerlo por ser edad un tipo primitivo. Escribe este código:

public abstract class Profesor extends Persona {


// Campo de la clase ejemplo aprenderaprogramar.com
private String IdProfesor;
// Constructores
public Profesor () { super(); IdProfesor = "Unknown"; }
public Profesor (String nombre, String apellidos, int edad, String id) { super(nombre, apellidos, edad); IdProfesor =
id; }
// Métodos
public void setIdProfesor (String IdProfesor) { this.IdProfesor = IdProfesor; }
public String getIdProfesor () { return IdProfesor; }
public void mostrarDatos() {
System.out.println ("Datos Profesor. Profesor de nombre: " + getNombre() + " " +
getApellidos() + " con Id de profesor: " + getIdProfesor() ); }
public String toString () { return super.toString().concat(" -IdProfesor: ").concat(IdProfesor); }
abstract public float importeNomina (); // Método abstracto
} //Cierre de la clase

Hemos declarado la clase Profesor como abstracta. De hecho, tenemos un método abstracto (definido como
abstract y sin cuerpo), lo cual de facto nos obliga a declarar la clase como abstracta. El método sobreescrito
toString llama al método toString de la superclase y lo concatena con nuevas cadenas. Como clases que heredan
de Profesor tenemos a ProfesorTitular y ProfesorInterino:

public class ProfesorTitular extends Profesor {


// Constructor ejemplo aprenderaprogramar.com
public ProfesorTitular(String nombre, String apellidos, int edad, String id) {
super(nombre, apellidos, edad, id); }
public float importeNomina () { return 30f * 43.20f; } //Método abstracto sobreescrito en esta clase
} //Cierre de la clase

import java.util.Calendar;
public class ProfesorInterino extends Profesor {
// Campo de la clase ejemplo aprenderaprogramar.com
private Calendar fechaComienzoInterinidad;
// Constructores
public ProfesorInterino (Calendar fechaInicioInterinidad) {
super(); fechaComienzoInterinidad = fechaInicioInterinidad; }
public ProfesorInterino (String nombre, String apellidos, int edad, String id, Calendar fechaInicioInterinidad) {
super(nombre, apellidos, edad, id);
fechaComienzoInterinidad = fechaInicioInterinidad; }
public Calendar getFechaComienzoInterinidad () { return fechaComienzoInterinidad; } //Método
public String toString () { // Sobreescritura del método
return super.toString().concat (" Fecha comienzo interinidad: ").concat
(fechaComienzoInterinidad.getTime().toString()); }
public float importeNomina () { return 30f * 35.60f ; } //Método abstracto sobreescrito en esta clase
} //Cierre de la clase
import java.util.ArrayList; import java.util.Iterator;
public class ListinProfesores {
private ArrayList <Profesor> listinProfesores; //Campo de la clase
public ListinProfesores () { listinProfesores = new ArrayList <Profesor> (); } //Constructor
public void addProfesor (Profesor profesor) { listinProfesores.add(profesor); } //Método
public void imprimirListin() { //Método
String tmpStr1 = ""; //String temporal que usamos como auxiliar
System.out.println ("Se procede a mostrar los datos de los profesores existentes en el listín \n");
for (Profesor tmp: listinProfesores) { System.out.println (tmp.toString () );
if (tmp instanceof ProfesorInterino) { tmpStr1 = "Interino";} else { tmpStr1 =
"Titular"; }
System.out.println("-Tipo de este profesor:"+tmpStr1+" -Nómina de este profesor: "+(tmp.importeNomina())+
"\n");}
} //Cierre método imprimirListin
public float importeTotalNominaProfesorado() {
float importeTotal = 0f; //Variable temporal que usamos como auxiliar
Iterator<Profesor> it = listinProfesores.iterator();
while (it.hasNext() ) { importeTotal = importeTotal + it.next().importeNomina(); }
return importeTotal;
} //Cierre del método importeTotalNominaProfesorado
} //Cierre de la clase ejemplo aprenderaprogramar.com

ProfesorTitular y ProfesorInterino se han definido como clases concretas que heredan de la clase abstracta
Profesor. Ambas clases redefinen (obligatoriamente han de hacerlo) el método abstracto importeNomina() de la
superclase. El método sobreescrito toString() de la clase ProfesorInterino llama al método toString() de la
superclase y lo concatena con nuevas cadenas. El cálculo de importeNomina en ambas clases es una trivialidad:
hemos incluido un cálculo sin mayor interés excepto que el de ver el funcionamiento de la implementación de
métodos abstractos. ProfesorTitular lo hemos dejado con escaso contenido porque aquí lo usamos solo a modo
de ejemplo de uso de clases abstractas y herencia. Su único cometido es mostrar que existe otra subclase de
Profesor. Por otro lado, en la clase ListinProfesores tenemos un ejemplo de uso de instanceof para determinar
qué tipo (ProfesorInterino o ProfesorTitular) es el que porta una variable Profesor. Iteramos con clase declarada
Profesor y clases dinámicas ProfesorTitular y ProfesorInterino. Dinámicamente se determina de qué tipo es cada
objeto y al invocar el método abstracto importeNomina() Java determina si debe utilizar el método propio de
un subtipo u otro. En imprimirListin llegamos incluso a mostrar por pantalla de qué tipo es cada objeto usando la
sentencia instanceof para determinarlo. Escribe y ejecuta el código del test:
import java.util.Calendar; //Ejemplo aprenderaprogramar.com
public class TestAbstract {
public static void main (String [ ] Args) {
Calendar fecha1 = Calendar.getInstance();
fecha1.set(2019,10,22); //Los meses van de 0 a 11, luego 10 representa noviembre
ProfesorInterino pi1 = new ProfesorInterino("José", "Hernández López", 45, "45221887-K", fecha1);
ProfesorInterino pi2 = new ProfesorInterino("Andrés", "Moltó Parra", 87, "72332634-L", fecha1);
ProfesorInterino pi3 = new ProfesorInterino ("José", "Ríos Mesa", 76, "34998128-M", fecha1);
ProfesorTitular pt1 = new ProfesorTitular ("Juan", "Pérez Pérez", 23, "73-K");
ProfesorTitular pt2 = new ProfesorTitular ("Alberto", "Centa Mota", 49, "88-L");
ProfesorTitular pt3 = new ProfesorTitular ("Alberto", "Centa Mota", 49, "81-F");
ListinProfesores listinProfesorado = new ListinProfesores ();
listinProfesorado.addProfesor (pi1); listinProfesorado.addProfesor(pi2); listinProfesorado.addProfesor (pi3);
listinProfesorado.addProfesor (pt1); listinProfesorado.addProfesor(pt2); listinProfesorado.addProfesor (pt3);
listinProfesorado.imprimirListin();
System.out.println ("El importe de las nóminas del profesorado que consta en el listín es " +
listinProfesorado.importeTotalNominaProfesorado()+ " euros");
} } //Cierre del main y cierre de la clase

Comprueba el resultado de ejecución. El resultado del test nos muestra que operamos exitosamente sobre las
dos clases usando abstracción:

Se procede a mostrar los datos de los profesores existentes en el listín

-Nombre: José -Apellidos: Hdez López -Edad: 45 -IdProfesor: 45221887-K Fecha czo interinidad: Fri Nov 22 11:55:28
CET 2019
-Tipo de este profesor: Interino -Nómina de este profesor: 1068.0
-Nombre: Andrés -Apellidos: Mltó Parra -Edad: 87 -IdProfesor: 72332634-L Fecha czo interinidad: Fri Nov 22 11:55:28
CET 2019
-Tipo de este profesor: Interino -Nómina de este profesor: 1068.0
-Nombre: José -Apellidos: Ríos Mesa -Edad: 76 -IdProfesor: 34998128-M Fecha czo interinidad: Fri Nov 22 11:55:28
CET 2019
-Tipo de este profesor: Interino -Nómina de este profesor: 1068.0
-Nombre: Juan -Apellidos: Pérez Pérez -Edad: 23 -IdProfesor: 73-K
-Tipo de este profesor: Titular -Nómina de este profesor: 1296.0
-Nombre: Alberto -Apellidos: Centa Mota -Edad: 49 -IdProfesor: 88-L
-Tipo de este profesor: Titular -Nómina de este profesor: 1296.0
-Nombre: Alberto -Apellidos: Centa Mota -Edad: 49 -IdProfesor: 81-F
-Tipo de este profesor: Titular -Nómina de este profesor: 1296.0

El importe de las nóminas del profesorado que consta en el listín es 7092.0 euros

CLASES ABSTRACTAS EN EL API DE JAVA

Java utiliza clases abstractas en el API de la misma forma que podemos nosotros usarlas en nuestros programas.
Por ejemplo, la clase AbstractList del paquete java.util es una clase abstracta con tres subclases:

Como vemos, entre las subclases dos de ellas son concretas mientras que una todavía es abstracta. En una
clase como AbstractList algunos métodos son abstractos, lo que obliga a que el método esté sobreescrito en las
subclases, mientras que otros métodos no son abstractos.

Sobre un objeto de una subclase, llamar a un método puede dar lugar a:

a) La ejecución del método tal y como esté definido en la subclase.


b) La búsqueda del método ascendiendo por las superclases hasta que se encuentra y puede ser ejecutado.
Es lo que ocurrirá por ejemplo con toString() si no está definido en la subclase.

También podría gustarte