Definición e implantación
Veamos un ejemplo de definición de interfaces. Supongamos que tenemos que desarrollar un software sobre simulación de sonidos de animales. Algunos de estos tienen la capacidad de emitir sonidos y otros no. Por lo tanto, tendrán el método:
String voz();
Sin embargo, cada animal ejecutará de manera distinta este método según su clase.
En Java, existe una forma de expresar esta capacidad común de algunas clases. Estas funcionalidades comunes las llamamos interfaces. Para definir una interfaz se utiliza la palabra clave interface.
interface MiInterfaz {
// Métodos de la interfaz sin definir
String metodo1();
String metodo2();
String metodo3();
}
En nuestro ejemplo hablaríamos de la interfaz Sonido que tiene el método voz(). Por ejemplo, las clases Perro, Gato y Lobo podrían implementar la interfaz Sonido, mientras que otras clases de animales como Caracol no.
Ejemplo de la definición de la interfaz Sonido:
public interface Sonido {
String voz();
}
El diagrama UML equivalente sería el siguiente:
En las interfaces se definen los métodos de los que constarán las clases que la implementen. Solo se escribe el prototipo, no una implementación concreta del método. En la definición de cada clase se indica qué hará en cada caso cada uno de los métodos definidos.
Una interfaz puede consistir en más de un método.
Implementación
Para que una clase implemente una interfaz, después del nombre de la clase añadimos la palabra reservada implements y el nombre de la interfaz que queremos implementar.
Por ejemplo, tenemos la clase Perro que implementa la interfaz Sonido:
public class Perro implements Sonido {
public String voz() {
return "Guau";
}
}
La implementación de Gato para la interfaz Sonido, por ejemplo, podría ser la siguiente:
public class Gato implements Sonido {
public String voz() {
return "Miau";
}
}
Si queremos implementar más de una interfaz, después de la palabra implements indicamos todas las interfaces separadas por comas.
public class NombreClase implements Interfaz1, Interfaz2, Interfaz3 {
// ....
}
Atributos en interfaces
Los atributos de una interfaz tienen características especiales comparados con los atributos de una clase:
- Son públicos, estáticos y finales por defecto: No es necesario especificar estos modificadores (
public,static,final) explícitamente, ya que Java los añade automáticamente. Esto significa que son constantes (no se pueden modificar después de ser inicializados), compartidos por todas las clases que implementan la interfaz, y accesibles directamente a través del nombre de la interfaz. - Deben ser inicializados: Ya que son
final, los atributos de una interfaz deben ser inicializados en el momento de su declaración. - Se acceden directamente con la interfaz: No necesitas una instancia de la clase que implementa la interfaz para acceder al atributo. Se puede usar el nombre de la interfaz directamente.
public interface EjemploInterfaz {
// Declaración de atributos (constantes)
// Ambos son public, static y final por defecto
int ATRIBUTO_CONSTANTE = 10;
String MENSAJE = "Hola desde la interfaz";
}
Interfaces con métodos por defecto
En Java 8 se introdujeron las interfaces con métodos por defecto (default methods) que trajeron grandes mejoras en la flexibilidad de las interfaces.
Los métodos por defecto permiten que las interfaces tengan implementaciones por defecto para ciertos métodos. Esto evita que las clases que implementan esa interfaz tengan que proporcionar una implementación para cada método, algo que antes obligaba a todas las clases a implementar los métodos de la interfaz, incluso si no los necesitaban.
Para declarar un método por defecto dentro de una interfaz usamos la palabra clave default, seguida de la implementación del método.
public interface Vehiculo {
// Método abstracto, las clases que implementen Vehiculo deben implementarlo
void conducir();
// Método por defecto, la clase que implementa la interfaz no necesita implementarlo
default void frenar() {
System.out.println("El vehículo está frenando.");
}
}
public class Coche implements Vehiculo {
@Override
public void conducir() {
System.out.println("El coche está en marcha.");
}
// No es necesario sobrescribir el método 'frenar' porque ya tiene una implementación por defecto
}
public class Bicicleta implements Vehiculo {
@Override
public void conducir() {
System.out.println("La bicicleta está pedaleando.");
}
// Podemos sobrescribir el método por defecto si queremos cambiar su comportamiento
@Override
public void frenar() {
System.out.println("La bicicleta está frenando con los frenos de mano.");
}
}
public class Main {
public static void main(String[] args) {
Vehiculo coche = new Coche();
coche.conducir(); // "El coche está en marcha."
coche.frenar(); // "El vehículo está frenando."
Vehiculo bicicleta = new Bicicleta();
bicicleta.conducir(); // "La bicicleta está pedaleando."
bicicleta.frenar(); // "La bicicleta está frenando con los frenos de mano."
}
}
Las clases que implementan la interfaz pueden sobrescribir el método por defecto si desean un comportamiento diferente. Si no se sobrescriben, las clases tienen acceso a la implementación por defecto.
Métodos estáticos en interfaces
En Java 8 también se introdujeron los métodos estáticos (static methods), que trajeron grandes mejoras en la flexibilidad de las interfaces.
public interface Calculadora {
// Método estático en interface
static int sumar(int a, int b) {
return a + b;
}
}
public class Main {
public static void main(String[] args) {
// Llamamos al método estático a través de la interfaz
int resultado = Calculadora.sumar(3, 5);
System.out.println("La suma es: " + resultado); // Salida: La suma es: 8
}
}
Los métodos estáticos en interfaces son similares a los de clases normales. La diferencia es que se definen dentro de una interfaz, pero no están relacionados con el objeto que implementa la interfaz. Solo pueden ser llamados a través de la propia interfaz, no de las instancias de las clases que la implementan.
interface Calculadora {
// Método estático en interface
static int sumar(int a, int b) {
return a + b;
}
}
class CalculadoraDigital implements Calculadora {
public int restar(int a, int b) {
return a - b;
}
}
public class Main {
public static void main(String[] args) {
CalculadoraDigital calc = new CalculadoraDigital();
int resta = calc.restar(10, 4); // Esto es correcto
int sumaError = calc.sumar(7, 2); // Error: sumar es un método estático de la interfaz
int suma = Calculadora.sumar(3, 5); // Esto es correcto
}
}
Las características de los métodos estáticos son:
- No son polimórficos: No se pueden sobrescribir en las clases que implementan la interfaz.
- Accedidos a través de la interfaz: Se llaman usando la propia interfaz, no a través de las instancias de las clases que la implementan.
- Usados para utilidades relacionadas con el comportamiento de la interfaz: A menudo se usarán para proporcionar métodos de ayuda o utilitarios que no dependen de las instancias de la clase.
Interfaces tipadas
En Java, una interfaz tipada se refiere al uso de generics para definir interfaces que pueden trabajar con diferentes tipos de datos de manera segura y flexible. Esto les permite ser reutilizables mientras mantienen la seguridad de tipos en tiempo de compilación.
Define uno o más parámetros de tipo, permitiendo que las implementaciones especifiquen los tipos concretos.
// Definición de una interfaz tipada
public interface Operacion<T> {
T ejecutar(T valor1, T valor2);
}
// Implementación de la interfaz tipada
class Suma implements Operacion<Integer> {
@Override
public Integer ejecutar(Integer valor1, Integer valor2) {
return valor1 + valor2;
}
}
public class Main {
public static void main(String[] args) {
Operacion<Integer> suma = new Suma();
System.out.println("Resultado: " + suma.ejecutar(10, 20));
}
}
Restricción de tipo genérico con interfaces
La restricción de tipo genérico (bounded type parameter) sirve para limitar qué tipos se pueden usar al crear una clase genérica.
Cuando escribes <T extends EjemploInterfaz> estás diciendo que la clase genérica sólo puede trabajar con tipos (T) que implementen la interfaz EjemploInterfaz.
Así, T no puede ser cualquier cosa, sino únicamente clases que cumplan (implementen) esa interfaz.
Esto permite garantizar que los objetos pasados como tipo genérico cumplan ciertos requisitos, como tener métodos específicos.
interface EjemploInterfaz {
void realizarAccion();
}
class ClaseGenerica<T extends EjemploInterfaz> {
private T elemento;
public void agregarElemento(T elemento) {
this.elemento = elemento;
}
public void ejecutarAccion() {
if (elemento != null) {
elemento.realizarAccion();
} else {
System.out.println("No hay ningún elemento para realizar la acción.");
}
}
}
Otro ejemplo más completo:
interface Animal {
void hacerSonido();
}
class Perro implements Animal {
@Override
public void hacerSonido() {
System.out.println("Guau guau!");
}
}
class Gato implements Animal {
@Override
public void hacerSonido() {
System.out.println("Miau!");
}
}
class Jaula<T extends Animal> {
private T animal;
public void meterAnimal(T animal) {
this.animal = animal;
}
public void escucharAnimal() {
if (animal != null) {
animal.hacerSonido(); // permitido porque T siempre es un Animal
} else {
System.out.println("La jaula está vacía.");
}
}
}
public class Main {
public static void main(String[] args) {
Jaula<Perro> jaulaPerro = new Jaula<>();
jaulaPerro.meterAnimal(new Perro());
jaulaPerro.escucharAnimal(); // imprime "Guau guau!"
Jaula<Gato> jaulaGato = new Jaula<>();
jaulaGato.meterAnimal(new Gato());
jaulaGato.escucharAnimal(); // imprime "Miau!"
// Utilizando la interfaz Animal
// La jaula acepta cualquier tipo de animal
Jaula<Animal> jaulaAnimal = new Jaula<>();
jaulaAnimal.meterAnimal(new Perro());
jaulaAnimal.escucharAnimal(); // imprime "Guau guau!"
}
}
Casos de uso reales de interfaces
A continuación, se muestran varios ejemplos de usos reales de interfaces.
Definir un contrato común para diferentes tipos de pago
En una app de compras, los usuarios pueden pagar con tarjeta, PayPal o criptomonedas. Usamos una interfaz para definir qué debe hacer cualquier método de pago.
interface MetodoPago {
void pagar(double cantidad);
}
class PagoTarjeta implements MetodoPago {
@Override
public void pagar(double cantidad) {
System.out.println("Pagando " + cantidad + "€ con tarjeta.");
}
}
class PagoPayPal implements MetodoPago {
@Override
public void pagar(double cantidad) {
System.out.println("Pagando " + cantidad + "€ con PayPal.");
}
}
El sistema de checkout puede usar cualquier método de pago sin importar su tipo:
MetodoPago pago = new PagoPayPal();
pago.pagar(49.99);
Sistemas de notificaciones
Una aplicación envía notificaciones por email, SMS o push. Cada tipo de notificación debe implementar el mismo comportamiento básico.
interface Notificador {
void enviar(String mensaje);
}
class NotificadorEmail implements Notificador {
@Override
public void enviar(String mensaje) {
System.out.println("Enviando email: " + mensaje);
}
}
class NotificadorSMS implements Notificador {
@Override
public void enviar(String mensaje) {
System.out.println("Enviando SMS: " + mensaje);
}
}
Esto permite cambiar el canal de notificación sin tocar el código principal.
Plugins o extensiones
Una aplicación permite agregar módulos adicionales desarrollados por terceros (como en un IDE o videojuego).
Cada módulo debe seguir una interfaz común:
interface Plugin {
void inicializar();
void ejecutar();
}
Los plugins implementan esa interfaz y el programa los carga dinámicamente.
Controladores de entrada en videojuegos
Un juego puede recibir input de teclado, mando o pantalla táctil. Cada tipo de control implementa la misma interfaz.
interface Controlador {
void moverJugador(String direccion);
}
Esto permite cambiar el método de entrada sin modificar la lógica del juego.
Acceso a bases de datos
Un programa puede usar diferentes motores de base de datos (MySQL, PostgreSQL, SQLite). Cada uno implementa la misma interfaz de acceso a datos:
interface RepositorioUsuario {
Usuario obtenerPorId(int id);
void guardar(Usuario usuario);
}