Java
Tutorial Java: Interfaces
Java interfaces: definición y uso. Aprende a definir y usar interfaces en Java con ejemplos prácticos y detallados.
Aprende Java y certifícateDefinición e implementación básica de interfaces
Las interfaces proporcionan un mecanismo para definir contratos que las clases deben cumplir. A diferencia de las clases, las interfaces establecen qué debe hacer una clase sin especificar cómo debe hacerlo.
Una interfaz se define mediante la palabra clave interface
y contiene métodos abstractos (sin implementación), constantes y, desde versiones más recientes de Java, también métodos con implementación por defecto. Las clases que implementan una interfaz se comprometen a proporcionar implementaciones para todos los métodos abstractos declarados en ella.
Para declarar una interfaz en Java, se utiliza la siguiente sintaxis:
public interface NombreInterfaz {
// Constantes
// Métodos abstractos
}
Por ejemplo, podemos crear una interfaz Dibujable
que defina un contrato para objetos que pueden ser dibujados:
public interface Dibujable {
void dibujar();
String obtenerColor();
}
En este ejemplo, cualquier clase que implemente la interfaz Dibujable
deberá proporcionar implementaciones para los métodos dibujar()
y obtenerColor()
.
Para implementar una interfaz, una clase utiliza la palabra clave implements
seguida del nombre de la interfaz:
public class Circulo implements Dibujable {
private String color;
private double radio;
public Circulo(String color, double radio) {
this.color = color;
this.radio = radio;
}
@Override
public void dibujar() {
System.out.println("Dibujando un círculo de radio " + radio);
}
@Override
public String obtenerColor() {
return color;
}
}
Al implementar una interfaz, se deben sobrescribir todos los métodos abstractos definidos en ella. Si una clase no implementa todos los métodos, debe declararse como abstracta.
Las interfaces en Java tienen varias características importantes:
- No pueden ser instanciadas directamente. No se puede crear un objeto a partir de una interfaz usando el operador
new
. - Todos los métodos son implícitamente
public
yabstract
(a menos que se especifique que son métodos default o static). - Todas las constantes son implícitamente
public
,static
yfinal
. - No pueden contener constructores.
Las constantes en interfaces se definen así:
public interface Configurable {
int TIEMPO_ESPERA = 1000; // Implícitamente public, static y final
String SERVIDOR_PREDETERMINADO = "localhost";
void configurar(String servidor);
boolean estaConfigurado();
}
Las interfaces también pueden extender otras interfaces utilizando la palabra clave extends
:
public interface DibujableEnCanvas extends Dibujable {
void establecerCanvas(String canvas);
String obtenerCanvas();
}
En este caso, cualquier clase que implemente DibujableEnCanvas
deberá proporcionar implementaciones para los métodos de ambas interfaces: dibujar()
y obtenerColor()
de Dibujable
, y establecerCanvas()
y obtenerCanvas()
de DibujableEnCanvas
.
Un aspecto de las interfaces es que permiten implementar un tipo de polimorfismo sin necesidad de herencia de clases. Por ejemplo:
public class GestorDibujo {
public void procesarDibujable(Dibujable elemento) {
System.out.println("Procesando elemento de color: " + elemento.obtenerColor());
elemento.dibujar();
}
}
Este método procesarDibujo
puede recibir cualquier objeto que implemente la interfaz Dibujable
, independientemente de su clase concreta:
public class EjemploInterfaces {
public static void main(String[] args) {
GestorDibujo gestor = new GestorDibujo();
Dibujable circulo = new Circulo("rojo", 5.0);
Dibujable rectangulo = new Rectangulo("azul", 10.0, 20.0);
gestor.procesarDibujable(circulo);
gestor.procesarDibujable(rectangulo);
}
}
Las interfaces también se utilizan para implementar el patrón de diseño Callback. Por ejemplo, podemos definir una interfaz para manejar eventos:
public interface ManejadorEventos {
void onEvento(String tipoEvento, Object datos);
}
Y luego implementarla en diferentes clases:
public class LoggerEventos implements ManejadorEventos {
@Override
public void onEvento(String tipoEvento, Object datos) {
System.out.println("LOG: " + tipoEvento + " - " + datos);
}
}
public class NotificadorEventos implements ManejadorEventos {
@Override
public void onEvento(String tipoEvento, Object datos) {
System.out.println("NOTIFICACIÓN: Se ha producido un " + tipoEvento);
// Código para enviar notificaciones
}
}
Un caso de uso común para las interfaces es la implementación de comparadores personalizados. La interfaz Comparator
de Java permite definir criterios de ordenación para colecciones:
import java.util.Comparator;
public class ComparadorPorPrecio implements Comparator<Producto> {
@Override
public int compare(Producto p1, Producto p2) {
return Double.compare(p1.getPrecio(), p2.getPrecio());
}
}
Este comparador se puede utilizar para ordenar una lista de productos por precio:
List<Producto> productos = new ArrayList<>();
// Añadir productos a la lista
productos.sort(new ComparadorPorPrecio());
Las interfaces en Java también pueden contener clases anidadas, que pueden ser útiles para agrupar tipos relacionados:
public interface Reproductor {
void reproducir();
void pausar();
void detener();
interface Controlable {
void ajustarVolumen(int nivel);
void silenciar(boolean activar);
}
}
Una clase puede implementar ambas interfaces:
public class ReproductorMP3 implements Reproductor, Reproductor.Controlable {
@Override
public void reproducir() {
System.out.println("Reproduciendo música");
}
@Override
public void pausar() {
System.out.println("Música en pausa");
}
@Override
public void detener() {
System.out.println("Reproducción detenida");
}
@Override
public void ajustarVolumen(int nivel) {
System.out.println("Volumen ajustado a: " + nivel);
}
@Override
public void silenciar(boolean activar) {
System.out.println("Silencio: " + (activar ? "activado" : "desactivado"));
}
}
A diferencia de las clases, en Java una clase puede implementar múltiples interfaces, lo que proporciona una forma de conseguir algo similar a la herencia múltiple:
public class SmartTV implements Reproductor, Configurable, Dibujable {
// Implementaciones de todos los métodos requeridos
}
Métodos default y static en interfaces
Las interfaces en Java evolucionaron mucho a partir de Java 8 con la introducción de los métodos default y métodos static. Estas características ampliaron las posibilidades de las interfaces, permitiéndoles proporcionar implementaciones concretas sin romper la compatibilidad con el código existente.
Métodos default
Los métodos default (o métodos por defecto) permiten añadir nuevas funcionalidades a las interfaces sin obligar a todas las clases que las implementan a proporcionar una implementación. Se declaran utilizando la palabra clave default
y deben incluir un cuerpo de método:
public interface Notificador {
void enviarNotificacion(String mensaje);
default void enviarNotificacionUrgente(String mensaje) {
System.out.println("URGENTE: " + mensaje);
enviarNotificacion(mensaje);
}
}
En este ejemplo, cualquier clase que implemente la interfaz Notificador
debe proporcionar una implementación para enviarNotificacion()
, pero puede utilizar la implementación predeterminada de enviarNotificacionUrgente()
o sobrescribirla si necesita un comportamiento diferente:
public class NotificadorEmail implements Notificador {
@Override
public void enviarNotificacion(String mensaje) {
System.out.println("Enviando email: " + mensaje);
}
// No es necesario implementar enviarNotificacionUrgente()
}
public class NotificadorSMS implements Notificador {
@Override
public void enviarNotificacion(String mensaje) {
System.out.println("Enviando SMS: " + mensaje);
}
@Override
public void enviarNotificacionUrgente(String mensaje) {
System.out.println("Enviando SMS prioritario: " + mensaje);
// Lógica específica para SMS urgentes
}
}
Los métodos default son útiles para:
- Evolucionar APIs sin romper la compatibilidad con implementaciones existentes
- Proporcionar métodos de utilidad que operan sobre los métodos abstractos de la interfaz
- Implementar el patrón Template Method donde se define un algoritmo con pasos personalizables
Dentro de un método default, se puede acceder a:
- Otros métodos de la interfaz (default o abstractos)
- Constantes definidas en la interfaz
- Métodos estáticos de la interfaz o de otras clases
public interface Validador {
boolean validar(String dato);
default boolean validarNoVacio(String dato) {
return dato != null && !dato.isEmpty() && validar(dato);
}
default String normalizarYValidar(String dato) {
if (dato == null) {
return null;
}
String normalizado = dato.trim().toLowerCase();
return validar(normalizado) ? normalizado : null;
}
}
Métodos static
Los métodos static en interfaces proporcionan funcionalidades de utilidad relacionadas con la interfaz sin requerir una instancia. Se declaran con la palabra clave static
y, al igual que los métodos default, incluyen un cuerpo de implementación:
public interface Convertidor {
double convertir(double valor);
static Convertidor invertir(Convertidor convertidor) {
return valor -> 1.0 / convertidor.convertir(valor);
}
static Convertidor componer(Convertidor primero, Convertidor segundo) {
return valor -> segundo.convertir(primero.convertir(valor));
}
}
Los métodos estáticos de una interfaz se invocan utilizando el nombre de la interfaz, no a través de una instancia:
public class EjemploConvertidores {
public static void main(String[] args) {
Convertidor celsiusAFahrenheit = celsius -> (celsius * 9/5) + 32;
// Usando el método estático invertir
Convertidor fahrenheitACelsius = Convertidor.invertir(
fahrenheit -> (fahrenheit - 32) * 5/9
);
// Usando el método estático componer
Convertidor celsiusAKelvin = Convertidor.componer(
celsiusAFahrenheit,
fahrenheit -> (fahrenheit + 459.67) * 5/9
);
double tempCelsius = 25.0;
System.out.println(tempCelsius + "°C = " +
celsiusAFahrenheit.convertir(tempCelsius) + "°F");
}
}
Los métodos estáticos en interfaces son útiles para:
- Proporcionar métodos de fábrica para crear instancias de implementaciones comunes
- Ofrecer utilidades relacionadas con la funcionalidad de la interfaz
- Agrupar operaciones relacionadas en un espacio de nombres lógico
Un ejemplo práctico de métodos estáticos es la interfaz Comparator
de Java, que proporciona métodos como comparing()
, naturalOrder()
y reverseOrder()
:
import java.util.Comparator;
import java.util.List;
import java.util.ArrayList;
public class EjemploComparadores {
public static void main(String[] args) {
List<Producto> productos = new ArrayList<>();
productos.add(new Producto("Laptop", 1200.0));
productos.add(new Producto("Teléfono", 800.0));
productos.add(new Producto("Tablet", 500.0));
// Usando método estático comparing
productos.sort(Comparator.comparing(Producto::getPrecio));
// Usando método estático reverseOrder
Comparator<Producto> porPrecioDescendente =
Comparator.comparing(Producto::getPrecio, Comparator.reverseOrder());
productos.sort(porPrecioDescendente);
}
}
class Producto {
private String nombre;
private double precio;
public Producto(String nombre, double precio) {
this.nombre = nombre;
this.precio = precio;
}
public String getNombre() { return nombre; }
public double getPrecio() { return precio; }
}
Diferencias con clases abstractas
Aunque los métodos default hacen que las interfaces se parezcan más a las clases abstractas, existen diferencias:
- Las interfaces no pueden tener estado (campos de instancia), mientras que las clases abstractas sí pueden.
- Las interfaces no pueden tener constructores.
- Los métodos default en interfaces no pueden acceder a estado mutable, ya que no hay campos de instancia.
- Una clase puede implementar múltiples interfaces pero solo puede extender una clase abstracta.
public abstract class FiguraGeometrica {
protected String color; // Estado
public FiguraGeometrica(String color) { // Constructor
this.color = color;
}
public abstract double calcularArea();
public void mostrarInfo() {
System.out.println("Figura de color " + color +
" con área " + calcularArea());
}
}
public interface Dibujable {
void dibujar();
default void dibujarConBorde() {
System.out.println("Dibujando borde...");
dibujar();
System.out.println("Borde completado");
}
static boolean esVisible(int opacidad) {
return opacidad > 0;
}
}
Casos de uso prácticos
Los métodos default y static en interfaces se utilizan mucho en la API estándar de Java:
- La interfaz
Collection
incluye métodos default comoremoveIf()
,stream()
yforEach()
. - La interfaz
List
proporciona métodos default comoreplaceAll()
ysort()
. - La interfaz
Comparator
ofrece métodos static comocomparing()
,thenComparing()
ynullsFirst()
.
Un ejemplo práctico de uso de métodos default es la implementación de un sistema de validación flexible:
public interface Validador<T> {
boolean esValido(T valor);
default Validador<T> and(Validador<T> otro) {
return valor -> this.esValido(valor) && otro.esValido(valor);
}
default Validador<T> or(Validador<T> otro) {
return valor -> this.esValido(valor) || otro.esValido(valor);
}
default Validador<T> not() {
return valor -> !this.esValido(valor);
}
static <T> Validador<T> siempre(boolean resultado) {
return valor -> resultado;
}
}
Este diseño permite crear validadores complejos mediante composición:
public class ValidacionEjemplo {
public static void main(String[] args) {
Validador<String> noVacio = s -> s != null && !s.isEmpty();
Validador<String> soloLetras = s -> s.matches("[a-zA-Z]+");
Validador<String> longitudMinima = s -> s.length() >= 5;
// Componiendo validadores con métodos default
Validador<String> validadorNombre = noVacio
.and(soloLetras)
.and(longitudMinima);
System.out.println(validadorNombre.esValido("Juan")); // false (longitud < 5)
System.out.println(validadorNombre.esValido("Carlos")); // true
System.out.println(validadorNombre.esValido("Ana123")); // false (tiene números)
// Usando el método estático
Validador<String> aceptarTodo = Validador.siempre(true);
Validador<String> rechazarTodo = Validador.siempre(false);
}
}
Implementación de múltiples interfaces
A diferencia de la herencia de clases, donde una clase solo puede extender una única superclase (herencia simple), Java permite que una clase implemente cualquier número de interfaces, lo que proporciona una forma de conseguir funcionalidad similar a la herencia múltiple sin sus complicaciones tradicionales.
La sintaxis para implementar múltiples interfaces es sencilla, utilizando la palabra clave implements
seguida de los nombres de las interfaces separados por comas:
public class MiClase implements Interface1, Interface2, Interface3 {
// Implementación de todos los métodos requeridos por las tres interfaces
}
Esta capacidad resulta útil cuando se desea que una clase adopte diferentes "roles" o "comportamientos" sin estar limitada por una jerarquía de herencia rígida. Veamos un ejemplo práctico:
public interface Nadador {
void nadar();
default void sumergirse() {
System.out.println("Sumergiéndose bajo el agua");
}
}
public interface Volador {
void volar();
double calcularAlturaMaxima();
}
public interface Cazador {
void cazar();
boolean estaEnBusqueda();
}
public class Pato implements Nadador, Volador, Cazador {
private boolean buscandoComida;
@Override
public void nadar() {
System.out.println("El pato nada en el lago");
}
@Override
public void volar() {
System.out.println("El pato vuela sobre el agua");
}
@Override
public double calcularAlturaMaxima() {
return 100.0; // metros
}
@Override
public void cazar() {
System.out.println("El pato caza pequeños peces e insectos");
this.buscandoComida = true;
}
@Override
public boolean estaEnBusqueda() {
return buscandoComida;
}
}
En este ejemplo, la clase Pato
implementa tres interfaces diferentes, cada una representando un comportamiento distinto. Esto permite que un objeto Pato
pueda ser tratado como un Nadador
, un Volador
o un Cazador
según el contexto, lo que facilita un diseño más flexible y modular.
Ventajas de la implementación múltiple
La implementación de múltiples interfaces ofrece varias ventajas:
- Flexibilidad en el diseño: Permite que una clase adopte diferentes comportamientos sin estar restringida por una única línea de herencia.
- Separación de responsabilidades: Cada interfaz puede representar un aspecto específico del comportamiento de una clase.
- Polimorfismo mejorado: Una clase puede ser tratada como cualquiera de las interfaces que implementa.
- Reutilización de código: Se pueden combinar interfaces existentes para crear nuevos comportamientos.
public class SistemaEcologico {
public void registrarComportamientoAcuatico(Nadador animal) {
System.out.println("Registrando comportamiento acuático:");
animal.nadar();
}
public void registrarVuelo(Volador animal) {
System.out.println("Registrando vuelo a altura: " +
animal.calcularAlturaMaxima() + " metros");
animal.volar();
}
public void observarCaza(Cazador animal) {
System.out.println("Observando comportamiento de caza:");
animal.cazar();
System.out.println("¿Sigue buscando presa? " + animal.estaEnBusqueda());
}
}
Este sistema puede trabajar con cualquier objeto que implemente las interfaces correspondientes, sin importar su clase concreta:
public class EjemploSistema {
public static void main(String[] args) {
SistemaEcologico sistema = new SistemaEcologico();
Pato pato = new Pato();
sistema.registrarComportamientoAcuatico(pato);
sistema.registrarVuelo(pato);
sistema.observarCaza(pato);
// También funcionaría con otras clases que implementen estas interfaces
Aguila aguila = new Aguila();
sistema.registrarVuelo(aguila);
sistema.observarCaza(aguila);
}
}
Resolución de conflictos entre interfaces
Cuando se implementan múltiples interfaces, pueden surgir conflictos si dos o más interfaces declaran métodos con la misma firma. Estos conflictos deben resolverse explícitamente en la clase que implementa las interfaces.
Existen varios escenarios de conflicto:
- Métodos abstractos con la misma firma: La clase debe proporcionar una única implementación que satisfaga a todas las interfaces.
- Métodos default con la misma firma: La clase debe sobrescribir el método y decidir qué implementación utilizar o proporcionar una nueva.
public interface Reproductor {
void iniciar();
default void detener() {
System.out.println("Deteniendo reproducción estándar");
}
}
public interface Dispositivo {
void encender();
void apagar();
default void detener() {
System.out.println("Deteniendo dispositivo");
}
}
public class ReproductorMP3 implements Reproductor, Dispositivo {
@Override
public void iniciar() {
System.out.println("Iniciando reproducción de música");
}
@Override
public void encender() {
System.out.println("Encendiendo reproductor MP3");
}
@Override
public void apagar() {
System.out.println("Apagando reproductor MP3");
}
@Override
public void detener() {
// Resolviendo el conflicto entre los métodos default
Reproductor.super.detener(); // Usando la implementación de Reproductor
// Dispositivo.super.detener(); // Alternativa: usar la implementación de Dispositivo
// También podemos proporcionar una implementación completamente nueva
System.out.println("Pausando reproducción y guardando estado");
}
}
La sintaxis InterfaceNombre.super.metodo()
permite acceder específicamente a la implementación default de una interfaz particular, lo que facilita la resolución de conflictos.
Interfaces como tipos
Una característica de las interfaces es que pueden utilizarse como tipos en Java. Esto significa que una variable puede declararse del tipo de una interfaz y referenciar cualquier objeto que implemente dicha interfaz:
public class GestorMultimedia {
public void procesarContenido(List<Reproductor> reproductores) {
for (Reproductor reproductor : reproductores) {
reproductor.iniciar();
// Procesar contenido
reproductor.detener();
}
}
}
Este método puede recibir una lista de cualquier tipo de objeto que implemente la interfaz Reproductor
, independientemente de qué otras interfaces pueda implementar o de su clase concreta.
Patrones de diseño con múltiples interfaces
La implementación de múltiples interfaces facilita varios patrones de diseño comunes:
- Patrón Adaptador: Permite que una clase existente se adapte a una nueva interfaz sin modificar su código.
// Clase existente que no implementa nuestras interfaces
public class DispositivoExterno {
public void play() {
System.out.println("Reproduciendo en dispositivo externo");
}
public void stop() {
System.out.println("Deteniendo dispositivo externo");
}
}
// Adaptador que implementa nuestras interfaces
public class AdaptadorDispositivoExterno implements Reproductor, Dispositivo {
private DispositivoExterno dispositivo;
public AdaptadorDispositivoExterno(DispositivoExterno dispositivo) {
this.dispositivo = dispositivo;
}
@Override
public void iniciar() {
dispositivo.play();
}
@Override
public void detener() {
dispositivo.stop();
}
@Override
public void encender() {
System.out.println("Encendiendo dispositivo externo");
}
@Override
public void apagar() {
System.out.println("Apagando dispositivo externo");
}
}
- Patrón Decorador: Permite añadir funcionalidades a objetos existentes dinámicamente.
public interface Mensaje {
String getContenido();
}
public class MensajeTexto implements Mensaje {
private String texto;
public MensajeTexto(String texto) {
this.texto = texto;
}
@Override
public String getContenido() {
return texto;
}
}
// Decorador que implementa la misma interfaz
public class MensajeEncriptado implements Mensaje {
private Mensaje mensajeOriginal;
public MensajeEncriptado(Mensaje mensaje) {
this.mensajeOriginal = mensaje;
}
@Override
public String getContenido() {
return encriptar(mensajeOriginal.getContenido());
}
private String encriptar(String texto) {
// Lógica de encriptación simple
return "ENCRIPTADO[" + texto + "]";
}
}
Interfaces anidadas
Java también permite definir interfaces anidadas dentro de otras interfaces o clases. Cuando se implementan múltiples interfaces, también se pueden implementar estas interfaces anidadas:
public interface Dispositivo {
void encender();
void apagar();
interface Configurable {
void configurar(String parametro, String valor);
Map<String, String> obtenerConfiguracion();
}
}
public interface Reproductor {
void iniciar();
void detener();
interface ControlVolumen {
void subirVolumen();
void bajarVolumen();
int obtenerNivelVolumen();
}
}
public class SmartTV implements Dispositivo, Reproductor,
Dispositivo.Configurable, Reproductor.ControlVolumen {
private boolean encendido;
private int volumen = 10;
private Map<String, String> configuracion = new HashMap<>();
@Override
public void encender() {
encendido = true;
System.out.println("TV encendida");
}
@Override
public void apagar() {
encendido = false;
System.out.println("TV apagada");
}
@Override
public void iniciar() {
if (encendido) {
System.out.println("Iniciando reproducción en TV");
}
}
@Override
public void detener() {
System.out.println("Deteniendo reproducción en TV");
}
@Override
public void configurar(String parametro, String valor) {
configuracion.put(parametro, valor);
System.out.println("Configurando " + parametro + " = " + valor);
}
@Override
public Map<String, String> obtenerConfiguracion() {
return new HashMap<>(configuracion);
}
@Override
public void subirVolumen() {
if (volumen < 100) volumen += 5;
System.out.println("Volumen: " + volumen);
}
@Override
public void bajarVolumen() {
if (volumen > 0) volumen -= 5;
System.out.println("Volumen: " + volumen);
}
@Override
public int obtenerNivelVolumen() {
return volumen;
}
}
Interfaces y herencia
Es posible combinar la implementación de múltiples interfaces con la herencia de clases. Una clase puede extender otra clase y al mismo tiempo implementar varias interfaces:
public abstract class DispositivoElectronico {
protected boolean encendido;
public void encender() {
encendido = true;
System.out.println("Dispositivo encendido");
}
public void apagar() {
encendido = false;
System.out.println("Dispositivo apagado");
}
public boolean estaEncendido() {
return encendido;
}
}
public class TabletMultimedia extends DispositivoElectronico
implements Reproductor, Dispositivo.Configurable {
private Map<String, String> configuracion = new HashMap<>();
@Override
public void iniciar() {
if (estaEncendido()) {
System.out.println("Iniciando reproducción en tablet");
}
}
@Override
public void detener() {
System.out.println("Deteniendo reproducción en tablet");
}
@Override
public void configurar(String parametro, String valor) {
configuracion.put(parametro, valor);
}
@Override
public Map<String, String> obtenerConfiguracion() {
return new HashMap<>(configuracion);
}
// Sobrescribimos un método heredado
@Override
public void encender() {
super.encender(); // Llamamos al método de la superclase
System.out.println("Iniciando sistema operativo de la tablet");
}
}
En este ejemplo, TabletMultimedia
hereda comportamiento de DispositivoElectronico
y además implementa las interfaces Reproductor
y Dispositivo.Configurable
.
Consideraciones prácticas
Al implementar múltiples interfaces, se deben tener en cuenta algunas consideraciones importantes:
- Principio de responsabilidad única: Aunque una clase puede implementar muchas interfaces, es recomendable que cada interfaz represente una responsabilidad o capacidad bien definida.
- Cohesión: Las interfaces relacionadas pueden agruparse mediante herencia de interfaces.
- Evitar interfaces demasiado grandes: Es preferible tener varias interfaces pequeñas y específicas que una interfaz grande con muchos métodos.
- Documentación clara: Es importante documentar el propósito de cada interfaz y cómo se espera que interactúen entre sí.
// Ejemplo de interfaces cohesivas y específicas
public interface Autenticable {
boolean autenticar(String usuario, String contraseña);
void cerrarSesion();
}
public interface Autorizable {
boolean tienePermiso(String accion);
List<String> obtenerPermisos();
}
// Una clase puede implementar ambas si necesita ambas capacidades
public class Usuario implements Autenticable, Autorizable {
private String nombre;
private String contraseñaHash;
private List<String> permisos;
// Implementaciones de los métodos...
}
Interfaces funcionales y su papel en la programación funcional
Una interfaz funcional se define como aquella que contiene exactamente un método abstracto, aunque puede incluir cualquier número de métodos default o static. Este único método abstracto define la "firma funcional" que determina cómo se puede utilizar la interfaz en contextos de programación funcional.
Para identificar explícitamente una interfaz como funcional, se utiliza la anotación @FunctionalInterface
:
@FunctionalInterface
public interface Calculadora {
int calcular(int a, int b);
}
Esta anotación no es obligatoria, pero proporciona dos beneficios importantes: documenta la intención del diseño y permite que el compilador verifique que la interfaz cumpla con los requisitos (tener exactamente un método abstracto). Si se intenta añadir un segundo método abstracto a una interfaz anotada con @FunctionalInterface
, el compilador generará un error.
Expresiones lambda y interfaces funcionales
Las expresiones lambda proporcionan una sintaxis concisa para implementar interfaces funcionales sin necesidad de crear clases anónimas explícitas:
// Implementación tradicional con clase anónima
Calculadora suma = new Calculadora() {
@Override
public int calcular(int a, int b) {
return a + b;
}
};
// Implementación equivalente con expresión lambda
Calculadora sumaLambda = (a, b) -> a + b;
// Uso de la interfaz funcional
int resultado = sumaLambda.calcular(5, 3); // resultado = 8
La expresión lambda (a, b) -> a + b
implementa implícitamente el método calcular
de la interfaz Calculadora
. El compilador infiere los tipos de parámetros basándose en el contexto.
Interfaces funcionales predefinidas
Java proporciona un conjunto de interfaces funcionales predefinidas en el paquete java.util.function
que cubren los patrones más comunes. Estas interfaces están diseñadas para ser utilizadas como tipos para expresiones lambda y referencias a métodos:
Consumer<T>
: Acepta un argumento de tipo T y no devuelve ningún resultado.
Consumer<String> impresor = mensaje -> System.out.println("Mensaje: " + mensaje);
impresor.accept("Hola mundo"); // Imprime: Mensaje: Hola mundo
Supplier<T>
: No acepta argumentos pero produce un resultado de tipo T.
Supplier<LocalDateTime> obtenerFechaHora = () -> LocalDateTime.now();
LocalDateTime ahora = obtenerFechaHora.get(); // Obtiene la fecha y hora actual
Function<T, R>
: Acepta un argumento de tipo T y produce un resultado de tipo R.
Function<String, Integer> longitud = texto -> texto.length();
int tamaño = longitud.apply("Programación funcional"); // tamaño = 22
Predicate<T>
: Acepta un argumento de tipo T y devuelve un boolean.
Predicate<String> esMayorDeEdad = texto -> {
try {
return Integer.parseInt(texto) >= 18;
} catch (NumberFormatException e) {
return false;
}
};
boolean resultado = esMayorDeEdad.test("20"); // resultado = true
BiFunction<T, U, R>
: Acepta dos argumentos de tipos T y U, y produce un resultado de tipo R.
BiFunction<String, String, String> concatenar = (a, b) -> a + " " + b;
String nombreCompleto = concatenar.apply("Juan", "Pérez"); // nombreCompleto = "Juan Pérez"
UnaryOperator<T>
: Caso especial de Function donde el tipo de entrada y salida es el mismo.
UnaryOperator<String> mayusculas = texto -> texto.toUpperCase();
String textoMayusculas = mayusculas.apply("java"); // textoMayusculas = "JAVA"
BinaryOperator<T>
: Caso especial de BiFunction donde todos los tipos son iguales.
BinaryOperator<Integer> multiplicar = (a, b) -> a * b;
int producto = multiplicar.apply(4, 5); // producto = 20
Composición de funciones
Una característica de las interfaces funcionales es la capacidad de componer operaciones mediante métodos default como andThen()
y compose()
:
Function<String, String> eliminarEspacios = s -> s.replace(" ", "");
Function<String, Integer> contarCaracteres = s -> s.length();
// Composición de funciones
Function<String, Integer> contarSinEspacios = eliminarEspacios.andThen(contarCaracteres);
int resultado = contarSinEspacios.apply("Hola mundo"); // resultado = 9
En este ejemplo, andThen()
crea una nueva función que primero aplica eliminarEspacios
y luego aplica contarCaracteres
al resultado. También existe el método compose()
que aplica las funciones en orden inverso.
Referencias a métodos
Las referencias a métodos proporcionan una sintaxis aún más concisa para implementar interfaces funcionales cuando la implementación simplemente llama a un método existente:
// Diferentes tipos de referencias a métodos
Function<String, Integer> parser = Integer::parseInt; // Referencia a método estático
Consumer<String> printer = System.out::println; // Referencia a método de instancia de objeto particular
Function<String, String> toUpper = String::toUpperCase; // Referencia a método de instancia de tipo
BiFunction<String, Integer, String> substring = String::substring; // Referencia a método con parámetros
Supplier<ArrayList<String>> listFactory = ArrayList::new; // Referencia a constructor
La sintaxis Clase::método
o objeto::método
permite referenciar métodos existentes sin necesidad de escribir una expresión lambda completa.
Creación de interfaces funcionales personalizadas
Aunque Java proporciona un amplio conjunto de interfaces funcionales predefinidas, a veces es necesario crear interfaces funcionales personalizadas para casos específicos:
@FunctionalInterface
public interface Validador<T> {
boolean validar(T valor, String contexto);
default Validador<T> and(Validador<T> otro) {
return (valor, contexto) -> this.validar(valor, contexto) &&
otro.validar(valor, contexto);
}
default Validador<T> or(Validador<T> otro) {
return (valor, contexto) -> this.validar(valor, contexto) ||
otro.validar(valor, contexto);
}
}
Esta interfaz funcional personalizada incluye métodos default que permiten combinar validadores mediante operaciones lógicas:
Validador<String> noVacio = (s, ctx) -> {
boolean valido = s != null && !s.isEmpty();
if (!valido) System.out.println(ctx + ": El valor no puede estar vacío");
return valido;
};
Validador<String> soloLetras = (s, ctx) -> {
boolean valido = s.matches("[a-zA-Z]+");
if (!valido) System.out.println(ctx + ": El valor debe contener solo letras");
return valido;
};
// Combinando validadores
Validador<String> validadorNombre = noVacio.and(soloLetras);
// Uso del validador combinado
validadorNombre.validar("", "Nombre"); // Imprime: Nombre: El valor no puede estar vacío
validadorNombre.validar("Juan123", "Nombre"); // Imprime: Nombre: El valor debe contener solo letras
boolean esValido = validadorNombre.validar("Juan", "Nombre"); // esValido = true
Interfaces funcionales en colecciones y streams
Las interfaces funcionales son fundamentales para la API Stream de Java, que permite el procesamiento declarativo de colecciones:
List<String> nombres = List.of("Ana", "Juan", "María", "Pedro", "Lucía");
// Uso de Predicate para filtrar
List<String> nombresFiltrados = nombres.stream()
.filter(nombre -> nombre.length() > 4)
.collect(Collectors.toList()); // [María, Pedro, Lucía]
// Uso de Function para transformar
List<Integer> longitudes = nombres.stream()
.map(String::length)
.collect(Collectors.toList()); // [3, 4, 5, 5, 5]
// Uso de Consumer para procesar elementos
nombres.forEach(nombre -> System.out.println("Hola, " + nombre));
En este ejemplo, filter()
utiliza un Predicate<String>
, map()
utiliza un Function<String, Integer>
, y forEach()
utiliza un Consumer<String>
.
Closures y captura de variables
Las expresiones lambda en Java pueden capturar variables del contexto circundante, creando lo que se conoce como closures:
public class EjemploClosure {
public static void main(String[] args) {
String prefijo = "Usuario: ";
// La lambda captura la variable prefijo
Consumer<String> saludar = nombre -> System.out.println(prefijo + nombre);
saludar.accept("María"); // Imprime: Usuario: María
}
}
Sin embargo, las variables capturadas deben ser efectivamente finales (no modificadas después de su inicialización). Si se intenta modificar una variable capturada, el compilador generará un error:
String prefijo = "Usuario: ";
Consumer<String> saludar = nombre -> System.out.println(prefijo + nombre);
prefijo = "Cliente: "; // Error de compilación: variable usada en lambda debe ser final o efectivamente final
Currying y aplicación parcial
El currying es una técnica de programación funcional que transforma una función con múltiples argumentos en una secuencia de funciones con un solo argumento. En Java, se puede implementar utilizando interfaces funcionales anidadas:
public class Currying {
public static void main(String[] args) {
// Función tradicional de dos parámetros
BiFunction<Integer, Integer, Integer> suma = (a, b) -> a + b;
// Versión currificada
Function<Integer, Function<Integer, Integer>> sumaC = a -> b -> a + b;
// Uso de la función currificada
Function<Integer, Integer> sumar5 = sumaC.apply(5); // Aplicación parcial
int resultado = sumar5.apply(3); // resultado = 8
// También se puede usar directamente
int resultado2 = sumaC.apply(2).apply(3); // resultado2 = 5
}
}
La aplicación parcial permite crear nuevas funciones especializadas a partir de funciones más generales, fijando algunos de sus parámetros.
Patrones de diseño funcionales
Las interfaces funcionales facilitan la implementación de varios patrones de diseño de manera más concisa:
- Patrón Strategy: Permite definir una familia de algoritmos y hacerlos intercambiables.
// Definición de la estrategia como interfaz funcional
@FunctionalInterface
interface EstrategiaOrdenamiento<T> {
List<T> ordenar(List<T> elementos);
}
class Ordenador<T> {
private EstrategiaOrdenamiento<T> estrategia;
public Ordenador(EstrategiaOrdenamiento<T> estrategia) {
this.estrategia = estrategia;
}
public List<T> ejecutarOrdenamiento(List<T> elementos) {
return estrategia.ordenar(elementos);
}
}
// Uso con expresiones lambda
EstrategiaOrdenamiento<Integer> ascendente = lista -> {
List<Integer> copia = new ArrayList<>(lista);
Collections.sort(copia);
return copia;
};
EstrategiaOrdenamiento<Integer> descendente = lista -> {
List<Integer> copia = new ArrayList<>(lista);
copia.sort(Collections.reverseOrder());
return copia;
};
Ordenador<Integer> ordenador = new Ordenador<>(ascendente);
List<Integer> resultado = ordenador.ejecutarOrdenamiento(List.of(3, 1, 4, 1, 5, 9));
- Patrón Command: Encapsula una solicitud como un objeto.
@FunctionalInterface
interface Comando {
void ejecutar();
}
class ControlRemoto {
private final Map<Integer, Comando> botones = new HashMap<>();
public void asignarComando(int boton, Comando comando) {
botones.put(boton, comando);
}
public void presionarBoton(int boton) {
Comando comando = botones.get(boton);
if (comando != null) {
comando.ejecutar();
}
}
}
// Uso con expresiones lambda
ControlRemoto control = new ControlRemoto();
control.asignarComando(1, () -> System.out.println("Encendiendo TV"));
control.asignarComando(2, () -> System.out.println("Cambiando canal"));
control.presionarBoton(1); // Imprime: Encendiendo TV
Integración con APIs tradicionales
Las interfaces funcionales permiten modernizar APIs tradicionales basadas en interfaces con un único método:
// API tradicional
public interface ActionListener {
void actionPerformed(ActionEvent e);
}
// Uso tradicional
button.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
System.out.println("Botón presionado");
}
});
// Uso moderno con lambda
button.addActionListener(e -> System.out.println("Botón presionado"));
Muchas APIs de Java que fueron diseñadas antes de Java 8 pueden beneficiarse de la sintaxis lambda si sus interfaces tienen un solo método abstracto.
Consideraciones de rendimiento
Las interfaces funcionales y las expresiones lambda están optimizadas en la JVM moderna, pero hay algunas consideraciones de rendimiento a tener en cuenta:
- Las expresiones lambda se compilan a clases anónimas, pero con optimizaciones especiales.
- La captura de variables puede tener un pequeño impacto en el rendimiento.
- Para operaciones intensivas en bucles, las implementaciones tradicionales pueden ser ligeramente más rápidas.
// Ejemplo de benchmark simple
long inicio = System.nanoTime();
// Versión imperativa tradicional
int suma = 0;
for (int i = 0; i < 1000000; i++) {
if (i % 2 == 0) {
suma += i;
}
}
// Versión funcional
int sumaFuncional = IntStream.range(0, 1000000)
.filter(i -> i % 2 == 0)
.sum();
long fin = System.nanoTime();
En la mayoría de los casos, la diferencia de rendimiento es insignificante comparada con los beneficios en legibilidad y mantenibilidad que ofrece el estilo funcional.
Otros ejercicios de programación de Java
Evalúa tus conocimientos de esta lección Interfaces con nuestros retos de programación de tipo Test, Puzzle, Código y Proyecto con VSCode, guiados por IA.
Streams: match
Gestión de errores y excepciones
CRUD en Java de modelo Customer sobre un ArrayList
Clases abstractas
Listas
Métodos de la clase String
Streams: reduce()
API java.nio 2
Polimorfismo
Pattern Matching
Streams: flatMap()
Llamada y sobrecarga de funciones
Métodos referenciados
Métodos de la clase String
Representación de Fecha
Operadores lógicos
Inferencia de tipos con var
Tipos de datos
Estructuras de iteración
Streams: forEach()
Objetos
Funciones lambda
Uso de Scanner
Tipos de variables
Streams: collect()
Operadores aritméticos
Arrays y matrices
Clases y objetos
Interfaz funcional Consumer
CRUD en Java de modelo Customer sobre un HashMap
Interfaces
Enumeraciones Enums
API Optional
Interfaz funcional Function
Encapsulación
Interfaces
Uso de API Optional
Representación de Hora
Herencia básica
Clases y objetos
Interfaz funcional Supplier
HashMap
Sobrecarga de métodos
Polimorfismo de tiempo de ejecución
OOP en Java
Sobrecarga de métodos
CRUD de productos en Java
Clases sealed
Creación de Streams
Records
Encapsulación
Streams: min max
Herencia
Métodos avanzados de la clase String
Funciones
Polimorfismo de tiempo de compilación
Reto sintaxis Java
Conjuntos
Estructuras de control
Recursión
Excepciones
Herencia avanzada
Estructuras de selección
Uso de interfaces
Operadores
Variables
HashSet
Objeto Scanner
Streams: filter()
Operaciones de Streams
Interfaz funcional Predicate
Streams: sorted()
Configuración de entorno
Uso de variables
Clases
Streams: distinct()
Streams: count()
ArrayList
Mapas
Datos de referencia
Interfaces funcionales
Métodos básicos de la clase String
Tipos de datos
Clases abstractas
Instalación
Funciones
Excepciones
Estructuras de control
Herencia de clases
La clase Scanner
Generics
Streams: map()
Funciones y encapsulamiento
Todas las lecciones de Java
Accede a todas las lecciones de Java y aprende con ejemplos prácticos de código y ejercicios de programación con IDE web sin instalar nada.
Instalación De Java
Introducción Y Entorno
Configuración De Entorno Java
Introducción Y Entorno
Tipos De Datos
Sintaxis
Variables
Sintaxis
Operadores
Sintaxis
Estructuras De Control
Sintaxis
Funciones
Sintaxis
Recursión
Sintaxis
Arrays Y Matrices
Sintaxis
Excepciones
Programación Orientada A Objetos
Clases Y Objetos
Programación Orientada A Objetos
Encapsulación
Programación Orientada A Objetos
Herencia
Programación Orientada A Objetos
Clases Abstractas
Programación Orientada A Objetos
Interfaces
Programación Orientada A Objetos
Sobrecarga De Métodos
Programación Orientada A Objetos
Polimorfismo
Programación Orientada A Objetos
La Clase Scanner
Programación Orientada A Objetos
Métodos De La Clase String
Programación Orientada A Objetos
Excepciones
Programación Orientada A Objetos
Records
Programación Orientada A Objetos
Pattern Matching
Programación Orientada A Objetos
Inferencia De Tipos Con Var
Programación Orientada A Objetos
Enumeraciones Enums
Programación Orientada A Objetos
Generics
Programación Orientada A Objetos
Clases Sealed
Programación Orientada A Objetos
Listas
Framework Collections
Conjuntos
Framework Collections
Mapas
Framework Collections
Funciones Lambda
Programación Funcional
Interfaz Funcional Consumer
Programación Funcional
Interfaz Funcional Predicate
Programación Funcional
Interfaz Funcional Supplier
Programación Funcional
Interfaz Funcional Function
Programación Funcional
Métodos Referenciados
Programación Funcional
Creación De Streams
Programación Funcional
Operaciones Intermedias Con Streams: Map()
Programación Funcional
Operaciones Intermedias Con Streams: Filter()
Programación Funcional
Operaciones Intermedias Con Streams: Distinct()
Programación Funcional
Operaciones Finales Con Streams: Collect()
Programación Funcional
Operaciones Finales Con Streams: Min Max
Programación Funcional
Operaciones Intermedias Con Streams: Flatmap()
Programación Funcional
Operaciones Intermedias Con Streams: Sorted()
Programación Funcional
Operaciones Finales Con Streams: Reduce()
Programación Funcional
Operaciones Finales Con Streams: Foreach()
Programación Funcional
Operaciones Finales Con Streams: Count()
Programación Funcional
Operaciones Finales Con Streams: Match
Programación Funcional
Api Optional
Programación Funcional
Transformación
Programación Funcional
Reducción Y Acumulación
Programación Funcional
Mapeo
Programación Funcional
Streams Paralelos
Programación Funcional
Agrupación Y Partición
Programación Funcional
Filtrado Y Búsqueda
Programación Funcional
Api Java.nio 2
Entrada Y Salida Io
Fundamentos De Io
Entrada Y Salida Io
Leer Y Escribir Archivos
Entrada Y Salida Io
Httpclient Moderno
Entrada Y Salida Io
Clases De Nio2
Entrada Y Salida Io
Api Java.time
Api Java.time
Localtime
Api Java.time
Localdatetime
Api Java.time
Localdate
Api Java.time
Executorservice
Concurrencia
Virtual Threads (Project Loom)
Concurrencia
Future Y Completablefuture
Concurrencia
Spring Framework
Frameworks Para Java
Micronaut
Frameworks Para Java
Maven
Frameworks Para Java
Gradle
Frameworks Para Java
Lombok Para Java
Frameworks Para Java
Quarkus
Frameworks Para Java
Ecosistema Jakarta Ee De Java
Frameworks Para Java
Introducción A Junit 5
Testing
Certificados de superación de Java
Supera todos los ejercicios de programación del curso de Java y obtén certificados de superación para mejorar tu currículum y tu empleabilidad.
En esta lección
Objetivos de aprendizaje de esta lección
- Entender qué es una interfaz y cómo se declara en Java
- Aprender a implementar una interfaz en una clase
- Comprender la importancia de las interfaces para la abstracción y la emulación de herencia múltiple
- Entender los métodos predeterminados en interfaces, introducidos en Java 8
- Aprender sobre las interfaces funcionales y cómo se utilizan para definir funciones lambda
- Comprender cómo las interfaces contribuyen a la creación de código limpio, modular y reutilizable