Ir al contenido principal

Introducción a la programación orientada a objetos

Introducción

La programación orientada a objetos (POO) es un paradigma fundamental en el desarrollo de software moderno que revolucionó la forma en que diseñamos y estructuramos nuestros programas. En lugar de pensar en términos de procedimientos y funciones aisladas, la POO nos permite modelar nuestro código como un conjunto de objetos que interactúan entre sí, reflejando de manera más natural el mundo que nos rodea. Java fue diseñado específicamente como un lenguaje orientado a objetos desde sus inicios, incorporando este paradigma en su núcleo y convirtiéndose en uno de sus máximos exponentes.

En este artículo, exploraremos los conceptos fundamentales de la programación orientada a objetos y cómo se implementan en Java. Comprenderás por qué este enfoque se ha vuelto tan predominante en el desarrollo de software y cómo puede ayudarte a crear aplicaciones más organizadas, mantenibles y escalables.

¿Qué es la programación orientada a objetos?

La programación orientada a objetos es un paradigma de programación que organiza el diseño de software en torno a datos u objetos, en lugar de funciones y lógica. Un objeto representa una entidad que tiene estado (atributos) y comportamiento (métodos).

Del mundo real a los objetos en código

Para entender mejor este concepto, pensemos en objetos del mundo real:

  • Un coche tiene atributos (color, marca, velocidad actual) y comportamientos (acelerar, frenar, girar).
  • Una cuenta bancaria tiene atributos (número de cuenta, saldo, titular) y comportamientos (depositar, retirar, consultar saldo).

En programación orientada a objetos, modelamos estas entidades en nuestro código de manera similar a como existen en el mundo real.

// Representación simplificada de un coche en Java
public class Coche {
    // Atributos (estado)
    private String marca;
    private String color;
    private int velocidadActual;
    
    // Métodos (comportamiento)
    public void acelerar(int incremento) {
        velocidadActual += incremento;
    }
    
    public void frenar(int decremento) {
        velocidadActual = Math.max(0, velocidadActual - decremento);
    }
    
    public void mostrarEstado() {
        System.out.println("Coche " + marca + " de color " + color);
        System.out.println("Velocidad actual: " + velocidadActual + " km/h");
    }
}

Ventajas frente a la programación procedimental 

La programación orientada a objetos ofrece numerosas ventajas frente a enfoques más tradicionales como la programación procedimental:

  1. Modularidad: El código se divide en unidades independientes y autocontenidas (objetos), facilitando su desarrollo, depuración y mantenimiento.
  2. Reutilización: Los objetos pueden reutilizarse en diferentes partes del programa o incluso en otros programas.
  3. Escalabilidad: Es más fácil expandir los programas orientados a objetos añadiendo nuevas clases que heredan propiedades de las existentes.
  4. Mantenimiento: Los cambios en una clase no afectan necesariamente a otras partes del código, siempre que la interfaz se mantenga.
  5. Organización natural: La POO permite representar entidades del mundo real de forma más intuitiva.

Principios fundamentales de la POO

La programación orientada a objetos se sustenta en cuatro principios fundamentales:

1. Encapsulamiento

El encapsulamiento consiste en ocultar los detalles internos de implementación de un objeto y exponer solo lo necesario. Esto se consigue mediante el uso de modificadores de acceso que controlan la visibilidad de los atributos y métodos.

public class CuentaBancaria {
    // Atributos privados (encapsulados)
    private String numeroCuenta;
    private double saldo;
    private String titular;
    
    // Constructor
    public CuentaBancaria(String numeroCuenta, String titular) {
        this.numeroCuenta = numeroCuenta;
        this.titular = titular;
        this.saldo = 0.0;
    }
    
    // Métodos públicos (interfaz)
    public void depositar(double cantidad) {
        if (cantidad > 0) {
            saldo += cantidad;
            System.out.println("Depósito realizado. Nuevo saldo: " + saldo);
        } else {
            System.out.println("La cantidad debe ser positiva");
        }
    }
    
    public void retirar(double cantidad) {
        if (cantidad > 0 && cantidad <= saldo) {
            saldo -= cantidad;
            System.out.println("Retiro realizado. Nuevo saldo: " + saldo);
        } else {
            System.out.println("Operación inválida o saldo insuficiente");
        }
    }
    
    public double consultarSaldo() {
        return saldo;
    }
}

En este ejemplo, los atributos numeroCuenta, saldo y titular están encapsulados (son privados), y solo se puede interactuar con ellos a través de los métodos públicos, que implementan validaciones y lógica de negocio.

2. Herencia

La herencia permite crear nuevas clases (clases derivadas) basadas en clases existentes (clases base), heredando sus atributos y comportamientos. Esto fomenta la reutilización de código y establece relaciones "es un" entre las clases.

// Clase base
public class Vehiculo {
    protected String matricula;
    protected String marca;
    protected int anioFabricacion;
    
    public Vehiculo(String matricula, String marca, int anioFabricacion) {
        this.matricula = matricula;
        this.marca = marca;
        this.anioFabricacion = anioFabricacion;
    }
    
    public void mostrarInfo() {
        System.out.println("Vehículo: " + marca);
        System.out.println("Matrícula: " + matricula);
        System.out.println("Año: " + anioFabricacion);
    }
}

// Clase derivada que hereda de Vehiculo
public class Coche extends Vehiculo {
    private int numeroPuertas;
    private boolean esAutomatico;
    
    public Coche(String matricula, String marca, int anioFabricacion,
                int numeroPuertas, boolean esAutomatico) {
        // Llamada al constructor de la clase base
        super(matricula, marca, anioFabricacion);
        this.numeroPuertas = numeroPuertas;
        this.esAutomatico = esAutomatico;
    }
    
    // Sobrescritura del método de la clase base
    @Override
    public void mostrarInfo() {
        // Llamada al método de la clase base
        super.mostrarInfo();
        System.out.println("Número de puertas: " + numeroPuertas);
        System.out.println("Transmisión: " + (esAutomatico ? "Automática" : "Manual"));
    }
    
    // Método específico de la clase Coche
    public void abrirMaletero() {
        System.out.println("Abriendo maletero del coche");
    }
}

En este ejemplo, Coche es una subclase de Vehiculo y hereda sus atributos y métodos. Además, añade sus propios atributos y métodos, y sobrescribe el método mostrarInfo() para adaptarlo a sus necesidades específicas.

3. Polimorfismo

El polimorfismo permite que objetos de diferentes clases respondan al mismo mensaje (invocación de método) de diferentes maneras. Esto se logra mediante la sobrecarga y la sobrescritura de métodos.

public class Forma {
    public double calcularArea() {
        return 0; // Implementación por defecto
    }
    
    public void dibujar() {
        System.out.println("Dibujando una forma genérica");
    }
}

public class Circulo extends Forma {
    private double radio;
    
    public Circulo(double radio) {
        this.radio = radio;
    }
    
    @Override
    public double calcularArea() {
        return Math.PI * radio * radio;
    }
    
    @Override
    public void dibujar() {
        System.out.println("Dibujando un círculo de radio " + radio);
    }
}

public class Rectangulo extends Forma {
    private double ancho;
    private double alto;
    
    public Rectangulo(double ancho, double alto) {
        this.ancho = ancho;
        this.alto = alto;
    }
    
    @Override
    public double calcularArea() {
        return ancho * alto;
    }
    
    @Override
    public void dibujar() {
        System.out.println("Dibujando un rectángulo de " + ancho + "x" + alto);
    }
}

// Uso del polimorfismo
public class EjemploPolimorfismo {
    public static void main(String[] args) {
        Forma[] formas = new Forma[3];
        formas[0] = new Circulo(5);
        formas[1] = new Rectangulo(4, 6);
        formas[2] = new Forma();
        
        // Mismo método, diferente comportamiento según la clase
        for (Forma forma : formas) {
            forma.dibujar();
            System.out.println("Área: " + forma.calcularArea());
            System.out.println();
        }
    }
}

En este ejemplo, aunque utilizamos una referencia del tipo Forma, el método que se ejecuta depende del tipo real del objeto, gracias al polimorfismo.

4. Abstracción

La abstracción consiste en identificar las características esenciales de un objeto, ignorando los detalles irrelevantes. En Java, se implementa mediante clases abstractas e interfaces.

// Clase abstracta
public abstract class InstrumentoMusical {
    protected String nombre;
    protected String tipo;
    
    public InstrumentoMusical(String nombre, String tipo) {
        this.nombre = nombre;
        this.tipo = tipo;
    }
    
    // Método abstracto (sin implementación)
    public abstract void tocar();
    
    // Método concreto (con implementación)
    public void mostrarInfo() {
        System.out.println("Instrumento: " + nombre);
        System.out.println("Tipo: " + tipo);
    }
}

// Clase concreta que implementa la abstracción
public class Guitarra extends InstrumentoMusical {
    private int numeroCuerdas;
    
    public Guitarra(String nombre, int numeroCuerdas) {
        super(nombre, "Cuerda");
        this.numeroCuerdas = numeroCuerdas;
    }
    
    // Implementación del método abstracto
    @Override
    public void tocar() {
        System.out.println("Tocando las " + numeroCuerdas + " cuerdas de la guitarra " + nombre);
    }
}

public class Piano extends InstrumentoMusical {
    private int numeroTeclas;
    
    public Piano(String nombre, int numeroTeclas) {
        super(nombre, "Teclado");
        this.numeroTeclas = numeroTeclas;
    }
    
    @Override
    public void tocar() {
        System.out.println("Presionando teclas en el piano " + nombre);
    }
}

En este ejemplo, InstrumentoMusical es una clase abstracta que define un comportamiento común (método mostrarInfo()) y declara un método abstracto tocar() que debe ser implementado por las clases derivadas.

Componentes básicos de la POO en Java

Clases

Una clase es la plantilla o plano que define las propiedades (atributos) y comportamientos (métodos) comunes a un conjunto de objetos. En Java, todas las clases heredan directa o indirectamente de la clase Object.

public class Estudiante {
    // Atributos
    private String nombre;
    private int edad;
    private String numeroMatricula;
    
    // Constructor
    public Estudiante(String nombre, int edad, String numeroMatricula) {
        this.nombre = nombre;
        this.edad = edad;
        this.numeroMatricula = numeroMatricula;
    }
    
    // Métodos
    public void estudiar() {
        System.out.println(nombre + " está estudiando");
    }
    
    public void presentarExamen() {
        System.out.println(nombre + " está presentando un examen");
    }
    
    // Métodos getter y setter
    public String getNombre() {
        return nombre;
    }
    
    public void setNombre(String nombre) {
        this.nombre = nombre;
    }
    
    // Demás getters y setters...
}

Objetos

Un objeto es una instancia de una clase, con valores específicos para sus atributos y la capacidad de ejecutar los métodos definidos en su clase.

public class PruebaEstudiante {
    public static void main(String[] args) {
        // Creación de objetos (instancias) de la clase Estudiante
        Estudiante estudiante1 = new Estudiante("Ana García", 20, "EST001");
        Estudiante estudiante2 = new Estudiante("Carlos López", 22, "EST002");
        
        // Uso de los objetos
        System.out.println("Primer estudiante: " + estudiante1.getNombre());
        estudiante1.estudiar();
        
        System.out.println("Segundo estudiante: " + estudiante2.getNombre());
        estudiante2.presentarExamen();
    }
}

Mensajes

Los mensajes son las comunicaciones entre objetos, que se realizan mediante la invocación de métodos.

public class Profesor {
    private String nombre;
    private String especialidad;
    
    public Profesor(String nombre, String especialidad) {
        this.nombre = nombre;
        this.especialidad = especialidad;
    }
    
    // Un método que envía un mensaje a otro objeto
    public void calificarExamen(Estudiante estudiante, int calificacion) {
        System.out.println("El profesor " + nombre + " está calificando a " + estudiante.getNombre());
        estudiante.recibirCalificacion(calificacion); // Envío de mensaje
    }
}

public class Estudiante {
    // Atributos y otros métodos...
    
    // Método que responde al mensaje recibido
    public void recibirCalificacion(int calificacion) {
        System.out.println(nombre + " ha recibido una calificación de " + calificacion);
        if (calificacion >= 60) {
            System.out.println("¡Aprobado!");
        } else {
            System.out.println("Reprobado. Necesita estudiar más.");
        }
    }
}

Aplicaciones de la POO en el mundo real

La programación orientada a objetos está presente en prácticamente todos los sistemas de software modernos:

Desarrollo de interfaces gráficas

Los frameworks de interfaces gráficas como JavaFX están completamente basados en POO, donde cada elemento visual (botones, ventanas, paneles) es un objeto con propiedades y comportamientos.

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;

public class EjemploInterfaz extends Application {
    @Override
    public void start(Stage primaryStage) {
        Button boton = new Button();
        boton.setText("Haz clic aquí");
        boton.setOnAction(e -> System.out.println("¡Botón presionado!"));
        
        StackPane root = new StackPane();
        root.getChildren().add(boton);
        
        Scene scene = new Scene(root, 300, 250);
        
        primaryStage.setTitle("Ejemplo JavaFX");
        primaryStage.setScene(scene);
        primaryStage.show();
    }
    
    public static void main(String[] args) {
        launch(args);
    }
}

Desarrollo de videojuegos

En los videojuegos, cada entidad (personajes, objetos, escenarios) se modela como un objeto con sus propias características y comportamientos.

public class Personaje {
    private String nombre;
    private int salud;
    private int fuerza;
    private Posicion posicion;
    
    public void mover(int x, int y) {
        posicion.actualizarA(x, y);
        System.out.println(nombre + " se ha movido a la posición " + posicion);
    }
    
    public void atacar(Personaje objetivo) {
        System.out.println(nombre + " ataca a " + objetivo.getNombre());
        objetivo.recibirDanio(fuerza);
    }
    
    public void recibirDanio(int cantidad) {
        salud -= cantidad;
        if (salud <= 0) {
            morir();
        }
    }
    
    private void morir() {
        System.out.println(nombre + " ha sido derrotado");
        // Lógica adicional...
    }
}

Sistemas de gestión

Las aplicaciones empresariales utilizan POO para modelar entidades como clientes, productos, pedidos, etc.

public class Sistema {
    public static void main(String[] args) {
        Cliente cliente = new Cliente("María Rodríguez", "12345678A");
        
        Producto producto1 = new Producto("Laptop", 899.99);
        Producto producto2 = new Producto("Monitor", 249.99);
        
        Pedido pedido = new Pedido(cliente);
        pedido.agregarProducto(producto1, 1);
        pedido.agregarProducto(producto2, 2);
        
        System.out.println("Total del pedido: " + pedido.calcularTotal() + "€");
        
        Factura factura = new Factura(pedido);
        factura.emitir();
    }
}

Buenas prácticas en POO

Para aprovechar al máximo los beneficios de la programación orientada a objetos, es recomendable seguir estas buenas prácticas:

Principio de responsabilidad única

Cada clase debe tener una única responsabilidad o razón para cambiar.

// Mal ejemplo (múltiples responsabilidades)
public class Usuario {
    // Datos del usuario
    private String nombre;
    private String email;
    
    // Métodos relacionados con autenticación
    public boolean autenticar(String password) {
        // Lógica de autenticación...
        return true;
    }
    
    // Métodos relacionados con la base de datos
    public void guardarEnBaseDeDatos() {
        // Lógica para guardar en BD...
    }
}

// Buen ejemplo (responsabilidades separadas)
public class Usuario {
    private String nombre;
    private String email;
    private String passwordHash;
    
    // Solo métodos relacionados con el estado del usuario
    public String getNombre() { return nombre; }
    public String getEmail() { return email; }
}

public class AutenticacionService {
    public boolean autenticar(Usuario usuario, String password) {
        // Lógica de autenticación...
        return true;
    }
}

public class UsuarioRepositorio {
    public void guardar(Usuario usuario) {
        // Lógica para guardar en BD...
    }
    
    public Usuario buscarPorEmail(String email) {
        // Lógica para buscar en BD...
        return null;
    }
}

Favorecer la composición sobre la herencia

La composición (incluir objetos como atributos) suele ser más flexible que la herencia para reutilizar código.

// Usando herencia
public class EmpleadoTiempoCompleto extends Empleado {
    private double salarioAnual;
    
    // Métodos específicos...
}

public class EmpleadoTiempoParcial extends Empleado {
    private double tarifaPorHora;
    private int horasSemanales;
    
    // Métodos específicos...
}

// Usando composición
public class Empleado {
    private String nombre;
    private String cargo;
    private ContratoLaboral contrato;
    
    // Métodos comunes...
}

public class ContratoLaboral {
    private TipoContrato tipo;
    private double salario;  // Salario anual o tarifa por hora
    private int horas;       // Solo para tiempo parcial
    
    // Métodos específicos según el tipo...
}

public enum TipoContrato {
    TIEMPO_COMPLETO, TIEMPO_PARCIAL
}

Principio abierto/cerrado

Las clases deben estar abiertas para extensión pero cerradas para modificación.

// Implementación usando interfaces
public interface CalculadoraImpuesto {
    double calcularImpuesto(double monto);
}

public class ImpuestoRegular implements CalculadoraImpuesto {
    @Override
    public double calcularImpuesto(double monto) {
        return monto * 0.21; // 21% IVA
    }
}

public class ImpuestoReducido implements CalculadoraImpuesto {
    @Override
    public double calcularImpuesto(double monto) {
        return monto * 0.10; // 10% IVA reducido
    }
}

// Si necesitamos un nuevo tipo de impuesto, creamos una nueva clase
// sin modificar el código existente
public class ImpuestoSuperReducido implements CalculadoraImpuesto {
    @Override
    public double calcularImpuesto(double monto) {
        return monto * 0.04; // 4% IVA super reducido
    }
}

Un ejemplo completo de aplicación POO

Veamos cómo todos estos conceptos se aplican juntos en un pequeño sistema de gestión de biblioteca:

// Clase base para los recursos de la biblioteca
public abstract class RecursoBiblioteca {
    protected String codigo;
    protected String titulo;
    protected boolean disponible;
    
    public RecursoBiblioteca(String codigo, String titulo) {
        this.codigo = codigo;
        this.titulo = titulo;
        this.disponible = true;
    }
    
    // Método abstracto
    public abstract String getTipo();
    
    // Métodos concretos
    public boolean isDisponible() {
        return disponible;
    }
    
    public void prestar() {
        if (disponible) {
            disponible = false;
            System.out.println(getTipo() + " '" + titulo + "' ha sido prestado.");
        } else {
            System.out.println(getTipo() + " '" + titulo + "' no está disponible.");
        }
    }
    
    public void devolver() {
        disponible = true;
        System.out.println(getTipo() + " '" + titulo + "' ha sido devuelto.");
    }
    
    public String getCodigo() {
        return codigo;
    }
    
    public String getTitulo() {
        return titulo;
    }
}

// Clase derivada para libros
public class Libro extends RecursoBiblioteca {
    private String autor;
    private int numeroPaginas;
    
    public Libro(String codigo, String titulo, String autor, int numeroPaginas) {
        super(codigo, titulo);
        this.autor = autor;
        this.numeroPaginas = numeroPaginas;
    }
    
    @Override
    public String getTipo() {
        return "Libro";
    }
    
    public String getAutor() {
        return autor;
    }
    
    public int getNumeroPaginas() {
        return numeroPaginas;
    }
    
    public void mostrarInfo() {
        System.out.println("LIBRO: " + titulo);
        System.out.println("Autor: " + autor);
        System.out.println("Páginas: " + numeroPaginas);
        System.out.println("Estado: " + (disponible ? "Disponible" : "Prestado"));
    }
}

// Clase derivada para revistas
public class Revista extends RecursoBiblioteca {
    private int numero;
    private String periodicidad;
    
    public Revista(String codigo, String titulo, int numero, String periodicidad) {
        super(codigo, titulo);
        this.numero = numero;
        this.periodicidad = periodicidad;
    }
    
    @Override
    public String getTipo() {
        return "Revista";
    }
    
    public int getNumero() {
        return numero;
    }
    
    public String getPeriodicidad() {
        return periodicidad;
    }
    
    public void mostrarInfo() {
        System.out.println("REVISTA: " + titulo);
        System.out.println("Número: " + numero);
        System.out.println("Periodicidad: " + periodicidad);
        System.out.println("Estado: " + (disponible ? "Disponible" : "Prestado"));
    }
}

// Clase para usuarios de la biblioteca
public class Usuario {
    private String id;
    private String nombre;
    private List<RecursoBiblioteca> recursosPrestados;
    
    public Usuario(String id, String nombre) {
        this.id = id;
        this.nombre = nombre;
        this.recursosPrestados = new ArrayList<>();
    }
    
    public String getId() {
        return id;
    }
    
    public String getNombre() {
        return nombre;
    }
    
    public void pedirPrestado(RecursoBiblioteca recurso) {
        if (recurso.isDisponible()) {
            recurso.prestar();
            recursosPrestados.add(recurso);
            System.out.println(nombre + " ha tomado prestado: " + recurso.getTitulo());
        } else {
            System.out.println("El recurso no está disponible para préstamo.");
        }
    }
    
    public void devolverRecurso(RecursoBiblioteca recurso) {
        if (recursosPrestados.contains(recurso)) {
            recurso.devolver();
            recursosPrestados.remove(recurso);
            System.out.println(nombre + " ha devuelto: " + recurso.getTitulo());
        } else {
            System.out.println(nombre + " no tiene ese recurso en préstamo.");
        }
    }
    
    public void listarRecursosPrestados() {
        System.out.println("\nRecursos prestados a " + nombre + ":");
        if (recursosPrestados.isEmpty()) {
            System.out.println("No tiene recursos en préstamo actualmente.");
        } else {
            for (RecursoBiblioteca recurso : recursosPrestados) {
                System.out.println("- " + recurso.getTipo() + ": " + recurso.getTitulo());
            }
        }
    }
}

// Clase principal del sistema
public class SistemaBiblioteca {
    private List<RecursoBiblioteca> catalogo;
    private List<Usuario> usuarios;
    
    public SistemaBiblioteca() {
        catalogo = new ArrayList<>();
        usuarios = new ArrayList<>();
    }
    
    public void agregarRecurso(RecursoBiblioteca recurso) {
        catalogo.add(recurso);
        System.out.println("Añadido al catálogo: " + recurso.getTitulo());
    }
    
    public void registrarUsuario(Usuario usuario) {
        usuarios.add(usuario);
        System.out.println("Usuario registrado: " + usuario.getNombre());
    }
    
    public RecursoBiblioteca buscarRecursoPorCodigo(String codigo) {
        for (RecursoBiblioteca recurso : catalogo) {
            if (recurso.getCodigo().equals(codigo)) {
                return recurso;
            }
        }
        return null;
    }
    
    public Usuario buscarUsuarioPorId(String id) {
        for (Usuario usuario : usuarios) {
            if (usuario.getId().equals(id)) {
                return usuario;
            }
        }
        return null;
    }
    
    public void mostrarCatalogo() {
        System.out.println("\n*** CATÁLOGO DE LA BIBLIOTECA ***");
        for (RecursoBiblioteca recurso : catalogo) {
            System.out.println(recurso.getCodigo() + " - " + recurso.getTipo() + ": " + 
                              recurso.getTitulo() + " (" + 
                              (recurso.isDisponible() ? "Disponible" : "Prestado") + ")");
        }
    }
    
    public static void main(String[] args) {
        // Creación del sistema
        SistemaBiblioteca biblioteca = new SistemaBiblioteca();
        
        // Agregar recursos
        biblioteca.agregarRecurso(new Libro("L001", "Don Quijote de la Mancha", 
                                          "Miguel de Cervantes", 863));
        biblioteca.agregarRecurso(new Libro("L002", "Cien años de soledad", 
                                          "Gabriel García Márquez", 471));
        biblioteca.agregarRecurso(new Revista("R001", "National Geographic", 
                                            245, "Mensual"));
        
        // Registrar usuarios
        biblioteca.registrarUsuario(new Usuario("U001", "Ana López"));
        biblioteca.registrarUsuario(new Usuario("U002", "Pedro Martínez"));
        
        // Mostrar catálogo inicial
        biblioteca.mostrarCatalogo();
        
        // Realizar préstamos
        Usuario ana = biblioteca.buscarUsuarioPorId("U001");
        RecursoBiblioteca libro1 = biblioteca.buscarRecursoPorCodigo("L001");
        
        if (ana != null && libro1 != null) {
            ana.pedirPrestado(libro1);
        }
        
        Usuario pedro = biblioteca.buscarUsuarioPorId("U002");
        RecursoBiblioteca revista1 = biblioteca.buscarRecursoPorCodigo("R001");
        
        if (pedro != null && revista1 != null) {
            pedro.pedirPrestado(revista1);
        }
        
        // Mostrar estado de préstamos
        if (ana != null) {
            ana.listarRecursosPrestados();
        }
        
        if (pedro != null) {
            pedro.listarRecursosPrestados();
        }
        
        // Mostrar catálogo actualizado
        biblioteca.mostrarCatalogo();
        
        // Realizar devoluciones
        if (ana != null && libro1 != null) {
            ana.devolverRecurso(libro1);
        }
        
        // Mostrar catálogo final
        biblioteca.mostrarCatalogo();
    }
}

Este ejemplo muestra cómo se pueden aplicar los diferentes conceptos de la POO para crear un sistema coherente:

  1. Encapsulamiento: Los atributos de las clases son privados o protegidos, y se accede a ellos mediante métodos.
  2. Herencia: Las clases Libro y Revista heredan de RecursoBiblioteca.
  3. Polimorfismo: Se usa el método getTipo() que devuelve valores diferentes según la clase concreta.
  4. Abstracción: La clase RecursoBiblioteca es abstracta y define un comportamiento común.

Resumen

La programación orientada a objetos es un paradigma poderoso que nos permite modelar sistemas complejos de manera intuitiva y estructurada. Java, como lenguaje fundamentalmente orientado a objetos, proporciona todas las herramientas necesarias para aplicar este enfoque de manera efectiva.

En este artículo hemos explorado los conceptos fundamentales de la POO: encapsulamiento, herencia, polimorfismo y abstracción. También hemos visto cómo estos principios se aplican en Java mediante clases, objetos y mensajes, y cómo pueden usarse para crear sistemas robustos y mantenibles.

A medida que avances en tu aprendizaje de Java, estos principios de POO serán la base sobre la que construirás aplicaciones más complejas y sofisticadas. En los próximos artículos, profundizaremos en cada uno de estos conceptos y veremos cómo se implementan en detalle en el lenguaje Java.