Ir al contenido principal

Clases abstractas e interfaces

Introducción

En la programación orientada a objetos, las clases abstractas e interfaces son mecanismos fundamentales para definir comportamientos comunes y establecer contratos que las clases concretas deben implementar. Estas herramientas son esenciales en Java para crear código reutilizable, extensible y bien estructurado. En este artículo, exploraremos cómo las clases abstractas e interfaces nos permiten crear jerarquías de clases flexibles y cómo cada una tiene su propio propósito dentro del diseño de aplicaciones.

Tanto las clases abstractas como las interfaces son pilares del polimorfismo en Java, pero tienen características y usos distintos que debes comprender para utilizarlas correctamente en tus programas.

Clases abstractas

¿Qué es una clase abstracta?

Una clase abstracta es una clase que no puede ser instanciada directamente y está diseñada para ser extendida por subclases concretas. Se usa principalmente cuando queremos definir una base común para un grupo de subclases relacionadas.

Características principales

  • Se declaran con la palabra clave abstract
  • Pueden contener métodos abstractos (declarados pero sin implementación)
  • Pueden contener métodos concretos (con implementación completa)
  • Pueden tener constructores y variables de instancia
  • Una clase que tiene al menos un método abstracto debe ser declarada como abstracta
  • No se pueden crear objetos directamente de una clase abstracta

Sintaxis básica

abstract class FiguraGeometrica {
    // Variable de instancia
    protected String nombre;
    
    // Constructor
    public FiguraGeometrica(String nombre) {
        this.nombre = nombre;
    }
    
    // Método abstracto (sin implementación)
    public abstract double calcularArea();
    
    // Método concreto (con implementación)
    public String getNombre() {
        return nombre;
    }
    
    // Otro método concreto
    public void mostrarInformacion() {
        System.out.println("Esta es una figura llamada " + nombre);
        System.out.println("Area: " + calcularArea());
    }
}

Ejemplo práctico de clase abstracta

Veamos un ejemplo completo de una clase abstracta y cómo se implementa en subclases concretas:

// Clase abstracta
abstract class Empleado {
    // Variables de instancia
    private String nombre;
    private String dni;
    
    // Constructor
    public Empleado(String nombre, String dni) {
        this.nombre = nombre;
        this.dni = dni;
    }
    
    // Métodos concretos
    public String getNombre() {
        return nombre;
    }
    
    public String getDni() {
        return dni;
    }
    
    // Método abstracto que deben implementar las subclases
    public abstract double calcularSalario();
    
    // Método concreto que usa el método abstracto
    public void imprimirNomina() {
        System.out.println("--------- NÓMINA ---------");
        System.out.println("Empleado: " + nombre);
        System.out.println("DNI: " + dni);
        System.out.println("Salario: " + calcularSalario() + " €");
        System.out.println("---------------------------");
    }
}

// Subclase concreta
class EmpleadoTiempoCompleto extends Empleado {
    private double salarioMensual;
    
    public EmpleadoTiempoCompleto(String nombre, String dni, double salarioMensual) {
        super(nombre, dni);
        this.salarioMensual = salarioMensual;
    }
    
    // Implementación del método abstracto
    @Override
    public double calcularSalario() {
        return salarioMensual;
    }
}

// Otra subclase concreta
class EmpleadoPorHoras extends Empleado {
    private double tarifaPorHora;
    private int horasTrabajadas;
    
    public EmpleadoPorHoras(String nombre, String dni, double tarifaPorHora, int horasTrabajadas) {
        super(nombre, dni);
        this.tarifaPorHora = tarifaPorHora;
        this.horasTrabajadas = horasTrabajadas;
    }
    
    // Implementación diferente del método abstracto
    @Override
    public double calcularSalario() {
        return tarifaPorHora * horasTrabajadas;
    }
}

// Clase principal para probar
public class PruebaEmpleados {
    public static void main(String[] args) {
        // No podemos instanciar la clase abstracta
        // Empleado emp = new Empleado("Juan", "12345678A"); // Esto daría error
        
        // Pero sí podemos crear objetos de las subclases
        EmpleadoTiempoCompleto empTC = new EmpleadoTiempoCompleto("Laura", "11223344B", 2000);
        EmpleadoPorHoras empPH = new EmpleadoPorHoras("Carlos", "22334455C", 15, 120);
        
        // Y podemos usar polimorfismo
        Empleado[] empleados = {empTC, empPH};
        
        for (Empleado e : empleados) {
            e.imprimirNomina();
        }
    }
}

Al ejecutar este código, veremos las nóminas de los dos tipos de empleados, aunque el cálculo del salario se realiza de manera diferente en cada caso.

Interfaces

¿Qué es una interfaz?

Una interfaz es una estructura que define un conjunto de métodos que una clase debe implementar. A diferencia de las clases abstractas, las interfaces no proporcionan implementación (salvo desde Java 8 con los métodos default y static).

Características principales

  • Se declaran con la palabra clave interface
  • Todos los métodos son implícitamente públicos y abstractos (hasta Java 8)
  • Desde Java 8, pueden contener métodos default (con implementación) y métodos static
  • Desde Java 9, pueden contener métodos privados
  • Todas las variables son implícitamente public, static y final (constantes)
  • Una clase puede implementar múltiples interfaces (a diferencia de la herencia que es única)
  • No tienen constructores

Sintaxis básica

interface Dibujable {
    // Constante (implícitamente public, static y final)
    String HERRAMIENTA = "Lápiz";
    
    // Métodos abstractos (implícitamente public y abstract)
    void dibujar();
    void cambiarColor(String color);
    
    // Método default (desde Java 8)
    default void mostrarInformacion() {
        System.out.println("Este objeto es dibujable con " + HERRAMIENTA);
    }
    
    // Método estático (desde Java 8)
    static void descripcion() {
        System.out.println("Interfaz para objetos que pueden ser dibujados");
    }
}

Ejemplo práctico de interfaces

Veamos un ejemplo completo de cómo se usan las interfaces:

// Definición de la interfaz
interface Reproductor {
    // Constante
    int VOLUMEN_MAXIMO = 100;
    
    // Métodos abstractos
    void reproducir();
    void pausar();
    void detener();
    void ajustarVolumen(int nivel);
    
    // Método default
    default void silenciar() {
        ajustarVolumen(0);
        System.out.println("Sonido silenciado");
    }
    
    // Método estático
    static void informacion() {
        System.out.println("Interfaz para dispositivos que reproducen contenido multimedia");
    }
}

// Otra interfaz
interface Grabador {
    void iniciarGrabacion();
    void detenerGrabacion();
}

// Clase que implementa una interfaz
class ReproductorAudio implements Reproductor {
    private String modelo;
    private int nivelVolumen;
    private boolean reproduciendo;
    
    public ReproductorAudio(String modelo) {
        this.modelo = modelo;
        this.nivelVolumen = 50; // Volumen medio por defecto
        this.reproduciendo = false;
    }
    
    @Override
    public void reproducir() {
        reproduciendo = true;
        System.out.println("Reproduciendo audio en " + modelo);
    }
    
    @Override
    public void pausar() {
        reproduciendo = false;
        System.out.println("Audio pausado");
    }
    
    @Override
    public void detener() {
        reproduciendo = false;
        System.out.println("Reproducción detenida");
    }
    
    @Override
    public void ajustarVolumen(int nivel) {
        if (nivel < 0) {
            nivelVolumen = 0;
        } else if (nivel > VOLUMEN_MAXIMO) {
            nivelVolumen = VOLUMEN_MAXIMO;
        } else {
            nivelVolumen = nivel;
        }
        System.out.println("Volumen ajustado a: " + nivelVolumen);
    }
}

// Clase que implementa múltiples interfaces
class DispositivoMultimedia implements Reproductor, Grabador {
    private String nombre;
    private int volumen;
    private boolean grabando;
    
    public DispositivoMultimedia(String nombre) {
        this.nombre = nombre;
        this.volumen = 50;
        this.grabando = false;
    }
    
    // Implementación de métodos de Reproductor
    @Override
    public void reproducir() {
        System.out.println(nombre + ": Reproduciendo contenido");
    }
    
    @Override
    public void pausar() {
        System.out.println(nombre + ": Contenido en pausa");
    }
    
    @Override
    public void detener() {
        System.out.println(nombre + ": Reproducción detenida");
    }
    
    @Override
    public void ajustarVolumen(int nivel) {
        volumen = Math.max(0, Math.min(nivel, VOLUMEN_MAXIMO));
        System.out.println(nombre + ": Volumen ajustado a " + volumen);
    }
    
    // Implementación de métodos de Grabador
    @Override
    public void iniciarGrabacion() {
        grabando = true;
        System.out.println(nombre + ": Iniciando grabación");
    }
    
    @Override
    public void detenerGrabacion() {
        grabando = false;
        System.out.println(nombre + ": Grabación finalizada");
    }
}

// Clase principal para probar
public class PruebaInterfaces {
    public static void main(String[] args) {
        // Uso del método estático de la interfaz
        Reproductor.informacion();
        
        // Creación de objetos
        ReproductorAudio radio = new ReproductorAudio("Radio XYZ");
        DispositivoMultimedia smartphone = new DispositivoMultimedia("Smartphone ABC");
        
        // Uso de polimorfismo con interfaces
        Reproductor[] reproductores = {radio, smartphone};
        
        for (Reproductor r : reproductores) {
            r.reproducir();
            r.ajustarVolumen(75);
            r.silenciar(); // Método default
            r.detener();
            System.out.println();
        }
        
        // Uso de la segunda interfaz
        smartphone.iniciarGrabacion();
        smartphone.detenerGrabacion();
    }
}

Diferencias entre clases abstractas e interfaces

Para elegir correctamente entre usar una clase abstracta o una interfaz, es importante entender sus diferencias:

Característica Clase Abstracta Interfaz
Herencia Una clase solo puede extender una clase abstracta Una clase puede implementar múltiples interfaces
Variables Puede tener variables de instancia y estáticas Solo puede tener constantes (public static final)
Constructores Puede tener constructores No puede tener constructores
Métodos Puede tener métodos abstractos y concretos Tradicionalmente solo métodos abstractos (pero desde Java 8 puede tener métodos default y static)
Acceso Puede tener diferentes modificadores de acceso Los métodos son implícitamente públicos
Propósito "Es un tipo de" (herencia) "Puede hacer esto" (capacidad)

¿Cuándo usar cada una?

  • Usa clases abstractas cuando:

    • Quieres compartir código entre clases estrechamente relacionadas
    • Necesitas definir variables de instancia o métodos no públicos
    • Quieres proporcionar una implementación parcial de una funcionalidad
    • Las clases que extienden de ella tienen muchas características en común
  • Usa interfaces cuando:

    • Quieres definir un comportamiento que puede ser implementado por clases no relacionadas
    • Necesitas que una clase implemente múltiples funcionalidades
    • Estás diseñando para API pública que puede cambiar
    • Quieres especificar el comportamiento de un objeto sin preocuparte por quién lo implementa

Interfaces funcionales y expresiones lambda

Desde Java 8, se introdujo el concepto de interfaces funcionales, que son interfaces con exactamente un método abstracto. Estas interfaces pueden ser implementadas usando expresiones lambda.

// Interfaz funcional
@FunctionalInterface
interface Operable {
    int operar(int a, int b);
}

public class PruebaLambda {
    public static void main(String[] args) {
        // Implementación tradicional
        Operable suma = new Operable() {
            @Override
            public int operar(int a, int b) {
                return a + b;
            }
        };
        
        // Usando expresión lambda
        Operable resta = (a, b) -> a - b;
        
        // Usando las implementaciones
        System.out.println("Suma: " + suma.operar(10, 5));  // Salida: 15
        System.out.println("Resta: " + resta.operar(10, 5)); // Salida: 5
    }
}

La anotación @FunctionalInterface es opcional pero recomendada, ya que ayuda al compilador a verificar que la interfaz sea realmente funcional.

Resumen

Las clases abstractas e interfaces son herramientas fundamentales en la programación orientada a objetos en Java. Las clases abstractas proporcionan una base común para las subclases relacionadas, permitiendo compartir código y definir comportamientos que las subclases deben implementar. Por otro lado, las interfaces definen contratos que las clases deben cumplir, facilitando la implementación de múltiples comportamientos independientemente de la jerarquía de herencia.

Con el tiempo, Java ha evolucionado para hacer que las interfaces sean más flexibles, incorporando métodos default y static desde Java 8, lo que difumina un poco la línea entre clases abstractas e interfaces. Sin embargo, entender las diferencias y el propósito de cada uno te permitirá diseñar mejor tus aplicaciones, creando código más modular, reutilizable y mantenible.