Guía de interfaces en Java

En esta guía, aprenda todo lo que necesita saber sobre las interfaces en Java: por qué usarlas, cómo están definidas, métodos estáticos y predeterminados, mejores prácticas, convenciones de nomenclatura, interfaces funcionales y herencia múltiple, así como herencia de interfaz.

Introducción

Las interfaces en Java son uno de los conceptos básicos de la programación orientada a objetos que se utilizan con bastante frecuencia junto con clases y clases abstractas. Una interfaz representa un tipo de referencia, lo que significa que es esencialmente solo una especificación que debe obedecer una clase particular que la implementa. Las interfaces pueden contener solo constantes, firmas de métodos, métodos predeterminados y métodos estáticos. Por defecto, las interfaces solo permiten el uso del especificador público, a diferencia de las clases que también pueden usar los especificadores protegido y privado.

En esta guía, echaremos un vistazo a las interfaces en Java: cómo funcionan y cómo usarlas. También cubriremos todos los conceptos que podría necesitar comprender al trabajar con interfaces en Java. Después de leer esta guía, debe tener una comprensión completa de las interfaces de Java.

Los cuerpos de los métodos solo existen para los métodos predeterminados y estáticos. Sin embargo, incluso si permiten que un cuerpo esté presente dentro de una interfaz, generalmente no es una buena práctica, ya que puede generar mucha confusión y hacer que el código sea menos legible. Las interfaces no se pueden instanciar; solo se pueden implementar mediante clases o extender mediante otras interfaces.

¿Por qué usar interfaces? {#por qué usar interfaces}

Ya deberíamos saber que las clases de Java admiten la herencia. Pero cuando se trata de herencias múltiples, las clases de Java simplemente no lo admiten, a diferencia de, por ejemplo, C#. ¡Para superar este problema usamos interfaces!

Las clases extienden otras clases, y las interfaces también pueden extender otras interfaces, pero una clase solo implementa una interfaz. Las interfaces también ayudan a lograr abstracción absoluta cuando es necesario.

Las interfaces también permiten acoplamiento suelto. El acoplamiento flexible en Java representa una situación en la que dos componentes tienen poca dependencia entre sí: los componentes son independientes entre sí. El único conocimiento que una clase tiene sobre la otra clase es lo que la otra clase ha expuesto a través de sus interfaces en acoplamiento flexible.

{.icon aria-hidden=“true”}

Nota: El acoplamiento suelto es deseable porque facilita la modularización y las pruebas. Cuanto más acopladas estén las clases, más difícil será probarlas individualmente y aislarlas de los efectos de otras clases. Un estado ideal de las relaciones de clase incluye acoplamiento débil y alta cohesión: se pueden separar por completo, pero también se habilitan entre sí con funcionalidad adicional. Cuanto más cerca estén los elementos de un módulo entre sí, mayor será la cohesión. Cuanto más cerca esté su arquitectura de este estado ideal, más fácil será escalar, mantener y probar su sistema.

Cómo definir interfaces en Java

Definir interfaces no es tan difícil. De hecho, es bastante similar a definir una clase. Por el bien de esta guía, definiremos una interfaz Animal simple y luego la implementaremos dentro de una variedad de clases diferentes:

1
2
3
4
5
6
public interface Animal {
    public void walk();
    public void eat();
    public void sleep();
    public String makeNoise();
}

Podemos hacer que tenga una variedad de métodos diferentes para describir diferentes comportamientos de los animales, pero la funcionalidad y el punto siguen siendo los mismos sin importar cuántas variables o métodos agreguemos. Por lo tanto, lo mantendremos simple con estos cuatro métodos.

Esta sencilla interfaz define algunos comportamientos de los animales. En términos más técnicos, hemos definido los métodos que deben encontrarse dentro de las clases específicas que implementan esta interfaz. Vamos a crear una clase Perro que implemente nuestra interfaz Animal:

1
2
3
4
5
6
7
public class Dog implements Animal{
    public String name;
    
    public Dog(String name){
        this.name = name;
    }
}

Es una clase simple que solo tiene una variable nombre. La palabra clave implementos nos permite implementar la interfaz Animal dentro de nuestra clase Perro. Sin embargo, no podemos dejarlo así. Si tratamos de compilar y ejecutar el programa implementando la clase Dog de esta manera, obtendremos un error similar a:

1
java: Dog is not abstract and does not override abstract method makeNoise() in Animal

Este error nos dice que no obedecimos las reglas establecidas por la interfaz que implementamos. Tal como está, nuestra clase Perro debe definir los cuatro métodos definidos dentro de la interfaz Animal, incluso si no devuelven nada y solo están vacíos. En realidad, siempre querremos que hagan algo y no definiremos ningún método redundante/específico de clase en una interfaz. Si no puede encontrar una implementación válida de un método de interfaz en una subclase, no debe definirse en la interfaz. En su lugar, sáltelo en la interfaz y defínalo como miembro de esa subclase. Alternativamente, si se trata de otra funcionalidad genérica, defina otra interfaz, que se pueda implementar junto con la primera. Nuestro ejemplo está un poco simplificado, pero el punto sigue siendo el mismo incluso en programas más complicados:

 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
public class Dog implements Animal{
    public String name;

    public Dog(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }
    
    public void walk() {
        System.out.println(getName() + " is walking!");
    }

    public void eat() {
        System.out.println(getName() + " is eating!");
    }

    public void sleep() {
        System.out.println(getName() + " is sleeping!");
    }

    public String makeNoise() {
        return getName() + " says woof!";
    }
}

Una vez que hayamos implementado nuestra interfaz dentro de nuestra clase objetivo, podemos usar todos esos métodos como solíamos hacer cada vez que usamos métodos públicos de cualquier clase:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
public class Main {
    public static void main(String[] args) {
        Dog dog = new Dog("Shiba Inu");

        dog.eat();
        System.out.println(dog.makeNoise());
        dog.walk();
        dog.sleep();
    }
}

Esto nos da la salida:

1
2
3
4
Shiba Inu is eating!
Shiba Inu says woof!
Shiba Inu is walking!
Shiba Inu is sleeping!

Herencia múltiple {#herencia múltiple}

Como mencionamos anteriormente, usamos interfaces para resolver el problema que tienen las clases con la herencia. Si bien una clase no puede extender más de una clase a la vez, puede implementar más de una interfaz a la vez. Esto se hace simplemente separando los nombres de las interfaces con una coma. Una situación en la que una clase implementa múltiples interfaces, o una interfaz extiende múltiples interfaces, se denomina herencia múltiple.

La pregunta surge naturalmente: ¿por qué no se admite la herencia múltiple en el caso de las clases, pero sí en el caso de las interfaces? La respuesta a esa pregunta también es bastante simple: ambigüedad. Diferentes clases pueden definir los mismos métodos de manera diferente, arruinando así la consistencia en todos los ámbitos. Mientras que en el caso de las interfaces no hay ambigüedad, la clase que implementa la interfaz proporciona la implementación de los métodos.

Para este ejemplo, nos basaremos en nuestra interfaz Animal anterior. Digamos que queremos crear una clase Bird. Las aves son obviamente animales, pero nuestra interfaz Animal no tiene métodos para simular un movimiento de vuelo. Esto podría resolverse fácilmente agregando un método fly() dentro de la interfaz Animal, ¿verdad?

Bueno, sí, pero en realidad no.

Dado que podemos tener una cantidad infinita de clases con nombres de animales que amplían nuestra interfaz, teóricamente necesitaríamos agregar un método que simule el comportamiento de un animal si faltara previamente, por lo que cada animal tendría que implementar fly ( ) método. Para evitar esto, ¡simplemente crearemos una nueva interfaz con un método fly()! Esta interfaz sería implementada por todos los animales voladores.

En nuestro ejemplo, dado que el ave necesitaría un método que simule volar, y digamos batir las alas, tendríamos algo como esto:

1
2
3
4
public interface Flying {
    public void flapWings();
    public void fly();
}

Una vez más, una interfaz muy simple. Ahora podemos crear la clase Bird como hemos discutido anteriormente:

 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
public class Bird implements Animal, Fly{
    public String name;

    public Bird(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public void walk() {
        System.out.println(getName() + " is walking!");
    }

    public void eat() {
        System.out.println(getName() + " is eating!");
    }

    public void sleep() {
        System.out.println(getName() + " is sleeping!");
    }

    public String makeNoise() {
        return getName() + " says: caw-caw!";
    }

    public void fly() {
        System.out.println(getName() + " is flying!");
    }

    public void flapWings(){
        System.out.println(getName() + " is flapping its wings!");
    }
}

Vamos a crear un objeto Bird dentro de nuestra clase principal y mostrar los resultados como lo hicimos antes:

1
2
3
4
5
6
Bird bird = new Bird("Crow");
System.out.println(bird.makeNoise());
bird.flapWings();
bird.fly();
bird.walk();
bird.sleep();

Da una salida simple:

1
2
3
4
5
Crow says: caw-caw!
Crow is flapping its wings!
Crow is flying!
Crow is walking!
Crow is sleeping!

{.icon aria-hidden=“true”}

Nota: Habrá casos (especialmente al implementar varias interfaces) en los que no todos los métodos declarados en todas las interfaces se definirán dentro de nuestra clase, a pesar de nuestros mejores esfuerzos. Por ejemplo, si nuestra interfaz principal Animal por cualquier razón tuviera un método swim(), dentro de nuestra clase Bird ese método permanecería vacío (o devolvería null), como las aves en su mayor parte no. nadar

Herencia de interfaz {#herencia de interfaz}

Al igual que cuando heredamos las propiedades de una clase de otra usando extends, podemos hacer lo mismo con las interfaces. Al extender una interfaz con otra, esencialmente eliminamos la necesidad de que una clase implemente múltiples interfaces en algunos casos. En nuestro ejemplo de la clase Bird, implementamos las interfaces Animal y Flying, pero no es necesario. Simplemente podemos dejar que nuestra interfaz Flying extienda la interfaz Animal, y obtendremos los mismos resultados:

1
2
3
4
public interface Flying extends Animal {
    public void flapWings();
    public void fly();
}

Y la clase Pájaro:

1
2
3
public class Bird implements Fly{
    // the same code as earlier   
}

El código tanto de la interfaz Flying como de la clase Bird permanece igual, lo único que cambia son líneas individuales dentro de ambos:

  • Flying ahora extiende Animal y
  • Bird implementa solo la interfaz Flying (y la interfaz Animal por extensión)

El método Principal que usamos para mostrar cómo instanciar estos objetos y usarlos también permanece igual que antes.

{.icon aria-hidden=“true”}

Nota: Cuando nuestra interfaz ‘Flying’ amplió la interfaz ‘Animal’, no necesitábamos definir todos los métodos indicados en la interfaz ‘Animal’; estarán disponibles de forma predeterminada, lo que es realmente el punto de extender dos interfaces.

Esto une ‘Flying’ y ‘Animal’ juntos. Esto podría ser lo que desea, pero también podría no ser lo que desea. Dependiendo de su caso de uso específico, si puede garantizar que cualquier cosa que vuele también debe ser un animal, es seguro unirlos. Sin embargo, si no está seguro de que lo que vuela debe ser un animal, no extienda Animal con Flying.

Interfaces frente a clases abstractas

Ya que hemos discutido las interfaces en abundancia en esta guía, mencionemos rápidamente cómo se comparan con clases abstractas, ya que esta distinción plantea muchas preguntas y hay similitudes entre ellas. Una clase abstracta le permite crear una funcionalidad que las subclases pueden implementar o anular. Una clase puede extender solo una clase abstracta a la vez. En la siguiente tabla, haremos una pequeña comparación de ambos y veremos los pros y los contras de usar interfaces y clases abstractas:


Clase abstracta de interfaz Solo puede tener métodos abstractos `públicos`. Todo lo que se define dentro de una interfaz se supone `público` Puede tener métodos `protegidos` y `públicos` La palabra clave `abstract` cuando se declaran métodos es opcional La palabra clave `abstract` cuando se declaran métodos es obligatoria Puede extender múltiples interfaces a la vez Puede extender solo una clase o una clase abstracta a la vez Puede heredar múltiples interfaces, pero no puede heredar una clase. Puede heredar una clase y múltiples interfaces. Una clase puede implementar múltiples interfaces Una clase solo puede heredar una clase abstracta No se pueden declarar constructores/destructores Se pueden declarar constructores/destructores Se usa para hacer una especificación que una clase debe obedecer Se usa para definir la identidad de una clase


Métodos predeterminados en las interfaces {#métodos predeterminados en las interfaces}

¿Qué sucede cuando crea un sistema, lo deja en vivo en producción y luego decide que tiene que actualizar una interfaz agregando un método? También debe actualizar todas las clases que lo implementan; de lo contrario, todo se detiene. Para permitir que los desarrolladores actualicen las interfaces con nuevos métodos sin romper el código existente, puede usar métodos predeterminados, que le permiten pasar por alto el límite de definir cuerpos de métodos en las interfaces.

A través de los métodos predeterminados, puede definir el cuerpo de un nuevo método común que se implementará en todas las clases, que luego se agregará automáticamente como el comportamiento predeterminado de todas las clases sin romperlas y sin implementarlas explícitamente. Esto significa que puede actualizar interfaces extendidas por cientos de clases, sin refactorizar.

{.icon aria-hidden=“true”}

Nota: El uso de métodos “predeterminados” está destinado a actualizar las interfaces existentes para preservar la compatibilidad con versiones anteriores, no para agregarlos desde el principio. Si está en la etapa de diseño, no use métodos “predeterminados”, solo cuando agregue una funcionalidad imprevista que no pudo haber implementado antes.

Digamos que su cliente está muy contento con su aplicación, pero se han dado cuenta de que las aves no solo vuelan() y flapWings() además de lo que hacen otros animales. ¡También bucean()! Ya implementó Crow, Pidgeon, Blackbird y Woodpecker.

La refactorización es molesta y difícil, y debido a la arquitectura que hiciste, es difícil implementar un dive() en todas las aves antes de que llegue la fecha límite. Puedes implementar un método default void dive() en la interfaz Flying.

1
2
3
4
5
public interface Flying {
    public void flapWings();
    public void fly();
    default void dive() {System.out.println("The bird is diving from the air!"}
}

Ahora, dentro de nuestra clase Bird, podemos simplemente omitir la implementación del método dive(), dado que ya hemos definido su comportamiento predeterminado en la interfaz:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
public class Bird implements Fly{
    public String name;

    public Bird(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }
    
    public void fly() {
        System.out.println(getName() + " is flying!");
    }

    public void flapWings(){
        System.out.println("The " + getName() + " is flapping its wings!");
    }
}

Una instancia de Bird puede dive() ahora, sin ninguna refactorización de la clase Bird, dándonos el tiempo que tanto necesitamos para implementarlo de una manera elegante y sin prisas:

1
2
Bird bird = new Bird("Crow");
bird.dive();

Esto resulta en:

1
The bird is diving from the air!

Métodos estáticos en interfaces

Finalmente, ¡también podemos definir métodos estáticos en las interfaces! Dado que estos no pertenecen a ninguna instancia específica, no se pueden anular y se les llama prefijándolos con el nombre de la interfaz.

Los métodos de interfaz estática se utilizan para métodos comunes de utilidad/ayuda, no para implementar una funcionalidad específica. El soporte se agregó para evitar tener clases auxiliares no instanciables además de las interfaces y agrupar los métodos auxiliares de clases separadas en interfaces. En efecto, el uso de métodos estáticos lo ayuda a evitar una definición de clase adicional que habría contenido algunos métodos auxiliares. En lugar de tener una interfaz Animal y AnimalUtils como clase auxiliar, ahora puede agrupar los métodos auxiliares de la clase AnimalUtils en métodos Animal estáticos.

Esto aumenta la cohesión en su arquitectura, ya que tiene menos clases y las que tiene son más linealmente separables.

Por ejemplo, digamos que le gustaría validar sus implementaciones de Animal, lo que signifique la validación para su aplicación específica (como verificar si un animal está registrado en un libro). Podrías definir esto como un método estático intrínseco de todos los Animals:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
interface Animal {
    public void walk();
    public void eat();
    public void sleep();
    public String makeNoise();
    
    static boolean checkBook(Animal animal, List book) {
        return book.contains(animal);
    }
}

La definición Perro es la misma que antes: no puede anular o alterar este método, y pertenece a la interfaz Animal. A continuación, puede utilizar la interfaz para comprobar si una instancia de ‘Perro’ pertenece a un libro arbitrario (por ejemplo, un registro de mascotas domésticas en una ciudad) a través del método de utilidad ‘Animal’:

1
2
3
4
5
6
7
Dog dog = new Dog("Shiba Inu");

boolean isInBook = Animal.checkBook(dog, new ArrayList());
System.out.println(isInBook); // false
        
isInBook = Animal.checkBook(dog, List.of(dog));
System.out.println(isInBook); // true

Interfaces funcionales

Las interfaces funcionales se introdujeron en Java 8 y representan una interfaz que contiene solo un único método abstracto dentro de ella. Puede definir sus propias interfaces funcionales, allí la plétora de interfaces funcionales integradas en Java, como Function, Predicate, UnaryOperator, BinaryOperator, Supplier, etc., son muy probables de cubrir sus necesidades. necesita fuera de la caja. Todo esto se puede encontrar dentro del paquete java.util.function. Sin embargo, no profundizaremos en estos, ya que en realidad no son el tema principal de esta guía.

If you'd like to read a holistic, in-depth and detailed guide to functional interfaces, read our "Guía de Interfaces Funcionales y Expresiones Lambda en Java"!

Convenciones de nombres de interfaz

Entonces, ¿cómo se nombran las interfaces? No hay una regla establecida y, según el equipo con el que esté trabajando, es posible que vea diferentes convenciones. Algunos desarrolladores prefijan los nombres de las interfaces con I, como IAnimal. Esto no es muy común entre los desarrolladores de Java y se transmite principalmente de los desarrolladores que trabajaron en otros ecosistemas antes.

Java tiene una convención de nomenclatura clara. Por ejemplo, List es una interfaz, mientras que ArrayList, LinkedList, etc. son implementaciones de esa interfaz. Además, algunas interfaces describen las capacidades de una clase, como Ejecutable, Comparable y Serializable. Depende principalmente de cuáles sean las intenciones de su interfaz:

  • Si su interfaz es una columna vertebral genérica para una familia común de clases donde cada conjunto puede describirse con bastante precisión por su familia, asígnele el nombre de la familia, como Set, y luego implemente un LinkedHashSet .
  • Si su interfaz es una columna vertebral genérica para una familia común de clases donde cada conjunto no se puede describir con bastante precisión por su familia, asígnele el nombre de la familia, como “Animal”, y luego implemente un “Pájaro”. , en lugar de un Animal volador (porque esa no es una buena descripción).
  • Si su interfaz se usa para describir las habilidades de una clase, nómbrela como una habilidad, como Ejecutable, Comparable.
  • Si su interfaz se usa para describir un servicio, nómbrelo como el servicio, como UserDAO y luego implemente un UserDaoImpl.

Conclusión

En esta guía, hemos cubierto uno de los conceptos básicos más importantes para la programación orientada a objetos en Java. Hemos explicado qué son las interfaces y discutido sus ventajas y desventajas. También mostramos cómo definirlos y usarlos en algunos ejemplos simples, cubriendo herencias múltiples y herencia de interfaz. Discutimos las diferencias y similitudes entre las interfaces y las clases abstractas, los métodos predeterminados y estáticos, las convenciones de nomenclatura y las interfaces funcionales.

Las interfaces son estructuras bastante simples con un objetivo simple en mente, pero son una herramienta muy poderosa que debe utilizarse siempre que se presente la oportunidad para que el código se vuelva más legible y claro.

Licensed under CC BY-NC-SA 4.0