Abstracción en POO: concepto y ejemplos prácticos en Java

📅 Actualizado en marzo 2026 ✍️ Ángel López ⏱️ 18 min de lectura ✓ Nivel intermedio ★ ★ ★ ★ ★ (5/5)

🔍 ¿Qué es la abstracción?

La abstracción es uno de los cuatro pilares fundamentales de la programación orientada a objetos, junto con la encapsulación, la herencia y el polimorfismo. En su sentido más amplio, abstraer significa aislar mentalmente las cualidades esenciales de un objeto descartando los detalles que no son relevantes para un contexto determinado.

Pensemos en un ejemplo cotidiano. Cuando conduces un automóvil, interactúas con un volante, unos pedales y una palanca de cambios. No necesitas saber cómo funciona internamente el motor de combustión, el sistema de inyección electrónica o el diferencial para poder conducir. El fabricante ha creado una interfaz simplificada —el volante, los pedales— que te permite usar el vehículo sin conocer los miles de piezas que lo componen. Eso es abstracción.

En programación ocurre exactamente lo mismo. Cuando usas el método System.out.println() en Java, no necesitas conocer cómo se envían los bytes al buffer de salida del sistema operativo ni cómo se renderiza cada carácter en la consola. Simplemente invocas el método y obtienes el resultado esperado. El equipo que diseñó la clase PrintStream abstrajo toda esa complejidad detrás de una interfaz clara y sencilla.

📘 Nota: La abstracción no es exclusiva de la programación. Es una capacidad cognitiva humana que usamos constantemente: un mapa es una abstracción del territorio, una partitura es una abstracción de la música, y un plano arquitectónico es una abstracción de un edificio.

🧩 La abstracción en la programación orientada a objetos

En el contexto de la POO, la abstracción opera en dos direcciones complementarias que permiten construir software robusto y mantenible:

▶️ Abstracción de datos (modelado del dominio)

Consiste en representar entidades del mundo real como objetos dentro del programa, seleccionando solo los atributos y comportamientos relevantes para el problema que estamos resolviendo. Por ejemplo, si desarrollamos un sistema de gestión hospitalaria, el objeto Paciente necesitará atributos como nombre, historialClinico y grupoSanguineo, pero probablemente no necesitará colorFavorito ni peliculaPreferida. La elección de qué incluir y qué descartar es el acto de abstracción.

🔹 Abstracción de comportamiento (contratos)

Consiste en definir qué debe hacer un objeto sin especificar cómo lo hace. En Java, esto se materializa a través de las clases abstractas y las interfaces. Por ejemplo, podemos definir que todo medio de pago debe ser capaz de procesarPago(double monto), sin decidir aún si el pago se procesa con tarjeta de crédito, transferencia bancaria o criptomoneda. El contrato queda establecido; la implementación se delega a las clases concretas.

💡 Buena práctica: Un buen diseño orientado a objetos comienza siempre por identificar las abstracciones correctas del dominio. Antes de escribir código, pregúntate: ¿cuáles son las entidades esenciales del problema? ¿Qué comportamientos comparten? ¿Dónde difieren?

📊 Niveles de abstracción en software

La abstracción en software no es un concepto monolítico, sino que se aplica en múltiples niveles. Comprender estos niveles ayuda a diseñar sistemas mejor organizados y más fáciles de mantener.

NivelDescripciónEjemplo en Java
Bajo nivelCercano al hardware. Se trabaja con bytes, direcciones de memoria y operaciones primitivas.Manipulación de bits con operadores &, |, <<
Medio nivelEstructuras de datos y algoritmos. Se agrupan datos en unidades lógicas.Clases como ArrayList, HashMap, algoritmos de ordenación
Alto nivelModelado del dominio del negocio. Los objetos representan conceptos del mundo real.Pedido, Cliente, Factura, MedioPago
ArquitectónicoPatrones de diseño y organización de módulos. Cada capa se comunica con las demás a través de abstracciones.Patrón MVC, capas de servicio/repositorio, inyección de dependencias

A medida que subimos de nivel, trabajamos con conceptos más cercanos al lenguaje del negocio y más alejados de los detalles técnicos. Un desarrollador senior pasa la mayor parte del tiempo trabajando en los niveles alto y arquitectónico, utilizando las abstracciones de los niveles inferiores sin necesidad de reimplementarlas.

🏗️ Clases abstractas en Java

Una clase abstracta es una clase que no puede instanciarse directamente y que puede contener tanto métodos con implementación como métodos sin ella (abstractos). Se declara con la palabra reservada abstract y sirve como base para que otras clases concretas la extiendan.

▶️ Sintaxis de una clase abstracta

Java
public abstract class Figura {

    // Atributo común a todas las figuras
    protected String color;

    // Constructor (las clases abstractas SÍ pueden tener constructores)
    public Figura(String color) {
        this.color = color;
    }

    // Método abstracto: cada figura calcula su área de forma distinta
    public abstract double calcularArea();

    // Método abstracto: cada figura calcula su perímetro de forma distinta
    public abstract double calcularPerimetro();

    // Método concreto: compartido por todas las subclases
    public String describir() {
        return "Figura de color " + color +
               " con área " + String.format("%.2f", calcularArea());
    }
}

Observa varios puntos clave en este ejemplo:

✅ La clase se declara con abstract class, lo que impide hacer new Figura("rojo"). Se produce un error de compilación si lo intentas.

✅ Tiene un constructor que recibe el color. Aunque no puedas instanciar la clase directamente, las subclases invocan este constructor con super(color).

✅ Los métodos calcularArea() y calcularPerimetro() son abstractos: no tienen cuerpo (no llevan llaves). Cada subclase concreta está obligada a implementarlos.

✅ El método describir() es concreto y ya tiene implementación. Todas las subclases lo heredan sin necesidad de reescribirlo, aunque pueden hacerlo si desean personalizarlo.

🔹 Subclases concretas que extienden la clase abstracta

Java
public class Circulo extends Figura {

    private double radio;

    public Circulo(String color, double radio) {
        super(color);          // Invoca el constructor de Figura
        this.radio = radio;
    }

    @Override
    public double calcularArea() {
        return Math.PI * radio * radio;
    }

    @Override
    public double calcularPerimetro() {
        return 2 * Math.PI * radio;
    }
}

public class Rectangulo extends Figura {

    private double base;
    private double altura;

    public Rectangulo(String color, double base, double altura) {
        super(color);
        this.base = base;
        this.altura = altura;
    }

    @Override
    public double calcularArea() {
        return base * altura;
    }

    @Override
    public double calcularPerimetro() {
        return 2 * (base + altura);
    }
}

Cada subclase concreta proporciona su propia implementación de los métodos abstractos. Ahora podemos usar el polimorfismo para tratar todas las figuras de forma uniforme:

Java
public class Main {
    public static void main(String[] args) {
        // Polimorfismo: la variable es de tipo Figura (abstracto)
        Figura f1 = new Circulo("azul", 5.0);
        Figura f2 = new Rectangulo("verde", 4.0, 7.0);

        System.out.println(f1.describir());
        // Figura de color azul con área 78,54

        System.out.println(f2.describir());
        // Figura de color verde con área 28,00
    }
}
📘 Nota: Gracias a la abstracción, si mañana necesitas añadir un Triangulo, solo debes crear una nueva subclase que extienda Figura e implemente los dos métodos abstractos. El resto del sistema (el método describir(), cualquier lista de figuras, etc.) funciona sin cambios. Esto se conoce como el principio abierto/cerrado (Open/Closed Principle).

📝 Métodos abstractos

Un método abstracto es un método que se declara sin cuerpo de implementación. Define la firma (nombre, parámetros, tipo de retorno) pero no incluye las llaves {}. Solo puede existir dentro de una clase abstracta o, de forma implícita, en una interfaz.

▶️ Reglas de los métodos abstractos

ReglaExplicación
Declaración con abstractSe escribe public abstract double calcularArea(); (con punto y coma al final, sin llaves)
Sin cuerpoNo pueden tener implementación. Si lo necesitan, deben ser métodos concretos
Obligación de implementarToda subclase concreta debe implementar todos los métodos abstractos heredados, o declararse ella misma como abstracta
No pueden ser finalUn método final no se puede sobrescribir, lo cual contradice el propósito de un método abstracto
No pueden ser privateUn método private no es visible para las subclases, así que nunca podría implementarse
No pueden ser staticLos métodos estáticos pertenecen a la clase, no a las instancias, y no participan en el polimorfismo de herencia
⚠️ Precaución: Si una subclase no implementa todos los métodos abstractos heredados de su clase padre, el compilador exigirá que esa subclase también sea declarada como abstract. De lo contrario, obtendrás un error de compilación.

🔌 Interfaces como mecanismo de abstracción

Las interfaces representan el nivel más alto de abstracción en Java. Una interfaz define un contrato puro: establece qué operaciones debe ofrecer una clase, sin indicar absolutamente nada sobre cómo debe implementarlas. Desde Java 8, las interfaces también pueden incluir métodos con implementación predeterminada (default) y métodos estáticos, pero su esencia sigue siendo la definición de contratos.

▶️ Declaración de una interfaz

Java
public interface Exportable {

    // Método abstracto (implícitamente public abstract)
    String exportarATexto();

    // Método abstracto
    byte[] exportarAPDF();

    // Método default (Java 8+): implementación predeterminada
    default String obtenerFormato() {
        return "Formato estándar v1.0";
    }

    // Método estático de utilidad (Java 8+)
    static boolean esFormatoValido(String formato) {
        return formato != null && !formato.isBlank();
    }
}

🔹 Implementación de la interfaz

Java
public class Informe implements Exportable {

    private String titulo;
    private String contenido;

    public Informe(String titulo, String contenido) {
        this.titulo = titulo;
        this.contenido = contenido;
    }

    @Override
    public String exportarATexto() {
        return "=== " + titulo + " ===\n" + contenido;
    }

    @Override
    public byte[] exportarAPDF() {
        // En un caso real, usaríamos una biblioteca como iText o Apache PDFBox
        return ("PDF:" + titulo + ":" + contenido).getBytes();
    }
    // obtenerFormato() se hereda del default de la interfaz
}

La ventaja fundamental de las interfaces es que una clase puede implementar múltiples interfaces, superando la limitación de la herencia simple de Java. Un Informe podría ser simultáneamente Exportable, Imprimible y Auditable:

Java
public class Informe implements Exportable, Imprimible, Auditable {
    // Debe implementar todos los métodos abstractos de las tres interfaces
}

⚖️ Clase abstracta vs interfaz: cuándo usar cada una

Esta es una de las preguntas más frecuentes en entrevistas técnicas de Java y una decisión de diseño crucial en cualquier proyecto. Ambos mecanismos proporcionan abstracción, pero tienen propósitos diferentes.

CaracterísticaClase abstractaInterfaz
HerenciaSolo se puede extender unaSe pueden implementar múltiples
Constructores✅ Sí puede tener❌ No puede tener
Atributos de instancia✅ Cualquier tipo y visibilidadSolo public static final (constantes)
Métodos con cuerpo✅ Siempre permitidoSolo default y static (Java 8+)
Modificadores de accesoCualquiera (public, protected, private)Métodos public por defecto
Propósito principalCompartir código y estado entre clases relacionadas (relación es-un)Definir capacidades que clases no relacionadas pueden compartir (relación puede-hacer)

🔹 Regla práctica de diseño

📌 Usa una clase abstracta cuando las subclases comparten una identidad común y necesitan heredar estado (atributos) y comportamiento base. Ejemplo: Vehiculo como clase abstracta para Coche, Moto y Camion.

📌 Usa una interfaz cuando quieras definir una capacidad que pueden tener clases de jerarquías completamente distintas. Ejemplo: la interfaz Serializable puede ser implementada tanto por un Pedido como por una Configuracion, que no tienen nada en común en su jerarquía.

💡 Buena práctica: Desde Java 8, la frontera entre ambos conceptos se ha difuminado porque las interfaces pueden tener métodos default. La regla de oro moderna es: programa orientado a interfaces siempre que sea posible y usa clases abstractas solo cuando necesites compartir estado mutable o lógica de constructor entre subclases.

💳 Ejemplo integrador: sistema de pagos

Veamos un ejemplo completo que combina clase abstracta, interfaz, métodos abstractos y polimorfismo para construir un sistema de procesamiento de pagos flexible y extensible.

Java — Interfaz del contrato
// Interfaz: define la capacidad de auditar operaciones
public interface Auditable {
    String generarRegistroAuditoria();
}

// Clase abstracta: base común para todos los medios de pago
public abstract class MedioPago implements Auditable {

    protected String identificador;
    protected String titular;

    public MedioPago(String identificador, String titular) {
        this.identificador = identificador;
        this.titular = titular;
    }

    // Método abstracto: cada medio de pago lo procesa de forma distinta
    public abstract boolean procesarPago(double monto);

    // Método abstracto: validación específica de cada medio
    public abstract boolean validar();

    // Método concreto compartido
    public String obtenerResumen() {
        return titular + " [" + identificador + "]";
    }
}
Java — Implementaciones concretas
public class TarjetaCredito extends MedioPago {

    private String numeroTarjeta;
    private String fechaExpiracion;

    public TarjetaCredito(String titular, String numero, String expiracion) {
        super(numero.substring(numero.length() - 4), titular);
        this.numeroTarjeta = numero;
        this.fechaExpiracion = expiracion;
    }

    @Override
    public boolean procesarPago(double monto) {
        if (!validar()) return false;
        System.out.println("Procesando pago de $" + monto +
                           " con tarjeta ****" + identificador);
        // Lógica de conexión con pasarela de pago
        return true;
    }

    @Override
    public boolean validar() {
        return numeroTarjeta.length() == 16 && fechaExpiracion != null;
    }

    @Override
    public String generarRegistroAuditoria() {
        return "TARJETA | Titular: " + titular +
               " | Últimos 4: " + identificador;
    }
}

public class TransferenciaBancaria extends MedioPago {

    private String iban;

    public TransferenciaBancaria(String titular, String iban) {
        super(iban.substring(iban.length() - 4), titular);
        this.iban = iban;
    }

    @Override
    public boolean procesarPago(double monto) {
        if (!validar()) return false;
        System.out.println("Procesando transferencia de $" + monto +
                           " desde IBAN ****" + identificador);
        return true;
    }

    @Override
    public boolean validar() {
        return iban != null && iban.length() >= 15;
    }

    @Override
    public String generarRegistroAuditoria() {
        return "TRANSFERENCIA | Titular: " + titular +
               " | IBAN: ****" + identificador;
    }
}
Java — Uso polimórfico
public class ProcesadorPagos {

    public static void ejecutarPago(MedioPago medio, double monto) {
        // No importa si es tarjeta, transferencia o cualquier medio futuro
        if (medio.procesarPago(monto)) {
            System.out.println("✔ Pago exitoso: " + medio.obtenerResumen());
            System.out.println("  Auditoría: " + medio.generarRegistroAuditoria());
        } else {
            System.out.println("✘ Pago rechazado para " + medio.obtenerResumen());
        }
    }

    public static void main(String[] args) {
        MedioPago tarjeta = new TarjetaCredito(
            "Ana García", "4532015678901234", "12/28");
        MedioPago transferencia = new TransferenciaBancaria(
            "Carlos López", "ES7921000813610123456789");

        ejecutarPago(tarjeta, 150.00);
        ejecutarPago(transferencia, 2500.00);
    }
}

Este diseño demuestra el poder de la abstracción: el método ejecutarPago() trabaja con el tipo abstracto MedioPago y no conoce ni necesita conocer los detalles de cada implementación concreta. Si mañana se incorpora un nuevo medio de pago —por ejemplo, Criptomoneda—, basta con crear una nueva subclase. El procesador de pagos funciona sin modificaciones.

🔄 Abstracción y el principio de inversión de dependencias

La abstracción no es solo una herramienta de organización del código; es la base del principio de inversión de dependencias (Dependency Inversion Principle — DIP), una de las cinco reglas SOLID del diseño orientado a objetos. Este principio establece que los módulos de alto nivel no deben depender de módulos de bajo nivel, sino que ambos deben depender de abstracciones.

▶️ Sin abstracción (acoplamiento rígido)

Java — ❌ Diseño acoplado
// La clase depende directamente de una implementación concreta
public class NotificadorPedidos {
    private EnvioEmail envioEmail = new EnvioEmail();  // Acoplamiento directo

    public void notificar(String mensaje) {
        envioEmail.enviar(mensaje);  // Solo puede enviar por email
    }
}

🔹 Con abstracción (inversión de dependencias)

Java — ✅ Diseño desacoplado
// Abstracción: interfaz que define el contrato
public interface ServicioNotificacion {
    void enviar(String mensaje);
}

// Implementaciones concretas
public class NotificacionEmail implements ServicioNotificacion {
    @Override
    public void enviar(String mensaje) {
        System.out.println("Enviando email: " + mensaje);
    }
}

public class NotificacionSMS implements ServicioNotificacion {
    @Override
    public void enviar(String mensaje) {
        System.out.println("Enviando SMS: " + mensaje);
    }
}

// Módulo de alto nivel: depende de la abstracción, no de la implementación
public class NotificadorPedidos {
    private ServicioNotificacion servicio;

    public NotificadorPedidos(ServicioNotificacion servicio) {
        this.servicio = servicio;  // Se inyecta la dependencia
    }

    public void notificar(String mensaje) {
        servicio.enviar(mensaje);  // Funciona con cualquier implementación
    }
}

Con este diseño, NotificadorPedidos puede enviar notificaciones por email, SMS, push o cualquier canal futuro sin cambiar una sola línea de su código. La abstracción ServicioNotificacion actúa como un contrato estable que desacopla completamente el módulo de alto nivel de los detalles de implementación.

🐛 Errores frecuentes con la abstracción

Incluso desarrolladores experimentados cometen errores al trabajar con abstracciones. Estos son los más habituales y cómo evitarlos:

🔹 1. Intentar instanciar una clase abstracta

Java — ❌ Error de compilación
Figura f = new Figura("rojo");
// Error: Figura is abstract; cannot be instantiated

El error: intentar crear una instancia directa de una clase abstracta. La solución: instanciar siempre una subclase concreta, como new Circulo("rojo", 5.0).

🔹 2. Olvidar implementar todos los métodos abstractos

Java — ❌ Error de compilación
public class Triangulo extends Figura {
    // Solo implementa calcularArea(), falta calcularPerimetro()
    @Override
    public double calcularArea() { return 0; }
}
// Error: Triangulo is not abstract and does not override
//        abstract method calcularPerimetro() in Figura

El error: implementar solo algunos métodos abstractos. La solución: implementar todos los métodos abstractos heredados o declarar la subclase también como abstract.

🔹 3. Sobreabstracción (exceso de abstracción)

El error: crear interfaces y clases abstractas para cada mínimo concepto, generando una jerarquía profunda y difícil de navegar. Esto se conoce como over-engineering.

La solución: aplica la regla YAGNI (You Aren't Gonna Need It). No crees una abstracción hasta que tengas al menos dos implementaciones concretas reales, o una necesidad clara de extensibilidad futura documentada.

🔹 4. Abstracciones que filtran detalles de implementación

El error: diseñar una interfaz RepositorioDatos que expone métodos como ejecutarSQL(String query). Esto filtra el detalle de que internamente se usa SQL, violando la abstracción. Si algún día migras a una base NoSQL, la interfaz queda obsoleta.

La solución: las abstracciones deben hablar en términos del dominio: guardar(Pedido p), buscarPorId(int id), eliminar(Pedido p). Estos métodos no revelan la tecnología subyacente.

✏️ Ejercicios prácticos

Pon a prueba tu comprensión de la abstracción con estos ejercicios progresivos. Cada uno incluye una solución de referencia que puedes consultar después de intentar resolverlo por tu cuenta.

▶️ Ejercicio 1 — Sistema de empleados

Crea una clase abstracta Empleado con los atributos nombre (String) y salarioBase (double), un método abstracto calcularSalario() que devuelva el salario final, y un método concreto mostrarInfo() que imprima el nombre y el salario calculado. Luego crea dos subclases: EmpleadoFijo (salario base + 10% de bono) y EmpleadoFreelance (recibe un número de horas trabajadas y cobra 25€ por hora).

Ver solución
Java
public abstract class Empleado {
    protected String nombre;
    protected double salarioBase;

    public Empleado(String nombre, double salarioBase) {
        this.nombre = nombre;
        this.salarioBase = salarioBase;
    }

    public abstract double calcularSalario();

    public void mostrarInfo() {
        System.out.printf("Empleado: %s | Salario: %.2f€%n",
                          nombre, calcularSalario());
    }
}

public class EmpleadoFijo extends Empleado {
    public EmpleadoFijo(String nombre, double salarioBase) {
        super(nombre, salarioBase);
    }

    @Override
    public double calcularSalario() {
        return salarioBase * 1.10;  // Salario base + 10% bono
    }
}

public class EmpleadoFreelance extends Empleado {
    private int horasTrabajadas;

    public EmpleadoFreelance(String nombre, int horas) {
        super(nombre, 0);
        this.horasTrabajadas = horas;
    }

    @Override
    public double calcularSalario() {
        return horasTrabajadas * 25.0;  // 25€/hora
    }
}

// Prueba
public class TestEmpleados {
    public static void main(String[] args) {
        Empleado e1 = new EmpleadoFijo("Laura Martín", 2500);
        Empleado e2 = new EmpleadoFreelance("Pedro Ruiz", 160);

        e1.mostrarInfo();  // Empleado: Laura Martín | Salario: 2750,00€
        e2.mostrarInfo();  // Empleado: Pedro Ruiz | Salario: 4000,00€
    }
}

▶️ Ejercicio 2 — Interfaz Reproducible

Define una interfaz Reproducible con los métodos reproducir(), pausar() y detener(). Implementa esta interfaz en dos clases: Cancion (con atributos título y artista) y Podcast (con atributos título y episodio). Crea una clase ReproductorMultimedia con un método iniciarReproduccion(Reproducible contenido) que demuestre el uso polimórfico.

Ver solución
Java
public interface Reproducible {
    void reproducir();
    void pausar();
    void detener();
}

public class Cancion implements Reproducible {
    private String titulo;
    private String artista;

    public Cancion(String titulo, String artista) {
        this.titulo = titulo;
        this.artista = artista;
    }

    @Override
    public void reproducir() {
        System.out.println("♪ Reproduciendo: " + titulo + " - " + artista);
    }

    @Override
    public void pausar() {
        System.out.println("⏸ Canción pausada: " + titulo);
    }

    @Override
    public void detener() {
        System.out.println("⏹ Canción detenida: " + titulo);
    }
}

public class Podcast implements Reproducible {
    private String titulo;
    private int episodio;

    public Podcast(String titulo, int episodio) {
        this.titulo = titulo;
        this.episodio = episodio;
    }

    @Override
    public void reproducir() {
        System.out.println("🎙 Reproduciendo: " + titulo +
                           " - Ep. " + episodio);
    }

    @Override
    public void pausar() {
        System.out.println("⏸ Podcast pausado: " + titulo);
    }

    @Override
    public void detener() {
        System.out.println("⏹ Podcast detenido: " + titulo);
    }
}

public class ReproductorMultimedia {
    public static void iniciarReproduccion(Reproducible contenido) {
        contenido.reproducir();
    }

    public static void main(String[] args) {
        Reproducible c = new Cancion("Bohemian Rhapsody", "Queen");
        Reproducible p = new Podcast("Tech Talks", 42);

        iniciarReproduccion(c);  // ♪ Reproduciendo: Bohemian Rhapsody - Queen
        iniciarReproduccion(p);  // 🎙 Reproduciendo: Tech Talks - Ep. 42
    }
}

▶️ Ejercicio 3 — Diseño completo con clase abstracta e interfaz

Diseña un sistema para una empresa de envíos. Crea una interfaz Rastreable con un método obtenerUbicacion(). Crea una clase abstracta Paquete con atributos peso (double) y destino (String), un método abstracto calcularCostoEnvio(), y que implemente Rastreable. Luego crea dos subclases concretas: PaqueteNacional (costo = peso × 5) y PaqueteInternacional (costo = peso × 15 + 20 de tasa aduanera). Cada una debe devolver una ubicación simulada en obtenerUbicacion().

Ver solución
Java
public interface Rastreable {
    String obtenerUbicacion();
}

public abstract class Paquete implements Rastreable {
    protected double peso;
    protected String destino;

    public Paquete(double peso, String destino) {
        this.peso = peso;
        this.destino = destino;
    }

    public abstract double calcularCostoEnvio();

    public void mostrarDetalles() {
        System.out.printf("Destino: %s | Peso: %.1f kg | Costo: %.2f€ | Ubicación: %s%n",
                          destino, peso, calcularCostoEnvio(), obtenerUbicacion());
    }
}

public class PaqueteNacional extends Paquete {
    public PaqueteNacional(double peso, String destino) {
        super(peso, destino);
    }

    @Override
    public double calcularCostoEnvio() {
        return peso * 5.0;
    }

    @Override
    public String obtenerUbicacion() {
        return "Centro de distribución nacional - Madrid";
    }
}

public class PaqueteInternacional extends Paquete {
    public PaqueteInternacional(double peso, String destino) {
        super(peso, destino);
    }

    @Override
    public double calcularCostoEnvio() {
        return peso * 15.0 + 20.0;  // Tasa aduanera
    }

    @Override
    public String obtenerUbicacion() {
        return "Aduana internacional - " + destino;
    }
}

// Prueba
public class TestEnvios {
    public static void main(String[] args) {
        Paquete p1 = new PaqueteNacional(3.5, "Barcelona");
        Paquete p2 = new PaqueteInternacional(2.0, "Londres");

        p1.mostrarDetalles();
        // Destino: Barcelona | Peso: 3,5 kg | Costo: 17,50€ | Ubicación: Centro de distribución nacional - Madrid

        p2.mostrarDetalles();
        // Destino: Londres | Peso: 2,0 kg | Costo: 50,00€ | Ubicación: Aduana internacional - Londres
    }
}

❓ Preguntas frecuentes sobre Abstracción en POO: concepto y ejemplos prácticos en Java

Las dudas más comunes respondidas de forma clara y directa.

La abstracción es el proceso de identificar las características esenciales de un objeto y descartar los detalles irrelevantes para un contexto determinado. En Java, se implementa principalmente mediante clases abstractas e interfaces, que permiten definir contratos sin especificar la implementación concreta.
Una clase abstracta puede tener métodos con implementación y sin ella, atributos de instancia, constructores y cualquier modificador de acceso. Una interfaz define un contrato puro: sus métodos son públicos por defecto, no tiene constructores y, desde Java 8, puede incluir métodos default y static. Una clase solo puede extender una clase abstracta, pero puede implementar múltiples interfaces.
No. Una clase abstracta no puede instanciarse directamente con new. Su propósito es servir de base para subclases concretas que implementen los métodos abstractos. Intentar crear una instancia de una clase abstracta produce un error de compilación.
Sí. Una clase abstracta puede combinar métodos abstractos (sin cuerpo, que las subclases deben implementar) con métodos concretos que ya tienen implementación. Esto permite compartir lógica común entre todas las subclases evitando duplicación de código.
Debes usar abstracción cuando necesites definir un comportamiento común para una familia de objetos sin comprometerte con una implementación concreta. Es especialmente útil cuando distintas clases comparten una estructura similar pero difieren en los detalles, como distintos medios de pago, tipos de empleado o formas geométricas.
Valora este artículo

💬 Foro de discusión

¿Tienes dudas sobre Abstracción en POO: concepto y ejemplos prácticos en Java? Comparte tu pregunta con la comunidad.

¿Tienes cuenta? o comenta como invitado ↓

Todavía no hay mensajes. ¡Sé el primero en participar!