Java

Tutorial Java: Encapsulación

Java encapsulación: técnicas y ejemplos. Domina las técnicas de encapsulación en Java con ejemplos prácticos y detallados.

Aprende Java y certifícate

Fundamentos: modificadores de acceso (public, private, protected)

La encapsulación permite ocultar los detalles internos de implementación de una clase y exponer solo lo necesario. En Java, este concepto se implementa principalmente a través de los modificadores de acceso, que determinan la visibilidad y accesibilidad de clases, atributos y métodos.

Java proporciona cuatro niveles de control de acceso, cada uno con un propósito específico. Estos modificadores se aplican directamente a la declaración de elementos como variables, métodos y clases.

Modificador public

El modificador public otorga el nivel más amplio de acceso. Cuando se declara un elemento como público, se puede acceder a él desde cualquier parte del programa, sin restricciones.

public class Usuario {
    public String nombre;
    
    public void mostrarNombre() {
        System.out.println("Nombre: " + nombre);
    }
}

En este ejemplo, tanto la clase Usuario como su atributo nombre y el método mostrarNombre() son accesibles desde cualquier otra clase del proyecto. Se utiliza public cuando se necesita que un elemento sea completamente visible y accesible.

Sin embargo, hacer todo público va en contra del principio de encapsulación. Se recomienda usar este modificador principalmente para:

  • Clases que representan una API pública
  • Métodos que forman parte de la interfaz de la clase
  • Constantes que deben ser accesibles globalmente

Modificador private

El modificador private representa el nivel más restrictivo de acceso. Los elementos marcados como privados solo son accesibles dentro de la propia clase donde se declaran.

public class CuentaBancaria {
    private double saldo;
    private String numeroCuenta;
    
    public CuentaBancaria(String numeroCuenta, double saldoInicial) {
        this.numeroCuenta = numeroCuenta;
        this.saldo = saldoInicial;
    }
    
    private void validarCantidad(double cantidad) {
        if (cantidad <= 0) {
            throw new IllegalArgumentException("La cantidad debe ser positiva");
        }
    }
    
    public void depositar(double cantidad) {
        validarCantidad(cantidad);
        saldo += cantidad;
    }
}

En este ejemplo, los atributos saldo y numeroCuenta son privados, lo que significa que no se puede acceder directamente a ellos desde fuera de la clase. El método validarCantidad() también es privado, ya que es un método auxiliar interno que no forma parte de la interfaz pública de la clase.

El modificador private se utiliza para:

  • Atributos que representan el estado interno de un objeto
  • Métodos auxiliares que no deben ser parte de la API pública
  • Implementaciones internas que podrían cambiar en el futuro

Modificador protected

El modificador protected proporciona un nivel intermedio de acceso. Los elementos marcados como protegidos son accesibles:

  • Dentro de la misma clase
  • Dentro de clases del mismo paquete
  • Dentro de clases que heredan (subclases), incluso si están en paquetes diferentes
package animales;

public class Animal {
    protected String nombre;
    protected int edad;
    
    protected void respirar() {
        System.out.println("El animal está respirando");
    }
}
package mascotas;

import animales.Animal;

public class Perro extends Animal {
    public void ladrar() {
        // Puede acceder a los miembros protected de Animal
        System.out.println(nombre + " está ladrando");
        respirar();
    }
}

En este ejemplo, la clase Perro puede acceder a los atributos nombre y edad, así como al método respirar() de la clase Animal, a pesar de estar en un paquete diferente, porque Perro extiende a Animal.

El modificador protected se utiliza principalmente para:

  • Atributos y métodos que deben ser accesibles para las subclases
  • Elementos que forman parte de la implementación de una jerarquía de clases
  • Cuando se quiere permitir la extensión y personalización en subclases

Modificador por defecto (package-private)

Cuando no se especifica ningún modificador, se aplica el nivel de acceso por defecto o package-private. Los elementos con este nivel de acceso son visibles solo dentro del mismo paquete.

package utilidades;

class Calculadora {
    int sumar(int a, int b) {
        return a + b;
    }
}
package utilidades;

public class Matematicas {
    void operacion() {
        Calculadora calc = new Calculadora();
        int resultado = calc.sumar(5, 3); // Acceso permitido (mismo paquete)
    }
}
package aplicacion;

import utilidades.Calculadora; // Error: Calculadora no es visible

public class Aplicacion {
    void iniciar() {
        Calculadora calc = new Calculadora(); // Error de compilación
    }
}

En este ejemplo, la clase Calculadora y su método sumar() tienen acceso por defecto, por lo que solo son accesibles desde otras clases del paquete utilidades.

Este nivel de acceso se utiliza para:

  • Clases auxiliares que solo se necesitan dentro de un paquete
  • Implementaciones internas que no deben ser parte de la API pública
  • Cuando se quiere agrupar funcionalidades relacionadas en un paquete

Tabla comparativa de modificadores de acceso

Para entender mejor las diferencias entre los modificadores, se puede visualizar su alcance:

Modificador Misma clase Mismo paquete Subclase (diferente paquete) Cualquier clase
private
(default)
protected
public

Aplicación práctica de los modificadores

Veamos un ejemplo completo que ilustra el uso adecuado de los diferentes modificadores:

package banco;

public class CuentaBancaria {
    private String titular;
    private double saldo;
    protected String numeroCuenta;
    public static final double TASA_INTERES = 0.05;
    
    public CuentaBancaria(String titular, String numeroCuenta) {
        this.titular = titular;
        this.numeroCuenta = numeroCuenta;
        this.saldo = 0.0;
    }
    
    public double consultarSaldo() {
        return saldo;
    }
    
    public void depositar(double cantidad) {
        validarCantidad(cantidad);
        saldo += cantidad;
        registrarMovimiento("Depósito", cantidad);
    }
    
    public boolean retirar(double cantidad) {
        validarCantidad(cantidad);
        if (saldo >= cantidad) {
            saldo -= cantidad;
            registrarMovimiento("Retiro", cantidad);
            return true;
        }
        return false;
    }
    
    private void validarCantidad(double cantidad) {
        if (cantidad <= 0) {
            throw new IllegalArgumentException("La cantidad debe ser positiva");
        }
    }
    
    protected void registrarMovimiento(String tipo, double cantidad) {
        System.out.println(tipo + " de " + cantidad + " en cuenta " + numeroCuenta);
    }
}

En este ejemplo:

  • Los atributos titular y saldo son privados para proteger la información sensible.
  • El atributo numeroCuenta es protegido para permitir que las subclases puedan acceder a él.
  • La constante TASA_INTERES es pública porque es información que puede ser útil para cualquier clase.
  • Los métodos consultarSaldo(), depositar() y retirar() son públicos porque forman parte de la interfaz de la clase.
  • El método validarCantidad() es privado porque es un método auxiliar interno.
  • El método registrarMovimiento() es protegido para permitir que las subclases puedan personalizarlo.

Consideraciones importantes

Al trabajar con modificadores de acceso, se deben tener en cuenta algunas consideraciones:

  • Se recomienda usar el principio de menor privilegio: proporcionar el nivel de acceso más restrictivo posible que permita que el código funcione.
  • Los atributos generalmente deben ser private para mantener el encapsulamiento.
  • Los métodos que forman parte de la API pública deben ser public.
  • Los métodos auxiliares internos deben ser private.
  • El modificador protected se debe usar con precaución, ya que crea una dependencia entre la clase base y sus subclases.

Getters y setters: implementación y buenas prácticas

Los getters y setters son métodos especiales que permiten acceder y modificar los atributos privados de una clase, manteniendo el principio de encapsulación. Estos métodos actúan como una interfaz controlada entre los atributos privados de un objeto y el código externo que necesita interactuar con ellos.

En Java, se implementan siguiendo una convención de nomenclatura específica que facilita su identificación y uso. Para un atributo llamado nombreAtributo, los métodos correspondientes serían:

private TipoDato nombreAtributo;

public TipoDato getNombreAtributo() {
    return nombreAtributo;
}

public void setNombreAtributo(TipoDato nombreAtributo) {
    this.nombreAtributo = nombreAtributo;
}

Implementación básica de getters y setters

Veamos un ejemplo práctico con una clase Empleado:

public class Empleado {
    private String nombre;
    private double salario;
    private int edad;
    
    // Constructor
    public Empleado(String nombre, double salario, int edad) {
        this.nombre = nombre;
        this.salario = salario;
        this.edad = edad;
    }
    
    // Getter para nombre
    public String getNombre() {
        return nombre;
    }
    
    // Setter para nombre
    public void setNombre(String nombre) {
        this.nombre = nombre;
    }
    
    // Getter para salario
    public double getSalario() {
        return salario;
    }
    
    // Setter para salario
    public void setSalario(double salario) {
        this.salario = salario;
    }
    
    // Getter para edad
    public int getEdad() {
        return edad;
    }
    
    // Setter para edad
    public void setEdad(int edad) {
        this.edad = edad;
    }
}

Con esta implementación, se puede acceder y modificar los atributos de manera controlada:

Empleado empleado = new Empleado("Ana García", 45000.0, 28);

// Acceder a los atributos mediante getters
System.out.println("Nombre: " + empleado.getNombre());
System.out.println("Salario: " + empleado.getSalario());

// Modificar atributos mediante setters
empleado.setSalario(48000.0);
empleado.setEdad(29);

Validación en setters

Una de las ventajas principales de los setters es que permiten validar los datos antes de asignarlos a los atributos. Esto ayuda a mantener la integridad de los objetos:

public class Producto {
    private String nombre;
    private double precio;
    private int stock;
    
    // Constructor
    public Producto(String nombre, double precio, int stock) {
        setNombre(nombre);
        setPrecio(precio);
        setStock(stock);
    }
    
    public String getNombre() {
        return nombre;
    }
    
    public void setNombre(String nombre) {
        if (nombre == null || nombre.trim().isEmpty()) {
            throw new IllegalArgumentException("El nombre no puede estar vacío");
        }
        this.nombre = nombre;
    }
    
    public double getPrecio() {
        return precio;
    }
    
    public void setPrecio(double precio) {
        if (precio < 0) {
            throw new IllegalArgumentException("El precio no puede ser negativo");
        }
        this.precio = precio;
    }
    
    public int getStock() {
        return stock;
    }
    
    public void setStock(int stock) {
        if (stock < 0) {
            throw new IllegalArgumentException("El stock no puede ser negativo");
        }
        this.stock = stock;
    }
}

Los setters validan los datos de entrada, asegurando que los objetos Producto siempre mantengan un estado válido. Además, se reutilizan los setters en el constructor para evitar duplicar la lógica de validación.

Getters con lógica adicional

Los getters también pueden incluir lógica adicional, no limitándose a simplemente devolver el valor del atributo:

public class Empleado {
    private String nombre;
    private String apellido;
    private double salarioBase;
    private double bonificacion;
    
    // Otros métodos y constructor...
    
    public String getNombreCompleto() {
        return nombre + " " + apellido;
    }
    
    public double getSalarioTotal() {
        return salarioBase + bonificacion;
    }
    
    // Getters y setters básicos para los atributos...
}

En este ejemplo, getNombreCompleto() y getSalarioTotal() son getters que calculan valores derivados a partir de los atributos básicos.

Buenas prácticas para getters y setters

Mantener la simplicidad

Los getters y setters deben ser concisos y enfocados. Un getter debe devolver el valor y un setter debe establecerlo, realizando solo las validaciones necesarias:

// Bien
public void setEdad(int edad) {
    if (edad < 0 || edad > 120) {
        throw new IllegalArgumentException("Edad fuera de rango válido");
    }
    this.edad = edad;
}

// Mal - Hace demasiadas cosas
public void setEdad(int edad) {
    if (edad < 0) {
        throw new IllegalArgumentException("Edad negativa");
    }
    this.edad = edad;
    System.out.println("Edad actualizada a: " + edad);
    actualizarBaseDeDatos();
    notificarCambio();
}

Usar nombres consistentes

Sigue la convención de nomenclatura de Java para getters y setters:

// Para atributos booleanos
private boolean activo;
public boolean isActivo() { return activo; }
public void setActivo(boolean activo) { this.activo = activo; }

// Para atributos no booleanos
private String nombre;
public String getNombre() { return nombre; }
public void setNombre(String nombre) { this.nombre = nombre; }

No exponer todos los atributos automáticamente

No todos los atributos necesitan getters y setters. Evalúa cada caso:

public class Usuario {
    private String nombre;
    private String contraseña; // No debería tener getter
    private boolean administrador;
    
    // Getter para nombre
    public String getNombre() {
        return nombre;
    }
    
    // No incluir getter para contraseña por seguridad
    
    // Setter para contraseña con validación
    public void setContraseña(String contraseña) {
        if (contraseña == null || contraseña.length() < 8) {
            throw new IllegalArgumentException("La contraseña debe tener al menos 8 caracteres");
        }
        this.contraseña = contraseña;
    }
    
    // Solo getter para administrador (inmutable después de creación)
    public boolean isAdministrador() {
        return administrador;
    }
}

Considerar métodos específicos en lugar de getters/setters genéricos

A veces, un método con nombre descriptivo es mejor que un getter o setter genérico:

// En lugar de:
usuario.setActivo(false);

// Mejor usar:
usuario.desactivar();

// En lugar de:
carrito.setItems(nuevaLista);

// Mejor usar:
carrito.agregarItem(item);
carrito.eliminarItem(item);
carrito.vaciar();

Evitar efectos secundarios en los getters

Los getters no deberían modificar el estado del objeto:

// Mal - tiene efecto secundario
public int getContadorAccesos() {
    return contadorAccesos++;  // Incrementa el contador cada vez que se accede
}

// Bien - sin efectos secundarios
public int getContadorAccesos() {
    return contadorAccesos;
}

// Método separado para incrementar
public void incrementarContador() {
    contadorAccesos++;
}

Usar getters en los métodos internos de la clase

Para mantener la consistencia, es recomendable usar los getters incluso dentro de la propia clase, especialmente si contienen lógica adicional:

public class Factura {
    private List<LineaFactura> lineas;
    private double descuento;
    
    public double getSubtotal() {
        double subtotal = 0;
        for (LineaFactura linea : lineas) {
            subtotal += linea.getImporte();
        }
        return subtotal;
    }
    
    public double getTotal() {
        // Usa el getter en lugar de calcular de nuevo
        return getSubtotal() * (1 - descuento);
    }
}

Generación automática de getters y setters

La mayoría de los IDEs modernos como IntelliJ IDEA, Eclipse o Visual Studio Code (con extensiones) ofrecen herramientas para generar automáticamente getters y setters:

// Definimos los atributos
public class Cliente {
    private String id;
    private String nombre;
    private String email;
    private LocalDate fechaRegistro;
    
    // El IDE puede generar automáticamente todos los getters y setters
    // con validaciones básicas si es necesario
}

Getters y setters en clases con relaciones

Cuando trabajamos con relaciones entre clases, debemos tener cuidado con los getters que devuelven colecciones:

public class Departamento {
    private String nombre;
    private List<Empleado> empleados = new ArrayList<>();
    
    // Getter que devuelve una copia defensiva
    public List<Empleado> getEmpleados() {
        return new ArrayList<>(empleados);
    }
    
    // Mejor enfoque: métodos específicos
    public void agregarEmpleado(Empleado empleado) {
        empleados.add(empleado);
    }
    
    public void eliminarEmpleado(Empleado empleado) {
        empleados.remove(empleado);
    }
    
    public int getCantidadEmpleados() {
        return empleados.size();
    }
}

En este ejemplo, getEmpleados() devuelve una copia de la lista para evitar que el código externo modifique directamente la colección interna. Alternativamente, se pueden proporcionar métodos específicos para manipular la colección de manera controlada.

Encapsulación en el mundo real: ejemplos prácticos

En entornos de producción, la encapsulación adecuada marca la diferencia entre un código mantenible y uno propenso a errores. Veamos cómo se aplica este principio en situaciones reales.

Sistemas de gestión de usuarios

Un caso típico donde la encapsulación resulta crucial es en los sistemas que manejan información sensible de usuarios:

public class Usuario {
    private String nombre;
    private String email;
    private String contraseña;
    private boolean cuentaVerificada;
    private LocalDate fechaRegistro;
    private List<String> rolesAsignados;
    
    // Constructor
    public Usuario(String nombre, String email, String contraseña) {
        this.nombre = nombre;
        this.email = validarEmail(email);
        this.contraseña = encriptarContraseña(contraseña);
        this.cuentaVerificada = false;
        this.fechaRegistro = LocalDate.now();
        this.rolesAsignados = new ArrayList<>();
    }
    
    // Getters controlados
    public String getNombre() {
        return nombre;
    }
    
    public String getEmail() {
        return email;
    }
    
    // No exponemos la contraseña
    
    public boolean isCuentaVerificada() {
        return cuentaVerificada;
    }
    
    public LocalDate getFechaRegistro() {
        return fechaRegistro;
    }
    
    public List<String> getRolesAsignados() {
        return Collections.unmodifiableList(rolesAsignados);
    }
    
    // Métodos específicos en lugar de setters genéricos
    public void cambiarNombre(String nuevoNombre) {
        if (nuevoNombre == null || nuevoNombre.trim().isEmpty()) {
            throw new IllegalArgumentException("El nombre no puede estar vacío");
        }
        this.nombre = nuevoNombre;
    }
    
    public void cambiarEmail(String nuevoEmail) {
        this.email = validarEmail(nuevoEmail);
    }
    
    public boolean verificarContraseña(String contraseñaIntroducida) {
        return this.contraseña.equals(encriptarContraseña(contraseñaIntroducida));
    }
    
    public void cambiarContraseña(String contraseñaActual, String nuevaContraseña) {
        if (!verificarContraseña(contraseñaActual)) {
            throw new SecurityException("Contraseña actual incorrecta");
        }
        this.contraseña = encriptarContraseña(nuevaContraseña);
    }
    
    public void verificarCuenta() {
        this.cuentaVerificada = true;
    }
    
    public void asignarRol(String rol) {
        if (!rolesAsignados.contains(rol)) {
            rolesAsignados.add(rol);
        }
    }
    
    public void revocarRol(String rol) {
        rolesAsignados.remove(rol);
    }
    
    // Métodos auxiliares privados
    private String validarEmail(String email) {
        if (email == null || !email.matches("^[\\w-\\.]+@([\\w-]+\\.)+[\\w-]{2,4}$")) {
            throw new IllegalArgumentException("Email inválido");
        }
        return email;
    }
    
    private String encriptarContraseña(String contraseña) {
        // En un caso real, usaríamos un algoritmo de hash seguro
        return contraseña.hashCode() + "";
    }
}

Observa cómo esta clase:

  • Oculta completamente la contraseña (sin getter)
  • Proporciona un método específico para verificar la contraseña sin exponerla
  • Devuelve una lista inmutable de roles para evitar modificaciones no controladas
  • Usa métodos específicos en lugar de setters genéricos para operaciones como asignar o revocar roles

Sistemas de transacciones financieras

En aplicaciones financieras, la encapsulación es fundamental para mantener la integridad de las operaciones:

public class CuentaBancaria {
    private final String numeroCuenta;
    private final String titular;
    private double saldo;
    private boolean bloqueada;
    private final List<Transaccion> historialTransacciones;
    
    public CuentaBancaria(String numeroCuenta, String titular, double saldoInicial) {
        this.numeroCuenta = numeroCuenta;
        this.titular = titular;
        this.saldo = Math.max(0, saldoInicial);
        this.bloqueada = false;
        this.historialTransacciones = new ArrayList<>();
        
        if (saldoInicial > 0) {
            registrarTransaccion(TipoTransaccion.DEPOSITO, saldoInicial, "Saldo inicial");
        }
    }
    
    // Getters esenciales
    public String getNumeroCuenta() {
        return numeroCuenta;
    }
    
    public String getTitular() {
        return titular;
    }
    
    public double getSaldo() {
        return saldo;
    }
    
    public boolean isBloqueada() {
        return bloqueada;
    }
    
    public List<Transaccion> getHistorialTransacciones() {
        return Collections.unmodifiableList(historialTransacciones);
    }
    
    // Operaciones específicas
    public boolean depositar(double cantidad, String concepto) {
        if (bloqueada) {
            return false;
        }
        
        if (cantidad <= 0) {
            throw new IllegalArgumentException("La cantidad debe ser positiva");
        }
        
        saldo += cantidad;
        registrarTransaccion(TipoTransaccion.DEPOSITO, cantidad, concepto);
        return true;
    }
    
    public boolean retirar(double cantidad, String concepto) {
        if (bloqueada || cantidad > saldo || cantidad <= 0) {
            return false;
        }
        
        saldo -= cantidad;
        registrarTransaccion(TipoTransaccion.RETIRO, cantidad, concepto);
        return true;
    }
    
    public boolean transferir(CuentaBancaria destino, double cantidad, String concepto) {
        if (bloqueada || destino.bloqueada || cantidad > saldo || cantidad <= 0) {
            return false;
        }
        
        saldo -= cantidad;
        destino.saldo += cantidad;
        
        registrarTransaccion(TipoTransaccion.TRANSFERENCIA_SALIENTE, 
                            cantidad, 
                            concepto + " → " + destino.getNumeroCuenta());
                            
        destino.registrarTransaccion(TipoTransaccion.TRANSFERENCIA_ENTRANTE, 
                                    cantidad, 
                                    concepto + " ← " + this.getNumeroCuenta());
        return true;
    }
    
    public void bloquearCuenta() {
        this.bloqueada = true;
        registrarTransaccion(TipoTransaccion.BLOQUEO, 0, "Bloqueo de cuenta");
    }
    
    public void desbloquearCuenta() {
        this.bloqueada = false;
        registrarTransaccion(TipoTransaccion.DESBLOQUEO, 0, "Desbloqueo de cuenta");
    }
    
    // Método privado para registro interno
    private void registrarTransaccion(TipoTransaccion tipo, double cantidad, String concepto) {
        Transaccion transaccion = new Transaccion(
            tipo,
            cantidad,
            concepto,
            LocalDateTime.now()
        );
        historialTransacciones.add(transaccion);
    }
    
    // Clase interna para representar transacciones
    public static class Transaccion {
        private final TipoTransaccion tipo;
        private final double cantidad;
        private final String concepto;
        private final LocalDateTime fecha;
        
        private Transaccion(TipoTransaccion tipo, double cantidad, String concepto, LocalDateTime fecha) {
            this.tipo = tipo;
            this.cantidad = cantidad;
            this.concepto = concepto;
            this.fecha = fecha;
        }
        
        public TipoTransaccion getTipo() {
            return tipo;
        }
        
        public double getCantidad() {
            return cantidad;
        }
        
        public String getConcepto() {
            return concepto;
        }
        
        public LocalDateTime getFecha() {
            return fecha;
        }
    }
    
    // Enum para tipos de transacción
    public enum TipoTransaccion {
        DEPOSITO, RETIRO, TRANSFERENCIA_ENTRANTE, TRANSFERENCIA_SALIENTE, BLOQUEO, DESBLOQUEO
    }
}

Este ejemplo muestra cómo:

  • Se encapsula el estado interno de la cuenta (saldo, estado de bloqueo)
  • Se proporcionan operaciones específicas (depositar, retirar, transferir) en lugar de setters directos
  • Se mantiene un historial inmutable de transacciones
  • Se usa una clase interna para representar las transacciones, también inmutable

Sistemas de comercio electrónico

En aplicaciones de e-commerce, la encapsulación ayuda a mantener la consistencia del carrito de compras:

public class CarritoCompra {
    private final String idCliente;
    private final Map<Producto, Integer> items;
    private boolean finalizado;
    
    public CarritoCompra(String idCliente) {
        this.idCliente = idCliente;
        this.items = new HashMap<>();
        this.finalizado = false;
    }
    
    public String getIdCliente() {
        return idCliente;
    }
    
    public boolean isFinalizado() {
        return finalizado;
    }
    
    public Map<Producto, Integer> getItems() {
        return Collections.unmodifiableMap(items);
    }
    
    public int getCantidadItems() {
        return items.values().stream().mapToInt(Integer::intValue).sum();
    }
    
    public double getTotal() {
        return items.entrySet().stream()
                .mapToDouble(entry -> entry.getKey().getPrecio() * entry.getValue())
                .sum();
    }
    
    public void agregarProducto(Producto producto, int cantidad) {
        if (finalizado) {
            throw new IllegalStateException("No se puede modificar un carrito finalizado");
        }
        
        if (cantidad <= 0) {
            throw new IllegalArgumentException("La cantidad debe ser positiva");
        }
        
        items.merge(producto, cantidad, Integer::sum);
    }
    
    public void eliminarProducto(Producto producto) {
        if (finalizado) {
            throw new IllegalStateException("No se puede modificar un carrito finalizado");
        }
        
        items.remove(producto);
    }
    
    public void actualizarCantidad(Producto producto, int nuevaCantidad) {
        if (finalizado) {
            throw new IllegalStateException("No se puede modificar un carrito finalizado");
        }
        
        if (nuevaCantidad <= 0) {
            eliminarProducto(producto);
        } else if (items.containsKey(producto)) {
            items.put(producto, nuevaCantidad);
        }
    }
    
    public void vaciar() {
        if (finalizado) {
            throw new IllegalStateException("No se puede modificar un carrito finalizado");
        }
        
        items.clear();
    }
    
    public Pedido finalizarCompra() {
        if (finalizado) {
            throw new IllegalStateException("El carrito ya ha sido finalizado");
        }
        
        if (items.isEmpty()) {
            throw new IllegalStateException("No se puede finalizar un carrito vacío");
        }
        
        finalizado = true;
        return new Pedido(this);
    }
}

En este ejemplo:

  • El mapa de items se devuelve como inmutable para evitar modificaciones no controladas
  • Se proporcionan métodos específicos para cada operación (agregar, eliminar, actualizar)
  • Se mantiene la consistencia verificando el estado del carrito antes de cada operación
  • Se encapsula la lógica de creación de pedidos dentro del método finalizarCompra()

Aplicaciones de monitoreo y sensores

En sistemas IoT o de monitoreo, la encapsulación permite abstraer los detalles de implementación:

public class SensorTemperatura {
    private final String id;
    private final String ubicacion;
    private double temperaturaActual;
    private double temperaturaMaxima;
    private double temperaturaMinima;
    private LocalDateTime ultimaLectura;
    private final List<Observer> observadores;
    
    public SensorTemperatura(String id, String ubicacion) {
        this.id = id;
        this.ubicacion = ubicacion;
        this.temperaturaActual = 0;
        this.temperaturaMaxima = Double.MIN_VALUE;
        this.temperaturaMinima = Double.MAX_VALUE;
        this.observadores = new ArrayList<>();
    }
    
    // Getters para información básica
    public String getId() {
        return id;
    }
    
    public String getUbicacion() {
        return ubicacion;
    }
    
    public double getTemperaturaActual() {
        return temperaturaActual;
    }
    
    public double getTemperaturaMaxima() {
        return temperaturaMaxima;
    }
    
    public double getTemperaturaMinima() {
        return temperaturaMinima;
    }
    
    public LocalDateTime getUltimaLectura() {
        return ultimaLectura;
    }
    
    // Método para registrar una nueva lectura
    public void registrarLectura(double temperatura) {
        this.temperaturaActual = temperatura;
        this.ultimaLectura = LocalDateTime.now();
        
        if (temperatura > temperaturaMaxima) {
            this.temperaturaMaxima = temperatura;
        }
        
        if (temperatura < temperaturaMinima) {
            this.temperaturaMinima = temperatura;
        }
        
        notificarObservadores();
    }
    
    // Métodos para el patrón Observer
    public void agregarObservador(Observer observador) {
        if (!observadores.contains(observador)) {
            observadores.add(observador);
        }
    }
    
    public void eliminarObservador(Observer observador) {
        observadores.remove(observador);
    }
    
    private void notificarObservadores() {
        for (Observer observador : observadores) {
            observador.actualizar(this);
        }
    }
    
    // Interfaz para el patrón Observer
    public interface Observer {
        void actualizar(SensorTemperatura sensor);
    }
}

Este ejemplo muestra:

  • Encapsulación de la lógica de actualización de temperaturas máximas y mínimas
  • Implementación del patrón Observer para notificar cambios sin exponer detalles internos
  • Inmutabilidad de propiedades como ID y ubicación

Aplicación en frameworks y bibliotecas

Los frameworks modernos de Java hacen un uso extensivo de la encapsulación. Por ejemplo, en Spring Framework:

@Entity
public class Producto {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @Column(nullable = false)
    private String nombre;
    
    private String descripcion;
    
    @Column(nullable = false)
    private BigDecimal precio;
    
    @Column(nullable = false)
    private int stock;
    
    @ManyToOne
    @JoinColumn(name = "categoria_id")
    private Categoria categoria;
    
    @Version
    private Long version;
    
    // Getters y setters controlados
    
    public boolean disponible() {
        return stock > 0;
    }
    
    public boolean reservar(int cantidad) {
        if (cantidad <= 0 || cantidad > stock) {
            return false;
        }
        
        stock -= cantidad;
        return true;
    }
    
    public void reponer(int cantidad) {
        if (cantidad <= 0) {
            throw new IllegalArgumentException("La cantidad debe ser positiva");
        }
        
        stock += cantidad;
    }
}

En este ejemplo de entidad JPA:

  • Los campos están encapsulados como privados
  • Se proporcionan métodos específicos para operaciones de negocio (reservar, reponer)
  • Se usa @Version para control de concurrencia optimista, encapsulando la lógica de bloqueo

Inmutabilidad y sus beneficios

La inmutabilidad representa un concepto avanzado de encapsulación donde, una vez creado un objeto, su estado no puede ser modificado. En Java, esto significa diseñar clases cuyos objetos mantienen los mismos valores durante toda su vida útil. Este enfoque, aunque inicialmente puede parecer restrictivo, ofrece ventajas en términos de seguridad, rendimiento y simplicidad del código.

Fundamentos de la inmutabilidad

Para crear una clase inmutable en Java, se deben seguir varios principios clave:

public final class Coordenada {
    private final double x;
    private final double y;
    
    public Coordenada(double x, double y) {
        this.x = x;
        this.y = y;
    }
    
    public double getX() {
        return x;
    }
    
    public double getY() {
        return y;
    }
    
    public Coordenada trasladar(double deltaX, double deltaY) {
        return new Coordenada(this.x + deltaX, this.y + deltaY);
    }
    
    @Override
    public String toString() {
        return "(" + x + ", " + y + ")";
    }
}

En este ejemplo se aplican los principios fundamentales de inmutabilidad:

  • La clase se declara como final para evitar que sea extendida
  • Todos los campos son private y final
  • No se proporcionan métodos que modifiquen el estado (setters)
  • Las operaciones que "modificarían" el objeto retornan una nueva instancia

Para usar esta clase inmutable:

Coordenada punto = new Coordenada(5, 10);
System.out.println("Punto original: " + punto);

// La operación no modifica el objeto original
Coordenada puntoDespazado = punto.trasladar(3, -2);
System.out.println("Punto original (sin cambios): " + punto);
System.out.println("Nuevo punto: " + puntoDespazado);

Inmutabilidad con colecciones

Cuando una clase inmutable contiene colecciones, se requiere especial cuidado para evitar que estas referencias puedan ser modificadas:

public final class Estudiante {
    private final String id;
    private final String nombre;
    private final List<String> cursos;
    
    public Estudiante(String id, String nombre, List<String> cursos) {
        this.id = id;
        this.nombre = nombre;
        // Copia defensiva en el constructor
        this.cursos = new ArrayList<>(cursos);
    }
    
    public String getId() {
        return id;
    }
    
    public String getNombre() {
        return nombre;
    }
    
    public List<String> getCursos() {
        // Copia defensiva en el getter
        return Collections.unmodifiableList(cursos);
    }
    
    public Estudiante agregarCurso(String curso) {
        List<String> nuevosCursos = new ArrayList<>(this.cursos);
        nuevosCursos.add(curso);
        return new Estudiante(this.id, this.nombre, nuevosCursos);
    }
}

Observa las técnicas defensivas empleadas:

  • Se crea una copia de la colección en el constructor
  • Se devuelve una vista no modificable en el getter
  • Para "añadir" un curso, se crea un nuevo objeto con la lista actualizada

Beneficios de la inmutabilidad

Seguridad en entornos multihilo

Uno de los beneficios más importantes de la inmutabilidad es la seguridad inherente en entornos concurrentes:

public final class ConfiguracionAplicacion {
    private final String nombreServidor;
    private final int puerto;
    private final Map<String, String> parametros;
    
    public ConfiguracionAplicacion(String nombreServidor, int puerto, 
                                  Map<String, String> parametros) {
        this.nombreServidor = nombreServidor;
        this.puerto = puerto;
        this.parametros = Collections.unmodifiableMap(new HashMap<>(parametros));
    }
    
    // Getters (sin setters)
    
    public ConfiguracionAplicacion conPuerto(int nuevoPuerto) {
        return new ConfiguracionAplicacion(this.nombreServidor, nuevoPuerto, this.parametros);
    }
}

Este objeto de configuración puede compartirse con seguridad entre múltiples hilos sin necesidad de sincronización, ya que ningún hilo puede modificar su estado.

Simplificación del razonamiento sobre el código

La inmutabilidad simplifica el razonamiento sobre el comportamiento del código:

public void procesarTransaccion(Transaccion transaccion) {
    // Podemos estar seguros de que la transacción no cambiará durante el procesamiento
    validarTransaccion(transaccion);
    registrarTransaccion(transaccion);
    notificarCliente(transaccion);
}

Al saber que transaccion es inmutable, no necesitamos preocuparnos por posibles cambios durante su procesamiento, eliminando toda una categoría de errores potenciales.

Hashcode consistente

Los objetos inmutables son ideales para usar como claves en mapas o elementos en conjuntos:

public final class ProductoId {
    private final String codigo;
    private final String paisOrigen;
    
    // Constructor y getters
    
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        ProductoId that = (ProductoId) o;
        return Objects.equals(codigo, that.codigo) && 
               Objects.equals(paisOrigen, that.paisOrigen);
    }
    
    @Override
    public int hashCode() {
        return Objects.hash(codigo, paisOrigen);
    }
}

// Uso en un mapa
Map<ProductoId, Inventario> inventarioPorProducto = new HashMap<>();

Como el estado de ProductoId nunca cambia, su hashcode permanece constante, garantizando el correcto funcionamiento en estructuras basadas en hash.

Facilita el patrón Builder

La inmutabilidad se complementa perfectamente con el patrón Builder para crear objetos complejos:

public final class Email {
    private final String destinatario;
    private final String remitente;
    private final String asunto;
    private final String cuerpo;
    private final List<String> adjuntos;
    private final boolean prioritario;
    
    private Email(Builder builder) {
        this.destinatario = builder.destinatario;
        this.remitente = builder.remitente;
        this.asunto = builder.asunto;
        this.cuerpo = builder.cuerpo;
        this.adjuntos = Collections.unmodifiableList(new ArrayList<>(builder.adjuntos));
        this.prioritario = builder.prioritario;
    }
    
    // Getters (sin setters)
    
    public static class Builder {
        private String destinatario;
        private String remitente;
        private String asunto;
        private String cuerpo;
        private List<String> adjuntos = new ArrayList<>();
        private boolean prioritario = false;
        
        public Builder destinatario(String destinatario) {
            this.destinatario = destinatario;
            return this;
        }
        
        public Builder remitente(String remitente) {
            this.remitente = remitente;
            return this;
        }
        
        // Otros métodos del builder
        
        public Builder adjunto(String adjunto) {
            this.adjuntos.add(adjunto);
            return this;
        }
        
        public Email build() {
            // Validaciones
            if (destinatario == null || remitente == null) {
                throw new IllegalStateException("Email requiere destinatario y remitente");
            }
            return new Email(this);
        }
    }
}

El uso del patrón Builder permite crear objetos inmutables complejos de manera fácil:

Email email = new Email.Builder()
    .destinatario("cliente@ejemplo.com")
    .remitente("soporte@miempresa.com")
    .asunto("Confirmación de pedido")
    .cuerpo("Su pedido ha sido procesado correctamente.")
    .adjunto("factura.pdf")
    .prioritario(true)
    .build();

Mejora del rendimiento en ciertos escenarios

La inmutabilidad puede mejorar el rendimiento al permitir:

  • Caching seguro: Los resultados de operaciones costosas pueden cachearse sin riesgo
  • Compartición de instancias: Objetos idénticos pueden compartirse con seguridad
public final class BigDecimalRange {
    private final BigDecimal min;
    private final BigDecimal max;
    
    // Constructor y getters
    
    // Cache para rangos comunes
    private static final Map<String, BigDecimalRange> CACHE = new ConcurrentHashMap<>();
    
    public static BigDecimalRange of(BigDecimal min, BigDecimal max) {
        String key = min + ":" + max;
        return CACHE.computeIfAbsent(key, k -> new BigDecimalRange(min, max));
    }
}

Inmutabilidad en las APIs estándar de Java

Java incorpora numerosas clases inmutables en sus APIs estándar:

  • String: Quizás el ejemplo más conocido
  • BigInteger y BigDecimal: Para cálculos de precisión arbitraria
  • LocalDate, LocalTime y LocalDateTime: Para representación de fechas y horas
  • Clases de colecciones inmutables como las proporcionadas por Collections.unmodifiableXXX()

Patrones para trabajar con objetos inmutables

Patrón de copia con modificación

Cuando necesitamos "modificar" un objeto inmutable, creamos una copia con los cambios deseados:

public final class Pedido {
    private final String id;
    private final Cliente cliente;
    private final List<LineaPedido> lineas;
    private final EstadoPedido estado;
    
    // Constructor y getters
    
    public Pedido conEstado(EstadoPedido nuevoEstado) {
        return new Pedido(this.id, this.cliente, this.lineas, nuevoEstado);
    }
    
    public Pedido agregarLinea(LineaPedido nuevaLinea) {
        List<LineaPedido> nuevasLineas = new ArrayList<>(this.lineas);
        nuevasLineas.add(nuevaLinea);
        return new Pedido(this.id, this.cliente, nuevasLineas, this.estado);
    }
}

Uso de Record (Java 16+)

A partir de Java 16, los records proporcionan una forma concisa de definir clases inmutables:

public record Punto3D(double x, double y, double z) {
    // Los getters, equals, hashCode y toString se generan automáticamente
    
    public Punto3D desplazar(double dx, double dy, double dz) {
        return new Punto3D(x + dx, y + dy, z + dz);
    }
    
    public double distancia(Punto3D otro) {
        double dx = this.x - otro.x();
        double dy = this.y - otro.y();
        double dz = this.z - otro.z();
        return Math.sqrt(dx*dx + dy*dy + dz*dz);
    }
}

Los records simplifican enormemente la creación de clases inmutables al generar automáticamente constructores, getters y métodos equals(), hashCode() y toString().

Consideraciones prácticas

Aunque la inmutabilidad ofrece numerosos beneficios, también presenta algunos desafíos:

  • Consumo de memoria: Crear nuevos objetos para cada "modificación" puede aumentar la presión sobre el recolector de basura
  • Complejidad en ciertos escenarios: Algunos algoritmos que requieren muchas modificaciones pueden volverse más complejos
  • Integración con frameworks: Algunos frameworks pueden requerir objetos mutables (aunque esto es cada vez menos común)

Para mitigar estos problemas, se pueden aplicar estrategias como:

  • Usar inmutabilidad selectiva para las partes críticas del modelo
  • Implementar estructuras de datos persistentes que optimizan la creación de copias
  • Utilizar patrones como Builder para la construcción de objetos complejos

Ejemplo completo: Sistema de gestión de documentos

Veamos un ejemplo más completo que ilustra los beneficios de la inmutabilidad en un sistema real:

public final class Documento {
    private final String id;
    private final String titulo;
    private final String contenido;
    private final Usuario autor;
    private final LocalDateTime fechaCreacion;
    private final LocalDateTime ultimaModificacion;
    private final Set<String> etiquetas;
    private final EstadoDocumento estado;
    
    // Constructor privado usado por el Builder
    private Documento(Builder builder) {
        this.id = builder.id;
        this.titulo = builder.titulo;
        this.contenido = builder.contenido;
        this.autor = builder.autor;
        this.fechaCreacion = builder.fechaCreacion;
        this.ultimaModificacion = builder.ultimaModificacion;
        this.etiquetas = Collections.unmodifiableSet(new HashSet<>(builder.etiquetas));
        this.estado = builder.estado;
    }
    
    // Getters (sin setters)
    
    // Métodos que devuelven nuevas instancias
    public Documento conTitulo(String nuevoTitulo) {
        return toBuilder().titulo(nuevoTitulo)
                .ultimaModificacion(LocalDateTime.now())
                .build();
    }
    
    public Documento conContenido(String nuevoContenido) {
        return toBuilder().contenido(nuevoContenido)
                .ultimaModificacion(LocalDateTime.now())
                .build();
    }
    
    public Documento agregarEtiqueta(String etiqueta) {
        Builder builder = toBuilder();
        builder.etiquetas.add(etiqueta);
        return builder.ultimaModificacion(LocalDateTime.now()).build();
    }
    
    public Documento cambiarEstado(EstadoDocumento nuevoEstado) {
        return toBuilder().estado(nuevoEstado)
                .ultimaModificacion(LocalDateTime.now())
                .build();
    }
    
    // Método para crear un Builder pre-configurado con los valores actuales
    public Builder toBuilder() {
        Builder builder = new Builder();
        builder.id = this.id;
        builder.titulo = this.titulo;
        builder.contenido = this.contenido;
        builder.autor = this.autor;
        builder.fechaCreacion = this.fechaCreacion;
        builder.ultimaModificacion = this.ultimaModificacion;
        builder.etiquetas = new HashSet<>(this.etiquetas);
        builder.estado = this.estado;
        return builder;
    }
    
    public static class Builder {
        private String id;
        private String titulo;
        private String contenido;
        private Usuario autor;
        private LocalDateTime fechaCreacion;
        private LocalDateTime ultimaModificacion;
        private Set<String> etiquetas = new HashSet<>();
        private EstadoDocumento estado = EstadoDocumento.BORRADOR;
        
        public Builder id(String id) {
            this.id = id;
            return this;
        }
        
        // Otros métodos del builder
        
        public Builder titulo(String titulo) {
            this.titulo = titulo;
            return this;
        }
        
        public Builder contenido(String contenido) {
            this.contenido = contenido;
            return this;
        }
        
        public Builder ultimaModificacion(LocalDateTime ultimaModificacion) {
            this.ultimaModificacion = ultimaModificacion;
            return this;
        }
        
        public Builder estado(EstadoDocumento estado) {
            this.estado = estado;
            return this;
        }
        
        public Documento build() {
            if (id == null) {
                id = UUID.randomUUID().toString();
            }
            
            if (fechaCreacion == null) {
                fechaCreacion = LocalDateTime.now();
            }
            
            if (ultimaModificacion == null) {
                ultimaModificacion = fechaCreacion;
            }
            
            // Validaciones
            if (titulo == null || contenido == null || autor == null) {
                throw new IllegalStateException("Documento requiere título, contenido y autor");
            }
            
            return new Documento(this);
        }
    }
    
    public enum EstadoDocumento {
        BORRADOR, REVISIÓN, PUBLICADO, ARCHIVADO
    }
}

Este ejemplo demuestra:

  • Inmutabilidad completa con todos los campos finales
  • Patrón Builder para facilitar la creación
  • Método toBuilder() para simplificar la creación de copias modificadas
  • Métodos específicos para cada tipo de "modificación"
  • Actualización automática de la fecha de modificación
Aprende Java online

Otras 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

Accede GRATIS a Java y certifícate

Ejercicios de programación de Java

Evalúa tus conocimientos de esta lección Encapsulación con nuestros retos de programación de tipo Test, Puzzle, Código y Proyecto con VSCode, guiados por IA.

Streams: match

Test

Gestión de errores y excepciones

Código

CRUD en Java de modelo Customer sobre un ArrayList

Proyecto

Clases abstractas

Test

Listas

Código

Métodos de la clase String

Código

Streams: reduce()

Test

API java.nio 2

Puzzle

Polimorfismo

Código

Pattern Matching

Código

Streams: flatMap()

Test

Llamada y sobrecarga de funciones

Puzzle

Métodos referenciados

Test

Métodos de la clase String

Código

Representación de Fecha

Puzzle

Operadores lógicos

Test

Inferencia de tipos con var

Código

Tipos de datos

Código

Estructuras de iteración

Puzzle

Streams: forEach()

Test

Objetos

Puzzle

Funciones lambda

Test

Uso de Scanner

Puzzle

Tipos de variables

Puzzle

Streams: collect()

Puzzle

Operadores aritméticos

Puzzle

Arrays y matrices

Código

Clases y objetos

Código

Interfaz funcional Consumer

Test

CRUD en Java de modelo Customer sobre un HashMap

Proyecto

Interfaces

Código

Enumeraciones Enums

Código

API Optional

Test

Interfaz funcional Function

Test

Encapsulación

Test

Interfaces

Código

Uso de API Optional

Puzzle

Representación de Hora

Test

Herencia básica

Test

Clases y objetos

Código

Interfaz funcional Supplier

Puzzle

HashMap

Puzzle

Sobrecarga de métodos

Test

Polimorfismo de tiempo de ejecución

Puzzle

OOP en Java

Proyecto

Sobrecarga de métodos

Código

CRUD de productos en Java

Proyecto

Clases sealed

Código

Creación de Streams

Test

Records

Código

Encapsulación

Código

Streams: min max

Puzzle

Herencia

Código

Métodos avanzados de la clase String

Puzzle

Funciones

Código

Polimorfismo de tiempo de compilación

Test

Reto sintaxis Java

Proyecto

Conjuntos

Código

Estructuras de control

Código

Recursión

Código

Excepciones

Puzzle

Herencia avanzada

Puzzle

Estructuras de selección

Test

Uso de interfaces

Test

Operadores

Código

Variables

Código

HashSet

Test

Objeto Scanner

Test

Streams: filter()

Puzzle

Operaciones de Streams

Puzzle

Interfaz funcional Predicate

Puzzle

Streams: sorted()

Test

Configuración de entorno

Test

Uso de variables

Test

Clases

Test

Streams: distinct()

Puzzle

Streams: count()

Test

ArrayList

Test

Mapas

Código

Datos de referencia

Test

Interfaces funcionales

Puzzle

Métodos básicos de la clase String

Test

Tipos de datos

Código

Clases abstractas

Código

Instalación

Test

Funciones

Código

Excepciones

Código

Estructuras de control

Código

Herencia de clases

Código

La clase Scanner

Código

Generics

Código

Streams: map()

Puzzle

Funciones y encapsulamiento

Test

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

  • Comprender el concepto y la importancia de la encapsulación en la programación orientada a objetos
  • Aprender a usar modificadores de acceso (private, public, protected) en Java
  • Aprender a implementar la encapsulación en Java mediante la creación de atributos privados y métodos getter y setter
  • Entender cómo la encapsulación contribuye a la seguridad, la flexibilidad y la mantenibilidad del código
  • Aprender a usar la encapsulación para controlar el acceso a los datos y garantizar la integridad de los mismos