⚕️Interfaces¶
❇️ Definición¶
Una interfaz es una clase que define métodos pero no los implementa. La idea es proporcionar un comportamiento común que pueda ser utilizado por varias clases que implemente una interfaz. No se pueden instanciar.
Te permiten definir tipos cuyos comportamientos pueden ser compartidos por varias clases que no están relacionadas, con el fin de crear instancias que se adapten a un dominio específico.
Las interfaces juegan un papel fundamental en la creación de aplicaciones Java ya que permiten interactuar a objetos no relacionados entre sí. Utilizando interfaces es posible que clases no relacionadas, situadas en distintas jerarquías de clases sin relaciones de herencia, tengan comportamientos comunes
Una interfaz es una lista de constantes y signaturas de métodos. Los métodos no están implementados en la interfaz (no hay cuerpo de método).
❇️ ¿Por qué se utilizan las interfaces?¶
Hay principalmente tres razones para usar la interfaz.
- Para lograr la abstracción.
- Dan algunas ventajas de herencia múltiple, sin las desventajas de la herencia.
- Para obtener un mayor desacoplamiento del código.
Las interfaces son muy usadas, de hecho, muchas de las librerías de Java hacen un uso extensivo de las interfaces.
Sabemos que Java tiene herencia única, es decir, una clase hija hereda solo de una clase padre. Esto, por lo general, es suficiente para codificar nuestras aplicaciones. Aunque a veces sería conveniente la herencia múltiple, donde una clase hija pudiera heredar características de varias clases padres. Pero esto puede llegar a ser confuso. ¿Qué sucede cuando dos padres tienen diferentes versiones del mismo método?
Una interfaz describe aspectos de una clase distintos de los que hereda de su padre. Una interfaz es un conjunto de requisitos que la clase debe implementar.
❇️ Interfaz vs Herencia¶
Una clase puede extender de una clase padre para heredar los métodos y las variables de instancia de ese padre.
Una clase también puede implementar una interfaz al incluir métodos y constantes adicionales. Sin embargo, los métodos en la interfaz deben escribirse explícitamente como parte de la definición de la clase. La interfaz es una lista de requisitos que debe incluir la definición de clase (a través de código explícito, no a través de herencia).
Por ejemplo, una clase Coche
podría extender de la clase Vehiculo
. La herencia le da todos los métodos y variables de instancia. Pero si Coche
también implementa la interfaz Impuestos
, entonces su definición debe contener código para TODOS los métodos enumerados en Impuestos
.
Los nombres de las interfaces suelen acabar en able aunque no es necesario: configurable,
arrancable, dibujable, etc
.
Una clase extiende de un solo padre, pero puede implementar varias interfaces.
❇️ Cómo crear una interfaz¶
Para crear una interfaz en IntelliJ, haremos lo siguiente:
- Botón derecho en el paquete de nuestra aplicación → New ---> Java class y seleccionamos Interface.
En Java, los nombres de las interfaces, por lo general, deberían ser adjetivos o nombres que describen el concepto abstracto que representa la interfaz. La primera letra de cada palabra separada en mayúscula. En algunos casos, las interfaces también pueden ser sustantivos cuando presentan una familia de clases, p. List
o Map
.
Una vez creada la interfaz definiremos los métodos que desarrollarán las clases que implementen esta interfaz teniendo en cuenta que, el compilador de Java agrega las palabras clave:
public abstract
cuando se define un método, por lo que se puede omitir en los encabezados de los métodos.public static final
en el caso de las constantes.
Warning
Los métodos abstractos NO pueden ser PRIVATE ni PROTECTED.
Se estructura de forma que primero se sitúan las constantes y luego los métodos.
Si ponemos public
IntelliJ nos avisa:
❇️ Relaciones entre interfaces y clases¶
Tenemos tres tipos de relaciones:
classB
extendsclassA
: una clase B hereda de una clase A.class
implementsinterface1
,interface2
, ...: una clase puede implementar una o varias interfaces, para ello usaremos la palabra reservada implements.interfaceB
extendsinterfaceA
,interfaceC
, ...: una interfaz B puede heredar los métodos de una o varias interfaces. Una interfaz NO PUEDE heredar de una clase.
También podemos combinar algunas relaciones:
classB
extendsclassA
implementsinterface1
,interface2
, ...: una clase B hereda de una clase A y también implementa los métodos definidos en las interfaces. (Simulación de la herencia múltiple)
❇️ Ejemplo de código¶
Por ejemplo, vamos a crear la clase TelefonoMovil:
Observamos que IntelliJ nos genera un error, ya que debemos definir o implementar los métodos que habíamos declarado en la interfaz. Si hacemos click en el error, IntelliJ nos ofrece crearlos:
IntelliJ nos ha creado TODOS los métodos que habíamos definido en la interfaz, como vemos con la anotación @Override
, ya que los está sobreescribiendo puesto que estaban declarados en la interfaz.
Note
TODOS los métodos definidos en la interfaz se han de implementar en la clase, no podríamos implementar solo algunos.
Veamos como probar el código en nuestra clase Main
:
public class MainTelefono {
public static void main(String[] args) {
Impuesto impuesto = new TelefonoMovil(123456789);
impuesto.imprimirImpuesto();
}
}
Warning
Las interfaces NO PUEDEN INSTANCIARSE, es decir, no podemos crear objetos de interfaces. Hay que usar una clase que haya implementado la funcionalidad definida por la interfaz.
❇️Otro ejemplo con herencia e interface¶
Queremos crear un videojuego en el que tenemos pieza de cubo que el usuario las utilizará para construir un mundo. Algunas de las piezas(no todas) cuando se toquen pueden explotar y otras pueden quemarse, otras pueden quemarse y explotar.
Por otro lado, tenemos otros elementos que puedes explotar, como puede ser una Nave. El diagrama de clases puedes ser
//*********Clase padre****
public class Cubo {
String nombre;
public Cubo(String nombre) {
this.nombre = nombre;
}
}
//********Interfaces**************
public interface Inicinerable {
void incinerar();
}
//******************************
public interface Explotable {
void explotar();
}
//******Clases hijas************
public class Cesped extends Cubo{
public Cesped(String nombre) {
super(nombre);
}
}
//******************************
public class Dinamita extends Cubo implements Explotable{
public Dinamita(String nombre) {
super(nombre);
}
@Override
public void explotar() {
System.out.println("Boooo!!!");
}
}
//********************************
public class Madera extends Cubo implements Inicinerable{
public Madera(String nombre) {
super(nombre);
}
@Override
public void incinerar() {
System.out.println("fuegooooo!!!");
}
}
//********************************
public class Butano extends Cubo implements Inicinerable,Explotable{
public Butano(String nombre) {
super(nombre);
}
@Override
public void explotar() {
System.out.println("Bataboooo!!!);
}
@Override
public void incinerar() {
System.out.println("Fuegoonnnn!!!);
}
}
//********************
public class Nave implements Explotable{
String nombre;
@Override
public void explotar() {
System.out.println("Exxxxxploooosiooooon!!!!");
}
}
//********************
public static void main(String[] args) {
//podemos crear un dinamita y explotarla
Dinamita dim1=new Dinamita("DinamitaCat1");
dim1.explotar();
//podemos crear madera y quemarla
Madera mad1=new Madera("Pino");
mad1.incinerar();
//podemos crear butano y explotarlo y quemarlo
Butano but1=new Butano("But10Litros");
but1.incinerar();
but1.explotar();
//pero no podemos crear cesped que se queme o explote
Cesped cesp1=new Cesped("artificial");
}
Suponer que queremos que el usuario pueda generar grandes explosiones o grandes fuegos amontonando cubos Explotables o Incinerables.
Con las Interface ocurre lo mismo que con las clases abstractas, podemos instanciar un objeto que implemente la interface sobre una variable del tipo que implementa.
Explotable explotable=new Dinamita("DinamitaCat1");
De esta forma, podemos tener un array de objetos explotables
public static void granExplosion(Explotable[] explotables){
for (Explotable explotable:explotables) {
//cada tipo de objeto genera su propia explosión
explotable.explotar();
}
}
public static void main(String[] args) {
//creamos array de explotables
Explotable[] miGranExplosion=new Explotable[3];
//creamos explotables de diferente tipo
miGranExplosion[0]=new Dinamita("DinamitaCat1");
miGranExplosion[1]=new Butano("But10Litros");
miGranExplosion[2]=new Nave();
//llamamos a la gran explosion
granExplosion(miGranExplosion);
}
❇️ Clase anónimas¶
Lo más común es que las interfaces sean implementadas por distintas clases que además, se crearán diversos objetos. Pero en muchas ocasiones, la creación de una interface es solo necesaria en una ocasión en ese lugar y no deseamos crear una clase para ese propósito.
Para estos casos tenemos las clases anónimas. Son clases sin nombre que implementan un interface y que utilizan el nombre de la Interface como constructor.
Vamos a suponer que en un momento del juego, queremos generar una explosión exclusiva que no está asociada a ningún objeto. Podemos crear una objeto de la clase anónima de la siguiente forma
Explotable miExplosion=new Explotable() {
@Override
public void explotar() {
System.out.println("boonnobooonnnoboonnnobonnbonn!");
}
};
//No hay una clase específica
miExplosion.explotar();
❇️ Clase anónimas y desarrollo en entornos gráficos¶
En el desarrollo de aplicaciones en entornos gráficos orientados a eventos, las clases anónimas se utilizan constantemente, ya que para definir los diferentes eventos como puede ser realizar una acción cuando hace click sobre un botón, el método correspondiente para asignar el evento al botón espera un objeto de una clase que implemente cierta interface, y como solamente defines una vez lo que tiene que hacerse cuando se pulsa el botón, no creas una clase concreta, creas una clase anónima que implementa la interface.
Por ejemplo, en JavaFx, podemos tener un botón que haga algo
Button boton= new Button();
Y vemos que EventHandler
es una interface
De esta forma, creamos una clase anónima que implementa la interface con la acción que realizar el botón
Button boton= new Button();
//creamos objeto de clase anónima que implementa EventHandler
EventHandler<? super MouseEvent> eventHandler = new EventHandler<MouseEvent>() {
@Override
public void handle(MouseEvent mouseEvent) {
//hacer algo....
}
};
//lo asignamos a la acción del botón
boton.setOnMouseClicked(eventHandler);
y solemos verlo simplificada de la siguiente forma
Button boton= new Button();
boton.setOnMouseClicked(new EventHandler<MouseEvent>() {
@Override
public void handle(MouseEvent mouseEvent) {
//hacer algo....
}
});
❇️ Interfaz Comparable¶
En programación hay ciertas operaciones que son tan comunes que se han desarrollado en la Api de Java. Una de estas operaciones es determinar el orden de una lista de objetos de la misma clase.
En Java, la interfaz Comparable
es una interfaz genérica que se utiliza para definir el orden natural de los objetos de una clase. Esta interfaz se encuentra en el paquete java.lang
y tiene un único método llamado compareTo()
.
La idea es que una clase que implementa la interfaz Comparable
puede ser comparada con otras instancias de la misma clase y se puede saber si es menor, mayor o igual.
La clase que implementa Comparable
tiene que implementar el método compareTo()
que devuelve un entero
int compareTo(Object ob)
De tal manera que
ob1.compareTo(ob2) < 0 si ob1 va antes que ob2
ob1.compareTo(ob2) > 0 si ob2 va antes que ob1
ob1.compareTo(ob2) = 0 si ob1 es igual que ob2
En este ejemplo, la comparación se realiza en función de la edad de las personas. Puedes personalizar la lógica dentro del método compareTo()
según los criterios de orden que desees establecer para tus objetos.
public class Persona implements Comparable<Persona> {
private String nombre;
private int edad;
// Constructor y otros métodos de la clase
@Override
public int compareTo(Persona otraPersona) {
// Comparación basada en la edad
return this.edad - otraPersona.edad;
}
public static void main(String[] args) {
Persona persona1 = new Persona("Juan", 25);
Persona persona2 = new Persona("Maria", 30);
if (persona1.compareTo(persona2) < 0) {
System.out.println(persona1.getNombre() + " es menor que " + persona2.getNombre());
} else if (persona1.compareTo(persona2) > 0) {
System.out.println(persona1.getNombre() + " es mayor que " + persona2.getNombre());
} else {
System.out.println(persona1.getNombre() + " tiene la misma edad que " + persona2.getNombre());
}
}
}
Comparable
para ordenar. Por ejemplo la clase Arrays.
public static void main(String[] args) {
Persona persona1 = new Persona("Juan", 25);
Persona persona2 = new Persona("Maria", 30);
Persona persona3 = new Persona("Carlos", 22);
Persona[] personas = {persona1, persona2, persona3};
imprimirArray(personas);
// Ordenar el array utilizando Arrays.sort()
Arrays.sort(personas);
System.out.println("\nArray de personas después de ordenar por edad:");
imprimirArray(personas);
}
// Método para imprimir el array de personas
private static void imprimirArray(Persona[] personas) {
for (Persona persona : personas) {
System.out.println(persona.getNombre() + " - Edad: " + persona.getEdad());
}
}
public static void main(String[] args) {
// Crear objetos Persona
Persona persona1 = new Persona("Juan", 25);
Persona persona2 = new Persona("Maria", 30);
Persona persona3 = new Persona("Carlos", 22);
// Crear ArrayList y agregar personas
ArrayList<Persona> listaPersonas = new ArrayList<>();
listaPersonas.add(persona1);
listaPersonas.add(persona2);
listaPersonas.add(persona3);
System.out.println("ArrayList de personas antes de ordenar:");
imprimirArrayList(listaPersonas);
// Ordenar el ArrayList utilizando Collections.sort()
Collections.sort(listaPersonas);
System.out.println("\nArrayList de personas después de ordenar por edad:");
imprimirArrayList(listaPersonas);
}
// Método para imprimir el ArrayList de personas
private static void imprimirArrayList(ArrayList<Persona> listaPersonas) {
for (Persona persona : listaPersonas) {
System.out.println(persona.getNombre() + " - Edad: " + persona.getEdad());
}
}
❇️ Interface Comparator¶
Mediante la interface Comparable
hemos visto que otras clases pueden ordenar un conjunto de objetos de una clase por una condición(en el ejemplo, por la edad), pero podemos tener la necesidad de tener que ordenar por otras condiciones(por ejemplo por el nombre).
Si vemos la a Api de Java para la clase Arrays, tenemos el método sort
sobrecargado, y una de las sobrecargas es
sort(T[] a, Comparator<? super T> c)
Vemos que acepta un objeto que implementa Comparator
. Esto nos permite ordenar por otras condiciones determinadas por Comparator
Por ejemplo, queremos ordenar un array de Personas por nombre, haríamos lo siguiente
public static void main(String[] args) {
Persona[] personas = new Persona[3];
personas[0]= new Persona("Pepe", 25);
personas[1]= new Persona("Maria", 30);
personas[2]= new Persona("Fernan", 20);
// Usando Comparator para ordenar por edad de forma ascendente, creamos una clase anónima
Comparator<Persona> comparadorNombreAscendente = new Comparator<Persona>() {
@Override
public int compare(Persona per1, Persona per2) {
//aprovechamos que String implementa Comparable
return per1.getNombre().compareTo(per2.getNombre());
}
};
//ordenamos por nombre
Arrays.sort(personas,comparadorNombreAscendente);
// Imprimiendo la lista ordenada ascendente
for (Persona persona : personas) {
System.out.println(persona.nombre + " - " + persona.edad);
}
}
ArrayList
.
//suponemos que personas es un ArrayList
Collections.sort(personas, comparadorNombreAscendente);
//ordenar por nombre de forma descendente
Comparator<Persona> comparadorNombreDescendente = new Comparator<Persona>() {
@Override
public int compare(Persona per1, Persona per2) {
//descendente por nombre
return per2.getNombre().compareTo(per1.getNombre());
}
};
Arrays.sort(personas,comparadorNombreDescendente);
Comparable vs Comparator¶
Comparable
nos va a permitir ordenar por un criterio principal el conjunto de objetos de una clase y Comparator
nos permitirá ordenar por otros criterios los objetos de la clase
//ordenamos por edad mediante Comparable
Arrays.sort(personas);
//ordenamos por nombre ascendente por Comparator
Arrays.sort(personas,comparadorNombreAscendente);
//ordenamos por nombre descendente por Comparator
Arrays.sort(personas,comparadorNombreDescendente);
❇️ Novedades¶
Desde Java 8 podemos incluir en una interfaz:
-
métodos con cuerpo o implementación: se denominan default methods. Estos métodos se heredan como cualquier método ordinario más y se pueden sobrescribir en la clase que implementa esa interfaz o sobrescribir en la interfaz que hereda de esa interfaz.
-
métodos estáticos con cuerpo o implementación. Estos métodos no pueden ser sobreescritos o cambiar en ninguna clase que implemente la interfaz. Aunque si se heredan.
public interface Impuesto {
//constantes
double TASA_DE_IMPUESTO = 0.06;
//métodos abstractos
double calcularImpuestoAnual();
void imprimirImpuesto();
//default methods
default void imprimirTasa() {
System.out.println("La tasa de impuesto es " + TASA_DE_IMPUESTO);
}
//métodos estáticos
static double tax(int precio) {
return TASA_DE_IMPUESTO * precio;
}
}
⚜️ ¿Por qué el uso de default o static methods en el interior de una interfaz?¶
Imagina que creamos una interfaz en el proyecto. Pasado un tiempo, un gran número de clases implementan esa interfaz.
Si ahora añadimos un nuevo método a esta interfaz, desencadena en que, todas las clases que implementen esa interfaz se verán afectadas con errores, hasta que implementen o le den cuerpo a ese nuevo método. Aunque no es una tarea complicada puede llegar a ser tediosa, o que no sepamos todavía como implementarlo en todas las clases.
Para solventar esto, Java introdujo los default methods y métodos estáticos.
- Desde Java 9, podemos tener métodos privados en una interfaz.
Los métodos privados se pueden implementar estáticos o no.
¿Cuáles son las ventajas de tener métodos privados?
Las interfaces pueden usar métodos privados para ocultar detalles sobre la implementación de las clases que implementan la interfaz. Como resultado, uno de los principales beneficios de tenerlos en las interfaces es la encapsulación.
public interface Impuesto {
//constantes
double TASA_DE_IMPUESTO = 0.06;
//métodos abstractos
double calcularImpuestoAnual();
void imprimirImpuesto();
//default methods
default void imprimirTasa() {
System.out.println("La tasa de impuesto es " + TASA_DE_IMPUESTO);
}
default void aumentarTasa() {
duplicarTasa();
}
//métodos privados de instancia
private double duplicarTasa() {
return TASA_DE_IMPUESTO * 2;
}
//métodos estáticos
static double tax(int precio) {
mostrarPrecio(precio);
return TASA_DE_IMPUESTO * precio;
}
//métodos privados estáticos
private static void mostrarPrecio(int precio) {
System.out.println("El precio es " + precio);
}
}