Programación avanzada de clases

Índice

1. Wrappers

La diferencia entre un tipo primitivo y un wrapper es que éste último es una clase. Si usamos wrappers, estamos trabajando con objetos y con los tipos no usamos objetos.

El uso de wrappers en lugar de tipos primitivos aporta ventajas, pero también puede darnos el problema que al pasar una variable a un método como argumento si es de un tipo primitivo se le pasa por valor y si se pasa como wrapper (objeto) se lo pasamos por referencia.

Una de las ventajas que tienen los wrappers es la facilidad de conversión entre tipos primitivos y cadenas de caracteres en ambos sentidos. Existen wrappers de todos los tipos primitivos numéricos., veámoslos en la tabla siguiente:

Tipo primitivo WRAPPER asociado
byte Byte
short Short
int Integer
long Long
double Double
char Character
boolean Boolean

1.1. Clase wrapper Integer

La clase Wrapper Integer tiene dos constructores:

  • Integer(int).
  • Integer(String).

Veamos un resumen de algunos de los métodos del wrapper Integer:

Contructores:

  • Integer(int)
  • Integer(String)

Funciones de conversión con datos primitivos:

  • byteValue()
  • shortValue()
  • intValue()
  • longValue()
  • doubleValue()
  • floatValue()

Conversiones a String

  • Integer decode (String)
  • Integer parseInt (String)
  • Integer parseInt (String, int)
  • Integer valueOf (String)
  • String toString ( )

Conversiones a otros sistemas de numeración:

  • String toBinaryString (int)
  • String toHexString (int)
  • String toOctalString (int)

Constantes:

  • MAX_VALUE
  • MIN_VALUE La creación de un objeto Integer es la misma que para cualquier tipo de objeto:
1
2
Integer i2 = Integer.valueOf("7");
Integer i1 = new Integer(5);

Veamos un programa que hace uso de algunos de los métodos anteriores:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Test {
	public static void main ( String [ ] args) {
		Integer i1 = new Integer(5);
		Integer i2 = new Integer("7");
		String s1 = i1.toString();
		System.out.println(s1); //muestra 5 por pantalla
		int i3 = Integer.parseInt("10", 10);
		int i4 = Integer.parseInt("10", 8);
		int i5 = Integer.parseInt("BABA", 16);
		System.out.println(i3); //muestra 10 por pantalla
		System.out.println(i4); //muestra 8 por pantalla
		System.out.println(i5); //muestra 47.802 por pantalla
		System.out.println(Integer.toOctalString(i4)); //muestra 10 por pantalla
		System.out.println(Integer.toHexString(i5)); //muestra baba por pantalla
		int i6 = Integer.valueOf("22").intValue( );
		System.out.println(i6); //muestra 22 por pantalla
	}
}

Los wrappers para los demás tipos primitivos tienen una funcionalidad y modo de utilización similar al wrapper Integer. Podéis verlos en:

http://docs.oracle.com/javase/8/docs/api/java/lang/Boolean.html

http://docs.oracle.com/javase/8/docs/api/java/lang/Byte.html

http://docs.oracle.com/javase/8/docs/api/java/lang/Character.html

http://docs.oracle.com/javase/8/docs/api/java/lang/Double.html

http://docs.oracle.com/javase/8/docs/api/java/lang/Float.html

http://docs.oracle.com/javase/8/docs/api/java/lang/Short.html

http://docs.oracle.com/javase/8/docs/api/java/lang/Long.html

Todas las clases las tenéis en:

http://docs.oracle.com/javase/8/docs/api/allclasses-noframe.html

2. Trabajando con fechas y horas (clase Date)

http://docs.oracle.com/javase/8/docs/api/java/sql/Date.html

La clase Date es una utilidad contenida en el paquete java.util. Con la clase Date podemos representar un instante dado con precisión de milisegundos. La fecha y hora se almacena en un entero de tipo Long que registra los milisegundos transcurridos desde el 1 de enero de 1970 GMT (Tiempo del Meridiano de Greenwich) a las 00:00:00 horas.

Para la utilización de fechas se suele trabajar con otro tipo de clases, como la clase GregorianCalendar, que deriva de la clase abstracta Calendar. Como Calendar es abstracta, cuando queramos utilizar variables de tipo fecha en un programa lo haremos a través de objetos de la clase GregorianCalendar.

En la clase GregorianCalendar las horas se representan como un número entre 0 y 23, los días entre 1y 31, los meses entre 0 y 11 y los años con cuatro dígitos. En esta clase hay muchas variables enteras como las siguientes: DAY_OF_WEEK, DAY_OF_MONTH, YEAR, MONTH, HOUR, MINUTE, SECOND, MILLISECOND, WEEK_OF_MONTH, WEEK_OF_YEAR, etc.

Veamos un ejemplo de uso de la clase Date:

1
2
3
4
5
6
7
8
9
10
11
12
13
import java.util.*;
class Fecha {
	public static void main(String[] args) {
		Date d = new Date();
		GregorianCalendar c = new GregorianCalendar();
		c.setTime(d);
		System.out.print("La fecha actual es: "+c.get(Calendar.DAY_OF_MONTH));
		System.out.print(" – ");
		System.out.print(c.get(Calendar.MONTH) + 1);
		System.out.print(" - ");
		System.out.print(c.get(Calendar.YEAR));
	}
}

Este programa muestra por pantalla la fecha actual.

3. Clases y métodos abstractos y finales

La abstracción es una de las características de la POO. Mediante la abstracción lo que se hace es extraer la esencia básica y su comportamiento para luego representarla en un lenguaje de programación. abstract en Java es lo mismo que genérico.

3.1. Clases y métodos abstractos

Las clases abstractas han sido pensadas para ser genéricas, es decir, no va a haber objetos de esas clases, se generarán subclases de la genérica que si tendrán objetos.

Por ejemplo, la clase vehículo es una clase genérica porque cuando implemente un programa con esta clase no se crearán vehículos sino objetos de la clase coche u objetos de la clase moto, etc. Todos son vehículos y por tanto esa clase abstracta solamente definirá atributos y métodos comunes a todos los vehículos (por ejemplo: color, peso, matrícula, getVelocidadActual( ), etc.). Por ejemplo:

1
2
3
4
5
6
7
8
9
public static class Vehiculo {
	private int peso;

	public void setPeso(int p){
      peso=p;
    }

	public abstract int getVelocidadActual();
}

Como se ve en el ejemplo, una clase abstracta puede implementar métodos abstractos y no abstractos.

Debemos recordar que:

  • De las clases abstractas no se pueden crear objetos.
  • Si una clase tiene métodos abstract por fuerza será una clase abstracta.
  • Un método abstract no puede ser static.
  • Las subclases de la clase abstracta tendrán que redefinir esos métodos o bien declararlos como abstract.

3.2. Objetos, clases y métodos finales

Objetos finales

Cuando un objeto se declara como final, éste impedirá que haya otro objeto con la misma referencia. Por ejemplo cuando hagamos:

1
2
3
	final Cuadrado c1=new Cuadrado(5);
	Cuadrado c2=new Cuadrado(15);
	c1=c2;

El compilador mostrará un mensaje de error en la tercera línea similar a:

1
2
Exception in thread "main" java.lang.Error: Problema de compilación no resuelto:
La variable local final c1 no puede asignarse. Debe estar en blanco y no utilizar una asignación compuesta

Métodos finales

Cuando declaramos un método como final, le estamos diciendo al compilador que ese método no va a cambiar, no va a ser sobrescrito, con lo cual el compilador puede colocar el bytecode del método en el sitio del programa donde va a ser invocado con la consiguiente ganancia de eficiencia.

1
2
3
public final void setColor (String s) { 
	color = s;
}

Clases finales

Cuando una clase se declara como final, esa clase no puede tener subclases. Por ejemplo, la siguiente clase triángulo no podrá tener subclases que deriven de ella.

1
2
3
4
5
public final class Triangulo {

	//.....

}

4. Polimorfismo

El polimorfismo en POO permite abstraer y programar de forma general agrupando objetos con características comunes y jerarquizándolos en clases. Existe una clase padre de todas las demás y ésta es java.lang.Object. Cualquier clase creada descenderá de la clase Object.

El polimorfismo en Java se consigue mediante clases abstractas y las interfaces. Concretamente las interfaces amplían enormemente las posibilidades del polimorfismo.

Un aspecto muy importante del polimorfismo es cuando se crea una referencia a un objeto de una clase base, esa misma referencia puede servir para referenciar a objetos de clases derivadas.

Por ejemplo, en el siguiente árbol jerárquico, tenemos la clase Persona de la cual desciende la clase Empleado. La clase Persona tendrá métodos genéricos que puedan ser utilizados por cualquier persona como por ejemplo establecer y devolver el nombre.

La clase Empleado tendrá otro tipo de métodos más específicos como obtenerSueldo, el cual devolverá el sueldo base así como setSueldo, que establecerá el sueldo base del empleado.

Los encargados son personas con responsabilidades en la empresa y sea cual sea su trabajo cobrarán un 10% más que un empleado normal.

La implementación de la jerarquía anterior será la siguiente:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class Persona {
	private String nombre;
	public void setNombre (String nombre){ 
		this.nombre = nombre; 
	}
	public String getNombre(){
		return nombre;
	}
} 

public class Empleado extends Persona{
	protected int sueldoBase;
	public int getSueldo(){
		return sueldoBase;
	}
	public void setSueldoBase(int s){
		sueldoBase = s;
	}
}
public class Encargado extends Empleado {
	protected String puesto;
	public int getSueldo(){
		Double d = new Double (sueldoBase * 1.1);
		return d.intValue();
	}
	public void setPuesto(String p){
		puesto= p;
	}
	public String getPuesto(){
		return puesto;
	}
}

Nota El modificador de acceso protected es una combinación de los accesos que proporcionan los modificadores public y private. protected proporciona acceso público para las clases derivadas y acceso privado (prohibido) para el resto de clases. Es decir la clase Empleado puede acceder al atributo sueldoBase de Empleado por ser protected , pero si fuera private no.

Imaginemos que en la clase Test realizamos el método main siguiente:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Test {
	public static void main(String[ ] args) {
		Persona p1;
		p1 = new Empleado();
		p1.setNombre("Isaac Sanchez");
		p1.setSueldoBase(1000); //dará error al compilar

		Empleado e1;
		e1 = new Encargado();
		e1.setSueldoBase(1500);
		e1.setPuesto("Jefe almacén"); //dará error al compilar
		System.out.println(e1.getSueldo());
		System.out.println(e1.getPuesto());
	}
}

Vamos a comentar el código de la clase anterior:

1
2
3
4
	Persona p1;
	p1 = new Empleado();
	p1.setNombre("Isaac Sanchez");
	p1.setSueldoBase(1000); //dará error al compilar

En este código creamos una referencia Persona que apunta a un objeto de la clase Empleado. La variable p1 podrá hacer llamadas a todos los métodos de la clase Persona pero no de la clase Empleado , por lo tanto la llamada al método setSueldoBase dará un error de compilación.

1
2
3
4
5
	Empleado e1;
	e1 = new Encargado();
	p1.setSueldoBase(1500);
	e1.setPuesto("Jefe almacén"); //dará error al compilar
	System.out.println(e1.getSueldo());

Por otra parte, el código anterior crea una referencia Empleado que apunta a un objeto de la clase Encargado. La llamada al método setPuesto dará error por lo explicado anteriormente pero no así la llamada al método getSueldo. La pregunta es la siguiente ¿qué mostrará el programa 1500 ó 1650?.

La solución es 1650, pero getSueldo() se creó para que se pueda rectificar el código (ya veremos luego cómo). Aunque la referencia se creó para la clase Empleado y solamente se pueden llamar a métodos de dicha clase, el método getSueldo() está sobrescrito y como e1 apunta a un objeto de la clase Encargado Java resuelve que tiene que ejecutar el método de dicha clase.

La sobreescritura de métodos la veremos en detalle en el punto siguiente.

Veamos más en detalle por qué el programa muestra 1650 y no 1500 por la salida estándar.

En Java existen dos tipos de vinculaciones (llamada realizada a un método y el código que se va a ejecutar en dicha llamada):

  • Vinculación temprana. Se realiza en tiempo de compilación. Con métodos normales o sobrecargado Java utiliza la vinculación temprana.
  • Vinculación tardía. Se realiza en tiempo de ejecución. Cuando se redefinen los métodos se realiza dicha vinculación (salvo en métodos definidos como final).

En nuestro caso se ha declarado el método getSueldo() en la clase Empleado (clase padre) y se ha sobrescrito en la clase derivada Encargado (clase hija). Al llamar a este método en ejecución, el tipo del objeto al que apunta la variable es prioritario al tipo de la referencia. Es en ejecución cuando se comprueba que aunque la referencia es de tipo Empleado , la variable e apunta a un objeto de tipo Encargado y el método de la clase es el que se va a ejecutar.

El polimorfismo tiene muchas ventajas para el lenguaje, pero también alguna limitación, y es el tipo de la referencia la que limita los métodos que podemos ejecutar (la llamada al método setPuesto dará error) o las variables miembro accesibles.

Para hacer que nuestro ejemplo funcione, debemos utilizar un cast explícito (obligar al compilador a transformar obligatoriamente el objeto Persona en Empleado y el objeto Empleado en Encargado). El cast lo veremos más adelante, de momento subsanamos los errores del programa ejemplo sustituyendo las líneas que dan problema por otras:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Test {
	public static void main(String[ ] args) {
		Persona p1;
		p1 = new Empleado();
		p1.setNombre("Isaac Sanchez");
		((Empleado)p1).setSueldoBase(1000); //línea corregida

		Empleado e1;
		e1 = new Encargado();
		e1.setSueldoBase(1500);
		((Encargado)e1).setPuesto("Jefe almacén"); //línea corregida
		System.out.println(e1.getSueldo( ));
		System.out.println(((Encargado)e1).getPuesto());
	}
}

5. Sobreescritura de métodos (overriding)

Una propiedad fundamental de los lenguajes OO es la sobreescritura o overriding de métodos. La sobreescritura permite modificar el comportamiento de la clase padre (también llamada clase principal o superclase).

Para que dicho método con diferente funcionalidad sea sobrescrito, deberá cumplir los siguientes preceptos:

  1. Debe tener el mismo nombre (esto es obvio).
  2. El retorno de la clase padre e hijo deberá ser del mismo tipo.
  3. Deberá de conservar la misma lista de argumentos que el mismo método en la clase padre.

Un ejemplo de sobrecarga de métodos sería el siguiente:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package sobreescribe;
	public class Pajaro {
		protected String nombre;
		protected String color;

	public String getDetalles( ) {
			return "Nombre: " + nombre + "\n" + "Color: " + color;
		}
	}

package sobreescribe;
	public class Loro extends Pajaro {
		protected String pedigri;

		public String getDetalles( ) {
		return "Nombre: " + nombre + "\n" + "Color: " + color +"\n" + "Pedigrí:" + pedigri;
		}
	}

Según el ejemplo anterior podemos observar lo siguiente:

  • La clase Loro desciende de la clase Pájaro.
  • La clase Loro sobrescribe el método getDetalles(), ambas con el mismo nombre.
  • El método getDetalles(), de la clase padre e hija tienen la misma lista de argumentos.
  • El método getDetalles( ), de la clase padre e hija devuelven un mismo tipo (objeto String).
  • El método getDetalles( ), de la clase padre e hija tienen el mismo modificador de acceso (public).

6. Sobrecarga de métodos (overloading)

La sobrecarga es la implementación varias veces del mismo método con ligeras diferencias adaptadas a las distintas necesidades de dicho método.

Para crear métodos sobrecargados debemos crear métodos con el mismo nombre pero con distinta lista de parámetros. A continuación se enumeran las reglas para sobrescribir un método:

  • Los métodos sobrecargados deben de cambiar la lista de argumentos obligatoriamente.
  • Un método puede estar sobrecargado en la clase o en una subclase.
  • Al sobrecargar un método se puede utilizar las mismas excepciones o añadir algunas.
  • Los métodos sobrecargados pueden cambiar el tipo de retorno o el modificador de acceso.

Ejemplo: Tenemos una clase Persona que almacena datos como el nombre, teléfono, dirección, etc. Tenemos que almacenar el primer y el segundo apellido por separado de todos los individuos. El problema está cuando aparece un inglés o un italiano que no tienen o no usan el segundo apellido. Es una buena ocasión para utilizar un método sobrecargado de la siguiente manera:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Persona {
	private boolean sinSegundo = false;
	private String nombre;
	private String apellido1;
	private String apellido2;
	public void setNombre(String nom, String ape1, String ape2){
		nombre = nom;
		apellido1 = ape1;
		apellido2 = ape2;
	}
	public void setNombre(String nom, String ape1) {
		nombre = nom;
		apellido1 = ape1;
		sinSegundo = true;
	}
}

El código anterior cumple con todas las reglas enumeradas anteriormente.

7. Conversiones entre objetos (casting)

El casting es la conversión de un objeto o tipo en otro. Para poder convertir un objeto en otro debe haber entre ambos una relación de herencia (uno debe ser una subclase del otro). Dado que la subclase contiene toda la información de la superclase es lógico pensar que el casting sea posible.

Supongamos la jerarquía de clases vista en el punto 4:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class Persona {
	private String nombre;
	public void setNombre(String nom){
		nombre = nom;
	}
	public String getNombre(){
		return nombre;
	}
}
public class Empleado extends Persona {
	protected int sueldoBase;
	public int getSueldo(){
		return sueldoBase;
	}
	public void setSueldoBase(int s){
		sueldoBase = s; 
	}
}
public class Encargado extends Empleado {
	protected String puesto;
	public int getSueldo(){
		Double d = new Double(sueldoBase*1.1);
		return d.intValue();
	}
	public void setPuesto(String p){
		puesto= p;
	}
	public String getPuesto(){
		return puesto;
	}
}

Si tenemos un método m() que espera un argumento de tipo Empleado, podemos pasarle un objeto de tipo Persona (caso 1) o un objeto de tipo Encargado (caso 2).

Caso 1: Si le pasamos un objeto de tipo Persona nos encontramos con una pérdida de precisión ya que no se pueden ejecutar todos los métodos de un objeto tipo Empleado (Persona contiene menos métodos que Empleado).

En este caso es necesario hacer un casting , sino el compilador dará error.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static void m(Empleado) {
	System.out.println(e.getNombre());
}
public class Test {
	public static void main(String[] args){
      Persona p1 = new Empleado();
      p1.setNombre("Isaac Sanchez");
      Encargado en1 = new Encargado();
      en1.setNombre("Andrés Rosique");

      m(en1);
      // m(p1); //dará error al compilar, llamada sin casting
      m((Empleado)p1);
      p2.setNombre("Juan Serrano");
      // m((Empleado)p2); //dará error en ejecución
	}
}

Como se puede observar tenemos un método m( ) que espera como parámetro un objeto de tipo Empleado. Primero hacemos que la referencia p1 a Persona apunte al objeto de tipo Empleado. Una vez hecho esto no tendremos problemas para utilizar esta referencia en el método. Para ejecutar el método tendremos que hacer explícitamente un casting como se puede ver en la siguiente línea de código:

1
m((Empleado)p1);

Si la referencia p1 apuntase a un objeto de tipo Persona, en ese caso utilizando la siguiente línea de código:

1
Persona p1 = new Persona( );

Java dará un error en ejecución, lanzará una excepción del tipo ClassCastException y avisará que no es posible realizar el casting “Persona cannot be cast to Empleado”.

Caso 2: Pasamos un objeto de tipo Encargado. En este caso al ser una subclase no tendremos problemas. Los errores se producen cuando se llama a métodos que el objeto destino no tiene.

Resumiendo:

1
2
3
4
Empleado emp = new Empleado( );
Encargado enc = new Encargado( );
emp = enc; //no necesita casting
enc = (Encargado)emp; //necesita casting explícito

Las reglas seguidas en el ejemplo anterior son las siguientes:

  • Cuando se utiliza una clase más específica (más abajo en la jerarquía) NO hace falta casting.
  • Cuando se utiliza una clase menos específica (más arriba en la jerarquía) SI hay que hacer un casting explícitamente.

8. Acceso a métodos de la superclase

Para acceder a métodos de la superclase se utilizará la palabra reservada super (palabra reservada disponible en cualquier método no estático de una subclase).

Es importante tener en cuenta que super es una referencia al objeto actual teniendo en cuenta la instancia de su superclase.

Veamos el siguiente código:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class Padre {
	protected int dato;
	public void m() {
		System.out.println("Método clase padre");
	}
}

class Hijo extends Padre {
	private int dato;
	public void m(){
		System.out.println("Método clase hijo");
		super.dato = 10;
		dato = 20;
	}
	public void getDato(){
		System.out.println(super.dato);
	}
	public void mostrar ( ) {
		this.m();
		m();
		super.m();
	}
}

class Test {
	public static void main(String[] args) {
		Hijo h = new hijo();
		h.mostrar();
		h.getDato();
	}
}

El resultado en pantalla de ejecutar este código será el siguiente:

1
2
3
4
Método clase hijo
Método clase hijo
Método clase padre
10

De este código extraemos lo siguiente:

  • Para acceder a métodos sobrescritos de la superclase usamos la palabra super.
  • Es posible acceder a miembros protected de la superclase usando la palabra super.

Debemos recordar que this se usa para acceder a campos y métodos de la clase y super para la superclase, no importa que estos estén sobrescritos.

Cambiemos un poco el código anterior para ver como lo visto anteriormente con super y this es aplicable en las llamadas a los constructores de los objetos.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
class Padre {
	protected int dato1, dato2;
	Padre(int x, int y){
		dato1 = x;
		dato2 = y;
	}
	Padre(){ 
		this(5,5); 
	}
}

class Hijo extends Padre {
	private int dato1, dato2;
	Hijo (int x, int y) {
		super(2,2);
		dato1 = x;
		dato2 = y;
	}
	Hijo() {
		dato1 = 3;
		dato2 = 3;
	}
	public void getDato ( ) {
		System.out.println("Padre dato1: " + super.dato1);
		System.out.println("Padre dato2: " + super.dato2);
		System.out.println("Hijo dato1: " + this.dato1);
		System.out.println("Hijo dato2: " + this.dato2);
	}
}

class Test {

	public static void main(String[] args) {
		Hijo h1 = new Hijo(1, 1);
		h1.getDato();
		Hijo h2 = new Hijo();
		h2.getDato();
	}
}

El resultado en pantalla de ejecutar este código será el siguiente:

1
2
3
4
5
6
7
8
Padre dato1: 2
Padre dato2: 2
Hijo dato1: 1
Hijo dato2: 1
Padre dato1: 5
Padre dato2: 5
Hijo dato1: 3
Hijo dato2: 3

Obsérvese cómo se usa this y super en las llamadas a los constructores.

9. Clases anidadas

Una clase anidada es una clase que es miembro de otra clase. La definición de una clase anidada sería la siguiente:

1
2
3
4
5
6
7
class Externa {
	// ....
	class Anidada {
		// ....
	}
	// ....
}

La clase anidada, al ser miembro de la clase externa, tendrá acceso a todos sus métodos y atributos (incluso los privados). Y como es lógico, al ser un miembro de la clase externa, la clase anidada podrá ser private, public, protected o privada al paquete.

Tipos de clases anidadas:

  • Estáticas también llamadas clases estáticas anidadas.
  • No estáticas. Clases internas.
1
2
3
4
5
6
7
8
9
10
11
12
class Externa {
	// ....


	static class EstaticaAnidada {
		// ....
	}
	class Interna {
		//....
	}
	// ....
}

Para instanciar una clase interna se seguirá el siguiente formato:

1
Externa.Interna objetoInterno = objetoExterno.new Interna();

Primero hay que instanciar la clase externa para luego instanciar la clase interna dentro del objeto externo.

¿Cuándo utilizar las clases anidadas?

Cuando la clase se va a utilizar en un solo lugar, en ese caso definir la clase como anidada puede hacer que el código sea más legible y su mantenimiento sea más sencillo. También incrementa la encapsulación dado que la clase anidada solo se necesita en la clase externa y de esta manera se mantienen juntas.

10. Interfaces

Un interface es una colección de declaraciones de métodos (sin definirlos) y también puede incluir constantes.

Runnable es un ejemplo de interface en el cual se declara, pero no se implemementa, una función miembro run().

1
2
3
public interface Runnable {
	public abstract void run;
}

Las clases que implementen (implements) el interface Runnable han de definir obligatoriamente la función run().

1
2
3
4
5
6
7
8
9
10
11
public class HelloRunnable implements Runnable {

    public void run() {
        System.out.println("Hello from a thread!");
    }

    public static void main(String args[]) {
        (new Thread(new HelloRunnable())).start();
    }

}

El papel del interface es el de describir algunas de las características de una clase. Por ejemplo, el hecho de que una persona sea un futbolista no define su personalidad completa, pero hace que tenga ciertas características que las distinguen de otras.

Clases que no están relacionadas pueden implementar el interface Runnable, por ejemplo, una clase que describa una animación, y también puede implementar el interface Runnable una clase que realice un cálculo intensivo.

Diferencias entre un interface y una clase abstracta

Un interface es simplemente una lista de métodos no implementados, además puede incluir la declaración de constantes. Una clase abstracta puede incluir métodos implementados y no implementados o abstractos, miembros dato constantes y otros no constantes.

Ahora bien, la diferencia es mucho más profunda. Una clase solamente puede derivar extends de una clase base, pero puede implementar varios interfaces. Los nombres de los interfaces se colocan separados por una coma después de la palabra reservada implements.

El lenguaje Java no fuerza por tanto, una relación jerárquica, simplemente permite que clases no relacionadas puedan tener algunas características de su comportamiento similares, como vemos en la siguiente sección.

Herencia simple

Creamos una clase abstracta denominada Animal de la cual deriva las clases Gato y Perro. Ambas clases redefinen la función habla declarada abstracta en la clase base Animal.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public abstract class Animal {
    public abstract void habla();
}

class Perro extends Animal{
    public void habla(){
        System.out.println("¡Guau!");
    }
}

class Gato extends Animal{
    public void habla(){
        System.out.println("¡Miau!");
    }
}

El polimorfismo nos permite pasar la referencia a un objeto de la clase Gato a una función hazleHablar que conoce al objeto por su clase base Animal

1
2
3
4
5
6
7
8
9
10
public class PoliApp {
  public static void main(String[] args) {
    Gato gato=new Gato();
    hazleHablar(gato);
 }

  static void hazleHablar(Animal sujeto){
    sujeto.habla();
  }
}

El compilador no sabe exactamente que objeto se le pasará a la función hazleHablar() en el momento de la ejecución del programa. Si se pasa un objeto de la clase Gato se imprimirá ¡Miau!, si se pasa un objeto de la clase Perro se imprimirá ¡Guau!. El compilador solamente sabe que se le pasará un objeto de alguna clase derivada de Animal. Por tanto, el compilador no sabe que función habla será llamada en el momento de la ejecución del programa.

El polimorfismo nos ayuda a hacer el programa más flexible, porque en el futuro podemos añadir nuevas clases derivadas de Animal, sin que cambie para nada elmétodo hazleHablar(). Por ejemplo, podemos crear más tarde la clase Pájaro y que el método hazleHablar() imprima ¡pío, pío, pío …!

1
2
3
4
5
class Pajaro extends Animal{
    public void habla(){
        System.out.println("¡pío, pío, pío ...!");
    }
}

Interfaces

Vamos a crear un interface denominado Parlanchín que contenga la declaración de una función denominada habla().

1
2
3
public interface Parlanchin {
    public abstract void habla();
}

Hacemos que la jeraquía de clases que deriva de Animal implemente el interface Parlanchin:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public abstract class Animal implements Parlanchin{
    public abstract void habla();
}

class Perro extends Animal{
    public void habla(){
        System.out.println("¡Guau!");
    }
}

class Gato extends Animal{
    public void habla(){
        System.out.println("¡Miau!");
    }
}

Ahora veamos otra jerarquía de clases completamente distinta, la que deriva de la clase base Reloj. Una de las clases de dicha jerarquía, Cucu, implementa el interface Parlanchin y por tanto, debe de definir obligatoriamente la función habla() declarada en dicho interface.

1
2
public abstract class Reloj {
}
1
2
3
4
5
class Cucu extends Reloj implements Parlanchin{
    public void habla(){
        System.out.println("¡Cucu, cucu, ..!");
    }
}

Definamos la función hazleHablar() de modo que conozca al objeto que se le pasa no por una clase base, sino por el interface Parlanchin. A dicha función le podemos pasar cualquier objeto que implemente el interface Parlanchin, este o no en la misma jerarquía de clases.

1
2
3
4
5
6
7
8
9
10
11
12
13
public class PoliApp {

  public static void main(String[] args) {
    Gato gato=new Gato();
    hazleHablar(gato);
    Cucu cucu=new Cucu();
    hazleHablar(cucu);
  }
  
  static void hazleHablar(Parlanchin sujeto){
    sujeto.habla();
  }
}

Al ejecutar el programa, veremos que se imprime en la consola ¡Miau!, por que a la función hazleHablar() se le pasa un objeto de la clase Gato, y después ¡Cucu, cucu, ..! por que a la función hazleHablar() se le pasa un objeto de la clase Cucu.

Si solamente hubiese herencia simple, Cucu tendría que derivar de la clase Animal (lo que no es lógico) o bien no se podría pasar a la función hazleHablar(). Con interfaces, cualquier clase en cualquier familia puede implementar el interface Parlanchin, y se podrá pasar un objeto de dicha clase a la función hazleHablar(). Esta es la razón por la cual los interfaces proporcionan más polimorfismo que el que se puede obtener de una simple jerarquía de clases.


Adaptado del siguiente material: