Anda di halaman 1dari 35

Conceptos de Programación Orientada

a Objetos aplicados a Java

Autor: Antonio J. Martín


ÍNDICE

1. INTRODUCCIÓN 3

2. Encapsulación4

2.1. MÉTODOS DE PROPIEDAD SET/GET ....................................................................... 4

2.2. JAVABEANS ......................................................................................................... 5

EJERCICIO 1.................................................................................................... 6

3. Sobrecarga de métodos 10

4. Herencia 11

4.1. RELACIÓN DE HERENCIA ..................................................................................... 11

4.2. EJEMPLOS DE HERENCIA ENTRE CLASES ............................................................. 13

4.3. DEFINICIÓN DE HERENCIA EN JAVA ...................................................................... 14

4.4. MIEMBROS PRIVADOS Y PROTEGIDOS .................................................................. 14

4.5. EJECUCIÓN DE CONSTRUCTORES EN LA HERENCIA............................................... 16

EJERCICIO 2.................................................................................................. 18

4.6. HERENCIA DE LA CLASE OBJECT ......................................................................... 19

5. Sobrescritura de métodos 20

EJERCICIO 3.................................................................................................. 21

6. Clases Abstractas e interfaces 22

6.1. CLASES ABSTRACTAS ......................................................................................... 22

6.2. INTERFACES ...................................................................................................... 23

6.2.1. Definición de una interfaz .......................................................................... 24

6.2.2. Implementación de una interfaz ................................................................ 24

6.2.3. Herencia de interfaces............................................................................... 25

7. Polimorfismo 26

EJERCICIO 4.................................................................................................. 28
1. INTRODUCCIÓN
Java es un lenguaje de programación totalmente orientado a objetos, lo que significa
que todos los conceptos definidos por este paradigma de programación son aplicables a este
lenguaje.

Durante este tema analizaremos dichos conceptos, su implementación en el lenguaje


Java y las ventajas que su uso nos proporciona en el desarrollo de las aplicaciones, no sólo a
la hora de crear nuestras propias jerarquías de clases sino también, y más importante, de cara
a utilizar adecuadamente las clases e interfaces del API Java para construir ciertos tipos de
aplicaciones.

Los conceptos que vamos a estudiar son los siguientes:

ƒ Encapsulación

ƒ Sobrecarga de métodos

ƒ Herencia

ƒ Sobreescritura de métodos

ƒ Clases abstractas e interfaces

ƒ Polimorfismo

Para poder afrontar con garantías este tema, es necesario haber estudiado
previamente el apartado “Clases y objetos” desarrollado en el tema anterior y comprender los
conceptos tratados en el mismo.
2. ENCAPSULACIÓN
La encapsulación, aplicada al contexto de creación de una clase, es un concepto que
se basa en mantener aislados del exterior los datos miembro o atributos de la clase. Para ello,
dichos atributos se definen con el modificador private, permitiendo que el acceso a los mismos
solo pueda realizarse desde el interior de la clase.

La encapsulación de los atributos es un mecanismo de protección de los datos pues, al


impedir el acceso directo a los mismos desde el exterior de la clase, evitamos que se pueda
asignar un valor inapropiado al atributo y dejar así al objeto en un estado inestable. Por
ejemplo, en el caso de la clase Coche con su atributo potencia, si no se protege el atributo
declarándolo como privado, desde un código externo a la clase se podría hacer algo como
esto:

objeto_coche.potencia=-10;

Lo cual, obviamente, no tendría sentido.

2.1. MÉTODOS DE PROPIEDAD SET/GET

De cara a proporcionar un acceso controlado desde el exterior a los atributos


encapsulados por la clase, ésta debe disponer de unos métodos de tipo get y set que permitan
realizar las operaciones de lectura y escritura sobre los atributos, respectivamente.

La nomenclatura de estos métodos, conocidos también como métodos de propiedad,


sigue el convenio:

getNombre_propiedad

setNombre_propiedad

Donde Nombre_propiedad es el nombre que se asigna a la propiedad que representa


el atributo encapsulado y que, normalmente (aunque no es obligatorio), coincide con el nombre
del atributo.

En cuanto al formato de los métodos, deberá ser el siguiente:

public tipo getNombre_propiedad()

public void setNombre_propiedad(tipo valor)

donde tipo es el tipo de dato del atributo encapsulado.

Por ejemplo, si quisiéramos encapsular el atributo potencia en la clase Coche y


proporcionar un acceso controlado al mismo a través de unos métodos de propiedad,
deberíamos implementar la clase como se indica en el siguiente listado:

public class Coche{


private int potencia;
public int getPotencia(){
return potencia;
}
public void setPotencia(int valor){
if(valor>0){ //evita la asignación de
//valores negativos
potencia=valor;
}
}
}
Obsérvese como la primera letra del nombre de la propiedad se escribe siempre en
mayúsculas; se trata de un convenio que habrá que respetar siempre, especialmente cuando
deleguemos en un framework (utilidades que nos facilitan la construcción de ciertos tipos de
aplicaciones) la manipulación de este tipo de métodos.

Otra ventaja que nos proporciona la encapsulación está relacionada con el


mantenimiento a posteriori de la clase, ya que, si por algún motivo nos vemos obligados a
modificar los criterios de asignación de valores a los atributos o, incluso, el nombre del atributo
mismo, los cambios realizados en el interior de la clase no afectarán a las aplicaciones
clientes de estas clases pues el formato de los métodos de propiedad permanece invariable.

2.2. JAVABEANS

En determinados contextos resulta extremadamente útil la creación de clases cuyo


único fin es la encapsulación de una serie de datos dentro de la misma a fin de tenerlos
agrupados en un mismo objeto de cara a facilitar su tratamiento. Estos datos representan
información asociada a alguna entidad manejada en distintas partes del programa, como una
cuenta bancaria, un producto de una tienda virtual, etc.

A este tipo de clases se las conoce como clases de encapsulación o JavaBeans y,


como hemos indicado, a parte de los atributos y constructores para su inicialización, solamente
dispone de métodos set/get.

El siguiente listado nos muestra un ejemplo de una clase JavaBean que encapsula la
información asociada a una cuenta bancaria:

public class Cuenta {


private String codigo_cuenta;
private String dni_cliente;
private float saldo;
public Empleado(String codigo, String dni, float saldo)
{
this.codigo_cuenta=codigo;
this.dni_cliente=dni;
this.saldo=saldo;
}
public void setCodigo(String n)
{
codigo_cuenta=n;
}
public String getCodigo()
{
return codigo_cuenta;
}
public void setDni(String n)
{
dni_cliente=n;
}
public String getDni()
{
return dni_cliente;
}
public void setSaldo(float s)
{
saldo=s;
}
public String getSaldo()
{
return saldo;
}
}

En este listado de ejemplo observamos el uso de uno de las palabras reservadas del
lenguaje Java; se trata de this y representa una referencia al objeto actual sobre el que se está
ejecutando el código. En este ejemplo, se utiliza para acceder a los atributos del propio objeto,
si bien resulta redundante en este caso pues podría referirse al atributo desde el interior de la
clase simplemente utilizando el nombre de éste:

codigo_cuenta=codigo;
aunque en el caso del atributo saldo si resulta necesario su uso, pues su nombre
coincide con el del parámetro:

this.saldo=saldo;
Otro aspecto importante a tener en cuenta con los JavaBeans es el hecho de que
resulta conveniente que dispongan de un constructor sin parámetros. Aunque no es un
requerimiento de obligado cumplimiento para la compilación y utilización de la clase, si resulta
imprescindible cuando esta va a ser manejada por algún framework.

EJERCICIO 1

En este primer ejercicio vamos a implementar una nueva versión del programa
desarrollado en el ejercicio 11 del tema anterior, consistente en la gestión de una lista de
contactos.

Para esta nueva versión, los contactos estarán caracterizados por un nombre, teléfono,
dirección de correo electrónico y DNI, utilizándose además este último dato como clave
asociada a cada contacto en la colección.

Recordemos que el menú de opciones presentadas al usuario es el siguiente:


1. Añadir contacto

2. Eliminar contacto

3. Mostrar contactos

4. Salir

Dicho menú volverá a ser mostrado al usuario después de realizarse la opción


correspondiente; así hasta que elija “salir”.

Lo primero será crear JavaBean al que llamaremos Contacto y que agrupará los cuatro
campos relativos a un contacto. El código de esta clase será el que se indica en el siguiente
listado:

package javabeans;

public class Contacto {


private String nombre;
private long telefono;
private String email;
private int dni;

public Contacto(){

}
public Contacto(String nombre, long telefono, String email,
int dni){
this.nombre=nombre;
this.telefono=telefono;
this.email=email;
this.dni=dni;
}
public String getNombre() {
return nombre;
}

public void setNombre(String nombre) {


this.nombre = nombre;
}

public long getTelefono() {


return telefono;
}
public void setTelefono(long telefono) {
this.telefono = telefono;
}

public String getEmail() {


return email;
}

public void setEmail(String email) {


this.email = email;
}

public int getDni() {


return dni;
}

public void setDni(int dni) {


this.dni = dni;
}
}

La colección Hashtable utilizada para almacenar los contactos deberá ser creada en
este ejercicio para un tipo específico Contacto. El siguiente listado corresponde a la nueva
versión de la clase de gestión de contactos:

import java.util.*;
import javabeans.*;
public class GestionContactos {
public static void main(String[] args) {
Hashtable<Integer,Contacto> lista= new
Hashtable<Integer,Contacto>();
int opcion;
Scanner sc=new Scanner(System.in);
sc.useDelimiter("\n");
do{
System.out.println("1. Añadir contacto");
System.out.println("2. Eliminar contacto");
System.out.println("3. Mostrar contactos");
System.out.println("4. Salir");
opcion=sc.nextInt();
String nombre, email;
Integer dni;
long telefono;
switch(opcion){
case 1:
System.out.println("Introduce Nombre: ");
nombre=sc.next();
System.out.println("Introduce emai: ");
email=sc.next();
System.out.println("Introduce telefono: ");
telefono=sc.nextLong();
System.out.println("DNI: ");
dni=sc.nextInt();
Contacto c=new Contacto(nombre,telefono,email,dni);
guardarContacto(c,dni,lista);
break;
case 2:

System.out.println("Introduzca el dni: ");


dni=sc.nextInt();
eliminarContacto(dni,lista);
break;
case 3:
mostrarContactos(lista);
break;
}
}
while(opcion!=4);
}
static void guardarContacto(Contacto ct,Integer clave,
Hashtable<Integer,Contacto> lista){
if(!lista.containsKey(clave)){
lista.put(clave,ct);
}
}
static void eliminarContacto(Integer clave,
Hashtable<Integer,Contacto> lista){
if(lista.containsKey(clave)){
lista.remove(clave);
}
}
static void mostrarContactos(Hashtable<Integer,Contacto>
lista){
System.out.println("Los contactos son: ");
Enumeration<Integer> claves=lista.keys();
while(claves.hasMoreElements()){
Integer k=claves.nextElement();
Contacto c = lista.get(k);
System.out.println(k.toString()+" - "+c.getNombre()+
" - "+c.getEmail()+" - "+c.getTelefono());
}
}
}

3. SOBRECARGA DE MÉTODOS
La sobrecarga de métodos consiste en la posibilidad de definir más de un método con
el mismo nombre dentro de una clase. La sobrecarga de métodos simplifica la utilización de
la clases por parte de los programadores puesto que permite disponer de distintas versiones de
una operación respetando el mismo nombre de método en todas ellas.

Un ejemplo de sobrecarga sería el de una clase que realizara operaciones matemáticas


en la que la suma de números se pudiera realizar de diferentes formas, por ejemplo, una que lo
hiciera a partir de los parámetros recibidos, otra a partir de los atributos de la clase y otra que
sumara el contenido de un array; las operaciones serían implementadas por tres métodos
diferentes con el mismo nombre pero con diferentes parámetros:

public int suma(int x, int y){


//suma parámetros
}
public int suma(){
//devuelve la suma de los atributos
}
public int suma(int [] numeros){
//suma de los miembros de un array
}
La regla que se debe seguir a la hora de sobrecargar métodos en una clase es
bastante simple y es que los métodos sobrecargados deben diferenciarse en el número de
parámetro y/o el tipo de los mismos, siendo irrelevante el tipo de devolución de los mismos.

Por ejemplo, el siguiente caso no es un ejemplo válido de sobrecarga puesto que,


aunque el tipo de devolución es distinto en los dos métodos, la lista de parámetros es idéntica:

void metodoprueba (int x)


int metodoprueba (int k)
La sobrecarga de métodos es algo muy habitual en las clases del API de Java, tanto
estándar como Enterprise. En la figura 1 vemos por ejemplo la sobrecarga del método
valueOf() en la clase String, existiendo hasta 9 versiones de este método.

Métodos
valueOf()

Figura. 1.

Como vimos en el tema anterior, la sobrecarga no sólo se aplica a métodos; también


podemos sobrecargar constructores en una clase, permitiendo así distintas opciones de
inicialización de objetos.

4. HERENCIA
La herencia es quizá la característica más interesante y potente que ofrecen los
lenguajes orientados a objetos. Mediante ella, es posible crear clases que dispongan de forma
automática de todos los miembros (atributos y métodos) definidos en clases ya existentes.

Esto es particularmente útil en aquellos contextos donde necesitamos utilizar una clase
con los métodos incluidos en otra ya existente, pero a la que queremos añadir una nueva
funcionalidad; en vez de modificar la clase original, emplearemos la herencia para crear una
nueva clase con todos los métodos definidos en la primera y sobre ella incluir los nuevos
elementos que se necesiten.

Como se desprende de lo comentado anteriormente, la herencia representa un


excelente mecanismo de reutilización de código, incorporando en las nuevas clases los
métodos definidos en otras sin tener que reescribirlos de nuevo.

4.1. RELACIÓN DE HERENCIA

La relación de herencia entre dos clases se expresa gráficamente como se indica en la


figura 2, mediante una flecha que sale desde la clase que hereda, conocida también como
subclase, y que apunta a la clase heredada, llamada también superclase.
superclase

subclase

Figura. 2.

Puede darse el caso también que una clase herede a otra clase que a su vez herede a
otra, es lo que se llama la herencia multinivel (figura 3).

ClaseA

ClaseB

ClaseC

Figura. 3.

En este caso, ClaseC heredaría todos los métodos definidos en ClaseA más todos
aquellos que hayan sido definidos en ClaseB.

Aunque hay lenguajes orientados a objetos como C++ donde si es posible utilizarla,
Java no permite la herencia múltiple, esto es, la posibilidad de heredar dos o más clases
diferentes (figura 4).
Clase1 Clase2

ClaseX

Figura. 4.

4.2. EJEMPLOS DE HERENCIA ENTRE CLASES

En la figura 5 tenemos algunos ejemplos de clases relacionadas a través de la


herencia.

Deportista Figura

Tenista Nadador Esfera Cubo

Informático

Programador TecnicoSistemas

Figura. 5.

En todas ellos, las superclases incluirían los miembros (atributos y métodos) generales
comunes a determinadas familias de objetos, añadiendo las subclases los métodos y atributos
específicos de cada tipo particular. Esto se muestra en otro nuevo ejemplo de clases
relacionadas mediante herencia que se muestra en la figura 6, donde la clase Vehiculo incluye
atributos y métodos que son comunes a todo tipo de vehículo, como el color o el numero de
ruedas de este, mientras que Coche añade características que son propias solamente de este
tipo de vehículos, tales como la potencia o las operaciones de acelerado y frenado.
Vehiculo
Atributos:

ƒ numeroRuedas
ƒ color

Métodos:

ƒ arrancar()
ƒ detener()

Coche
Atributos:

ƒ potencia

Métodos:

ƒ acelerar()
ƒ frenar()
ƒ cambiarDireccion()

Figura. 6.

4.3. DEFINICIÓN DE HERENCIA EN JAVA

Cuando creamos un clase Java y queramos que esta clase herede a otra existente,
debemos utilizar la palabra reservada extends en la definición de la misma, indicando a
continuación el nombre de la clase a heredar:

class NuevaClase extends ClaseExistente{

Por ejemplo:

class Coche extends Vehiculo{


:
}

4.4. MIEMBROS PRIVADOS Y PROTEGIDOS

Hay que indicar que a pesar de que la subclase hereda todos los miembros definidos
en la superclase, los miembros que estén definidos dentro de ésta como private no serán
accesibles directamente desde la subclase. Así, si tenemos la siguiente definición de la clase
Vehiculo:

class Vehiculo{
private String color;
:
}

No será posible utilizar directamente el atributo color desde el interior de Coche:

class Coche extends Vehiculo{


public Vehiculo(){
color=”verde”; //error de compilación al
//no reconocer color
}
}

Esto no es ni mucho menos una limitación puesto que, además de los métodos set/get
que suelen proporcionar las clases para el acceso controlado a ciertos de sus atributos, las
superclases pueden realizar de manera indirecta desde su constructor la inicialización de los
atributos privados de la superclase, tal y como veremos en la siguiente sección.

No obstante, es posible definir un atributo o método dentro de una clase de manera que
sea accesible únicamente desde el interior de su clase y de sus subclases. Para ello, se
utilizará el modificador protected en la definición del miembro:

class Vehiculo{
private String color;
protected void detener(){
:
}
:
}

Al definir el método como protected, las subclases tendrán acceso al mismo:

class Coche extends Vehiculo{


public void frenar(){
detener(); //acceso a método protegido
//en la supeclase
:
}
}

Ninguna otra clase que no sea subclase de Vehiculo tendrá acceso al método
protegido, aunque Vehiculo esté declarada como pública o aunque ambas clases estén en el
mismo paquete:

class Prueba{
public void método(){
//la siguiente instrucción compila si Vehiculo
//es visible desde prueba
Vehiculo v = new Vehiculo();
//la siguiente instrucción no compilaría en ningún caso:
v.frenar();
}
}

4.5. EJECUCIÓN DE CONSTRUCTORES EN LA HERENCIA

Cuando se compila una clase Java, el compilador introduce en todos los constructores
de la clase, como primera línea de código, la siguiente instrucción:

super();

Y, ¿qué es lo que hace esta instrucción?. La expresión super() es simplemente una


llamada al constructor sin parámetros de la superclase. Quiere esto decir, que cada vez que
se crea un objeto de una clase, antes de ejecutarse las instrucciones definidas en el
constructor correspondiente, se ejecutará implícitamente el constructor sin parámetros de
la superclase, permitiendo así la inicialización de los atributos definidos en ambas clases.

Por ejemplo, supongamos que tenemos definidas las clases Vehiculo y Coche de la
siguiente manera:

public class Vehiculo{


private String color;
:
public Vehiculo(){
color=”verde”;
}
public String getColor(){
return color;
}
:
}
public class Coche extends Vehiculo{
private int potencia;
public Coche(int potencia){
this.potencia=potencia;
}
public int getPotencia(){
return potencia;
}
:
}

Ahora, supongamos que en otra clase incluimos el siguiente código:

class Prueba{
public static void main(String [] args){
Coche c=new Coche(100);
System.out.println(“color “+c.getColor());
System.out.println(“potencia “+c.getPotencia());

}
}

Al ejecutarse el código anterior se mostrará en pantalla:

color verde

potencia 100

Como vemos, el color mostrado es el que se asigna en el constructor sin parámetros de


la clase Vehiculo, lo que demuestra que este es ejecutado al crear el objeto Coche.

Lo anterior debe ser tenido en cuenta a la hora de crear una clase que herede otra
clase, ya que si la superclase no tiene definido (explícita o implícitamente) un constructor sin
parámetros no será posible crear un objeto de la subclase.

Pero, ¿qué ocurre si en vez de ejecutar el constructor sin parámetros de la superclase


queremos que se ejecute otro diferente? En este caso, será necesario indicar de forma
explícita en el constructor de la subclase qué constructor de la superclase se quiere ejecutar, lo
cual se realiza utilizando la expresión:

super(lista_parametros)

donde lista_parametros representa los parámetros que se van a pasar al constructor


que queremos ejecutar. Por ejemplo, si la clase Vehiculo estuviera definida de la siguiente
manera:

public class Vehiculo{


private String color;
:
public Vehiculo(){
color=”verde”;
}
public Vehiculo(String color){
this.color=color;
}
public String getColor(){
return color;
}
:
}

Podríamos tener un constructor en la clase Coche que permitiera al usuario de la


misma inicializar sus dos datos miembro al crear el objeto, color (dato miembro de la
superclase) y potencia. Dicho constructor se definiría de la siguiente manera:

public Coche(int potencia, String color){


super(color);
this.potencia=potencia;
}

En este caso, el compilador, al detectar la instrucción super(color), no incluirá la


llamada al constructor por defecto. Es importante resaltar, que esta instrucción debe ser la
primera línea de código del constructor de la superclase.

EJERCICIO 2

Vamos a crear en este ejercicio un sencillo programa que opere sobre una cuenta
bancaria de tipo vivienda. El núcleo del programa estará formado por una clase que defina los
atributos y operaciones sobre la cuenta.

En primer lugar definiremos una clase base con los atributos y métodos comunes a
cualquier tipo de clase a la que llamaremos Cuenta. El código de dicha clase se muestra en el
siguiente listado:

package banco;
public class Cuenta {
private float saldo;
private String codigo;
public Cuenta(float saldo, String codigo){
this.saldo=saldo;
this.codigo=codigo;
}
public float getSaldo(){
return saldo;
}
public String getCodigo(){
return codigo;
}
public void ingresar(float c){
saldo+=c;
}
public void extraer(float c){
saldo-=c;
}
}

La clase sobre la que se realizarán las operaciones será una subclase de esta a la que
llamaremos CuentaVivienda. Dicha clase añadirá un atributo llamado limite que represente el
número máximo de años de vida de la cuenta, así como un método llamado ampliarLimite() que
permita ampliar la vida de la cuenta si el saldo está por debajo de una determinada cantidad.

El código de esta clase es el siguiente:


package banco;
public class CuentaVivienda extends Cuenta{
private int limite;
public CuentaVivienda(int limite, float saldo, String
codigo){
super(saldo, codigo);
this.limite=limite;
}
public int getLimite(){
return limite;
}
public void ampliarLimite(int n){
if(getSaldo()<10000){
limite+=n;
}
}
}

El siguiente código corresponde a un ejemplo simple de uso de la clase


CuentaVivienda, donde se pone de manifiesto como el objeto de ésta clase expone tanto los
métodos definidos explícitamente en ella como en la superclase:

import banco.*;
public class Principal {
public static void main(String[] args) {
CuentaVivienda cv=new CuentaVivienda(3, 200, "28900");
cv.ingresar(200);
System.out.println("Saldo después de ingreso: "+
cv.getSaldo());
cv.extraer(100);
System.out.println("Saldo después de extracción: "+
cv.getSaldo());
cv.ampliarLimite(2);
System.out.println("Nuevo límite: "+cv.getLimite());
}
}

4.6. HERENCIA DE LA CLASE OBJECT

La clase java.lang.Object representa el primer nivel de la jerarquía de clases Java.


Toda clase Java, directa o indirectamente, hereda esta clase.
Cuando definimos una clase sin heredar específicamente a ninguna otra, el compilador
Java añade la sentencia extends Object en la cabecera de la declaración de nuestra clase,
forzando de esta manera a que se herede dicha clase.

Así pues, la clase Object proporciona métodos básicos que son aplicables a cualquier
clase, como:

ƒ toString(). Devuelve la representación en forma de cadena de caracteres del


objeto. La implementación por defecto que hace Object de este método
consiste en generar una cadena de caracteres formada por el nombre de la
clase, seguido del carácter “@” y de un número que representa el código hash
del objeto (número que identifica de forma única a cada objeto en ejecución).

ƒ equals(Object ob). Método utilizado para comparar dos objetos. En la


implementación por defecto que hace Object, este método devolverá true si
ambas referencias, sobre la que se aplica el método y la que se pasa como
parámetro, son iguales, es decir, si están apuntando al mismo objeto.

ƒ hashCode(). Este método devuelve el código hash asociado al objeto. El valor


devuelto por este método puede ser cualquier número entero. Lo único que
debe cumplirse siempre es que dos objetos que resulten iguales al
compararlos con equals() deben tener el mismo valor de hashcode.

El objetivo de incluir estos métodos en la clase Object es forzar a que todas las clases
dispongan de estos métodos, pero dado que la versión de estos métodos definida en Object
puede no ser adecuada en toda las clases, será recomendable que cada nueva clase que se
cree sobrescriba estos métodos a fin de adaptarlos a sus requerimientos.

5. SOBRESCRITURA DE MÉTODOS
En determinados contextos de herencia, no todos los métodos heredados por la
subclase se ajustan a los requerimientos de la misma. Puede ocurrir que alguno de los
métodos heredados deba ser redefinido en la nueva clase para poder cumplir mejor con su
funcionalidad.

Por ejemplo, podría ocurrir que las cuentas viviendas tuvieran un saldo máximo
alcanzado el cual no se pudieran realizar más ingresos en las cuentas. En este caso, el método
ingresar(), tal y como está definido en la clase Cuenta, no sería válido para la clase
CuentaVivienda, por lo que tendría que volver a definirse en la misma.

A esta redefinición de métodos heredados en la subclase se conoce como


sobrescritura de métodos y su objetivo es volver a definir en la subclase un método que
ha sido heredado de la superclase, respetando el formato original del mismo. Esto significa
que el nuevo método tiene que tener exactamente el mismo nombre, tipo de devolución y
lista de parámetros definidos en la superclase. Lo único que podrá modificarse es el
modificador de acceso del método, pudiendo ser menos restrictivo que el de la superclase.

Por ejemplo, si en una determinada clase tenemos definido el método:

void ejemplo (int k){...}

una posible superclase podría sobrescribir dicho método con el siguiente formato:

public void ejemplo (int p){...}

Como vemos, la nueva versión del método está definida como public, que es menos
restrictivo que el acceso de paquete (por defecto cuando no se utiliza modificador) definido en
la superclase.
Si la subclase define el método ejemplo() de la siguiente manera:

void ejemplo () {...}

no estaríamos ante un caso de sobrescritura, pues se ha modificado la lista de


parámetros. Sin embargo la inclusión de dicho método en la clase no produciría ningún tipo
de error, ya que se trataría de un caso válido de sobrecarga de métodos.

Es importante no confundir los conceptos de sobrescritura y sobrecarga. Mientras el


primero consiste en definir de nuevo un método heredado en la superclase anulando el
anterior, la sobrecarga se basa en tener más de un método con el mismo nombre dentro de la
clase.

EJERCICIO 3

En este ejercicio vamos a crear una nueva versión de la clase CuentaVivienda


presentada en el ejercicio anterior. En este caso, la clase tendrá una limitación consistente en
limitar el saldo de la cuenta a una cantidad definida por un atributo llamado saldoMaximo.

En este caso, el método ingresar() heredado de la clase Cuenta no será válido, por lo
que tendremos que volver a definirlo para que compruebe el saldo actual antes de realizar el
ingreso.

El código de la nueva versión de la clase se muestra a continuación:

package banco;
public class CuentaVivienda extends Cuenta{
private int limite;
private float saldoMaximo;
public CuentaVivienda(int limite, float saldoMaximo,
float saldo, String codigo){
super(saldo, codigo);
this.limite=limite;
this.saldoMaximo=saldoMaximo;
}
public int getLimite(){
return limite;
}
public void ampliarLimite(int n){
if(getSaldo()<10000){
limite+=n;
}
}
public void ingresar(float cantidad){
if((getSaldo()+cantidad)<=saldoMaximo){
super.ingresar(cantidad);
}
}
}

En el código anterior fijémonos en la instrucción marcada en negrita. En ella vemos un


nuevo uso de la palabra reservada super, empleada en este caso para llamar a la versión del
método ingresar() definida en la superclase, reutilizándose así el código definido por este
método.

Es importante destacar que al sobrescribir el método ingresar() en la clase


CuentaVivienda, la versión heredada del método queda anulada para los objetos de esta
clase, los cuales sólo podrán acceder a la nueva versión del método definida en
CuentaVivienda. El acceso a la versión “antigua” solamente es posible desde el interior de la
subclase.

6. CLASES ABSTRACTAS E INTERFACES

6.1. CLASES ABSTRACTAS

Un clase abstracta es una clase en la que alguno de sus métodos se encuentra sin
definir. Es decir, se ha declarado su existencia pero no se ha realizado su implementación.

Por ejemplo, si tenemos una clase llamada FiguraGeometrica con los miembros
comunes a cualquier figura geométrica, uno de esos miembros podría ser el método
calculoArea() encargado de calcular el área de una figura geométrica. Este método estaría
declarado en la clase FiguraGeometrica porque sabemos que todas las figuras lo deben tener,
pero no podríamos implementarlo porque desconocemos la forma de hacerlo, ya que esto
depende del tipo de figura en cuestión.

A los métodos que se declaran en una clase pero no se implementan se les llama
métodos abstractos y se declaran indicando el modificador abstract delante del tipo de
devolución. Así mismo, cuando una clase contiene un método abstracto deberá ser declarada
también con el modificador abstract. El siguiente código de ejemplo corresponde a una posible
implementación de la clase FiguraGeometrica:

public abstract class FiguraGeometrica{


private int dimension;
public FiguraGeometrica(){
dimension = 2;
}
public FiguraGeometrica (int dimension){
this.dimension= dimension;
}
public int getDimension(){
return dimension;
}
public abstract float calculoArea();
}
Como se puede ver, la declaración del método abstracto finaliza con el carácter “;”.

Dado que las clases abstractas disponen de métodos que no están definidos, no es
posible crear objetos de una clase abstracta. Por ejemplo, la siguiente instrucción no
compilaría:

FiguraGeometrica fg = new FiguraGeometrica(); //error

El objetivo pues de las clases abstractas no es la creación directa de objetos, sino


servir de base a futuras clases que al heredarla sobrescribirán los métodos abstractos
definidos en ella para proporcionarles una funcionalidad.

Así pues, cuando una clase abstracta es heredada por otra clase, la nueva clase está
obligada a sobrescribir todos los métodos abstractos definidos en la clase abstracta, de no
hacerlo tendrá que ser declarada también como abstracta.

Por ejemplo, una posible clase de figura geométrica de tipo esfera podría ser definida a
partir de FiguraGeometrica de la siguiente manera:

public class Esfera extends FiguraGeometrica{


private int radio;
public Esfera(){
super(3);
this.radio=1;
}
public Esfera (int dimension, int radio){
super(dimension);
this.radio=radio;
}
public float calculoArea(){
return (float)(4*Math.PI*radio*radio);
}
}

Vemos como el método calculoArea(), al ser sobrescrito por la clase Esfera y dejar de
ser abstracto ya no tiene que ser declarado con el modificador abstract.

Obsérvese como, a pesar de no poder crear objetos de ella, el constructor de la clase


abstracta sigue cumpliendo su función de inicialización de atributos cuando es creado un objeto
de una de sus subclases.

6.2. INTERFACES

Una interfaz es un conjunto de métodos abstractos. A diferencia de las clases


abstractas que además de métodos abstractos pueden contener métodos normales, atributos y
constructores, las interfaces únicamente pueden contener métodos abstractos.

Su objetivo es simplemente definir como tiene que ser el formato de determinados


métodos que deben existir en determinados tipos de clases.
6.2.1. Definición de una interfaz

La sintaxis para la definición de una interfaz es la siguiente:

interface Nombre_interfaz{

tipo nombre_metodo1(parámetros);

tipo nombre_metodo2(parámetros);

El siguiente ejemplo corresponde a la definición de una interfaz que declara una serie
de métodos para la realización de movimientos:

interface Movimientos{
void giro (int direccion);
void elevacion();
void descenso();
}

Como vemos, no es necesario incluir la palabra abstract en la declaración de los


métodos, como tampoco el modificador public, ya que todos los métodos declarados en la
interfaz son implícitamente públicos.

6.2.2. Implementación de una interfaz

Para obligar a una clase a codificar los métodos de la interfaz respetando el formato
establecido, dicha clase deberá implementar la interfaz, lo cual debe ser indicado durante la
definición de la clase utilizando la palabra implements seguido del nombre de la interfaz:

class NombreClase implements interfaz

Por ejemplo, si la clase Avion quisiera disponer de los métodos de movimiento


definidos en la interfaz Movimiento debería definirse:

class Avion implements Movimientos{


public void giro (int direccion){
//implementación del método giro
}
public void elevacion(){
//implementación del método elevacion
}
public void descenso(){
//implementación del método descenso
}
}

Dado que una interfaz es tan sólo declara una serie de métodos abstractos, es posible
definir una clase que herede otra clase existente y al mismo tiempo implemente una
interfaz:
class Esfera extends FiguraGeometrica implements Movimientos{
:
}

Las interfaces pues nos proporcionan una enorme flexibilidad a la hora de establecer el
formato universal de determinados métodos, pues dicho formato podrá ser seguido no solo por
las clases de una determinada jerarquía, como sucedería en el caso de que los hubiéramos
declarado en una clase abstracta, sino por cualquier clase Java herede a quién herede.

Pero no solamente una clase puede heredar otra clase e implementar una interfaz al
mismo tiempo, también puede implementar cualquier número de interfaces. Si Interfaz1 e
Interfaz2 son los nombres de dos interfaces existentes, para definir una clase que implemente
las dos interfaces sería:

class Nombre_clase implements Interfaz1, Interfaz2{

En estos casos, la clase estaría obligada a implementar los métodos de ambas


interfaces.

6.2.3. Herencia de interfaces

Una interfaz puede heredar otra interfaz.

Para indicar que una interfaz hereda a otra interfaz se utiliza, como en el caso de las
clases, la palabra extends:

interface InterfazA extends InterfazB{


:
}

En este caso, la subinterfaz hereda simplemente la declaración de los métodos


incluidos en la superinterfaz.

Por ejemplo, si la interfaz MovimientoPlano está definida de esta manera:

interface MovimientoPlano{
void giro (int direccion);
}

Y definimos la interfaz MovimientoCompleto como se indica a continuación:

interface MovimientoCompleto extends MovimientoPlano{


void elevacion();
void descenso();
}

Cualquier clase que implemente MovimientoCompleto estará obligada a implementar


los tres métodos: giro(), elevacion() y descenso().
7. POLIMORFISMO
La principal utilidad de las clases abstractas y las interfaces es poder aplicar el
polimorfismo en las aplicaciones, el cual tiene a su vez como objetivo permitir la reutilización de
código.

Básicamente, el polimorfismo consiste en poder utilizar una misma expresión para


invocar a distintas versiones de un mismo método. Para comprender su funcionamiento
supongamos que tenemos una clase abstracta llamada Principal en la que se encuentra
definido un método abstracto llamado metodoEjemplo():

abstract class Principal{


public abstract void metodoEjemplo();
:
}

Además, disponemos de dos clases llamadas Clase1 y Clase2 que heredan Principal y
proporcionan cada una de ellas su propia implementación de metodoEjemplo():

class Clase1 extends Principal{


public void metodoEjemplo(){
//implementación de metodoEjemplo en Clase1
}
}
class Clase2 extends Principal{
public void metodoEjemplo(){
//implementación de metodoEjemplo en Clase2
}
}

Una de los pilares en los que se basa el polimorfismo es en la posibilidad de almacenar


en una variable de una clase cualquier objeto de sus subclases, pudiendo utilizar esta variable
para invocar a cualquiera de los métodos o propiedades del objeto que hayan sido heredados
de la superclase.

En nuestro ejemplo, podríamos utilizar una variable de tipo Principal para almacenar
objetos de las clases Clase1 y Clase2 y utilizar esa variable para invocar a las dos versiones
del método metodoEjemplo() definidas en cada clase:

Principal pr;
pr=new Clase1()
pr.MetodoEjemplo() 'versión del método definida en Clase1
pr=new Clase2()
pr.MetodoEjemplo() 'versión del método definida en Clase2

Como se puede ver, las dos instrucciones utilizadas para llamar a las dos versiones del
método se representan mediante una misma expresión. Es esta precisamente la potencia del
polimorfismo: utilización de una misma expresión para llamar a distintas versiones de un mismo
método.
En el ejemplo anterior sólo se ha aplicado el polimorfismo para invocar a un único
método, por lo que no se aprecia del todo su potencia. Pero imaginemos que además de
metodoEjemplo, la clase Principal proporciona otros dos métodos (abstractos o no) llamados
“metodo1” y “metodo2”. En este caso, y suponiendo que quisiéramos llamar a los tres métodos
para los objetos de las dos subclases de Principal, el código quedaría:

Principal pr;
pr=new Clase1()
pr.MetodoEjemplo()
pr.metodo1();
pr.metodo2();
pr=new Clase2()
pr.MetodoEjemplo()
pr.metodo1();
pr.metodo2();

Según podemos comprobar, las instrucciones marcadas en negrita se repiten dos


veces, una para cada objeto. Esto sugiere que, aplicando el polimorfismo, podríamos reutilizar
el código de llamada a los métodos de manera que se defina una sola vez y pueda ser utilizado
por cualquier objeto de las subclases de Principal:

//utilización del bloque de instrucciones con


//un objeto Clase1
metodoResumen(new Clase1());
//utilización del bloque de instrucciones con
//un objeto Clase2
metodoResumen(new Clase2());
:
void metodoResumen(Principal pr){
pr.MetodoEjemplo()
pr.metodo1();
pr.metodo2();
}

El polimorfismo también se puede aplicar con interfaces. Es decir, en una variable de


tipo interfaz es posible almacenar cualquier objeto de las clases que la implementan para
llamar a las distintas versiones de los métodos de la interfaz implementados en cada clase. Si
Avion y Esfera son dos clases que implementan Movimientos, se podría escribir:

gestionMovimientos (new Esfera(...));


gestionMovimientos (new Avion(...));
:
void gestionMovimientos (Movimientos m){
m.giro(5);
m.elevacion();
m.descenso();
}
EJERCICIO 4

Partiendo de la clase FiguraGeometrica creada en los ejemplos anteriores, vamos a


definir una nueva subclase de ella llamada Cilindro. A continuación, aplicando el polimorfismo,
desarrollaremos un programa que cree un objeto de la clase Esfera y otro de la clase Cilindro y
muestre en pantalla la dimensión y superficie de ambas figuras.

Tanto en ambos tipo de figura, Esfera y Cilindro, se sobrescribirá además el método


toString() para que devuelva un texto con el tipo de figura que representa la clase.

El siguiente listado correspondería a la clase Cilindro:

package figuras;
import static java.lang.Math.*;
public class Cilindro extends FiguraGeometrica{
private int radio;
private int altura;
public Cilindro(){
super(3);
radio=1;
altura=1;
}
public Cilindro(int dimension, int radio, int altura){
super(dimension);
this.radio=radio;
this.altura=altura;
}
public float calculoArea(){
float areaBase=(float)(PI*radio*radio);
float areaCuerpo=(float)(2*PI*radio*altura);
return areaBase+areaCuerpo;
}
public String toString(){
return "figura de tipo Cilindro";
}

Mientras que el de Esfera será:

package figuras;
import static java.lang.Math.*;
public class Esfera extends FiguraGeometrica{
private int radio;
public Esfera(){
super(3);
this.radio=1;
}
public Esfera (int dimension, int radio){
super(dimension);
this.radio=radio;
}
public float calculoArea(){
return (float)(4*PI*radio*radio);
}
public String toString(){
return "figura de tipo esfera";
}
}

Omitimos el código de FiguraGeometrica ya que es el mismo que el indicado en


ejemplos anteriores.

Finalmente, la clase que realiza la instanciación de los objetos y las llamadas a los
métodos de forma polimórfica quedará como se indica a continuación:

import figuras.*;
public class ManejaFiguras {
public static void main(String[] args) {
//crea un objeto Esfera cualquiera
volcarDatos(new Esfera(5,4));
//crea un objeto Cilindro cualquiera
volcarDatos(new Cilindro(3,6,10));
}
//polimorfismo
private static void volcarDatos(FiguraGeometrica fg){
System.out.println(fg.toString());
System.out.println("Dimensión: "+fg.getDimension());
System.out.println("Área: "+fg.calculoArea());
}
}
AUTOEVALUACIÓN

1. Para indicar que una clase debe heredar a otra ya existente se utiliza la palabra
reservada:

A. implements

B. inherits

C. extends

D. inner

2. En el interior de una clase se encuentra definido un método cuyo formato es:

public void test (long f)

Indica cual de los siguientes métodos no podría estar definido también en dicha
clase:

A. int test ()

B. String test (long m)

C. void test()

D. void test (int f)

3. Indica cual de las siguientes afirmaciones sobre la sobrescritura de métodos es


falsa:

A. La nueva versión del método debe tener exactamente los mismos parámetros
que la original

B. La nueva versión del método puede modificar el tipo de devolución

C. La nueva versión del método puede invocar desde su interior al original

D. La nueva versión del método puede tener un modificador de acceso menos


restrictivo que el de la original

4. Supongamos que tenemos las siguientes clases:

class ClaseUno{

public ClaseUno(){

System.out.println(“Primer constructor”);

public ClaseUno (int n){


System.out.println(“Segundo constructor”);

public class ClaseDos extends ClaseUno{

public ClaseDos(){

System.out.println(“constructor uno del objeto”);

public ClaseDos (int p){

System.out.println(“constructor dos del objeto”);

Si desde el método main() de otra clase escribimos la siguiente instrucción:

ClaseDos cd = new ClaseDos(10);

A. Se mostrarán en pantalla:

constructor dos del objeto

Primer constructor

B. Se mostrarán en pantalla:

Segundo constructor

constructor dos del objeto

C. Se mostrarán en pantalla:

Primer constructor

constructor dos del objeto

D. El programa no compilará

5. Supongamos que tenemos las siguientes clases:

class ClaseUno{
public ClaseUno (int n){

System.out.println(“Segundo constructor”);

public class ClaseDos extends ClaseUno{

public ClaseDos(){

System.out.println(“constructor uno del objeto”);

public ClaseDos (int p){

System.out.println(“constructor dos del objeto”);

Si desde el método main de otra clase escribimos la siguiente instrucción:

ClaseDos cd = new ClaseDos(10);

A. Se mostrarán en pantalla:

constructor dos del objeto

Segundo constructor

B. Se mostrarán en pantalla:

Segundo constructor

constructor dos del objeto

C. Se mostrarán en pantalla:

constructor dos del objeto

D. El programa no compilará

6. Indica cual de las siguientes afirmaciones sobre las clases abstractas es correcta

A. Una clase abstracta no puede ser heredada

B. Una clase abstracta no puede tener constructores

C. Una clase abstracta no puede tener atributos


D. No es posible crear objetos de una clase abstracta

7. Si ClaseUno es una superclase de ClaseDos y esta a su vez es superclase de


ClaseTres, indica cual de las siguientes instrucciones es correcta suponiendo que
ninguna de las clases es abstracta:

A. ClaseUno cu = new ClaseTres();

B. ClaseTres ct = new ClaseDos();

C. ClaseDos cd = ClaseTres;

D. ClaseDos cd = new ClaseUno();

8. Si InterfazUno e InterfazDos son dos interfaces independientes una de otra, indica


cual de las siguientes definiciones de InterfazTres es correcta:

A. interface InterfazTres extends InterfazDos, InterfazUno

B. interface InterfazTres extends InterfazDos implements InterfazUno

C. interface InterfazTres implements InterfazDos, InterfazUno

D. interface InterfazTres implements InterfazDos&InterfazUno


Ejercicios Propuestos

1. Desarrollar una nueva versión del programa propuesto en el ejercicio 5 del tema
anterior, encargado de la gestión de una base de datos de productos.

En esta nueva versión, cada producto estará caracterizado por un nombre de


producto, precio, descripción y código. Además, la opción 3, en vez de mostrar el
precio medio de los productos, mostrará la lista de todos los productos
almacenados, indicando para cada uno de ellos su nombre y precio.

2. Crear una clase llamada Empleado que tenga los siguientes miembros:

• Atributos. La clase dispondrá de los siguientes atributos:

¾ antiguedad. Número de años que el empleado lleva en la empresa

¾ codigo. Código identificativo del empleado

¾ cuota_salarial. Valor de tipo float que sirve para calcular el salario mensual
del empleado

• Constructores: Dispondrá de un constructor con tres parámetros para


inicializar los atributos del objeto. Tendrá además otro constructor sin
parámetro que permita inicializar los atributos con valores por defecto
(antigüedad de un año, código igual a xxxx y cuota salarial un valor aleatorio
entre 1 y 50)

• Métodos: Dispondrá del siguiente método:

¾ Salario(). Calcula el salario mensual del empleado según la fórmula:


antiguedad*cuota_salarial

A continuación, crearemos una segunda clase que herede la anterior a la que


llamaremos Gerente, que representará un tipo de empleado especial que además
realiza funciones comerciales. Esta nueva clase dispondrá, además de los
miembros heredados, de un atributo adicional llamado clientes que será de tipo
ArrayList<String> y que contendrá la lista de nombres de todos los clientes del
empleado. Por tanto, tendrá que tener un constructor con cuatro parámetros que
permita inicializar todos sus atributos. Así mismo, se deberá sobrescribir el método
salario() en la nueva clase, pues ahora deberá calcularse según la fórmula:

antiguedad*cuota_salarial+numero_clientes*10

Por último, la nueva clase Gerente dispondrá de un nuevo método llamado


agregar_cliente () que recibirá un String con el nombre del nuevo cliente y lo
añadirá a la lista de clientes.

Se debería crear después una clase con un método main() que probara el
funcionamiento de la clase Gerente, creando un objeto de esta, añadiéndole
clientes y mostrando el salario asociado al Gerente.

3. Desarrollar una interfaz llamada Dibujo que disponga de un único método llamado
pintar() que no reciba ningún parámetro y su tipo de devolución sea void. A
continuación, crearemos dos clases una llamada Punto que encapsule las
coordenadas de un punto de tres dimensiones y otra llamada Linea caracterizada
por una longitud y un ángulo de inclinación. Ambas clases implementarán la
interfaz Dibujo, proporcionando cada una de ellas su versión del método pintar()
que en definitiva tiene como misión mostrar en pantalla los valores de los datos
miembro del objeto.

Anda mungkin juga menyukai