11.- Encapsulamiento y métodos accesores.
Encapsulamiento en Java
Imaginemos que se crea una clase, una docena de programadores tienen acceso a dicha clase y la utilizan a discreción, posteriormente dicha clase comienza a comportarse de una manera inesperada debido a que los valores que algunas variables han tomado no fueron anticipados y todo comienza a desmoronarse. Para corregir el problema se crea una versión más nueva de dicha clase y listo.
Bueno, a esto le llamamos flexibilidad y capacidad de mantenimiento, ambas son características y beneficios de la programación Orientada a Objetos (OO) pero para que una clase pueda cumplir dichas funciones los programadores debemos de hacer algo. Imaginemos que creamos una clase con variables de instancia públicas a las cuales podemos acceder sin problemas desde fuera de la misma clase...
Analizando el código anterior podemos darnos cuenta de que las variables enteras tipo y clase son públicas y pueden ser accedidas directamente a través de una instancia de la clase MiClase, esto compila sin ningún problema, digamos que es 'legal', sin embargo, ¿qué pasa si ingresamos un valor que no se supone debe de tener una variable (en este caso el -5 que le asignamos a tipo)?, simplemente no hay nada que nos detenga para hacerlo. La única manera de proteger el código es escribiendo un método que nos permita regular los valores que cada variable puede tener y escondiendo las variables para que no se pueda acceder a ellas de manera directa, esto es el principio básico de encapsulamiento.
Si se desea flexibilidad, buen mantenimiento y extensibilidad, nuestro diseño en el código debe de incluir encapsulamiento, para ello debemos de hacer lo siguiente:
Si nos fijamos un poquito, en el método setTipo() no existen validaciones para prevenir que un valor no válido sea asignado a la variable, sin embargo, el proveer de un método de este tipo desde el diseño inicial de la aplicación nos permite posteriormente modificar el comportamiento de la misma sin afectar los métodos utilizados, tal vez en un futuro se desee que dicha variable solamente pueda tener uno entre un rango de valores y se podrán aplicar posteriormente los cambios sin que haya repercusiones negativas.
Bueno, a esto le llamamos flexibilidad y capacidad de mantenimiento, ambas son características y beneficios de la programación Orientada a Objetos (OO) pero para que una clase pueda cumplir dichas funciones los programadores debemos de hacer algo. Imaginemos que creamos una clase con variables de instancia públicas a las cuales podemos acceder sin problemas desde fuera de la misma clase...
Si se desea flexibilidad, buen mantenimiento y extensibilidad, nuestro diseño en el código debe de incluir encapsulamiento, para ello debemos de hacer lo siguiente:
- Mantener las variables de instancia protegidas (puede ser con un modificador de acceso, p.ej., private).
- Hacer métodos de acceso públicos para forzar al acceso a las variables por medio de dichos métodos en lugar de acceder directamente.
- Utilizar las convenciones de código para los nombres de los métodos, p. ej., set y get.
Los métodos get y set, son simples métodos que usamos en las clases para mostrar (get) o modificar (set) el valor de un atributo. El nombre del método siempre sera get o set y a continuación el nombre del atributo, su modificador siempre es public ya que queremos mostrar o modificar desde fuera la clase. Por ejemplo, getNombre o setNombre.
Esta es la sintaxis de cada uno:
public tipo_dato_atributo getAtributo (){
return atributo;
}
public void setAtributo (tipo_dato_atributo variable){
this.atributo = variable;
}
Si usamos Eclipse tenemos una manera aún mas sencilla de generarlos automáticamente. Pinchamos en Source -> Generate Getters and Setters, nos aparecerá una ventana donde elegiremos que atributos generar su get o set, al pinchar en OK nos lo generara automáticamente estos métodos.
Veamos un ejemplo:
Clase
/** * Clase Empleado * * Contiene informacion de cada empleado * * @author Fernando * @version 1.0 */ public class Empleado { //Atributos /** * Nombre del empleado */ private String nombre; /** * Apellido del empleado */ private String apellido; /** * Edad del empleado */ private int edad; /** * Salario del empleado */ private double salario; //Metodos publicos /** * Devuelve el nombre del empleado * @return nombre del empleado */ public String getNombre() { return nombre; } /** * Modifica el nombre de un empleado * @param nombre */ public void setNombre(String nombre) { this.nombre = nombre; } /** * Devuelve la edad de un empleado * @return edad del empleado */ public int getEdad() { return edad; } /** * Modifica la edad de un empleado * @param edad */ public void setEdad(int edad) { this.edad = edad; } /** * Devuelve el salario de un empleado * @return salario del empleado */ public double getSalario() { return salario; } /** * Suma un plus al salario del empleado si el empleado tiene mas de 40 años * @param sueldoPlus * @return <ul> * <li>true: se suma el plus al sueldo</li> * <li>false: no se suma el plus al sueldo</li> * </ul> */ public boolean plus (double sueldoPlus){ boolean aumento=false; if (edad>40 && compruebaNombre()){ salario+=sueldoPlus; aumento=true; } return aumento; } //Metodos privados private boolean compruebaNombre(){ if(nombre.equals("")){ return false; } return true; } //Constructor /** * Constructor por defecto */ public Empleado(){ this.nombre=""; this.apellido=""; this.edad=0; this.salario=0; } /** * Constructor con 4 parametros * @param nombre nombre del empleado * @param apellido nombre del empleado * @param edad edad del empleado * @param salario salario del empleado */ public Empleado(String nombre, String apellido, int edad, double salario){ this.nombre=nombre; this.apellido=apellido; this.edad=edad; this.salario=salario; } }
Clase ejecutable
public class EmpleadoApp { public static void main(String[] args) { Empleado empleado1=new Empleado ("Fernando", "Ureña", 23, 800); Empleado empleado2=new Empleado ("", "Lopez", 50 ,1800); //Mostramos el valor actual del salario del empleado1 System.out.println("El salario del empleado1 es "+empleado1.getSalario()); //Modificamos la edad del empleado1 empleado1.setEdad(43); empleado1.plus(100); //Mostramos el salario de nuevo, ahora tendra 100 mas System.out.println("El salario actual del empleado1 es "+empleado1.getSalario()); //Modificamos el nombre del empleado2 empleado2.setNombre("Antonio"); empleado2.plus(100); //Mostramos el salario de nuevo, ahora tendra 100 mas System.out.println("El salario del empleado2 es "+empleado2.getSalario()); } }
13.- Miembros estáticos de una clase.
Static
La palabra clave static declara miembros (atributos, métodos y clases anidadas) que están asociados a la clase en lugar de a una instancia de la clase.
Utilidad
A veces puede ser útil tener una variable compartida por todos los objetos de la clase (algo parecido a una variable global). En este caso marcaríamos esta variable como static.
Si una variable static no se marca como private, es posible acceder a ella desde fuera de la clase. Para hacerlo, no se necesita ningún objeto, se hace referencia a ella mediante el nombre de la clase.
Por ejemplo, un caso común es un contador de los objetos instanciados de una clase:
1
2
3
4
5
6
7
8
9
10
11
12
| public class Leccion{ public static int contador; //variable estática, asociada a la clase private int idLeccion; //atributos, variables no estáticas private String desLeccion; //asociadas a los objetos public Leccion() { contador = contador + 1 ; //podemos usar el contador para el id de cada objeto Leccion. idLeccion = contador; desLeccion = "Sin nombre" ; } } |
1
2
3
4
5
6
7
8
9
| public class MainLeccion{ public static void main (String[] args){ System.out.println( "Contador: " + Leccion.contador); //Muestra 0 Leccion l1 = new Leccion(); Leccion l2 = new Leccion(); System.out.println( "Contador: " + Leccion.contador); //Muestra 2 } } |
O cuando necesitemos un método cuyo código no dependa de los atributos de un objeto de la clase. En este caso marcaríamos este método como static.
Por ejemplo, podríamos hacer la variable estática anterior privada y obtener el número de objetos Leccion creados mediante un método getContador().
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| public class Leccion{ private static int contador; //variable estática, asociada a la clase public static int getContador(){ //método estático return contador; } private int idLeccion; //atributos, variables no estáticas private String desLeccion; //asociadas a los objetos public Leccion() { contador = contador + 1 ; //podemos usar el contador para el id de cada objeto Leccion. idLeccion = contador; desLeccion = "Sin nombre" ; } } |
1
2
3
4
5
6
7
| public class MainLeccion{ public static void main (String[] args){ System.out.println( "Contador: " + Leccion.getContador()); //Muestra 0 Leccion l1 = new Leccion(); Leccion l2 = new Leccion(); System.out.println( "Contador: " + Leccion.getContador()); //Muestra 2 } |
Accediendo a Miembros Estáticos
Anteriormente hemos visto que para acceder a las variables y métodos estáticos hemos utilizado el nombre de la clase y el operador punto. Es lo más lógico dado que las variables y métodos estáticos pertenecen a la clase, pero Java permite también acceder a los miembros estáticos utilizando un objeto.
Java lo permite pero el compilador realmente lo sustituirá por el nombre de la clase.
En el ejemplo anterior podíamos haber escrito:
1
| System.out.println( "Contador: " + l2.getContador()); |
Esto último solo lo podremos hacer si existe una instancia de la clase.
Redefinición en Métodos Estáticos
Los métodos estáticos no se pueden sobrescribir (override), sin embargo si es posible tener el mismo método en una subclase. Esto se denomina redefinición o ocultación.
Si se llama al método con una referencia de objeto, el método llamado será el correspondiente a la clase para la que se haya declarado la variable.
¿Qué diferencia existe entre la sobrescritura y la redefinición?
La sobrescritura (override) está íntimamente ligada al polimorfismo. Como los métodos estáticos están asociados a la clase en lugar de a los objetos, el polimorfismo en tiempo de ejecución no es posible y por lo tanto no es sobrescritura sino redefinición.
Veamos la diferencia. Creamos una clase Persona y una subclase Cliente. Creamos un método no estático, mostrar() y un método estático, mostrarStatic() en cada una de las clases.
Redefinición en Métodos Estáticos
Los métodos estáticos no se pueden sobrescribir (override), sin embargo si es posible tener el mismo método en una subclase. Esto se denomina redefinición o ocultación.
Si se llama al método con una referencia de objeto, el método llamado será el correspondiente a la clase para la que se haya declarado la variable.
¿Qué diferencia existe entre la sobrescritura y la redefinición?
La sobrescritura (override) está íntimamente ligada al polimorfismo. Como los métodos estáticos están asociados a la clase en lugar de a los objetos, el polimorfismo en tiempo de ejecución no es posible y por lo tanto no es sobrescritura sino redefinición.
Veamos la diferencia. Creamos una clase Persona y una subclase Cliente. Creamos un método no estático, mostrar() y un método estático, mostrarStatic() en cada una de las clases.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| public class Persona{ private String nif; private String nombre; private int edad; public Persona(String nif, String nombre, int edad){ this .nif = nif; this .nombre = nombre; this .edad = edad; } ... //getters, setters public void mostrar(){ System.out.println( "Persona " + nif + ": \n Nombre: " + nombre + "\n Edad: " + edad); } public static void mostrarStatic(){ System.out.println( "Clase Persona" ); } } |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| public class Cliente extends Persona{ private int id; private int antiguedad; private int descuento; public Cliente(String nif, String nombre, int edad, int id, int antiguedad, int descuento) { super (nif, nombre, edad); this .id = id; this .antiguedad = antiguedad; this .descuento = descuento; } ... // getters, setters public void mostrar(){ System.out.println( "Cliente" + id + ": \n Nombre: " + getNombre() + "\n Antigüedad: " + antiguedad + "\n Descuento: " + descuento); } public static void mostrarStatic(){ System.out.println( "Clase Cliente" ); } } |
Con esta definición de clases si creamos los siguientes objetos tendríamos los siguientes resultados:
1
2
3
4
5
6
7
8
9
10
11
| public class MainSobrescritura{ public static void main(String[] args){ Persona per = new Persona( "18520147L" , "Maria Victoria Rodriguez" , 18 ); Cliente cli = new Cliente( "20415789M" , "Secundino Jimenez" , 25 , 504 , 5 , 1 ); Persona per2 = new Cliente( "41605788R" , "Luis Martin" , 30 , 905 , 3 , 2 ); per.mostrar(); //llama a mostrar() de Persona cli.mostrar(); //llama a mostrar() de Cliente per2.mostrar(); //llama a mostrar() de Cliente } } |
1
2
3
4
5
6
7
8
9
10
11
| public class MainRedefinicion{ public static void main(String[] args){ Persona per = new Persona( "18520147L" , "Maria Victoria Rodriguez" , 18 ); Cliente cli = new Cliente( "20415789M" , "Secundino Jimenez" , 25 , 504 , 5 , 1 ); Persona per2 = new Cliente( "41605788R" , "Luis Martin" , 30 , 905 , 3 , 2 ); per.mostrarStatic(); //llama a mostrarStatic de Persona cli.mostrarStatic(); //llama a mostrarStatic de Cliente per2.mostrarStatic(); //llama a mostrarStatic de Persona y no de Cliente como arriba. } } |
Vemos que en los métodos estáticos el método llamado será el de la clase declarada en tiempo de compilación y no en tiempo de ejecución como pasa con los métodos no estáticos sobrescritos.
Acceso en un método estático
Un método estático no puede acceder a ninguna variable salvo a las variables locales, los atributos static y sus parámetros. Cualquier intento de acceder a atributos que no sean estáticos directamente (sin crear un objeto) provoca un error de compilación.
Nota
El método main es también un método estático. Podemos comprobar que no creamos ningún objeto para ejecutarlo.Si necesita datos miembro se deben crear objetos dentro de main.
Acceso en un método estático
Un método estático no puede acceder a ninguna variable salvo a las variables locales, los atributos static y sus parámetros. Cualquier intento de acceder a atributos que no sean estáticos directamente (sin crear un objeto) provoca un error de compilación.
Nota
El método main es también un método estático. Podemos comprobar que no creamos ningún objeto para ejecutarlo.Si necesita datos miembro se deben crear objetos dentro de main.
14.- Herencia.
JEMPLO 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.
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
|
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.
15.- Sobreescritura de miembros.
Sobreescritura de métodos
Una subclase hereda todos los métodos de su superclase que son accesibles a dicha subclase a menos que la subclase sobreescriba los métodos.
Una subclase sobreescribe un método de su superclase cuando define un método con las mismas características ( nombre, número y tipo de argumentos) que el método de la superclase.
Las subclases emplean la sobreescritura de métodos la mayoría de las veces para agregar o modificar la funcionalidad del método heredado de la clase padre.
Una subclase hereda todos los métodos de su superclase que son accesibles a dicha subclase a menos que la subclase sobreescriba los métodos.
Una subclase sobreescribe un método de su superclase cuando define un método con las mismas características ( nombre, número y tipo de argumentos) que el método de la superclase.
Las subclases emplean la sobreescritura de métodos la mayoría de las veces para agregar o modificar la funcionalidad del método heredado de la clase padre.
Ejemplo
class ClaseA
{
void miMetodo(int var1, int var2)
{ ... }
String miOtroMetodo( )
{ ... }
}
class ClaseB extends ClaseA
{
/* Estos métodos sobreescriben a los métodos
de la clase padre */
void miMetodo (int var1 ,int var2)
{ ... }
String miOtroMetodo( )
{ ... }
}
16.- Clases y métodos abstractos.
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 claridad para nuestros programas. Escribe este código:
public class Persona {
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 {
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 {
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 {
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;
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
|
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.
17.- Polimorfismo.
Qué es el polimorfismo en la Programación Orientada a Objetos, el motivo de su existencia y cómo implementar polimorfismo en clases y objetos.
El concepto de polimorfismo es en realidad algo muy básico. Realmente, cuando estamos aprendiendo Programación Orientada a Objetos (también conocida por sus siglas POO / OOP) muchos estudiantes nos hacemos un embolado tremendo al tratar de entender el concepto, pero en su base es algo extremadamente sencillo.
Trataremos de explicarlo en este artículo con palabras sencillas, pero para los valientes, aquí va una primera definición que no es mía y que carece de la prometida sencillez. Pero no te preocupes, pues la entiendas o no, luego lo Definición: El polimorfismo es una relajación del sistema de tipos, de tal manera que una referencia a una clase (atributo, parámetro o declaración local o elemento de un vector) acepta direcciones de objetos de dicha clase y de sus clases derivadas (hijas, nietas, …).
Nota: Esta es la definición académica que nos ofrece el profesor de la UPM Luis Fernández, del que fui alumno en la universidad y en EscuelaIT.
Herencia y las clasificaciones en Programación Orientada a Objetos
Para poder entender este concepto de OOP necesitas entender otras cosas previas, como es el caso de la herencia. Esto lo hemos explicado en un artículo anterior en DesarrolloWeb.com: Herencia en la Programación Orientada a Objetos.
Veremos que el polimorfismo y la herencia son dos conceptos estrechamente ligados. Conseguimos implementar polimorfismo en jerarquías de clasificación que se dan a través de la herencia. Por ejemplo, tenemos una clase vehículo y de ella dependen varias clases hijas como coche, moto, autobús, etc.
Pero antes de entender todo esto, queremos ir un poco más hacia atrás, entendiendo lo que es un sistema de tipos.
Por qué el sistema de tipos es importante en Polimorfismo
Muchos de los lectores que asumo se introducen en el concepto de polimorfismo a través de este artículo han aprendido a programar en lenguajes débilmente tipados, como es el caso de PHP y Javascript. Por ello es conveniente entender cómo es un lenguaje fuertemente tipado, como es el caso de Java o C.
En estos lenguajes, cuando defino una variable, siempre tengo que decir el tipo de datos que va a contener esta variable. Por ejemplo:
int miNumero;
Así le indicamos que la variable declarada "miNumero" va a contener siempre un entero. Podrás asignarle diversos valores, pero siempre deben de ser números enteros. De lo contrario el compilador te lanzará un mensaje de error y no te permitirá compilar el programa que has realizado.
Esto incluso pasa con los objetos. Por ejemplo, si en Java defino la clase "Largometraje" (una cinta que se puede exhibir en la televisión o el cine), cuando creo objetos de la clase "Largometraje" debo declarar variables en las que indique el tipo de objeto que va a contener.
Largometraje miLargo = new Largometraje("Lo que el viento se llevó");
Esa variable "miLargo", por declaración tendrá una referencia a un objeto de la clase "Largometraje". Pues bien, durante toda su vida, deberá tener siempre una referencia a cualquier objeto de la misma clase. O sea, mañana no podremos guardar un entero en la variable, ni una cadena u otro objeto de otra clase.
Volviendo al ejemplo de los vehículos, si defino una variable que apunta a un objeto de clase "Coche", durante toda la vida de esa variable tendrá que contener un objeto de la clase Coche, no pudiendo más adelante apuntar a un objeto de la clase Moto o de la clase Bus. Esta rigidez, como decimos, no existe en los lenguajes débilmente tipados como es el caso de Javascript o PHP, sin embargo es una característica habitual de lenguajes como Java, que son fuertemente tipados.
Coche miCoche = new Coche("Ford Focus 2.0"); //la variable miCoche apunta a un objeto de la clase coche //si lo deseo, mañana podrá apuntar a otro objeto diferente, pero siempre tendrá que ser de la clase Coche miCoche = new Coche("Renault Megane 1.6");
Lo que nunca podré hacer es guardar en esa variable, declarada como tipo Coche, otra cosa que no sea un objeto de la clase Coche.
//si miCoche fue declarada como tipo Coche, no puedo guardar un objeto de la clase Moto miCoche = new Moto("Yamaha YBR"); //la línea anterior nos daría un error en tiempo de compilación
Fíjate que en este punto no te estoy hablando todavía de polimorfismo, sino de algo de la programación en general como es el sistema de tipos. Sin embargo, tienes que amoldar la cabeza a esta restricción de lenguajes fuertemente tipados para que luego puedas entender por qué el polimorfismo es importante y clave en la programación orientada a objetos. Y ojo, insisto que esto es algo relacionado con lenguajes fuertemente tipados (también llamados de tipado estático), en PHP no habría problema en cambiar el tipo de una variable, asignando cualquier otra cosa, dado que no se declaran los tipos al crear las variables.
Entendida esa premisa, pensemos en el concepto de función y su uso en lenguajes de tipado estático.
Nota: A veces, a los lenguajes fuertemente tipados se les llama de "tipado estático" y a los débilmente tipados se les llama "tipado dinámico". Si quieres saber más sobre lenguajes tipados y no tipados, te recomiendo ver el #programadorIO tipados Vs no tipados.
Cuando en un lenguaje fuertemente tipado declaramos una función, siempre tenemos que informar el tipo de los parámetros que va a recibir. Por ejemplo, la función "sumaDosNumeros()" recibirá dos parámetros, que podrán ser de tipo entero.
function sumaDosNumeros(int num1, int num2)
A esta función, tal como está declarada, no le podremos pasar como parámetros otra cosa que no sean variables -o literales- con valores de número entero. En caso de pasar otros datos con otros tipos, el compilador te alertará. Osea, si intentas invocar sumaDosNumeros("algo", "otro"), el compilador no te dejará compilar el programa porque no ha encontrado los tipos esperados en los parámetros de la función.
Esto mismo de los parámetros en las funciones te ocurre también con los atributos de las clases, cuyos tipos también se declaran, con los datos que se insertan en un array, etc. Como ves, en estos lenguajes como Java el tipado se lleva a todas partes.
Polimorfismo en objetos
Ahora párate a pensar en clases y objetos. Quédate con esto: Tal como funcionan los lenguajes fuertemente tipados, una variable siempre deberá apuntar a un objeto de la clase que se indicó en el momento de su declaración. Una función cuyo parámetro se haya declarado de una clase, sólo te aceptará recibir objetos de esa clase. Un array que se ha declarado que es de elementos de una clase determinada, solo aceptará que rellenemos sus casillas con objetos de esa clase declarada.
Vehiculo[] misVehiculos = new Vehiculo[3];
Esa variable misVehiculos es un array y en ella he declarado que el contenido de las casillas serán objetos de la clase "Vehiculo". Como se ha explicado, en lenguajes fuertemente tipados sólo podría contener objetos de la clase Vehiculo. Pues bien, polimorfismo es el mecanismo por el cual podemos "relajar el sistema de tipos", de modo que nos acepte también objetos de las clases hijas o derivadas.
Por tanto, la "relajación" del sistema de tipos no es total, sino que tiene que ver con las clasificaciones de herencia que tengas en tus sistemas de clases. Si defines un array con casillas de una determinada clase, el compilador también te aceptará que metas en esas casillas objetos de una clase hija de la que fue declarada. Si declaras que una función recibe como parámetros objetos de una determinada clase, el compilador también te aceptará que le envíes en la invocación objetos de una clase derivada de aquella que fue declarada.
En concreto, en nuestro array de vehículos, gracias al polimorfismo podrás contener en los elementos del array no solo vehículos genéricos, sino también todos los objetos de clases hijas o derivadas de la clase "Vehiculo", osea objetos de la clase "Coche", "Moto", "Bus" o cualquier hija que se haya definido.
Para qué nos sirve en la práctica el polimorfismo
Volvamos a la clase "Largometraje" y ahora pensemos en la clase "Cine". En un cine se reproducen largometrajes. Puedes, no obstante, tener varios tipos de largometrajes, como películas o documentales, etc. Quizás las películas y documentales tienen diferentes características, distintos horarios de audiencia, distintos precios para los espectadores y por ello has decidido que tu clase "Largometraje" tenga clases hijas o derivadas como "Película" y "Documental".
Imagina que en tu clase "Cine" creas un método que se llama "reproducir()". Este método podrá recibir como parámetro aquello que quieres emitir en una sala de cine y podrán llegarte a veces objetos de la clase "Película" y otras veces objetos de la clase "Documental". Si has entendido el sistema de tipos, y sin entrar todavía en polimorfismo, debido a que los métodos declaran los tipos de los parámetros que recibes, tendrás que hacer algo como esto:
reproducir(Pelicula peliculaParaReproducir)
Pero si luego tienes que reproducir documentales, tendrás que declarar:
reproducir(Documental documentaParaReproducir)
Probablemente el código de ambos métodos sea exactamente el mismo. Poner la película en el proyector, darle al play, crear un registro con el número de entradas vendidas, parar la cinta cuando llega al final, etc. ¿Realmente es necesario hacer dos métodos? De acuerdo, igual no te supone tanto problema, ¿pero si mañana te mandan otro tipo de cinta a reproducir, como la grabación de la final del mundial de fútbol en 3D? ¿Tendrás que crear un nuevo método reproducir() sobre la clase "Cine" que te acepte ese tipo de emisión? ¿es posible ahorrarnos todo ese mantenimiento?
Aquí es donde el polimorfismo nos ayuda. Podrías crear perfectamente un método "reproducir()" que recibe un largometraje y donde podrás recibir todo tipo de elementos, películas, documentales y cualquier otra cosa similar que sea creada en el futuro.
Entonces lo que te permiten hacer los lenguajes es declarar el método "reproducir()" indicando que el parámetro que vas a recibir es un objeto de la clase padre "Largometraje", pero donde realmente el lenguaje y compilador te aceptan cualquier objeto de la clase hija o derivada, "Película", "Documental", etc.
reproducir(Largometraje elementoParaReproducir)
Podremos crear películas y reproducirlas, también crear documentales para luego reproducir y lo bonito de la historia es que todos estos objetos son aceptados por el método "reproducir()", gracias a la relajación del sistema de tipos. Incluso, si mañana quieres reproducir otro tipo de cinta, no tendrás que tocar la clase "Cine" y el método "reproducir()". Siempre que aquello que quieras reproducir sea de la clase "Largometraje" o una clase hija, el método te lo aceptará.
Pongamos otro ejemplo por si acaso no ha quedado claro con lo visto hasta el momento, volviendo de nuevo a la clase Vehiculo. Además nos centramos en la utilidad del polimorfismo y sus posibilidades para reducir el mantenimiento de los programas informáticos, que es lo que realmente me gustaría que se entienda.
Tenemos la clase Parking. Dentro de ésta tenemos un método estacionar(). Puede que en un parking tenga que estacionar coches, motos o autobuses. Sin polimorfismo tendría que crear un método que permitiese estacionar objetos de la clase "Coche", otro método que acepte objetos de la clase "Moto" para estacionarlos, etc. Pero todos estaremos de acuerdo que estacionar un coche, una moto o un bus es bastante similar: "entrar en el parking, recoger el ticket de entrara, buscar una plaza, situar el vehículo dentro de esa plaza...".
Lo ideal sería que nuestro método me permita permita recibir todo tipo de vehículos para estacionarlos, primero por reutilización del código, ya que es muy parecido estacionar uno u otro vehículo, pero además porque así si mañana el mercado trae otro tipo de vehículos, como una van, todoterreno hibrido, o una nave espacial, mi software sea capaz de aceptarlos sin tener que modificar la clase Parking.
Gracias al polimorfismo, cuando declaro la función estacionar() puedo decir que recibe como parámetro un objeto de la clase "Vehiculo" y el compilador me aceptará no solamente vehículos genéricos, sino todos aquellos objetos que hayamos creado que hereden de la clase Vehículo, osea, coches, motos, buses, etc. Esa relajación del sistema de tipos para aceptar una gama de objetos diferente es lo que llamamos polimorfismo.
En fin, esto es lo que significa polimorfismo. A partir de aquí puede haber otra serie de consideraciones y recomendaciones, así como características implementadas en otros lenguajes, pero explicar todo eso no es el objetivo de este artículo. Esperamos que con lo que has aprendido puedas orientar mejor tus estudios de Programación Orientada a Objetos. Si quieres más información sobre el tema lee el artículo Qué es Programación Orientada a Objetos, que seguro te será de gran utilidad.
Comentarios
Publicar un comentario