Java Interfaces
Concepto de Interface
En el tema dedicado a la herencia en Java se explica el concepto de clase abstracta. Una clase que contenga un método abstracto se debe declarar como abstracta. También sabemos que una clase abstracta puede contener atributos, constructores y métodos no abstractos. De una clase abstracta no se pueden crear objtos y su finalidad es ser la base para construir la jerarquía de herencia entre clases y aplicar el polimorfismo.Llevando estos conceptos un poco más allá aparece en concepto de Interface.
Podemos considerar una interface como una clase que sólo puede contener:
- Métodos abstractos
- Atributos constantes
- Métodos por defecto
- Métodos estáticos
- Tipos anidados
- Métodos privados
Una Interface tiene en común con una clase lo siguiente:
- Puede contener métodos.
- Se escribe en un archivo con extensión .java que debe llamarse exactamente igual que la interface.
- Al compilar el programa, el byte-code de la Interface se guarda en un archivo .class
Pero una inteface se diferencia de una clase en lo siguiente:
- No se pueden instanciar. No podemos crear objetos a partir de una Interface.
- No contiene constructores.
- Si contiene atributos todos deben ser public static final
- Una interface puede heredar de varias interfaces. Con las interfaces se permite la herencia múltiple.
Conceptos importantes a tener en cuenta cuando trabajamos con interfaces:
- Las clases no heredan las interfaces. Las clases implementan las interfaces.
- Una clase puede implementar varias interfaces.
Explicándolo de una manera simple, implementar una interface sería
equivalente a copiar y pegar el código de la interface dentro de la clase. Cuando
una clase implementa una interface está obligada a implementar todos los
métodos abstractos que contiene ya que de otra forma debería declararse como
clase abstracta.
Usando Interfaces, si varias clases distintas implementan la misma
interface nos aseguramos que todas tendrán implementados una serie de métodos
en comunes.
Las interfaces juegan un papel fundamental en la creación de
aplicaciones Java.
Las interfaces definen un protocolo de comportamiento y proporcionan
un formato común para implementarlo en las clases.
Utilizando interfaces es posible que clases no
relacionadas, situadas en distintas jerarquías de clases sin relaciones de
herencia, tengan comportamientos comunes.
Una
interface se crea utilizando la palabra clave interface en lugar de class.
[public] interface NombreInterface [extends Interface1, Interface2, ...]{
[métodos abstractos]
[métodos default]
[métodos static]
[métodos privados]
[atributos constantes]
[tipos anidados]
}
Algunas características de las interfaces:
La interface puede definirse public o sin modificador de acceso, y tiene el mismo significado que para las clases. Si tiene el
modificador public el archivo .java
que la contiene debe tener el mismo nombre que la interfaz.
En los métodos abstractos
no es necesario escribir abstract.
Los métodos por defecto se especifican mediante el modificador default.
Los métodos estáticos se
especifican mediante la palabra reservada static.
Los métodos privados se
especifican mediante el modificador de acceso private.
Todos los atributos son constates públicos y estáticos. Por lo tanto, se pueden omitir los modificadores public static final cuando se declara el
atributo. Se deben inicializar en la misma instrucción de declaración.
Los nombres de las interface suelen acabar en able aunque no es necesario: Configurable, Arrancable,
Dibujable, Comparable, Clonable, etc.Ejemplo: Creamos una interface llamada Relacionable con tres métodos abstractos.
//Interfaz que define relaciones de orden entre objetos.
public interface Relacionable {
boolean esMayorQue(Relacionable a);
boolean esMenorQue(Relacionable a);
boolean esIgualQue(Relacionable a);
}
IMPLEMENTACIÓN
Para
indicar que una clase implementa una interface se utiliza la palabra clave implements.
public class UnaClase implements Interface1{
......
}
En UML una
clase que implementa una interface se representa mediante una flecha con línea
discontinua apuntando a la interface:
O también se puede
representar de forma abreviada:
Las clases que implementan
una interfaz deben implementar todos los métodos abstractos. De lo contrario
serán clases abstractas y deberán declararse como tal.
Ejemplo: La clase Fraccion implementa la interfaz
Relacionable. En ese caso se dice que la clase Fraccion es Relacionable
public class Fraccion implements Relacionable {
private int num;
private int den;
//Constructores
public Fraccion() {
this.num = 0;
this.den = 1;
}
public Fraccion(int num, int den) {
this.num = num;
this.den = den;
simplificar();
}
public Fraccion(int num) {
this.num = num;
this.den = 1;
}
//Setters y Getters
public void setDen(int den) {
this.den = den;
this.simplificar();
}
public void setNum(int num) {
this.num = num;
this.simplificar();
}
public int getDen() {
return den;
}
public int getNum() {
return num;
}
//sumar fracciones
public Fraccion sumar(Fraccion f) {
Fraccion aux = new Fraccion();
aux.num = num * f.den + den * f.num;
aux.den = den * f.den;
aux.simplificar();
return aux;
}
//restar fracciones
public Fraccion restar(Fraccion f) {
Fraccion aux = new Fraccion();
aux.num = num * f.den - den * f.num;
aux.den = den * f.den;
aux.simplificar();
return aux;
}
//multiplicar fracciones
public Fraccion multiplicar(Fraccion f) {
Fraccion aux = new Fraccion();
aux.num = num * f.num;
aux.den = den * f.den;
aux.simplificar();
return aux;
}
//dividir fracciones
public Fraccion dividir(Fraccion f) {
Fraccion aux = new Fraccion();
aux.num = num * f.den;
aux.den = den * f.num;
aux.simplificar();
return aux;
}
//Método para simplificar una fracción
private void simplificar() {
int n = mcd(); //se calcula el mcd de la fracción
num = num / n;
den = den / n;
}
//Cálculo del máximo común divisor por el algoritmo de Euclides
//Lo utiliza el método simplificar()
private int mcd() {
int u = Math.abs(num); //valor absoluto del numerador
int v = Math.abs(den); //valor absoluto del denominador
if (v == 0) {
return u;
}
int r;
while (v != 0) {
r = u % v;
u = v;
v = r;
}
return u;
}
//Sobreescritura del método toString heredado de Object
@Override
public String toString() {
simplificar();
return num + "/" + den;
}
//Implementación del método abstracto esMayorQue de la interface
@Override
public boolean esMayorQue(Relacionable a) {
if (a == null) {
return false;
}
if (!(a instanceof Fraccion)) {
return false;
}
Fraccion f = (Fraccion) a;
this.simplificar();
f.simplificar();
if ((num / (double) den) <= (f.num / (double) f.den)) {
return false;
}
return true;
}
//Implementación del método abstracto esMenorQue de la interface
@Override
public boolean esMenorQue(Relacionable a) {
if (a == null) {
return false;
}
if (!(a instanceof Fraccion)) {
return false;
}
Fraccion f = (Fraccion) a;
this.simplificar();
f.simplificar();
if ((num / (double) den) >= (f.num / (double) f.den)) {
return false;
}
return true;
}
//Implementación del método abstracto esIgualQue de la interface
@Override
public boolean esIgualQue(Relacionable a) {
if (a == null) {
return false;
}
if (!(a instanceof Fraccion)) {
return false;
}
Fraccion f = (Fraccion) a;
this.simplificar();
f.simplificar();
if (num != f.num) {
return false;
}
if (den != f.den) {
return false;
}
return true;
}
}
Ejemplo de utilización de la clase
Fraccion:
public static void main(String[] args) {
//Creamos dos fracciones y mostramos cuál es la mayor y cuál menor.
Fraccion f1 = new Fraccion(3, 5);
Fraccion f2 = new Fraccion(2, 8);
if (f1.esMayorQue(f2)) {
System.out.println(f1 + " > " + f2);
} else {
System.out.println(f1 + " <= " + f2);
}
//Creamos un ArrayList de fracciones y las mostramos ordenadas de menor a mayor
ArrayList<Fraccion> fracciones = new ArrayList();
fracciones.add(new Fraccion(10, 7));
fracciones.add(new Fraccion(-2, 3));
fracciones.add(new Fraccion(1, 9));
fracciones.add(new Fraccion(6, 25));
fracciones.add(new Fraccion(3, 8));
fracciones.add(new Fraccion(8, 3));
Collections.sort(fracciones, new Comparator<Fraccion>(){
@Override
public int compare(Fraccion o1, Fraccion o2) {
if(o1.esMayorQue(o2)){
return 1;
}else if(o1.esMenorQue(o2)){
return -1;
}else{
return 0;
}
}
});
System.out.println("Fracciones ordenadas de menor a mayor");
for(Fraccion f: fracciones){
System.out.print(f + " ");
}
}
Una clase puede implementar más de una
interface. Los nombres de las interfaces se escriben a continuación de implements y separadas por comas:
public class UnaClase implements Interface1, Interface2, Interface3{
......
}
En el
ejemplo, para ordenar las fracciones hemos utilizado un Comparator como parámetro
de Collections.sort. También podríamos ordenarlas haciendo que la clase
Fraccion implemente además la interfaz Comparable:
public class Fraccion implements Relacionable, Comparable<Fraccion> {
......
//Código de la clase Fraccion
......
//Añadimos a la clase el método compareTo
@Override
public int compareTo(Fraccion o) {
if(this.esMenorQue(o)){
return -1;
}else if(this.esMayorQue(o)){
return 1;
}else{
return 0;
}
}
} //fin de la clase Fraccion
De este modo para ordenar
escribiríamos:
Collections.sort(fracciones);
Una clase solo puede tener una clase base
pero puede implementar múltiples interfaces. El lenguaje Java no permite herencia múltiple entre
clases, pero las interfaces proporcionan una alternativa para implementar algo
parecido a la herencia múltiple de otros lenguajes.
Una clase que además de implementar
interfaces herede de otra se declarará de esta forma:
public class UnaClase extends ClaseBase implements Interface1, Interface2, Interface3{
......
}
Una interfaz la puede implementar
cualquier clase. Por ejemplo, podemos tener una clase Linea que también
implementa la interfaz Relacionable:
public class Linea implements Relacionable {
private double x1;
private double y1;
private double x2;
private double y2;
public Linea(double x1, double y1, double x2, double y2) {
this.x1 = x1;
this.y1 = y1;
this.x2 = x2;
this.y2 = y2;
}
public double longitud() {
return Math.sqrt((x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1));
}
//Implementación del método abstracto esMayorQue de la interface
@Override
public boolean esMayorQue(Relacionable a) {
if (a == null) {
return false;
}
if (!(a instanceof Linea)) {
return false;
}
Linea linea = (Linea) a;
return this.longitud() > linea.longitud();
}
//Implementación del método abstracto esMenorQue de la interface
@Override
public boolean esMenorQue(Relacionable a) {
if (a == null) {
return false;
}
if (!(a instanceof Linea)) {
return false;
}
Linea linea = (Linea) a;
return this.longitud() < linea.longitud();
}
//Implementación del método abstracto esIgualQue de la interface
@Override
public boolean esIgualQue(Relacionable a) {
if (a == null) {
return false;
}
if (!(a instanceof Linea)) {
return false;
}
Linea linea = (Linea) a;
return this.longitud() == linea.longitud();
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("Coordenadas inicio linea: ");
sb.append(x1);
sb.append(" , ");
sb.append(y1);
sb.append("\nCoordenadas final linea: ");
sb.append(x2);
sb.append(" , ");
sb.append(y2);
sb.append("\nLongitud: ");
sb.append(longitud());
return sb.toString();
}
}
Podemos hacer un ejemplo de utilización
de la clase Linea similar al que hemos hecho para la clase Fracción:
public static void main(String[] args) {
Linea l1 = new Linea(2, 2, 4, 1);
Linea l2 = new Linea(5, 2, 10, 8);
if (l1.esMayorQue(l2)) {
System.out.println(l1 + "\nes mayor que" + l2);
} else {
System.out.println(l1 + "\nes menor o igual que" + l2);
}
ArrayList<Linea> lineas = new ArrayList();
lineas.add(new Linea(0, 7, 1, 4));
lineas.add(new Linea(2, -1, 3, 5));
lineas.add(new Linea(1, 9, 0, -3));
lineas.add(new Linea(15, 3, 9, 5));
Collections.sort(lineas, new Comparator<Linea>(){
@Override
public int compare(Linea o1, Linea o2) {
if(o1.esMayorQue(o2)){
return 1;
}else if(o1.esMenorQue(o2)){
return -1;
}else{
return 0;
}
}
});
System.out.println("\nLineas ordenadas por longitud de menor a mayor");
for (Linea l : lineas) {
System.out.println(l);
}
}
INTERFACES Y POLIMORFISMO
La
definición de una interface implica una definición de un nuevo tipo de
referencia y por ello se puede usar el nombre de la interface como nombre de tipo.
El
nombre de una interfaz se puede utilizar en cualquier lugar donde pueda
aparecer el nombre de un tipo de datos.
Si se define una variable cuyo
tipo es una interface, se le puede asignar un objeto instancia de una clase que
implementa la interface.
Volviendo
al ejemplo, las clases Linea y Fraccion implementan la interfaz Relacionable.
Podemos escribir las instrucciones:
Relacionable r1 = new
Linea(2,2,4,1);
Relacionable r2 = new
Fraccion(4,7);
En Java, utilizando interfaces como tipo se puede aplicar el polimorfismo para
clases que no están relacionadas por herencia.
Por ejemplo, podemos escribir:
System.out.println(r1); //ejecuta toString de Linea
System.out.println(r2); //ejecuta toString de Fraccion
También podemos crear un array de tipo
Relacionable y guardar objetos de clases que
implementan la interfaz.
ArrayList<Relacionable> array = new ArrayList();
array.add(new Linea(15, 3, 9, 5));
array.add(new Fraccion(10, 7));
array.add(new Fraccion(6, 25));
array.add(new Linea(3, 4, 10, 15));
array.add(new Fraccion(8, 3));
array.add(new Linea(0, 7, 1, 4));
array.add(new Linea(2, -1, 3, 5));
array.add(new Fraccion(1, 9));
array.add(new Linea(1, 9, 0, -3));
array.add(new Fraccion(3, 8));
array.add(new Fraccion(-2, 3));
for (Relacionable r : array) {
System.out.println(r);
}
En
este caso dos clases no relacionadas, Linea
y Fraccion, por implementar la misma
interfaz Relacionable podemos
manejarlas a través de referencias a la interfaz y aplicar polimorfismo.
MÉTODOS DEFAULT
A partir de Java 8 las interfaces además de
métodos abstractos pueden contener métodos por defecto o métodos default.
En la interfaz se escribe el código del
método. Este método estará disponible para todas las clases que la implementen,
no estando obligadas a escribir su código. Solo lo incluirán en el caso de
querer modificarlo.
De este modo, si se modifica una interfaz añadiéndole una nueva
funcionalidad, se evita tener que modificar el código en todas las clases que
la implementan.
Ejemplo:
Vamos a añadir un nuevo método a la interfaz Relacionable que devuelva el
nombre de la clase (String) del objeto que la está utilizando. Si lo añadimos
como abstracto tendremos que modificar las clases Linea y Fraccion y añadir en
cada una el nuevo método. En lugar de esto vamos a crear el método como default
y de este modo las clases Linea y Fraccion lo pueden usar sin necesidad de
escribirlo.
public interface Relacionable {
boolean esMayorQue(Relacionable a);
boolean esMenorQue(Relacionable a);
boolean esIgualQue(Relacionable a);
default String nombreClase(){ //método por defecto
String clase = getClass().toString();
int posicion = clase.lastIndexOf(".");
return clase.substring(posicion+1);
}
}
Ejemplo
de uso:
ArrayList<Relacionable> array = new ArrayList();
array.add(new Linea(15, 3, 9, 5));
array.add(new Fraccion(10, 7));
array.add(new Fraccion(6, 25));
array.add(new Linea(3, 4, 10, 15));
array.add(new Fraccion(8, 3));
array.add(new Linea(0, 7, 1, 4));
array.add(new Linea(2, -1, 3, 5));
array.add(new Fraccion(1, 9));
array.add(new Linea(1, 9, 0, -3));
array.add(new Fraccion(3, 8));
array.add(new Fraccion(-2, 3));
for (Relacionable r : array) {
System.out.println(r.nombreClase()); //usamos el método por defecto
System.out.println(r);
System.out.println();
}
Puede darse el caso de una clase que
implemente varias interfaces y las interfaces contengan métodos default iguales
(mismo nombre y lista de parámetros).
Ejemplo:
La interface Modificable contiene dos
métodos abstractos y un método default nombreClase
que es igual que el contenido en la interface Relacionable:
public interface Modificable{
void aumentar(int n);
void disminuir(int n);
default String nombreClase(){
String clase = getClass().toString();
int posicion = clase.lastIndexOf(".");
return clase.substring(posicion+1);
}
}
Supongamos que la clase Linea implementa
las dos interfaces. En la clase Linea se debe escribir el código de los dos
métodos abstractos, pero el método default provoca un error de compilación:
La clase Linea hereda dos métodos default iguales. Esto provoca un conflicto que tenemos que resolver. Lo
podemos hacer de dos formas. La primera forma es redefinir el método nombreClase()
en la clase Linea:
public class Linea implements Relacionable, Modificable{
......
//Código de la clase Linea
......
//Implementación del método abstracto de la interface Modificable
@Override
public void aumentar(int n) {
this.x1+=n;
this.y1+=n;
this.x2+=n;
this.y2+=n;
}
//Implementación del método abstracto de la interface Modificable
@Override
public void disminuir(int n) {
this.x1-=n;
this.y1-=n;
this.x2-=n;
this.y2-=n;
}
//redefinir el método default de la interface
@Override
public String nombreClase(){
String clase = getClass().toString();
int posicion = clase.lastIndexOf(".");
return clase.substring(posicion+1);
}
}//Final de la clase Linea
En este caso no se ha modificado el
código del método aunque podríamos haberlo hecho. Nos hemos limitado a copiar y
pegar el código cambiando el modificador default
por public.
Para
estos casos en los que no es necesario cambiar el método podemos usar la
segunda forma de resolver el conflicto: indicar cuál de los dos métodos se
tiene que ejecutar.
public class Linea implements Relacionable, Modificable{
.....
//Código de la clase Linea
....
//Redefinición del método default
//Se ejecutará el método de la interface Relacionable
@Override
public String nombreClase(){
return Relacionable.super.nombreClase();
}
}//Final de la clase Linea
La forma de indicar el método de la
interface que se ejecuta es:
nombreInterface.super.metodo
MÉTODOS STATIC
A partir de Java 8 las interfaces también
pueden contener métodos static. En la interfaz se escribe el código del método.
Los métodos static NO pueden ser redefinidos en las clases que la implementan.
Para utilizarlos se escribe:
nombreInterface.metodoStatic
Ejemplo: Añadimos a la interfaz
Relacionable un método estático esNull.
El método comprueba si la variable a
contiene o no la dirección de un objeto.
interface Relacionable {
boolean esMayorQue(Relacionable a);
boolean esMenorQue(Relacionable a);
boolean esIgualQue(Relacionable a);
default String nombreClase(){
String clase = getClass().toString();
int posicion = clase.lastIndexOf(".");
return clase.substring(posicion+1);
}
static boolean esNull(Relacionable a){
return a == null;
}
}
Ejemplo
de uso de un método static en una interfaz Java: supongamos que disponemos de un Array en el que algunos elementos
pueden ser null, es decir, no se les ha asignado un objeto. Utilizaremos el
método esNull para comprobarlo y
evitar que se produzca un error al intentar acceder a uno de estos elementos.
Relacionable[] array = new Relacionable[20];
array[1] = new Linea(15, 3, 9, 5);
array[5] = new Fraccion(10, 7);
array[9] = new Fraccion(6, 25);
array[11] = new Linea(3, 4, 10, 15);
array[14] = new Fraccion(8, 3);
array[15] = new Linea(0, 7, 1, 4);
array[18] = new Linea(2, -1, 3, 5);
for (Relacionable r : array) {
if (!Relacionable.esNull(r)) { //usamos el método static
System.out.println(r.nombreClase());
System.out.println(r);
System.out.println();
}
}
MÉTODOS PRIVATE
A partir de Java 9 las
interfaces también pueden contener métodos privados. Estos métodos solo son
accesibles dentro de la propia interface, por lo tanto, las clases que la
implementen no podrán utilizaros.
Estos
métodos se han incorporado para evitar la duplicidad de código en los métodos
no abstractos de la interface. Si hay métodos no abstractos dentro de la
interface que tienen una serie de líneas de código iguales, se puede crear un
método privado que realice estas instrucciones e invocarlo y de esta forma se
evita tener el mismo código repetido en varios métodos.
ATRIBUTOS CONSTANTES
Una interfaz también puede contener
atributos constantes.
Ejemplo:
//Interfaz que contiene días relevantes en una aplicación de banca.
public interface IDiasOperaciones {
int DIA_PAGO_INTERESES = 5;
int DIA_COBRO_HIPOTECA = 30;
int DIA_COBRO_TARJETA = 28;
}
Una clase que la implemente puede usar
las constantes como si fuesen propias:
public class Banco implements IDiasOperaciones{
......
public void mostrarInformacionIntereses(){
System.out.println("El día " + DIA_PAGO_INTERESES
+ " de cada mes se realiza el pago de intereses");
}
......
}
Una clase que no implemente la interfaz
puede usar las constantes escribiendo antes el nombre de la interfaz.
public static void main(String[] args) {
......
System.out.println("Los intereses se pagan el día "
+ IDiasOperaciones.DIA_PAGO_INTERESES);
System.out.println("La hipoteca se paga el día "
+ IDiasOperaciones.DIA_COBRO_HIPOTECA);
......
}
HERENCIA ENTRE INTERFACES
Se puede establecer una jerarquía de herencia entre
interfaces igual que con las clases.
public interface Interface2 extends Interface1{
…….
}
public interface Interface3 extends
Interface1{
…….
}
public interface Interface4 extends
Interface2{
…….
}
Cada interface hereda el
contenido de las interfaces que están por encima de ella en la jerarquía y
puede añadir nuevo contenido o modificar lo que ha heredado siempre que sea
posible.
Los métodos static no se pueden redefinir.
Los métodos abstract heredados se pueden convetir en métodos default.
Los métodos default se pueden redefinir o convertir en abstract.
Los métodos static no se pueden redefinir.
Los métodos abstract heredados se pueden convetir en métodos default.
Los métodos default se pueden redefinir o convertir en abstract.
public interface Interface4 extends Interface1,
Interface2, Interface3{
…….
}
Conceptos muy claros. Fantastico!
ResponderEliminarGracias, la idea es esa, intentar explicarlo de la forma más clara y sencilla posible aunque no siempre se consigue xd
Eliminarmuy bien explicado, ¿como puedo hacer un metodo publico y estatico que devuelva un objeto de la interfaz?
ResponderEliminarGracias, super entendible
ResponderEliminarGracias por los comentarios, me alegro de que os sea útil la entrada.
ResponderEliminar